USB send keystrokes for all BIP-85 secret types

This commit is contained in:
scgbckbone 2026-01-19 23:56:41 +01:00 committed by doc-hex
parent 440421731e
commit 01c27fe568
5 changed files with 44 additions and 37 deletions

View File

@ -7,6 +7,7 @@ This lists the new changes that have not yet been published in a normal release.
- New Feature: Export [BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) extended key expression.
Navigate to `Advanced/Tools -> Export Wallet -> Key Expression`
- New Feature: Support for v3 transactions
- New Feature: Send keystrokes with all derived BIP-85 secrets
# Mk4 Specific Changes

View File

@ -548,8 +548,9 @@ class ApproveTransaction(UserAuthorizedAction):
dis.fullscreen('Co-Signing...')
gc.collect()
CCCFeature.sign_psbt(self.psbt)
else:
# maybe capture new min-height for velocity limit
if SSSPFeature.is_enabled():
# capture new min-height for velocity limit
SSSPFeature.update_last_signed(self.psbt)
except FraudulentChangeOutput as exc:
@ -883,7 +884,8 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
# In that case this would just return dict and keep producing signed
# files on SD infinitely (would never actually prompt).
ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt,
txid=txid, title=title, force_prompt=not first_time,
key6="for QR Code of TXID", title=title,
force_prompt=not first_time,
no_qr=not version.has_qwerty or not allow_qr)
if ch == KEY_CANCEL:
UserAuthorizedAction.cleanup()
@ -964,7 +966,7 @@ async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_
del_after = settings.get('del', 0)
def _chunk_write(file_d, ofs, chunk=2048):
def _chunk_write(file_d, ofs, chunk=4096):
written = 0
while written < data_len:
if (written + chunk) > data_len:

View File

@ -205,14 +205,12 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
if new_secret:
msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii')
# Add the standard export prompt at the end, with extra (5) option sometimes.
key6 = 'to type %s over USB' % s_mode
key0 = None
if encoded is not None:
key0 = 'to switch to derived secret'
elif s_mode == 'pw':
key0 = 'to type password over USB'
prompt, escape = export_prompt_builder('data', key0=key0,
prompt, escape = export_prompt_builder('data', key0=key0, key6=key6,
no_qr=(not qr), force_prompt=True)
title = None
if node:
@ -224,7 +222,9 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
ch = await ux_show_story(msg+'\n\n'+prompt, title=title, escape=escape,
strict_escape=True, sensitive=True)
choice = import_export_prompt_decode(ch)
if isinstance(choice, dict):
if choice == KEY_CANCEL:
break
elif isinstance(choice, dict):
# write to SD card or Virtual Disk: simple text file
dis.fullscreen("Saving...")
try:
@ -247,27 +247,27 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
story = "Filename is:\n\n%s" % out_fn
story += "\n\nSignature filename is:\n\n%s" % sig_nice
await ux_show_story(story, title='Saved')
elif choice == KEY_CANCEL:
break
elif choice == KEY_QR:
from ux import show_qr_code
await show_qr_code(qr, qr_alnum, is_secret=True)
elif choice == '0':
if s_mode == 'pw':
# gets confirmation then types it
await single_send_keystrokes(qr, path)
elif encoded is not None:
# switch over to new secret!
dis.fullscreen("Applying...")
from actions import goto_top_menu
from glob import settings
xfp_str = xfp2str(settings.get("xfp", 0))
await seed.set_ephemeral_seed(
encoded,
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
)
goto_top_menu()
break
elif (choice == '0') and (encoded is not None):
# switch over to new secret!
dis.fullscreen("Applying...")
from actions import goto_top_menu
from glob import settings
xfp_str = xfp2str(settings.get("xfp", 0))
await seed.set_ephemeral_seed(
encoded,
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
)
goto_top_menu()
break
elif choice == "6":
# gets confirmation then types it
await single_send_keystrokes(qr, path)
elif NFC and choice == KEY_NFC:
# Share any of these over NFC

View File

@ -394,14 +394,14 @@ def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False):
def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offer_kt=False,
force_prompt=False, txid=None):
force_prompt=False, key6=None):
# Build the prompt for export
# - key0 can be for special stuff
from glob import NFC, VD
prompt, escape = None, KEY_CANCEL+"x"
if (NFC or VD) or (num_sd_slots>1) or key0 or force_prompt or offer_kt or txid or (not no_qr):
if (NFC or VD) or (num_sd_slots>1) or key0 or force_prompt or offer_kt or key6 or (not no_qr):
# no need to spam with another prompt, only option is SD card
prompt = "Press (1) to save %s to SD Card" % what_it_is
@ -431,10 +431,6 @@ def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offe
prompt += ", (4) to show QR code"
escape += '4'
if txid:
prompt += ", (6) for QR Code of TXID"
escape += "6"
if offer_kt:
prompt += ", (T) to " + offer_kt
escape += 't'
@ -443,6 +439,10 @@ def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offe
prompt += ', (0) ' + key0
escape += '0'
if key6:
prompt += ", (6) " + key6
escape += "6"
prompt += "."
return prompt, escape
@ -481,7 +481,7 @@ def import_export_prompt_decode(ch):
async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
no_nfc=False, title=None, intro='', footnotes='',
offer_kt=False, slot_b_only=False, force_prompt=False,
txid=None):
key0=None, key6=None):
# Show story allowing user to select source for importing/exporting
# - return either str(mode) OR dict(file_args)
@ -494,7 +494,7 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
if is_import:
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only)
else:
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, txid=txid,
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, key6=key6, key0=key0,
force_prompt=force_prompt, offer_kt=offer_kt)
# TODO: detect if we're only asking A or B, when just one card is inserted

View File

@ -77,6 +77,7 @@ def derive_bip85_secret(goto_home, press_select, pick_menu_item, cap_story, ente
seed = Mnemonic.to_seed(' '.join(got))
node = BIP32Node.from_master_secret(seed)
assert node.fingerprint().hex().upper() in title
assert "(6) to type words over USB" in story
elif 'XPRV' in mode:
assert 'Derived XPRV:' in story
@ -87,12 +88,14 @@ def derive_bip85_secret(goto_home, press_select, pick_menu_item, cap_story, ente
node = BIP32Node.from_hwif(story.split("\n\n")[0].split("\n")[-1])
assert node.fingerprint().hex().upper() in title
assert "(6) to type xprv over USB" in story
elif 'WIF' in mode:
assert 'WIF (privkey)' in story
assert f"m/83696968h/2h/{index}h" in story
if expect:
assert expect in story
assert "(6) to type wif over USB" in story
elif 'bytes hex' in mode:
width = int(mode.split('-')[0])
@ -101,13 +104,14 @@ def derive_bip85_secret(goto_home, press_select, pick_menu_item, cap_story, ente
assert f"m/83696968h/128169h/{width}h/{index}h" in story
if expect:
assert expect in story
assert "(6) to type hex over USB" in story
elif 'Passwords' == mode:
assert "Password:" in story
assert f"m/83696968h/707764h/21h/{index}h" in story
if expect:
assert expect in story
assert "(0) to type password over USB" in story
assert "(6) to type pw over USB" in story
else:
raise ValueError(mode)