diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md index e70390e6..f7077e54 100644 --- a/releases/Next-ChangeLog.md +++ b/releases/Next-ChangeLog.md @@ -44,6 +44,8 @@ Spending policies for "Single Signers" adds new spending policy options: ## 1.3.4Q - 2025-09-2x +- Enhancement: Enter "forever calculator" mode when Q would otherwise be e-waste: after 13 + PIN failures, when device is bricked. - Bugfix: Correct line positioning when 24 seed words displayed. diff --git a/shared/calc.py b/shared/calc.py index 4c1e10ba..8e31e4a4 100644 --- a/shared/calc.py +++ b/shared/calc.py @@ -8,8 +8,9 @@ import utime, ngu, re from utils import B2A, word_wrap from ux_q1 import ux_input_text -async def login_repl(allow_login=True): +async def login_repl(): from glob import dis + from pincodes import pa NUM_LINES = 7 # 10 - title - 2 for prompt @@ -64,12 +65,11 @@ Example Commands: elif ln in ('help', 'cls', 'rand'): # no need for () for these commands ans = state[ln]() - elif allow_login and re_pin.match(ln) and (len(ln) <= 13): + elif pa.attempts_left and re_pin.match(ln) and (len(ln) <= 13): # try login m = re_pin.match(ln) ln = m.group(1)+ '-' + m.group(2) - print(ln) - from pincodes import pa + try: pa.setup(ln) ok = pa.login() @@ -83,7 +83,7 @@ Example Commands: else: ans = 'Error: ' + repr(exc.args) - elif allow_login and re_prefix.match(ln) and (len(ln) <= 7): + elif re_prefix.match(ln) and (len(ln) <= 7): # show words from pincodes import pa ans = pa.prefix_words(ln[:-1].encode()) diff --git a/shared/login.py b/shared/login.py index bd9636eb..97da5b25 100644 --- a/shared/login.py +++ b/shared/login.py @@ -181,14 +181,22 @@ class LoginUX: async def we_are_ewaste(self, num_fails): msg = '''After %d failed PIN attempts this Coldcard is locked forever. \ By design, there is no way to reset or recover the secure element, and its contents \ -are now forever inaccessible. +are now forever inaccessible.\n\n''' % num_fails -Restore your seed words onto a new Coldcard.''' % num_fails + if has_qwerty: + msg += 'Calculator mode starts now.' + else: + msg += 'Restore your seed words onto a new Coldcard.' while 1: ch = await ux_show_story(msg, title='I Am Brick!', escape='6') if ch == '6': break + if has_qwerty: + from calc import login_repl + await login_repl() + + async def confirm_attempt(self, attempts_left, value): ch = await ux_show_story('''You have %d attempts left before this Coldcard BRICKS \ diff --git a/shared/main.py b/shared/main.py index 87c5822c..afacb7e2 100644 --- a/shared/main.py +++ b/shared/main.py @@ -90,7 +90,7 @@ async def more_setup(): from pincodes import pa # check for bricked system early # bricked CC not going past this point - pa.enforce_brick() + await pa.enforce_brick() pa.setup(b'') # just to see where we stand. is_blank = pa.is_blank() diff --git a/shared/pincodes.py b/shared/pincodes.py index 863b8601..d2cea54f 100644 --- a/shared/pincodes.py +++ b/shared/pincodes.py @@ -130,9 +130,10 @@ class PinAttempt: # seed, we are not going to let them see it, nor sign things we dont like, etc. self.hobbled_mode = False - assert MAX_PIN_LEN == 32 # update FMT otherwise - assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1 - assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1 + #assert MAX_PIN_LEN == 32 # update FMT otherwise + #assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1 + #assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) \ + # == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1 def __repr__(self): return '' % ( @@ -535,11 +536,10 @@ class PinAttempt: # check for bricked system early if get_is_bricked(): try: - # regardless of settings.calc forever calculator after brickage - # for Q models fom version 5.X.X - if version.has_qwerty: + # regardless of settings, become a forever calculator after brickage. + while version.has_qwerty: from calc import login_repl - await login_repl(allow_login=False) + await login_repl() finally: # die right away if it's not going to work enter_dfu(3) diff --git a/unix/README.md b/unix/README.md index 481102dc..27e2575d 100644 --- a/unix/README.md +++ b/unix/README.md @@ -53,7 +53,7 @@ wallet (on testnet, always with the same seed). But there are other options: - `--deriv` => go to the Derive Entropy menu inside settings, also loads XPRV from BIP - `--secret 01abababab...` => directly set contents of SE secret, see SecretStash.encode() - `--eject` => pretend no (simulated) SD Card is inserted -- `--eff` => (mk4) wipe setttings at startup, use simulator defaults +- `--eff` => wipe setttings at startup, use simulator defaults, save nothing. - `--seq 1234yx34` => after start, enter those keypresses to get you to some submenu - `--seq 2ENTER` => (Q) press 2 then ENTER, does QR at startup - `--bootup-movie` => begin a movie on startup, to capture boot sequence @@ -61,6 +61,8 @@ wallet (on testnet, always with the same seed). But there are other options: - `--battery` => (Q) assume the USB cable is NOT connected (ie. on battery power) - `--early-usb` => start simulated USB interface even before user is login (useful for login testing) - `--segregate` => scroll down to `Running simulators in parallel` section +- `--bricked` => simulate a system w/ bricked SE1: no more pin tries, etc. +- `--fails N` => simulate N wrong PIN attempts before login, where (1 <= N <= 13) See `variant/sim_settings.py` for the details of settings-related options. diff --git a/unix/variant/ckcc.py b/unix/variant/ckcc.py index 99b46f3f..d634a3c9 100644 --- a/unix/variant/ckcc.py +++ b/unix/variant/ckcc.py @@ -98,6 +98,9 @@ def gate(method, buf_io, arg2): if method == 5: # are we a brick? No. + if '--bricked' in sys.argv: + # if SE1 has pairing secret rotated; wont be able to do much + return 1 return 0 if method == 6: diff --git a/unix/variant/sim_quickstart.py b/unix/variant/sim_quickstart.py index 476d8ab9..41459839 100644 --- a/unix/variant/sim_quickstart.py +++ b/unix/variant/sim_quickstart.py @@ -155,6 +155,7 @@ if '--enter' in sys.argv: # keep at end of file: extra enter to confirm something from above numpad.inject('y') + # not best place for this import hsm hsm.POLICY_FNAME = hsm.POLICY_FNAME.replace('/flash/', '') diff --git a/unix/variant/sim_settings.py b/unix/variant/sim_settings.py index 78235451..01392d8f 100644 --- a/unix/variant/sim_settings.py +++ b/unix/variant/sim_settings.py @@ -144,6 +144,13 @@ if '-g' in sys.argv: # do login.. but does not work if _skip_pin got saved into settings already sim_defaults.pop('_skip_pin', 0) +if '--fails' in sys.argv: + # fast-forward as if N PIN failures have already happened. + count = int(sys.argv[sys.argv.index('--fails') + 1]) + import ckcc + ckcc.SE_STATE.force_fails(count) + sim_defaults.pop('_skip_pin', 0) + if '--nick' in sys.argv: nick = sys.argv[sys.argv.index('--nick') + 1] sim_defaults['nick'] = nick