import pytest, time, json from constants import simulator_fixed_words, simulator_fixed_tprv from pycoin.key.BIP32Node import BIP32Node from mnemonic import Mnemonic def decode_backup(txt): import json vals = dict() trimmed = dict() for ln in txt.split('\n'): if not ln: continue if ln[0] == '#': continue k, v = ln.split(' = ', 1) v = json.loads(v) if k.startswith('duress_') or k.startswith('fw_'): # no space in USB xfer for thesE! trimmed[k] = v else: vals[k] = v return vals, trimmed @pytest.fixture def backup_system(settings_set, settings_remove, goto_home, pick_menu_item, cap_story, need_keypress, cap_screen_qr, pass_word_quiz, get_setting): def doit(reuse_pw=False, save_pw=False, st=None, ct=False): # st -> seed type # ct -> cleartext backup if reuse_pw: settings_set('bkpw', ' '.join('zoo' for _ in range(12))) else: settings_remove('bkpw') goto_home() pick_menu_item('Advanced/Tools') pick_menu_item('Backup') pick_menu_item('Backup System') title, body = cap_story() if st: if st == "b39pass": assert "BIP39 passphrase is in effect" in body assert "ignores passphrases and produces backup of main seed" in body assert "(2) to back-up BIP39 passphrase wallet" in body if st == "eph": assert "An temporary seed is in effect" in body assert "so backup will be of that seed" in body need_keypress("y") time.sleep(.1) title, body = cap_story() if ct: # cleartext backup if ' 1: zoo' in body: need_keypress("x") need_keypress("6") time.sleep(.1) _, story = cap_story() assert "Are you SURE ?!?" in story assert "**NOT** be encrypted" in story need_keypress("y") return # nothing more to be done if reuse_pw: assert ' 1: zoo' in body assert '12: zoo' in body need_keypress('y') words = ['zoo'] * 12 time.sleep(0.1) title, body = cap_story() else: assert title == 'NO-TITLE' assert 'Record this' in body assert 'password:' in body words = [w[3:].strip() for w in body.split('\n') if w and w[2] == ':'] assert len(words) == 12 print("Passphrase: %s" % ' '.join(words)) if 'QR Code' in body: need_keypress('1') got_qr = cap_screen_qr().decode('ascii').lower().split() assert [w[0:4] for w in words] == got_qr need_keypress('y') # pass the quiz! count, title, body = pass_word_quiz(words) assert count >= 4 assert "same words next time" in body assert "Press (1) to save" in body if save_pw: need_keypress('1') time.sleep(.1) assert get_setting('bkpw') == ' '.join(words) else: need_keypress('x') time.sleep(.01) assert get_setting('bkpw', 'xxx') == 'xxx' return words return doit @pytest.mark.qrcode @pytest.mark.parametrize('multisig', [False, 'multisig']) @pytest.mark.parametrize('st', ["b39pass", "eph", None]) @pytest.mark.parametrize('reuse_pw', [False, True]) @pytest.mark.parametrize('save_pw', [False, True]) @pytest.mark.parametrize('seedvault', [False, True]) def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypress, st, open_microsd, microsd_path, unit_test, cap_menu, word_menu_entry, pass_word_quiz, reset_seed_words, import_ms_wallet, get_setting, cap_screen_qr, reuse_pw, save_pw, settings_set, settings_remove, generate_ephemeral_words, set_bip39_pw, verify_backup_file, check_and_decrypt_backup, restore_backup_cs, clear_ms, seedvault, restore_main_seed, import_ephemeral_xprv, backup_system): # Make an encrypted 7z backup, verify it, and even restore it! clear_ms() reset_seed_words() settings_set("seedvault", int(seedvault)) settings_set("seeds", [] if seedvault else None) # need to make multisig in my main wallet if multisig and st != "eph": import_ms_wallet(15, 15) need_keypress('y') time.sleep(.1) assert len(get_setting('multisig')) == 1 if st == "b39pass": xfp_pass = set_bip39_pw("coinkite", reset=False, seed_vault=seedvault) assert not get_setting('multisig', None) elif st == "eph": eph_seed = generate_ephemeral_words(num_words=24, dice=False, from_main=True, seed_vault=seedvault) if multisig: # make multisig in ephemeral wallet import_ms_wallet(15, 15, dev_key=True, common="605'/0'/0'") need_keypress('y') time.sleep(.1) assert len(get_setting('multisig')) == 1 else: # create ephemeral seed - add to seed vault if necessary # and restore master (just so we have something in setting.seeds) node = import_ephemeral_xprv("sd", from_main=True, seed_vault=seedvault) restore_main_seed(seed_vault=seedvault, preserve_settings=True) words = backup_system(reuse_pw=reuse_pw, save_pw=save_pw, st=st) time.sleep(.1) title, body = cap_story() if st == "b39pass" and multisig: # correct settings switch back? # multisig is only in main wallet # must not be copied from main to b39pass # must not be available after backup done assert not get_setting('multisig', None) files = [] for copy in range(2): if copy == 1: title, body = cap_story() assert 'written:' in body fn = [ln.strip() for ln in body.split('\n') if ln.endswith('.7z')][0] print("filename %d: %s" % (copy, fn)) files.append(fn) # write extra copy. need_keypress('2') time.sleep(.01) bk_a = open_microsd(files[0]).read() bk_b = open_microsd(files[1]).read() assert bk_a == bk_b, "contents mismatch" need_keypress('x') time.sleep(.01) verify_backup_file(fn) decrypted = check_and_decrypt_backup(fn, words) avail_settings = [] if seedvault and (st in [None, "b39pass"]): assert "seedvault" in decrypted assert "seeds" in decrypted avail_settings.append("seeds") avail_settings.append("seedvault") else: assert "seedvault" not in decrypted assert "seeds" not in decrypted for i in range(10): need_keypress('x') time.sleep(.01) # test verify on device (CRC check) if multisig: avail_settings.append("multisig") restore_backup_cs(files[0], words, avail_settings=avail_settings) @pytest.mark.parametrize("stype", ["words12", "words24", "xprv"]) def test_backup_ephemeral_wallet(stype, pick_menu_item, need_keypress, goto_home, cap_story, pass_word_quiz, get_setting, verify_backup_file, microsd_path, check_and_decrypt_backup, sim_execfile, unit_test, word_menu_entry, cap_menu, restore_backup_cs, generate_ephemeral_words, import_ephemeral_xprv, reset_seed_words): reset_seed_words() goto_home() if "words" in stype: num_words = int(stype.replace("words", "")) sec = generate_ephemeral_words(num_words, from_main=True, seed_vault=False) else: sec = import_ephemeral_xprv("sd", from_main=True, seed_vault=False) target = sim_execfile('devtest/get-secrets.py') assert 'Error' not in target pick_menu_item("Advanced/Tools") pick_menu_item("Backup") pick_menu_item("Backup System") time.sleep(.1) title, story = cap_story() assert "An temporary seed is in effect" in story assert "so backup will be of that seed" in story need_keypress("y") time.sleep(.1) title, story = cap_story() if "Use same backup file password as last time?" in story: need_keypress("x") time.sleep(.1) title, story = cap_story() assert title == 'NO-TITLE' assert 'Record this' in story assert 'password:' in story words = [w[3:].strip() for w in story.split('\n') if w and w[2] == ':'] assert len(words) == 12 # pass the quiz! count, title, body = pass_word_quiz(words) assert count >= 4 assert "same words next time" in body assert "Press (1) to save" in body need_keypress('x') time.sleep(.01) assert get_setting('bkpw', 'xxx') == 'xxx' title, story = cap_story() assert "Backup file written:" in story fn = story.split("\n\n")[1] assert fn.endswith(".7z") verify_backup_file(fn) contents = check_and_decrypt_backup(fn, words) if "words" in stype: assert "mnemonic" in contents else: assert "mnemonic" not in contents assert simulator_fixed_words not in contents assert simulator_fixed_tprv not in contents assert target == contents if "words" in stype: words_str = " ".join(sec) assert words_str in contents seed = Mnemonic.to_seed(words_str) expect = BIP32Node.from_master_secret(seed, netcode="XTN") else: expect = sec target_esk = None target_epk = None esk = expect.hwif(as_private=True) epk = expect.hwif(as_private=False) for line in contents.split("\n"): if line.startswith("xprv ="): target_esk = line.split("=")[-1].strip().replace('"', '') if line.startswith("xpub ="): target_epk = line.split("=")[-1].strip().replace('"', '') assert target_epk == epk assert target_esk == esk restore_backup_cs(fn, words) @pytest.mark.parametrize('seedvault', [False, True]) @pytest.mark.parametrize("passphrase", ["@coinkite rulez!!", "!@#!@", "AAAAAAAAAAA"]) def test_backup_bip39_wallet(passphrase, set_bip39_pw, pick_menu_item, need_keypress, goto_home, cap_story, pass_word_quiz, get_setting, verify_backup_file, microsd_path, check_and_decrypt_backup, sim_execfile, unit_test, word_menu_entry, cap_menu, restore_backup_cs, seedvault, settings_set, reset_seed_words): reset_seed_words() goto_home() settings_set("seedvault", int(seedvault)) settings_set("seeds", [] if seedvault else None) set_bip39_pw(passphrase, seed_vault=True) target = sim_execfile('devtest/get-secrets.py') assert 'Error' not in target pick_menu_item("Advanced/Tools") pick_menu_item("Backup") pick_menu_item("Backup System") time.sleep(.1) title, story = cap_story() assert "BIP39 passphrase is in effect" in story assert "ignores passphrases and produces backup of main seed" in story assert "(2) to back-up BIP39 passphrase wallet" in story need_keypress("2") time.sleep(.1) title, story = cap_story() if "Use same backup file password as last time?" in story: need_keypress("x") time.sleep(.1) title, story = cap_story() assert title == 'NO-TITLE' assert 'Record this' in story assert 'password:' in story words = [w[3:].strip() for w in story.split('\n') if w and w[2] == ':'] assert len(words) == 12 # pass the quiz! count, title, body = pass_word_quiz(words) assert count >= 4 assert "same words next time" in body assert "Press (1) to save" in body need_keypress('x') time.sleep(.01) assert get_setting('bkpw', 'xxx') == 'xxx' title, story = cap_story() assert "Backup file written:" in story fn = story.split("\n\n")[1] assert fn.endswith(".7z") verify_backup_file(fn) contents = check_and_decrypt_backup(fn, words) assert "mnemonic" not in contents assert "seedvault" not in contents assert "seeds" not in contents assert simulator_fixed_words not in contents assert simulator_fixed_tprv not in contents assert target == contents seed = Mnemonic.to_seed(simulator_fixed_words, passphrase=passphrase) expect = BIP32Node.from_master_secret(seed, netcode="XTN") esk = expect.hwif(as_private=True) epk = expect.hwif(as_private=False) target_esk = None target_epk = None for line in contents.split("\n"): if line.startswith("xprv ="): target_esk = line.split("=")[-1].strip().replace('"', '') if line.startswith("xpub ="): target_epk = line.split("=")[-1].strip().replace('"', '') assert target_epk == epk assert target_esk == esk restore_backup_cs(fn, words) def test_trick_backups(goto_trick_menu, clear_all_tricks, repl, unit_test, new_trick_pin, new_pin_confirmed, pick_menu_item, need_keypress): from test_se2 import TC_REBOOT, TC_BLANK_WALLET clear_all_tricks() # - make wallets of all duress types (x2 each) # - plus a few simple ones # - perform a backup and check result for n in range(8): goto_trick_menu() pin = '123-%04d' % n new_trick_pin(pin, 'Duress Wallet', None) item = 'BIP-85 Wallet #%d' % (n % 4) if (n % 4 != 0) else 'Legacy Wallet' pick_menu_item(item) need_keypress('y') new_pin_confirmed(pin, item, None, None) for pin, op_mode, expect, _, xflags in [ ('11-33', 'Just Reboot', 'Reboot when this PIN', False, TC_REBOOT), ('11-55', 'Look Blank', 'Look and act like a freshly', False, TC_BLANK_WALLET), ]: new_trick_pin(pin, op_mode, expect) new_pin_confirmed(pin, op_mode, xflags) # works, but not the best test # unit_test('devtest/backups.py') bk = repl.exec('import backups; RV.write(backups.render_backup_contents())', raw=1) assert 'Coldcard backup file' in bk # decode it vals, trimmed = decode_backup(bk) assert 'duress_xprv' in trimmed assert 'duress_1001_words' in trimmed assert 'duress_1002_words' in trimmed assert 'duress_1003_words' in trimmed unit_test('devtest/clear_seed.py') repl.exec(f'import backups; backups.restore_from_dict_ll({vals!r})') # recover from recovery repl.exec(f'import backups; pa.setup(pa.pin); pa.login(); from actions import goto_top_menu; goto_top_menu()') bk2 = repl.exec('import backups; RV.write(backups.render_backup_contents())', raw=1) assert 'Traceback' not in bk2 vals2, tr2 = decode_backup(bk2) assert vals == vals2 assert trimmed == tr2 def test_seed_vault_backup(settings_set, reset_seed_words, generate_ephemeral_words, import_ephemeral_xprv, restore_main_seed, settings_get, repl, pick_menu_item, need_keypress, cap_story, get_setting, pass_word_quiz, verify_backup_file, check_and_decrypt_backup, restore_backup_cs, cap_menu, verify_ephemeral_secret_ui): reset_seed_words() settings_set("seedvault", 1) settings_set("seeds", []) expect_count = 0 ui_xfps = [] for num_words in [12, 24]: mnemonic = generate_ephemeral_words(num_words=num_words, seed_vault=True) xfp = verify_ephemeral_secret_ui(mnemonic=mnemonic, seed_vault=True) ui_xfps.append(xfp) expect_count += 1 # Ephemeral seeds - extended keys node = import_ephemeral_xprv("sd", seed_vault=True) xfp = verify_ephemeral_secret_ui(xpub=node.hwif(), seed_vault=True) ui_xfps.append(xfp) expect_count += 1 restore_main_seed(seed_vault=True) assert expect_count == 3 assert len(ui_xfps) == expect_count # check all saved okay seeds = settings_get('seeds') assert len(seeds) == expect_count bk = repl.exec('import backups; RV.write(backups.render_backup_contents())', raw=1) assert 'Coldcard backup file' in bk pick_menu_item("Advanced/Tools") pick_menu_item("Backup") pick_menu_item("Backup System") time.sleep(.1) title, story = cap_story() if "Use same backup file password as last time?" in story: need_keypress("x") time.sleep(.1) title, story = cap_story() assert title == 'NO-TITLE' assert 'Record this' in story assert 'password:' in story words = [w[3:].strip() for w in story.split('\n') if w and w[2] == ':'] assert len(words) == 12 # pass the quiz! count, title, body = pass_word_quiz(words) assert count >= 4 assert "same words next time" in body assert "Press (1) to save" in body need_keypress('x') time.sleep(.01) assert get_setting('bkpw', 'xxx') == 'xxx' title, story = cap_story() assert "Backup file written:" in story fn = story.split("\n\n")[1] assert fn.endswith(".7z") verify_backup_file(fn) contents = check_and_decrypt_backup(fn, words) assert "mnemonic" in contents assert simulator_fixed_words in contents assert simulator_fixed_tprv in contents assert "setting.seedvault = 1" in contents assert "setting.seeds" in contents restore_backup_cs(fn, words) time.sleep(.2) m = cap_menu() assert "Seed Vault" in m pick_menu_item('Seed Vault') m = cap_menu() assert len(m) == expect_count sv_xfp_menu = [i.split(" ")[-1][1:-1] for i in m] for xfp_ui in ui_xfps: assert xfp_ui in sv_xfp_menu def test_seed_vault_backup_frozen(reset_seed_words, settings_set, repl): from test_ephemeral import SEEDVAULT_TEST_DATA reset_seed_words() settings_set("seedvault", 1) sv = [] for item in SEEDVAULT_TEST_DATA: xfp, entropy, mnemonic = item # build stashed encoded secret entropy_bytes = bytes.fromhex(entropy) if mnemonic: vlen = len(entropy_bytes) assert vlen in [16, 24, 32] marker = 0x80 | ((vlen // 8) - 2) stored_secret = bytes([marker]) + entropy_bytes else: stored_secret = entropy_bytes sv.append((xfp, stored_secret.hex(), f"[{xfp}]", "meta")) settings_set("seeds", sv) bk = repl.exec('import backups; RV.write(backups.render_backup_contents())', raw=1) assert 'Coldcard backup file' in bk target = json.dumps(sv) assert target in bk