Wipe on duress-secret view/activate in Delta Mode
What changed, and why it matters
This commit fixes a security bug in the COLDCARD hardware wallet's 'Delta Mode' duress feature. Delta Mode is designed to make the device look normal while silently protecting secrets. Previously, a coercer could force someone to open the Trick PINs menu and either view the full seed words of a duress wallet, or activate that wallet, without triggering a wipe. This exposed the duress wallet's funds and weakened the user's deniability. The fix makes both actions trigger a secure wipe when in Delta Mode, matching how other secret-revealing flows already behaved. Browsing the menu still works so Delta Mode remains plausible.
Treat this as a security fix and include it in the next firmware release. Users relying on duress wallets and Delta Mode should upgrade once a release containing this commit is available. No immediate user action is described in the commit beyond applying the update.
Security signals we found
Missing authorization/safeguard in two secret-revealing flows
Duress/deniability feature bypass allowing secret disclosure under coercion
Addition of wipe_if_deltamode() guard to align with other secret-revealing flows
Regression test demonstrates pre-patch failure and post-patch pass
Evidence from the diff
In shared/trick_pins.py, two code paths in the Trick PINs menu now call wipe_if_deltamode() before revealing or applying a duress wallet secret: (1) duress_details() when the user presses (6) to view associated seed words/XPRV, and (2) activate_wallet() before loading the duress secret as the active temporary seed. The master seed is not directly exposed because duress wallets are derived via hardened derivation, but the duress wallet secret itself and any funds it protects were at risk. A regression test in testing/test_se2.py verifies that both paths now trigger a wipe in Delta Mode and that menu browsing still works.
Changed components
shared/trick_pins.py: activate_wallet()shared/trick_pins.py: duress_details()Delta Mode / Trick PINs duress wallet handlingInspect captured patch +70 / −0
### releases/Next-ChangeLog.md
@@ -32,6 +32,9 @@ This lists the new changes that have not yet been published in a normal release.
flash region.
- Security hardening: Remove the unused USB CDC/VCP serial interface from normal
operation and keyboard emulation.
+- Bugfix: In Delta Mode, wipe the seed if anyone tries to view or activate a duress
+ wallet's secret from the Trick PINs menu, instead of revealing it. Browsing the menu
+ itself still works, so Delta Mode continues to look like normal operation.
# Mk Specific Changes
### shared/trick_pins.py
@@ -808,6 +808,11 @@ async def activate_wallet(self, m, l, item):
b, slot = tp.get_by_pin(pin)
assert slot
+ # loading a duress wallet's secret is a secret-revealing action; in
+ # Delta Mode it would hand the coercer working keys, so wipe instead
+ from utils import wipe_if_deltamode
+ wipe_if_deltamode()
+
# TC_BLANK_WALLET here would be nice, but no support working w/ fake empty secret
# emulate stash.py encoding
@@ -888,6 +893,11 @@ async def duress_details(self, m, l, item):
ch = await ux_show_story(msg + '\n\nPress (6) to view associated secrets.', escape='6')
if ch != '6': return
+ # viewing the duress wallet's seed words/XPRV reveals a live secret;
+ # in Delta Mode it would hand the coercer the keys, so wipe instead
+ from utils import wipe_if_deltamode
+ wipe_if_deltamode()
+
b, s = tp.get_by_pin(pin)
if s is None:
title = None
### testing/test_se2.py
@@ -925,6 +925,63 @@ def test_deltamode_toggle(get_deltamode, set_deltamode):
assert get_deltamode() == False
+def test_deltamode_duress_secret_view_wipes(clear_all_tricks, new_trick_pin,
+ new_pin_confirmed, pick_menu_item, cap_story, need_keypress, press_select,
+ set_deltamode, get_deltamode, sim_exec, goto_trick_menu):
+ # In Delta Mode, browsing the Trick PINs menu is allowed (looks like normal
+ # operation), but attempting to *view* a duress wallet's seed or *activate* it must
+ # wipe the device rather than hand the coercer a live secret.
+ clear_all_tricks()
+
+ new_pin = '11-55'
+ new_trick_pin(new_pin, 'Duress Wallet', 'Goes directly to a specific duress wallet')
+ pick_menu_item('BIP-85 Wallet #1')
+ _, story = cap_story()
+ assert "functional 'duress' wallet" in story
+ press_select()
+ new_pin_confirmed(new_pin, 'BIP-85 Wallet #1', TC_WORD_WALLET, 1001)
+
+ # spy on the wipe (record the call on the fast_wipe referenced by the guard)
+ sim_exec('import callgate\n'
+ 'callgate._w=[0]\n'
+ 'def _spy(silent=True):\n'
+ ' callgate._w[0]+=1\n'
+ 'callgate._fw=callgate.fast_wipe\n'
+ 'callgate.fast_wipe=_spy\n'
+ 'RV.write("spy ok")')
+ set_deltamode(True)
+ assert get_deltamode() == True
+
+ def wipe_count():
+ return int(sim_exec('import callgate; RV.write(str(callgate._w[0]))'))
+
+ # -- attempt to view the duress seed words via the (6) viewer -> wipe, no secret shown
+ goto_trick_menu()
+ pick_menu_item(f'↳{new_pin}')
+ pick_menu_item('↳Duress Wallet')
+ _, story = cap_story()
+ assert 'Press (6) to view associated' in story
+ need_keypress('6')
+ time.sleep(.5)
+ # the wipe fired (with the real fast_wipe the device reboots before rendering the
+ # secret; the spy only skips the reboot so the test can continue)
+ assert wipe_count() == 1, 'viewing duress secret in delta mode did not wipe'
+
+ # -- attempt to activate the duress wallet -> wipe again, secret never applied
+ goto_trick_menu()
+ pick_menu_item(f'↳{new_pin}')
+ pick_menu_item('Activate Wallet')
+ _, story = cap_story()
+ assert 'This will temporarily load' in story
+ press_select()
+ time.sleep(.5)
+ assert wipe_count() == 2, 'activating duress wallet in delta mode did not wipe'
+
+ # restore real fast_wipe and leave delta mode
+ sim_exec('import callgate; callgate.fast_wipe=callgate._fw')
+ set_deltamode(False)
+
+
# TODO
# - make trick and do login, check arrives right state?
# - out of slotsWhy this scored 76/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.