USB send keystrokes for all BIP-85 secret types
What changed, and why it matters
This commit adds a new convenience feature: after deriving a secret using the BIP-85 standard, the COLDCARD can now type that secret out as USB keyboard keystrokes for all supported secret types (seed words, xprv, WIF, hex, password). Previously this USB typing was only available for password-type secrets. The change also cleans up how the on-screen prompt offers the '(6)' keystroke option. There is no direct evidence in the commit that this fixes a security bug; it appears to be a user-experience feature addition.
No immediate security action required. Reviewers may want to confirm that sending secrets as USB keystrokes requires the same user confirmation (single_send_keystrokes) as before and that the feature is only reachable through the existing authenticated BIP-85 menu flow.
Security signals we found
No security-relevant keywords in commit title or message
No CVE, advisory, or security-fix language present
Change is feature-oriented: expands USB keystroke typing to all BIP-85 secret types
Refactoring of prompt option mapping from hardcoded txid to generic key6
No input validation, buffer handling, or privilege changes observed
Evidence from the diff
The patch refactors export_prompt_builder() and import_export_prompt() in shared/ux.py to accept a generic key6 parameter instead of a txid-specific parameter, allowing any caller to offer a ‘(6)’ action. In shared/drv_entro.py, the BIP-85 derivation flow now sets key6 to ‘to type
Changed components
shared/ux.py - export/import prompt builder and prompt displayshared/drv_entro.py - BIP-85 derived secret export flowshared/auth.py - signing completion prompt (mechanical key6 usage)testing/test_drv_entro.py - updated test expectationsInspect captured patch +40 / −34
diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
index d8b3dc8..ad26563 100644
--- a/releases/Next-ChangeLog.md
+++ b/releases/Next-ChangeLog.md
@@ -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
diff --git a/shared/auth.py b/shared/auth.py
index 4e3ff43..53cec10 100644
--- a/shared/auth.py
+++ b/shared/auth.py
@@ -871,7 +871,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)
if ch == KEY_CANCEL:
UserAuthorizedAction.cleanup()
diff --git a/shared/drv_entro.py b/shared/drv_entro.py
index 8f52d94..7610767 100644
--- a/shared/drv_entro.py
+++ b/shared/drv_entro.py
@@ -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
diff --git a/shared/ux.py b/shared/ux.py
index 36646ee..5fffd42 100644
--- a/shared/ux.py
+++ b/shared/ux.py
@@ -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
diff --git a/testing/test_drv_entro.py b/testing/test_drv_entro.py
index 8daa4b3..fb5fde6 100644
--- a/testing/test_drv_entro.py
+++ b/testing/test_drv_entro.py
@@ -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)
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.