diff --git a/shared/login.py b/shared/login.py index 17ef498a..af8977e1 100644 --- a/shared/login.py +++ b/shared/login.py @@ -189,7 +189,7 @@ Restore your seed words onto a new Coldcard.''' % num_fails ch = await ux_show_story(msg, title='I Am Brick!', escape='6') if ch == '6': break - async def confirm_attempt(self, attempts_left, num_fails, value): + async def confirm_attempt(self, attempts_left, value): ch = await ux_show_story('''You have %d attempts left before this Coldcard BRICKS \ ITSELF FOREVER. @@ -224,7 +224,7 @@ Press OK to continue, X to stop for now. if pa.num_fails > 3: # they are approaching brickage, so warn them each attempt - await self.confirm_attempt(pa.attempts_left, pa.num_fails, pin) + await self.confirm_attempt(pa.attempts_left, pin) dis.fullscreen("Loading...") pa.setup(pin) diff --git a/testing/conftest.py b/testing/conftest.py index 4024bd83..6aac221e 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1801,10 +1801,11 @@ def load_export_and_verify_signature(microsd_path, virtdisk_path, verify_detache @pytest.fixture def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_text, nfc_read_json, - load_export_and_verify_signature, is_q1, press_cancel, press_select): + load_export_and_verify_signature, is_q1, press_cancel, press_select, readback_bbqr, + cap_screen_qr): def doit(way, label, is_json, sig_check=True, addr_fmt=AF_CLASSIC, ret_sig_addr=False, tail_check=None, sd_key=None, vdisk_key=None, nfc_key=None, ret_fname=False, - fpattern=None): + fpattern=None, qr_key=None): s_label = None if label == "Address summary": @@ -1814,6 +1815,7 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_ "sd": sd_key or "1", "vdisk": vdisk_key or "2", "nfc": nfc_key or (KEY_NFC if is_q1 else "3"), + "qr": qr_key or (KEY_QR if is_q1 else "4"), } time.sleep(0.2) title, story = cap_story() @@ -1834,6 +1836,23 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_ time.sleep(0.3) press_cancel() # exit NFC animation return nfc_export + elif way == "qr": + need_keypress(key_map["qr"]) + time.sleep(0.3) + try: + file_type, data = readback_bbqr() + if file_type == "J": + return json.loads(data) + elif file_type == "U": + return data + else: + raise NotImplementedError + except: + res = cap_screen_qr().decode('ascii') + try: + return json.loads(res) + except: + return res else: # virtual disk if f"({key_map['vdisk']}) to save to Virtual Disk" not in story: diff --git a/testing/login_settings_tests.py b/testing/login_settings_tests.py index 71d6d890..9d4d8735 100644 --- a/testing/login_settings_tests.py +++ b/testing/login_settings_tests.py @@ -1,10 +1,15 @@ # (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # to run it on both Mk4 and Q: -# python login_settings_tests.py; sleep 10; python --Q login_settings_tests.py +# pytest login_settings_tests.py; sleep 10; pytest --Q login_settings_tests.py +# +# or use test runner: +# python run_sim_tests --login +# +# python run_sim_tests --q1 --login -k countdown --pdb # import pytest, time, pdb -from charcodes import KEY_ENTER, KEY_DOWN, KEY_UP, KEY_HOME, KEY_DELETE +from charcodes import KEY_ENTER, KEY_DOWN, KEY_UP, KEY_HOME from ckcc_protocol.client import ColdcardDevice, CCProtocolPacker, CKCC_SIMULATOR_PATH from run_sim_tests import ColdcardSimulator, clean_sim_data @@ -149,7 +154,7 @@ def _set_kill_key(device, val, is_Q): if is_Q: assert "press this key at any point during login" in story else: - assert "press this key while the anti-phishing words are shown during login" in story + assert "press this key while the anti- phishing words are shown during login" in story assert ("Best if this does not match the first number" " of the second half of your PIN.") in story @@ -167,9 +172,11 @@ def _remap_pin(pin, key_map): remap_pin += ch return remap_pin -def _login(device, pin, is_Q, scrambled=False, mk4_kbtn=None): +def _login(device, pin, is_Q, scrambled=False, mk4_kbtn=None, num_failed=None): orig_pin = pin scr = _cap_screen(device) + if num_failed: + assert f"{num_failed} failures, {13-num_failed} tries left" in scr if is_Q: top = scr.split("\n")[0].split() is_scrambled = len(top) == 10 @@ -215,6 +222,7 @@ def _login(device, pin, is_Q, scrambled=False, mk4_kbtn=None): _need_keypress(device, ch) _press_select(device, is_Q) + @pytest.mark.parametrize("nick", [100*"$", "$", 10*"20"+ " "+"8080"+ " " + "XX"+ " "+ "YY"]) def test_set_nickname(nick, request): is_Q = request.config.getoption('--Q') @@ -244,6 +252,7 @@ def test_set_nickname(nick, request): assert nick == target sim.stop() + def test_randomize_pin_keys(request): is_Q = request.config.getoption('--Q') clean_sim_data() # remove all from previous @@ -407,6 +416,64 @@ def test_terms_ok(request): sim.stop() +@pytest.mark.parametrize("brick", [True, False]) +def test_wrong_pin_input(request, brick): + is_Q = request.config.getoption('--Q') + clean_sim_data() # remove all from previous + sim = ColdcardSimulator(args=["--early-usb", "--q1" if is_Q else "", "--pin", "22-22"]) + sim.start(start_wait=6) + device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + time.sleep(.1) + num_attmeptss = 13 + for ii, i in enumerate(range(31, 43), start=1): + # pdb.set_trace() + pin = f"{i}-{i}" + scr_num_failed = (ii - 1) if ii > 1 else None + _login(device, pin, is_Q, num_failed=scr_num_failed) + time.sleep(.5) + title, story = _cap_story(device) + if ii > 4: + assert title == "WARNING" + assert pin in story # showing to user to double-check his input + assert "BRICKS ITSELF FOREVER" in story + assert f"{num_attmeptss - ii + 1} attempts left" in story + _press_select(device, is_Q) + time.sleep(.1) + title, story = _cap_story(device) + + assert "WRONG PIN" in title + assert f"{num_attmeptss - ii} attempts left" in story + assert f"{ii} failure" in story + _press_select(device, is_Q) + time.sleep(.1) + + if brick: + # one more wrong pin + _login(device, "91-11", is_Q, num_failed=12) + time.sleep(.5) + title, story = _cap_story(device) + assert "WARNING" == title + _press_select(device, is_Q) + time.sleep(.1) + title, story = _cap_story(device) + assert title == "I Am Brick!" + assert "After 13 failed PIN attempts this Coldcard is locked forever" in story + assert "no way to reset or recover the secure element" in story + assert "forever inaccessible" in story + assert "Restore your seed words onto a new Coldcard" in story + else: + _login(device, "22-22", is_Q, num_failed=12) + time.sleep(.5) + title, story = _cap_story(device) + assert "WARNING" == title + _press_select(device, is_Q) + time.sleep(.1) + m = _cap_menu(device) + assert "Ready To Sign" in m + + sim.stop() + + @pytest.mark.parametrize("nick", [None, "In trust we trust NOT"]) @pytest.mark.parametrize("randomize", [False, True]) @pytest.mark.parametrize("login_ctdwn", [None, " 5 minutes", "15 minutes"]) diff --git a/testing/run_sim_tests.py b/testing/run_sim_tests.py index f77ba04c..1471a51d 100644 --- a/testing/run_sim_tests.py +++ b/testing/run_sim_tests.py @@ -17,6 +17,7 @@ python run_sim_tests.py # same as with '- python run_sim_tests.py -m all --onetime --veryslow # run all tests (cca 252 minutes) python run_sim_tests.py -m test_multisig.py -k cosigning # run only tests that match expression from test_multisig.py python run_sim_tests.py -m test_export.py --pdb # run only export tests and attach debugger +python run_sim_tests.py -m test_attended.py --q1 -w 6 --login # run attended test + all login tests Onetime/veryslow tests are completely separated form the rest of the test suite. @@ -96,13 +97,11 @@ def is_ok(ec: ExitCode) -> bool: return False -def _run_tests_with_simulator(test_module: str, simulator_args: List[str], pytest_marks: str, - pytest_k: str, pdb: bool, failed_first: bool, psbt2=False) -> ExitCode: - sim = ColdcardSimulator(args=simulator_args) - sim.start() - time.sleep(1) +def _run_pytest_tests(test_module: str, pytest_marks: str, pytest_k: str, pdb: bool, + failed_first: bool, psbt2=False, is_Q=False) -> ExitCode: cmd_list = [ - "--cache-clear", "-m", pytest_marks, "--sim", test_module if test_module is not None else "" + "--cache-clear", "-m", pytest_marks, "--sim", + test_module if test_module is not None else "" ] if pytest_k: cmd_list += ["-k", pytest_k] @@ -112,28 +111,43 @@ def _run_tests_with_simulator(test_module: str, simulator_args: List[str], pytes cmd_list.append("--ff") if psbt2: cmd_list.append("--psbt2") + if is_Q: + cmd_list.insert(0, "--Q") # only changes behavior in login_settings_test - exit_code = pytest.main(cmd_list) - sim.stop() - time.sleep(1) - clean_sim_data() + return pytest.main(cmd_list) + +def _run_coldcard_tests(test_module: str, simulator_args: List[str], pytest_marks: str, + pytest_k: str, pdb: bool, failed_first: bool, psbt2=False, + is_Q=False) -> ExitCode: + if simulator_args: + sim = ColdcardSimulator(args=simulator_args) + sim.start() + time.sleep(1) + + exit_code = _run_pytest_tests(test_module, pytest_marks, pytest_k, pdb, + failed_first, psbt2, is_Q) + + if simulator_args: + sim.stop() + time.sleep(1) + clean_sim_data() return exit_code -def run_tests_with_simulator(test_module=None, simulator_args=None, pytest_k=None, pdb=False, - failed_first=False, psbt2=False, - pytest_marks="not onetime and not veryslow and not manual"): +def run_coldcard_tests(test_module=None, simulator_args=None, pytest_k=None, pdb=False, + failed_first=False, psbt2=False, is_Q=False, + pytest_marks="not onetime and not veryslow and not manual"): failed = [] - exit_code = _run_tests_with_simulator(test_module, simulator_args, pytest_marks, pytest_k, - pdb, failed_first, psbt2=psbt2) + exit_code = _run_coldcard_tests(test_module, simulator_args, pytest_marks, pytest_k, + pdb, failed_first, psbt2, is_Q) if not is_ok(exit_code): # no success, no nothing - give failed another try, each alone with its own simulator last_failed = get_last_failed() print("Running failed from last run", last_failed) exit_codes = [] for failed_test in last_failed: - exit_code_2 = _run_tests_with_simulator(failed_test, simulator_args, pytest_marks, - pytest_k, pdb, failed_first, psbt2=psbt2) + exit_code_2 = _run_coldcard_tests(failed_test, simulator_args, pytest_marks, + pytest_k, pdb, failed_first, psbt2, is_Q) exit_codes.append(exit_code_2) if not is_ok(exit_code_2): failed.append(failed_test) @@ -200,6 +214,8 @@ def main(): parser.add_argument("--onetime", action="store_true", default=False, help="run tests marked as 'onetime'") parser.add_argument("--veryslow", action="store_true", default=False, + help="run 'login_settings_tests.py'") + parser.add_argument("--login", action="store_true", default=False, help="run tests marked as 'veryslow'") parser.add_argument("--collect", type=str, metavar="MARK", help="Collect marked test and print them to stdout") @@ -216,7 +232,7 @@ def main(): print(collect_marked_tests(args.collect)) return - if args.module is None and (args.onetime is False and args.veryslow is False): + if args.module is None and (args.onetime is False and args.veryslow is False and args.login is False): args.module = ["all"] DEFAULT_SIMULATOR_ARGS = ["--eff", "--set", "nfc=1"] @@ -262,9 +278,9 @@ def main(): if args.q1 and '--q1' not in test_args: test_args.append('--q1') - ec, failed_tests = run_tests_with_simulator(test_module, simulator_args=test_args, - pytest_k=args.pytest_k, pdb=args.pdb, - failed_first=args.ff, psbt2=args.psbt2) + ec, failed_tests = run_coldcard_tests(test_module, simulator_args=test_args, + pytest_k=args.pytest_k, pdb=args.pdb, + failed_first=args.ff, psbt2=args.psbt2) result.append((test_module, ec, failed_tests)) print("Done", test_module) print(80 * "=") @@ -272,10 +288,10 @@ def main(): # run veryslow is specified if args.veryslow: print("started veryslow tests") - ec, failed_tests = run_tests_with_simulator(test_module=None, pytest_marks="veryslow", - pytest_k=args.pytest_k, pdb=args.pdb, - simulator_args=DEFAULT_SIMULATOR_ARGS, - failed_first=args.ff, psbt2=args.psbt2) + ec, failed_tests = run_coldcard_tests(test_module=None, pytest_marks="veryslow", + pytest_k=args.pytest_k, pdb=args.pdb, + simulator_args=DEFAULT_SIMULATOR_ARGS, + failed_first=args.ff, psbt2=args.psbt2) result.append(("veryslow", ec, failed_tests)) # run onetime is specified (each test against its own simulator) @@ -283,12 +299,19 @@ def main(): print("started onetime tests") onetime_tests = collect_marked_tests("onetime") for onetime_test in onetime_tests: - ec, failed_tests = run_tests_with_simulator(test_module=onetime_test, pdb=args.pdb, - failed_first=args.ff, pytest_marks="onetime", - simulator_args=DEFAULT_SIMULATOR_ARGS, - psbt2=args.psbt2) + ec, failed_tests = run_coldcard_tests(test_module=onetime_test, pdb=args.pdb, + failed_first=args.ff, pytest_marks="onetime", + simulator_args=DEFAULT_SIMULATOR_ARGS, + psbt2=args.psbt2) result.append((f"onetime: {onetime_test}", ec, failed_tests)) + if args.login: + print("start login settings tests") + ec, failed_tests = run_coldcard_tests(test_module="login_settings_tests.py", pdb=args.pdb, + failed_first=args.ff, pytest_k=args.pytest_k, + is_Q=True if args.q1 else False) + result.append((f"login_settings_tests", ec, failed_tests)) + print("All done") any_failed = False diff --git a/testing/test_export.py b/testing/test_export.py index e0357cdd..0240d139 100644 --- a/testing/test_export.py +++ b/testing/test_export.py @@ -21,7 +21,7 @@ from charcodes import KEY_NFC @pytest.mark.bitcoind @pytest.mark.parametrize('acct_num', [None, '0', '99', '123']) -@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) +@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"]) def test_export_core(way, dev, use_regtest, acct_num, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, virtdisk_path, bitcoind_wallet, bitcoind_d_wallet, enter_number, nfc_read_text, load_export, bitcoind, press_select): @@ -160,7 +160,7 @@ def test_export_core(way, dev, use_regtest, acct_num, pick_menu_item, goto_home, #assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1) -@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) +@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"]) @pytest.mark.parametrize('testnet', [True, False]) def test_export_wasabi(way, dev, pick_menu_item, goto_home, cap_story, press_select, microsd_path, nfc_read_json, virtdisk_path, testnet, use_mainnet, load_export): @@ -199,7 +199,7 @@ def test_export_wasabi(way, dev, pick_menu_item, goto_home, cap_story, press_sel @pytest.mark.parametrize('mode', [ "Classic P2PKH", "P2SH-Segwit", "Segwit P2WPKH"]) @pytest.mark.parametrize('acct_num', [ None, '0', '9897']) -@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) +@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"]) @pytest.mark.parametrize('testnet', [True, False]) def test_export_electrum(way, dev, mode, acct_num, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, nfc_read_json, virtdisk_path, use_mainnet, testnet, load_export, @@ -266,7 +266,7 @@ def test_export_electrum(way, dev, mode, acct_num, pick_menu_item, goto_home, ca @pytest.mark.parametrize('acct_num', [ None, '99', '1236']) -@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) +@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"]) @pytest.mark.parametrize('testnet', [True, False]) @pytest.mark.parametrize('app', [ ("Generic JSON", "Generic Export"), @@ -351,7 +351,7 @@ def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap else: assert False -@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) +@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"]) @pytest.mark.parametrize('testnet', [True, False]) @pytest.mark.parametrize('acct_num', [None, '0', '99', '123']) def test_export_unchained(way, dev, pick_menu_item, goto_home, cap_story, need_keypress, acct_num, @@ -403,7 +403,7 @@ def test_export_unchained(way, dev, pick_menu_item, goto_home, cap_story, need_k assert node.hwif() == sk.hwif() -@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) +@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"]) @pytest.mark.parametrize('testnet', [True, False]) def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, microsd_path, addr_vs_path, virtdisk_path, nfc_read_text, cap_story, use_mainnet, @@ -546,7 +546,7 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home press_cancel() @pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"]) -@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"]) +@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc", "qr"]) @pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC]) @pytest.mark.parametrize("acct_num", [None, 0, 1, (2 ** 31) - 1]) @pytest.mark.parametrize("int_ext", [True, False])