diff --git a/shared/login.py b/shared/login.py index af8977e1..1e7acac7 100644 --- a/shared/login.py +++ b/shared/login.py @@ -86,11 +86,11 @@ class LoginUX: callgate.fast_wipe(False) # NOT REACHED - if ch in KEY_DELETE+KEY_LEFT: + if has_qwerty and ch in KEY_DELETE+KEY_LEFT: if self.pin: self.pin = self.pin[:-1] self.show_pin() - elif has_qwerty and self.pin_prefix: + elif self.pin_prefix: # trying to delete past start of second half, take them # to first part again. Q only ux_show_phish_words(dis, None) @@ -277,7 +277,6 @@ suffix break point is correct.\n\n''' if story: # give them background ch = await ux_show_story(story, title=title) - if ch == 'x': return None # first first one diff --git a/shared/notes.py b/shared/notes.py index 89124dee..be24bd1c 100644 --- a/shared/notes.py +++ b/shared/notes.py @@ -369,14 +369,14 @@ class PasswordContent(NoteContentBase): # Edit, also used for add new title = await ux_input_text(self.title, max_len=ONE_LINE, confirm_exit=False, - prompt='Title', placeholder='(required for menu)') + prompt='Title', placeholder='(required for menu)') if not title: return None # blank is OK for all other values user = await ux_input_text(self.user, max_len=ONE_LINE, scan_ok=True, confirm_exit=False, - prompt='Username', placeholder='(optional)') + prompt='Username', placeholder='(optional)') if user is None: user = self.user @@ -385,12 +385,12 @@ class PasswordContent(NoteContentBase): self.password = await get_a_password(self.password) site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False, - prompt='Website', placeholder='(optional)') + prompt='Website', placeholder='(optional)') if site is None: site = self.site misc = await ux_input_text(self.misc, max_len=None, scan_ok=True, confirm_exit=False, - prompt='More Notes', placeholder='(optional)') + prompt='More Notes', placeholder='(optional)') if misc is None: misc = self.misc @@ -459,13 +459,13 @@ class NoteContent(NoteContentBase): # Edit, also used for add new title = await ux_input_text(self.title, confirm_exit=False, max_len=CHARS_W-2, - prompt='Title', placeholder='(required for menu)') + prompt='Title', placeholder='(required for menu)') if not title: return misc = await ux_input_text(self.misc, confirm_exit=False, - max_len=None, scan_ok=True, - prompt='Your Notes', placeholder='(freeform text)') + max_len=None, scan_ok=True, + prompt='Your Notes', placeholder='(freeform text)') if misc is None: misc = self.misc diff --git a/shared/seed.py b/shared/seed.py index 8f89856e..56f8c615 100644 --- a/shared/seed.py +++ b/shared/seed.py @@ -349,12 +349,12 @@ async def add_dice_rolls(count, seed, judge_them, nwords=None, enforce=False): # this is slow enough to see md.update(ch) - elif ch == KEY_CANCEL: + elif ch in KEY_CANCEL+"x": # Because the change (roll) has already been applied, # only let them abort if it's early still if count < 10 and judge_them: return 0, seed - elif ch == KEY_ENTER: + elif ch in KEY_ENTER+"y": if count < threshold and judge_them: if not count: return 0, seed @@ -1126,7 +1126,7 @@ OK to continue or press (2) to hide this message forever. if version.has_qwerty and not PassphraseSaver.has_file(): # no need for any menus if Q and no card present pp = await ux_input_text('', prompt="Your BIP-39 Passphrase", - b39_complete=True, scan_ok=True, max_len=100) + b39_complete=True, scan_ok=True, max_len=100) if not pp: return await apply_pass_value(pp) diff --git a/shared/tapsigner.py b/shared/tapsigner.py index 76e6eae9..97323b9b 100644 --- a/shared/tapsigner.py +++ b/shared/tapsigner.py @@ -88,7 +88,8 @@ async def import_tapsigner_backup_file(_1, _2, item): while True: backup_key = await ux_input_text("", confirm_exit=False, hex_only=True, - min_len=32, max_len=32, prompt='Backup Password (32 hex digits)') + min_len=32, max_len=32, + prompt='Backup Password (32 hex digits)') if backup_key is None: return diff --git a/shared/ux.py b/shared/ux.py index c617a254..382722ce 100644 --- a/shared/ux.py +++ b/shared/ux.py @@ -347,10 +347,10 @@ def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False): from version import has_qwerty, num_sd_slots, has_qr from glob import NFC, VD - prompt, escape = None, KEY_CANCEL + prompt, escape = None, KEY_CANCEL+"x" if (NFC or VD) or num_sd_slots>1: - if slot_b_only: + if slot_b_only and (num_sd_slots>1): prompt = "Press (B) to import %s from lower slot SD Card" % title escape += "b" else: diff --git a/shared/ux_mk4.py b/shared/ux_mk4.py index f784ad51..dac8bf51 100644 --- a/shared/ux_mk4.py +++ b/shared/ux_mk4.py @@ -287,7 +287,10 @@ async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100, min_ ch = await press.wait() if ch == 'y': if len(pw) < min_len: - # enforce a minimum length, better: say so. + ch = await ux_show_story('Need %d characters at least. Press OK ' + 'to continue X to exit.' % min_len, escape="xy", + strict_escape=True) + if ch == "x": return continue return str(pw, 'ascii') elif ch == 'x': diff --git a/shared/xor_seed.py b/shared/xor_seed.py index 9761a3b8..aa147eb0 100644 --- a/shared/xor_seed.py +++ b/shared/xor_seed.py @@ -135,7 +135,6 @@ async def xor_all_done(new_words): num_parts = len(import_xor_parts) enc_parts = [bip39.a2b_words(w) for w in import_xor_parts] seed = xor(*enc_parts) - num_parts = len(import_xor_parts) msg = "You've entered %d parts so far.\n\n" % num_parts if num_parts >= 2: @@ -151,7 +150,6 @@ async def xor_all_done(new_words): msg += "Press (1) to enter next list of words, or (2) if done with all words." ch = await ux_show_story(msg, strict_escape=True, escape='12x'+KEY_CANCEL, sensitive=True) - if ch == 'x': # give up import_xor_parts.clear() # concern: we are contaminated w/ secrets @@ -164,7 +162,7 @@ async def xor_all_done(new_words): await seed_word_entry("Part %s Words" % chr(65+len(import_xor_parts)), target_words, done_cb=xor_all_done) else: - nxt = XORWordNestMenu(num_words=target_words) + nxt = XORWordNestMenu(num_words=target_words, done_cb=xor_all_done) the_ux.push(nxt) elif ch == '2': diff --git a/testing/conftest.py b/testing/conftest.py index 268d6407..df794f84 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -127,7 +127,7 @@ def sim_card_ejected(sim_exec, is_simulator): return # see unix/frozen-modules/pyb.py class SDCard - cmd = f'import pyb; pyb.SDCard.ejected={ejected}; RV.write("ok")' + cmd = f'import files; files.CardSlot.sd_detect = lambda: int({ejected}); RV.write("ok")' assert sim_exec(cmd) == 'ok' yield doit @@ -601,7 +601,7 @@ def cap_screen_qr(cap_image): def get_pp_sofar(sim_exec): # get entry value for bip39 passphrase def doit(): - resp = sim_exec('import seed; RV.write(seed.pp_sofar)') + resp = sim_exec('import seed; RV.write(seed.PassphraseMenu.pp_sofar)') assert 'Error' not in resp return resp @@ -675,7 +675,7 @@ def press_right(need_keypress, has_qwerty): return doit @pytest.fixture -def goto_home(cap_menu, press_cancel, press_select, pick_menu_item, has_qwerty, cap_screen): +def goto_home(cap_menu, press_cancel, press_select, pick_menu_item, cap_screen): def doit(): # get to top, force a redraw @@ -685,18 +685,19 @@ def goto_home(cap_menu, press_cancel, press_select, pick_menu_item, has_qwerty, m = cap_menu() + if 'CANCEL' in m: + # special case to get out of passphrase menu + pick_menu_item('CANCEL') + time.sleep(.01) + if "Are you SURE ?" in cap_screen(): + press_select() + chk = cap_screen() if m[0] not in chk: # menu vs. screen wrong ... happens if looking at a story, not a menu press_cancel() continue - if 'CANCEL' in m: - # special case to get out of passphrase menu - pick_menu_item('CANCEL') - time.sleep(.01) - press_select() - if m[0] in { 'New Seed Words', 'Ready To Sign'}: break if len(m) > 1 and (m[1] == "Ready To Sign") and (m[0][0] == "["): diff --git a/testing/devtest/set_seed.py b/testing/devtest/set_seed.py index 9a645a46..6b2be19b 100644 --- a/testing/devtest/set_seed.py +++ b/testing/devtest/set_seed.py @@ -6,7 +6,7 @@ import stash, chains from pincodes import pa from glob import settings import stash -from seed import set_seed_value +from seed import set_seed_value, PassphraseMenu from utils import xfp2str from actions import goto_top_menu from nvstore import SettingsObject @@ -19,6 +19,7 @@ settings.current = sim_defaults import main pa.tmp_value = None +PassphraseMenu.pp_sofar = '' SettingsObject.master_sv_data = {} SettingsObject.master_nvram_key = None set_seed_value(main.WORDS) diff --git a/testing/test_change_pins.py b/testing/test_change_pins.py index e4eb6747..a1e481ef 100644 --- a/testing/test_change_pins.py +++ b/testing/test_change_pins.py @@ -104,7 +104,7 @@ def my_enter_pin(cap_screen, need_keypress, is_q1, press_right, press_select): scr = cap_screen().split('\n') assert scr[-1] == 'Enter rest of PIN' - press_select() + press_select() time.sleep(0.1) return title, words @@ -115,7 +115,7 @@ def my_enter_pin(cap_screen, need_keypress, is_q1, press_right, press_select): @pytest.fixture def change_pin(cap_screen, cap_story, cap_menu, press_select, my_enter_pin, press_cancel): def doit(old_pin, new_pin, hdr_text, expect_fail=None): - # use standard menus and UX to change a PIN + # use standard menus and UX to change a PIN title, story = cap_story() assert title == hdr_text assert "changing the main PIN used to unlock your Coldcard" in story diff --git a/testing/test_ephemeral.py b/testing/test_ephemeral.py index 6f31623a..90598a53 100644 --- a/testing/test_ephemeral.py +++ b/testing/test_ephemeral.py @@ -608,7 +608,7 @@ def test_ephemeral_seed_import_tapsigner(way, testnet, pick_menu_item, cap_story def test_ephemeral_seed_import_tapsigner_fail(pick_menu_item, cap_story, fail, cap_screen, need_keypress, reset_seed_words, enter_hex, tapsigner_encrypted_backup, goto_eph_seed_menu, - microsd_path, ephemeral_seed_disabled, is_q1, + microsd_path, ephemeral_seed_disabled, settings_set, press_select, press_cancel): @@ -648,8 +648,8 @@ def test_ephemeral_seed_import_tapsigner_fail(pick_menu_item, cap_story, fail, c enter_hex(backup_key_hex) time.sleep(0.3) - if fail == "key_len" and is_q1: - assert "Need 32 char" in cap_screen() + if fail == "key_len": + assert "Need 32" in cap_screen() press_cancel() return @@ -1043,8 +1043,9 @@ def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item # first entry again press_select() pick_menu_item("Rename") - for _ in range(11): + for _ in range(10 if is_q1 else 9): press_delete() + if is_q1: do_keypresses("AA") else: @@ -1054,7 +1055,7 @@ def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item # name changed to AA press_select() - + time.sleep(.1) m = cap_menu() assert m[0] == "AA" assert "Rename" in m @@ -1083,7 +1084,7 @@ def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item assert "Delete" in m pick_menu_item("Rename") - for _ in range(11): + for _ in range(10 if is_q1 else 9): press_delete() if is_q1: diff --git a/testing/test_hsm.py b/testing/test_hsm.py index c273e91c..e3010dfc 100644 --- a/testing/test_hsm.py +++ b/testing/test_hsm.py @@ -71,7 +71,7 @@ def compute_policy_hash(policy): if type_ == Deriv: rv = [] for orig in value or []: - rv.append(orig if orig in ["any", "p2sh"] else orig.replace('p', "'").replace('h', "'")) + rv.append(orig if orig in ["any", "p2sh"] else orig.replace('p', "h").replace("'", 'h')) elif type_ == WhitelistOpts: rv = OrderedDict() rv["mode"] = value.get("mode", "BASIC") @@ -165,17 +165,17 @@ def hsm_reset(dev, sim_exec): (DICT(boot_to_hsm='123123'), 'Boot to HSM enabled'), # msg signing - (DICT(msg_paths=["m/1'/2p/3H"]), "m/1'/2'/3'"), + (DICT(msg_paths=["m/1'/2p/3H"]), "m/1h/2h/3h"), (DICT(msg_paths=["m/1", "m/2"]), "m/1 OR m/2"), (DICT(msg_paths=["any"]), "(any path)"), # data sharing - (DICT(share_addrs=["m/1'/2p/3H"]), ['Address values values will be shared', "m/1'/2'/3'"]), + (DICT(share_addrs=["m/1'/2p/3H"]), ['Address values values will be shared', "m/1h/2h/3h"]), (DICT(share_addrs=["m/1", "m/2"]), ['Address values values will be shared', "m/1 OR m/2"]), (DICT(share_addrs=["any"]), ['Address values values will be shared', "(any path)"]), (DICT(share_addrs=["p2sh", "any"]), ['Address values values will be shared', "(any P2SH)", "(any path"]), - (DICT(share_xpubs=["m/1'/2p/3H"]), ['XPUB values will be shared', "m/1'/2'/3'"]), + (DICT(share_xpubs=["m/1'/2p/3H"]), ['XPUB values will be shared', "m/1h/2h/3h"]), (DICT(share_xpubs=["m/1", "m/2"]), ['XPUB values will be shared', "m/1 OR m/2"]), (DICT(share_xpubs=["any"]), ['XPUB values will be shared', "(any path)"]), @@ -898,7 +898,7 @@ def test_multiple_signings_multisig(cc_first, M_N, dev, quick_start_hsm, attempt_psbt(psbt) -def test_sign_msg_good(quick_start_hsm, change_hsm, attempt_msg_sign, addr_fmt=AF_CLASSIC): +def test_sign_msg_good(quick_start_hsm, change_hsm, attempt_msg_sign): # message signing, but only at certain derivations permit = ['m/73', "m/*'", 'm/1p/3h/4/5/6/7' ] block = ['m', 'm/72', permit[-1][:-2]] @@ -907,15 +907,14 @@ def test_sign_msg_good(quick_start_hsm, change_hsm, attempt_msg_sign, addr_fmt=A policy = DICT(msg_paths=permit) quick_start_hsm(policy) - if 1: - for addr_fmt in [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH]: + for addr_fmt in [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH]: - for p in permit: - p = p.replace('*', '75333') - attempt_msg_sign(None, msg, p, addr_fmt=addr_fmt) + for p in permit: + p = p.replace('*', '75333') + attempt_msg_sign(None, msg, p, addr_fmt=addr_fmt) - for p in block: - attempt_msg_sign('not enabled for that path', msg, p, addr_fmt=addr_fmt) + for p in block: + attempt_msg_sign('not enabled for that path', msg, p, addr_fmt=addr_fmt) policy = DICT(msg_paths=['any']) change_hsm(policy) diff --git a/testing/test_se2.py b/testing/test_se2.py index 34d58acf..d08fa42a 100644 --- a/testing/test_se2.py +++ b/testing/test_se2.py @@ -589,7 +589,7 @@ def test_ux_duress_choices(with_wipe, subchoice, expect, xflags, xargs, words12, words = seed_story_to_words(story) else: ln = story.split('\n') - assert ln[0] == 'Seed words (24):' + assert ln[0] == ('Seed words (12):' if words12 else 'Seed words (24):') words = [i[4:] for i in ln[1:25]] seed = Mnemonic.to_seed(' '.join(words), passphrase='') @@ -699,7 +699,7 @@ from test_change_pins import change_pin, goto_pin_options, my_enter_pin def force_main_pin(change_pin, goto_pin_options, pick_menu_item, repl): # make main-pin match needs def doit(want_pin, expect_fail=None): - pin_b4 = repl.eval('pa.pin').decode('ascii') + pin_b4 = repl.eval('pa.pin').decode('ascii') if pin_b4 == want_pin: assert not expect_fail return diff --git a/testing/test_ux.py b/testing/test_ux.py index 336045b8..d0237003 100644 --- a/testing/test_ux.py +++ b/testing/test_ux.py @@ -16,7 +16,7 @@ def test_get_secrets(get_secrets, master_xpub): assert v['xpub'] == master_xpub def test_home_menu(cap_menu, cap_story, cap_screen, need_keypress, reset_seed_words, - press_select, press_cancel, is_q1): + press_select, press_cancel, press_down, is_q1): reset_seed_words() # get to top, force a redraw press_cancel() @@ -41,7 +41,17 @@ def test_home_menu(cap_menu, cap_story, cap_screen, need_keypress, reset_seed_wo # check 4 lines of menu are shown right scr = cap_screen().rstrip() chk = '\n'.join(m) - assert scr == chk + if is_q1: + assert scr == chk + else: + # does not fit to single screen on mk4 + assert scr in chk + # go down to the bottom + for i in range(6): + press_down() + + scr = cap_screen().rstrip() + assert scr in chk # pick first item, expect a story need_keypress('0') @@ -120,7 +130,8 @@ def word_menu_entry(cap_menu, pick_menu_item, is_q1, do_keypresses, cap_screen): assert which, "cant find: " + word pick_menu_item(which) - if '-' not in which: break + if '-' not in which: + break return doit @@ -615,7 +626,6 @@ def test_bip39_complex(target, goto_home, pick_menu_item, cap_story, enter_complex(target, apply=True) press_select() - # import pdb;pdb.set_trace() verify_ephemeral_secret_ui(xpub=expect.hwif(), is_b39pw=True)