show effective seed in View Seed Words
What changed, and why it matters
This commit changes what the COLDCARD wallet displays on its 'View Seed Words' screen when a BIP-39 passphrase is active. Previously it showed the original 24 seed words plus a masked passphrase and the derived extended private key. Now it shows only the effective extended private key (and a note that a passphrase is in effect), hiding the underlying seed words. This is a deliberate UX/security hardening change, not a fix for an exploitable vulnerability.
No immediate security response required; treat as a normal UX hardening improvement. Verify the updated tests pass and that users understand the new screen behavior.
Security signals we found
UI change reduces exposure of master seed words when a BIP-39 passphrase is in use
Effective secret shown is now the derived extended private key rather than the raw mnemonic
No cryptographic, memory-safety, or authentication changes
No CVE, advisory, or researcher attribution present in commit
Evidence from the diff
The patch modifies shared/actions.py so that render_master_secrets() no longer appends the BIP-39 passphrase indicator or the seed+passphrase extended key when displaying words. view_seed_words() now uses a single SensitiveValues context (no bypass_tmp) and, when a passphrase is active, prefixes the output with ‘BIP-39 Passphrase in effect’ and renders only the effective xprv. Tests are updated to assert that the seed words and passphrase text are absent and that the xprv is shown instead.
Changed components
COLDCARD firmware shared/actions.pyView Seed Words user flowBIP-39 passphrase wallet displayInspect captured patch +50 / −50
### releases/Next-ChangeLog.md
@@ -23,6 +23,7 @@ This lists the new changes that have not yet been published in a normal release.
download (signed txn, visualization, backup), require an encrypted session,
and are invalidated by any upload, newly staged transaction, or new session.
Thanks to [@drk1wi](https://github.com/drk1wi).
+- Change: When a BIP-39 passphrase is active, View Seed Words now shows only the effective extended private key instead of the underlying seed words.
# Mk Specific Changes
### shared/actions.py
@@ -613,7 +613,7 @@ async def clear_seed(*a):
def render_master_secrets(mode, raw, node):
# Render list of words, or XPRV / master secret to text.
- import stash, chains
+ import chains
c = chains.current_chain()
qr_alnum = False
@@ -635,12 +635,6 @@ def render_master_secrets(mode, raw, node):
msg += ux_render_words(words)
- if stash.bip39_passphrase:
- msg += '\n\nBIP-39 Passphrase:\n *****'
- if node:
- msg += '\n\nSeed+Passphrase:\n%s' % c.serialize_private(node)
-
-
elif mode == 'xprv':
title = "Extended Private Key" if version.has_qwerty else None
msg = c.serialize_private(node)
@@ -657,36 +651,29 @@ def render_master_secrets(mode, raw, node):
return title, msg, qr, qr_alnum
async def view_seed_words(*a):
- if not await ux_confirm('The next screen will show the seed words'
- ' (and if defined, your BIP-39 passphrase).'
- '\n\nAnyone with knowledge of those words '
+ if not await ux_confirm('The next screen will show the secret seed words'
+ ' (or extended private key).'
+ '\n\nAnyone with knowledge of the secret '
'can control all funds in this wallet.'):
return
import stash
from glob import dis, NFC
dis.fullscreen("Wait...")
- dis.busy_bar(True)
- # preserve old UI where we show words + passphrase
- # instead of just calculated seed + passphrase = extended privkey
- # new: calculated xprv is now also shown for BIP39 passphrase wallet
- raw = mode = None
- if stash.bip39_passphrase:
- # get main secret - bypass tmp
- with stash.SensitiveValues(bypass_tmp=True, enforce_delta=True) as sv:
- assert sv.mode == "words"
- raw = sv.raw[:]
- mode = sv.mode
+ # CHANGED: old UI where we show words + passphrase
+ # instead just calculated seed + passphrase = extended privkey for all BIP39 passphrase wallets
- stash.SensitiveValues.clear_cache()
+ with stash.SensitiveValues(enforce_delta=True) as sv:
+ msg = ""
+ if stash.bip39_passphrase:
+ # on the top of the page - so visible on Mk4/5
+ msg += "BIP-39 Passphrase in effect\n\n"
+
+ title, sub_msg, qr, qr_alnum = render_master_secrets(sv.mode, sv.raw, sv.node)
+ msg += sub_msg
- with stash.SensitiveValues(bypass_tmp=False, enforce_delta=True) as sv:
- dis.busy_bar(False)
- title, msg, qr, qr_alnum = render_master_secrets(mode or sv.mode,
- raw or sv.raw,
- sv.node)
esc = "1"
if not version.has_qwerty:
msg += '\n\nPress (1) to view as QR Code'
@@ -709,7 +696,6 @@ async def view_seed_words(*a):
stash.blank_object(qr)
stash.blank_object(msg)
- stash.blank_object(raw)
async def export_seedqr(*a):
# see standard: <https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md>
### testing/test_bip39pw.py
@@ -363,7 +363,7 @@ def test_bip39pass_on_ephemeral_seed(generate_ephemeral_words, import_ephemeral_
enter_complex(passphrase, apply=True)
tmp_seed = Mnemonic.to_seed(" ".join(sec), passphrase=passphrase)
- tmp_node = BIP32Node.from_master_secret(tmp_seed)
+ tmp_node = BIP32Node.from_master_secret(tmp_seed, netcode="XTN")
tmp_fp = tmp_node.fingerprint().hex().upper()
time.sleep(.2)
@@ -391,6 +391,24 @@ def test_bip39pass_on_ephemeral_seed(generate_ephemeral_words, import_ephemeral_
else:
press_select() # do not store
+ goto_home()
+ pick_menu_item("Advanced/Tools")
+ pick_menu_item("Danger Zone")
+ pick_menu_item("Seed Functions")
+ pick_menu_item("View Seed Words")
+ time.sleep(.1)
+ _, story = cap_story()
+ assert "secret seed words" in story
+ press_select()
+ time.sleep(.1)
+ _, story = cap_story()
+ assert tmp_node.hwif(as_private=True) in story
+ assert story.startswith("BIP-39 Passphrase in effect\n\n")
+ assert passphrase not in story
+ assert "Seed words" not in story
+ press_select()
+ goto_home()
+
if seed_vault:
# check correct meta in seed vault
pick_menu_item("Seed Vault")
### testing/test_ux.py
@@ -493,7 +493,12 @@ def test_show_seed(mode, b39_word, goto_home, pick_menu_item, cap_story, need_ke
reset_seed_words()
if mode == 'words':
set_bip39_pw(b39_word, reset=False)
- words = simulator_fixed_words.split(" ")
+ if b39_word:
+ seed = Mnemonic.to_seed(simulator_fixed_words, passphrase=b39_word)
+ node = BIP32Node.from_master_secret(seed, netcode="XTN")
+ expect = node.hwif(as_private=True)
+ else:
+ words = simulator_fixed_words.split(" ")
else:
if b39_word: return
@@ -519,6 +524,8 @@ def test_show_seed(mode, b39_word, goto_home, pick_menu_item, cap_story, need_ke
title, body = cap_story()
where = title if is_q1 else body
assert 'Are you SURE' in where
+ assert 'secret seed words' in body
+ assert 'or extended private key' in body
assert 'can control all funds' in body
press_select() # skip warning
time.sleep(0.01)
@@ -527,7 +534,7 @@ def test_show_seed(mode, b39_word, goto_home, pick_menu_item, cap_story, need_ke
if not is_q1:
assert title == 'NO-TITLE'
- if mode == 'words':
+ if mode == 'words' and not b39_word:
assert '24' in (title if is_q1 else body)
lines = body.split('\n')
@@ -536,30 +543,18 @@ def test_show_seed(mode, b39_word, goto_home, pick_menu_item, cap_story, need_ke
else:
assert lines[1:25] == ['%2d: %s' % (n+1, w) for n,w in enumerate(words)]
- if b39_word:
- if is_q1:
- assert lines[9] == 'BIP-39 Passphrase:'
- assert "*" in lines[10]
- assert "Seed+Passphrase" in lines[12]
- ek = lines[13]
- else:
- assert lines[26] == 'BIP-39 Passphrase:'
- assert "*" in lines[27]
- assert "Seed+Passphrase" in lines[29]
- ek = lines[30]
-
- seed = Mnemonic.to_seed(simulator_fixed_words, passphrase=b39_word)
- expect = BIP32Node.from_master_secret(seed, netcode="XTN")
- esk = expect.hwif(as_private=True)
- assert esk == ek
- else:
- assert "BIP-39 Passphrase" not in body
-
+ assert "BIP-39 Passphrase" not in body
qr_expect = ' '.join(w[0:4].upper() for w in words)
else:
assert expect in body
qr_expect = expect
+ if b39_word:
+ assert body.startswith("BIP-39 Passphrase in effect\n\n")
+ assert b39_word not in body
+ assert "Seed words" not in body
+ else:
+ assert "BIP-39 Passphrase" not in body
if not is_q1:
assert '(1) to view as QR Code' in bodyWhy this scored 22/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.