diff --git a/shared/actions.py b/shared/actions.py index d45d9600..a75384ce 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -991,14 +991,13 @@ def make_top_menu(): elif pa.is_blank(): # let them play a little before picking a PIN first time m = MenuSystem(VirginSystem, should_cont=lambda: pa.is_blank()) - elif pa.hobbled_mode: - # let them do a few things, but not all the things. - m = MenuSystem(HobbledTopMenu) else: assert pa.is_successful(), "nonblank but wrong pin" if pa.has_secrets(): - _cls = NormalSystem[:] + # let them do a few things, but not all the things, when "hobbled" + _cls = HobbledTopMenu[:] if pa.hobbled_mode else NormalSystem[:] + if pa.tmp_value or settings.get("hmx", False): active_xfp = settings.get("xfp", 0) sl, sr = ("[", "]") if pa.tmp_value else ("<", ">") diff --git a/shared/ccc.py b/shared/ccc.py index 22225653..ed7f3fa4 100644 --- a/shared/ccc.py +++ b/shared/ccc.py @@ -1194,7 +1194,7 @@ class SSSPConfigMenu(MenuSystem): menu=lambda *a: SpendingPolicyMenu.be_a_submenu(SSSPFeature.get_policy())), SSSPCheckedMenuItem('Word Check', 'words', 'To change Spending Policy, in addition to special PIN, you must provide the first and last seed words.'), SSSPCheckedMenuItem('Allow Notes', 'notes', 'Allow (read-only) access to secure notes and passwords? Otherwise, they are inaccessible.'), - SSSPCheckedMenuItem('Related Keys', 'okeys', 'Allow access to BIP-39 passphrase wallets based on master seed, or Seed Vault (if any). Same spending Policy applies to all.'), + SSSPCheckedMenuItem('Related Keys', 'okeys', 'Allow access to BIP-39 passphrase wallets based on master seed, and Seed Vault (read-only). Single Spending Policy applies to all.'), #MenuItem('Test Word Challenge', f=sssp_word_challenge), # XXX test only? ] diff --git a/shared/flow.py b/shared/flow.py index 3ddf6c43..17b0e179 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -519,6 +519,8 @@ HobbledAdvancedMenu = [ MenuItem('Export Wallet', menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt MenuItem('Teleport Multisig PSBT', predicate=qr_and_ms, f=kt_send_file_psbt), MenuItem("View Identity", f=view_ident), + MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu, + predicate=lambda: sssp_spending_policy('okeys')), MenuItem('Paper Wallets', f=make_paper_wallet), MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC), MenuItem("Destroy Seed", f=clear_seed), diff --git a/shared/seed.py b/shared/seed.py index c266cbc2..55f0ae3c 100644 --- a/shared/seed.py +++ b/shared/seed.py @@ -493,6 +493,10 @@ async def add_seed_to_vault(encoded, origin=None, label=None): if in_seed_vault(encoded): return + # stay "read only" in hobbled mode + if pa.hobbled_mode: + return + main_xfp = settings.master_get("xfp", 0) # parse encoded @@ -531,7 +535,7 @@ async def add_seed_to_vault(encoded, origin=None, label=None): async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='', is_restore=False, origin=None, label=None): # Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp. - if not is_restore: + if not is_restore and not pa.hobbled_mode: await add_seed_to_vault(encoded, origin=origin, label=label) dis.fullscreen("Wait...") @@ -1034,11 +1038,11 @@ class SeedVaultMenu(MenuSystem): seeds = list(seed_vault_iter()) if not seeds: - assert not pa.hobbled_mode rv.append(MenuItem('(none saved yet)')) - if pa.tmp_value: - rv.append(add_current_tmp) - rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu)) + if not pa.hobbled_mode: + if pa.tmp_value: + rv.append(add_current_tmp) + rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu)) else: wipe_if_deltamode() @@ -1162,7 +1166,7 @@ class EphemeralSeedMenu(MenuSystem): ] rv = [ - MenuItem("Generate Words", menu=gen_ephemeral_menu), + MenuItem("Generate Words", menu=gen_ephemeral_menu, predicate=not pa.hobbled_mode), MenuItem('Import from QR Scan', predicate=version.has_qr, shortcut=KEY_QR, f=scan_any_qr, arg=(True, True)), MenuItem("Import Words", menu=import_ephemeral_menu), @@ -1175,7 +1179,6 @@ class EphemeralSeedMenu(MenuSystem): async def make_ephemeral_seed_menu(*a): - assert not pa.hobbled_mode if (not pa.tmp_value) and (not settings.master_get("seedvault", False)): # force a warning on them, unless they are already doing it. diff --git a/testing/test_ephemeral.py b/testing/test_ephemeral.py index db9764b9..87a1f383 100644 --- a/testing/test_ephemeral.py +++ b/testing/test_ephemeral.py @@ -221,10 +221,14 @@ def restore_main_seed(goto_home, pick_menu_item, cap_story, cap_menu, @pytest.fixture def confirm_tmp_seed(need_keypress, cap_story, press_select): - def doit(seedvault=False, expect_xfp=None): + def doit(seedvault=False, expect_xfp=None, check_sv_not_offered=False): time.sleep(0.3) title, story = cap_story() + + if check_sv_not_offered: + assert "to store temporary seed into Seed Vault" not in story + if "Press (1) to store temporary seed into Seed Vault" in story: if seedvault: need_keypress("1") # store it diff --git a/testing/test_hobble.py b/testing/test_hobble.py index 79f6eef9..f55d8126 100644 --- a/testing/test_hobble.py +++ b/testing/test_hobble.py @@ -11,15 +11,12 @@ from bbqr import join_qrs from charcodes import KEY_QR, KEY_NFC from base64 import b32encode from constants import * -from test_ephemeral import SEEDVAULT_TEST_DATA -from test_notes import need_some_notes +from test_ephemeral import SEEDVAULT_TEST_DATA, WORDLISTS +from test_ephemeral import confirm_tmp_seed, verify_ephemeral_secret_ui +from test_ux import word_menu_entry -'''TODO - -When hobbled... - -- temp seeds are read-only: no create, no rename, etc. -- seed vault can be accessed tho +''' +TODO -- When hobbled... - login sequence 1) system has lgto value: should get bypass pin, main pin, delay, then main pin again @@ -128,6 +125,9 @@ def test_menu_contents(set_hobble, pick_menu_item, cap_menu, en_okeys, en_notes, if en_nfc: adv_expect.add('NFC Tools') + if en_okeys: + adv_expect.add('Temporary Seed') + m = cap_menu() assert set(m) == adv_expect, "Adv menu wrong" @@ -209,7 +209,7 @@ def test_h_seedvault(sv_empty, set_hobble, pick_menu_item, cap_menu, settings_se else: settings_set('seeds', []) xfp, enc = SEEDVAULT_TEST_DATA[0][0:2] - settings_set("seeds", [(xfp, '80'+enc, f"Menu Label", "meta")]) + settings_set("seeds", [(xfp, '80'+enc, f"Menu Label", "meta-source")]) set_hobble(True, {'okeys'}) @@ -228,7 +228,7 @@ def test_h_seedvault(sv_empty, set_hobble, pick_menu_item, cap_menu, settings_se pick_menu_item(m[0]) title, story = cap_story() - assert 'Origin:\nmeta' in story + assert 'Origin:\nmeta-source' in story press_cancel() pick_menu_item('Use This Seed') @@ -263,7 +263,97 @@ def test_h_seedvault(sv_empty, set_hobble, pick_menu_item, cap_menu, settings_se m = cap_menu() assert 'Seed Vault' not in m -# BIP-39 passphrases +@pytest.mark.parametrize('mode', [ 'words', 'qr', 'xprv', 'tapsigner', 'coldcard' ]) +def test_h_tempseeds(mode, set_hobble, pick_menu_item, cap_menu, settings_set, is_q1, sim_exec, settings_remove, restore_main_seed, settings_get, press_cancel, press_select, cap_story, word_menu_entry, confirm_tmp_seed, verify_ephemeral_secret_ui, scan_a_qr, tapsigner_encrypted_backup, need_keypress, enter_hex, open_microsd): + ''' + - can import and use a key for signing + - NOT offered chance to save into seedvault + ''' + if not is_q1 and mode == 'qr': return + settings_set('seedvault', True) + settings_set('seeds', []) + + set_hobble(True, {'okeys'}) + pick_menu_item("Advanced/Tools") + pick_menu_item('Temporary Seed') + + m = cap_menu() + assert 'Generate Words' not in m + assert all(i.startswith("Import ") or i.endswith(' Backup') for i in m), m + + words, expect_xfp = WORDLISTS[12] + + if mode == 'words': + # just quick tests here, not in-depth + # - from test_ephemeral_seed_import_words() + pick_menu_item("Import Words") + pick_menu_item(f"12 Words") + time.sleep(0.1) + word_menu_entry(words.split()) + elif mode == 'qr': + pick_menu_item("Import from QR Scan") + val = ' '.join(words.split()).upper() + scan_a_qr(val) + time.sleep(0.1) + elif mode == 'tapsigner': + # like test_ephemeral_seed_import_tapsigner() + fname, backup_key_hex, node = tapsigner_encrypted_backup('sd', testnet=True) + expect_xfp = node.fingerprint().hex().upper() + pick_menu_item("Tapsigner Backup") + time.sleep(0.1) + need_keypress('1') + time.sleep(0.1) + pick_menu_item(fname) + + time.sleep(0.1) + _, story = cap_story() + assert "your TAPSIGNER" in story + + press_select() # yes I have backup key + enter_hex(backup_key_hex) + + elif mode == 'coldcard': + # like test_temporary_from_backup() + # - but skip making new bk file + fn = 'data/tip-index-famous-embark-tobacco-rice-attitude-interest-mask-random-amazing-initial.7z' + pw = fn[5:-3].split('-') + + contents = open(fn, 'rb').read() + with open_microsd('example.7z', 'wb') as fd: + fd.write(contents) + + pick_menu_item("Coldcard Backup") + time.sleep(0.1) + need_keypress('1') + time.sleep(0.1) + pick_menu_item('example.7z') + + word_menu_entry(pw, has_checksum=False) + + title, story = cap_story() + assert title == 'FAILED' + assert 'successfully tested recovery' in story + + press_select() + + return + elif mode == 'xprv': + # meh todo ... XPRV case from file + pick_menu_item("Import XPRV") + press_cancel() + return + else: + raise pytest.fail(f'{mode} not done') + + confirm_tmp_seed(seedvault=False, check_sv_not_offered=True) + + verify_ephemeral_secret_ui(expected_xfp=expect_xfp, mnemonic=None, seed_vault=False) + + pick_menu_item("Restore Master") + press_select() + +# TODO: BIP-39 passphrases and temp seeds +# TODO: test usb commands are blocked # EOF