# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # test drv_entro.py features # import pytest, time, re from binascii import a2b_hex, b2a_hex from bip32 import BIP32Node, PrivateKey from mnemonic import Mnemonic from charcodes import KEY_QR HISTORY = set() # XPRV from spec: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb EXAMPLE_XPRV = '011b67969d1ec69bdfeeae43213da8460ba34b92d0788c8f7bfcfa44906e8a589c3f15e5d852dc2e9ba5e9fe189a8dd2e1547badef5b563bbe6579fc6807d80ed900000000000000' @pytest.fixture def derive_bip85_secret(goto_home, press_select, pick_menu_item, cap_story, enter_text, set_encoded_secret, set_seed_words, settings_set, is_q1, seed_story_to_words): def doit(mode, index, expect=None, entropy=None, sim_sec=None, chain="BTC"): if sim_sec: if len(sim_sec.split(" ")) in (12,18,24): set_seed_words(sim_sec) else: set_encoded_secret(a2b_hex(sim_sec)) if chain: settings_set('chain', chain) goto_home() time.sleep(.1) pick_menu_item('Advanced/Tools') time.sleep(.1) pick_menu_item('Derive Seed B85' if not is_q1 else 'Derive Seeds (BIP-85)') time.sleep(0.1) title, story = cap_story() assert 'seed value' in story assert 'other wallet systems' in story press_select() time.sleep(0.1) title, story = cap_story() if "You have a temporary seed active - deriving from temporary" in story: press_select() time.sleep(0.1) pick_menu_item(mode) enter_text(str(index) if index is not None else '') time.sleep(0.1) title, story = cap_story() assert f'Path Used (index={index}):' in story assert "m/83696968h/" in story assert f"/{index}h" in story if entropy is not None: assert f"Raw Entropy:\n{entropy}" in story can_import = False if ' words' in mode: num_words = int(mode.split()[0]) assert f'Seed words ({num_words}):' in story assert f"m/83696968h/39h/0h/{num_words}h/{index}h" in story assert '1:' in story assert f'{num_words}:' in story got = seed_story_to_words(story) if expect: assert ' '.join(got) == expect can_import = 'words' seed = Mnemonic.to_seed(' '.join(got)) node = BIP32Node.from_master_secret(seed) assert node.fingerprint().hex().upper() in title assert "(6) to type words over USB" in story elif 'XPRV' in mode: assert 'Derived XPRV:' in story assert f"m/83696968h/32h/{index}h" in story if expect: assert expect in story can_import = 'xprv' node = BIP32Node.from_hwif(story.split("\n\n")[0].split("\n")[-1]) assert node.fingerprint().hex().upper() in title assert "(6) to type xprv over USB" in story elif 'WIF' in mode: assert 'WIF (privkey)' in story assert f"m/83696968h/2h/{index}h" in story if expect: assert expect in story assert "(6) to type wif over USB" in story elif 'bytes hex' in mode: width = int(mode.split('-')[0]) assert width in {32, 64} assert f'Hex ({width} bytes):' in story assert f"m/83696968h/128169h/{width}h/{index}h" in story if expect: assert expect in story assert "(6) to type hex over USB" in story elif 'Passwords' == mode: assert "Password:" in story assert f"m/83696968h/707764h/21h/{index}h" in story if expect: assert expect in story assert "(6) to type pw over USB" in story else: raise ValueError(mode) return can_import, story return doit @pytest.fixture def activate_bip85_ephemeral(need_keypress, cap_story, sim_exec, reset_seed_words, confirm_tmp_seed): def doit(do_import, reset=True, expect=None, entropy=None, save_to_vault=False): _, story = cap_story() assert '(0) to switch to derived secret' in story try: time.sleep(0.1) need_keypress('0') confirm_tmp_seed(seedvault=save_to_vault) encoded = sim_exec('from pincodes import pa; RV.write(repr(pa.fetch()))') print(encoded) assert 'Error' not in encoded encoded = eval(encoded) assert len(encoded) == 72 marker = encoded[0] if do_import == 'words': assert marker & 0x80 == 0x80 width = ((marker & 0x3) + 2) * 8 assert width in {16, 24, 32} if entropy: assert encoded[1:1 + width] == a2b_hex(entropy) elif do_import == 'xprv': assert marker == 0x01 if expect: node = BIP32Node.from_hwif(expect) ch, pk = encoded[1:33], encoded[33:65] assert node.chain_code() == ch assert bytes(node.node.private_key) == pk finally: # required cleanup if reset: reset_seed_words() return doit @pytest.mark.parametrize('mode,index,entropy,expect', [ ('12 words', 0, '6250b68daf746d12a24d58b4787a714b', 'girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose'), ('18 words', 0, '938033ed8b12698449d4bbca3c853c66b293ea1b1ce9d9dc', 'near account window bike charge season chef number sketch tomorrow excuse sniff circle vital hockey outdoor supply token'), ('24 words', 0, 'ae131e2312cdc61331542efe0d1077bac5ea803adf24b313a4f0e48e9c51f37f', 'puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano'), ('WIF (privkey)', 0, '7040bb53104f27367f317558e78a994ada7296c6fde36a364e5baf206e502bb1', 'Kzyv4uF39d4Jrw2W7UryTHwZr1zQVNk4dAFyqE6BuMrMh1Za7uhp'), ('XPRV (BIP-32)', 0, None, 'xprv9s21ZrQH143K2srSbCSg4m4kLvPMzcWydgmKEnMmoZUurYuBuYG46c6P71UGXMzmriLzCCBvKQWBUv3vPB3m1SATMhp3uEjXHJ42jFg7myX'), ('32-bytes hex', 0, None, 'ea3ceb0b02ee8e587779c63f4b7b3a21e950a213f1ec53cab608d13e8796e6dc'), ('64-bytes hex', 0, None, '492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e71aa2d645442f878555d078fd1f1f67e368976f04137b1f7a0d19232136ca50c44614af72b5582a5c'), ('Passwords', 0, None, "dKLoepugzdVJvdL56ogNV"), ]) def test_bip_vectors(mode, index, entropy, expect, cap_story, need_keypress, load_export_and_verify_signature, derive_bip85_secret, activate_bip85_ephemeral, press_select, press_cancel): do_import, story = derive_bip85_secret(mode, index, expect, entropy, sim_sec=EXAMPLE_XPRV) # write to SD msg = story.split('Press', 1)[0] assert 'Press (1) to save' in story need_keypress('1') time.sleep(0.1) title, story = cap_story() contents,_, _ = load_export_and_verify_signature(story, "sd", fpattern="drv", label=None) assert contents.strip() == msg.strip() press_select() time.sleep(0.1) title, story = cap_story() if do_import: activate_bip85_ephemeral(do_import, expect=expect, entropy=entropy) else: assert 'show QR code' in story press_cancel() def test_allow_bip32_max_int(pick_menu_item, goto_home, enter_number, is_q1, press_select, press_cancel, cap_story, settings_set): max_int = 2147483647 to_input = 9999999999 mi = 'Derive Seed B85' if not is_q1 else 'Derive Seeds (BIP-85)' goto_home() # by default only indexes up to 9999 are allowed pick_menu_item("Advanced/Tools") pick_menu_item(mi) press_select() pick_menu_item("12 words") enter_number(to_input) time.sleep(.1) _, story = cap_story() assert "index=9999" in story # does not allow to go over this value press_cancel() goto_home() pick_menu_item("Advanced/Tools") pick_menu_item("Danger Zone") pick_menu_item("B85 Idx Values") time.sleep(.1) _, story = cap_story() assert "Allow unlimited indexes for BIP-85 derivations?" in story assert "DANGER" in story press_select() pick_menu_item("Unlimited") goto_home() pick_menu_item("Advanced/Tools") pick_menu_item(mi) press_select() pick_menu_item("12 words") enter_number(to_input) time.sleep(.1) _, story = cap_story() assert f"index={max_int}" in story press_cancel() settings_set("b85max", 0) @pytest.mark.qrcode @pytest.mark.parametrize('mode,pattern', [ ('WIF (privkey)', r'[1-9A-HJ-NP-Za-km-z]{51,52}' ), ('XPRV (BIP-32)', r'[tx]prv[1-9A-HJ-NP-Za-km-z]{107}'), ('32-bytes hex', r'[a-f0-9]{64}'), ('64-bytes hex', r'[a-f0-9]{128}'), ('12 words', r'[a-f0-9]{32}'), ('18 words', r'[a-f0-9]{48}'), ('24 words', r'[a-f0-9]{64}'), ('Passwords', r'[a-zA-Z0-9+/]{21}'), ]) @pytest.mark.parametrize('index', [0, 1, 10, 100, 1000, 9999, 2147483647]) def test_path_index(mode, pattern, index, need_keypress, cap_screen_qr, seed_story_to_words, derive_bip85_secret, reset_seed_words, is_q1, press_cancel, settings_set): reset_seed_words() settings_set("b85max", 1) # Uses any key on Simulator; just checking for operation + entropy level _, story = derive_bip85_secret(mode, index) assert f'Path Used (index={index}):' in story assert "m/83696968h/" in story assert f"/{index}h" in story got = re.findall(pattern, story)[0] assert len(set(got)) >= 12 global HISTORY assert got not in HISTORY HISTORY.add(got) if mode == "Passwords": from base64 import b64encode raw = re.findall(r'[a-f0-9]{64}', story)[0] exp = b64encode(a2b_hex(raw)).decode('ascii')[0:21] assert exp == got elif 'words' in mode: exp = Mnemonic('english').to_mnemonic(a2b_hex(got)).split() assert seed_story_to_words(story) == exp elif 'XPRV' in mode: node = BIP32Node.from_hwif(got) assert str(b2a_hex(node.chain_code()), 'ascii') in story assert bytes(node.node.private_key).hex() in story elif 'WIF' in mode: key = PrivateKey.from_wif(got) assert bytes(key).hex() in story if index == 0: assert 'show QR code' in story need_keypress(KEY_QR if is_q1 else '4') qr = cap_screen_qr().decode('ascii') if mode == "Passwords": assert qr == exp == got elif 'words' in mode: gw = qr.lower().split() assert gw == [i[0:4] for i in exp] elif 'hex' in mode: assert qr.lower() == got elif 'XPRV' in mode: assert qr == got elif 'WIF' in mode: assert qr == got press_cancel() settings_set("b85max", 0) def test_type_passwords(dev, cap_menu, pick_menu_item, goto_home, OK, cap_story, press_select, cap_screen, enter_text): goto_home() pick_menu_item('Settings') pick_menu_item('Keyboard EMU') _, story = cap_story() story1 = "This mode adds a top-level menu item for typing deterministically-generated passwords (BIP-85), directly into an attached USB computer (as an emulated keyboard)." assert story1 == story press_select() pick_menu_item('Enable') time.sleep(0.3) goto_home() menu = cap_menu() assert "Type Passwords" in menu pick_menu_item("Type Passwords") time.sleep(0.1) # here we accessed index loop and can derive for index in [0, 10, 100, 1000, 9999]: time.sleep(0.5) enter_text(str(index)) time.sleep(1) _, story = cap_story() assert f"Place mouse at required password prompt, then press {OK} to send keystrokes." in story split_story = story.split("\n\n") _, pwd = split_story[1].split("\n") _, path = split_story[2].split("\n") assert path == f"m/83696968h/707764h/21h/{index}h" assert len(pwd) == 21 assert "=" not in pwd press_select() # does nothing on simulator time.sleep(0.2) # exit Enter Password menu goto_home() pick_menu_item('Settings') pick_menu_item('Keyboard EMU') pick_menu_item('Default Off') menu = cap_menu() assert "Type Passwords" not in menu def test_export_nfc_when_disabled(pick_menu_item, goto_home, cap_story, press_select, is_q1, derive_bip85_secret, press_nfc, cap_menu): goto_home() pick_menu_item("Settings") pick_menu_item("Hardware On/Off") pick_menu_item("NFC Sharing") time.sleep(.1) _, story = cap_story() if "Near Field Communications" in story: press_select() pick_menu_item("Default Off") time.sleep(.1) goto_home() _, story = derive_bip85_secret('12 words', 999) assert "NFC" not in story press_nfc() time.sleep(.1) goto_home() m = cap_menu() assert "Ready To Sign" in m def test_bip85_index_cancel(goto_home, pick_menu_item, press_select, press_cancel, cap_screen, is_q1): mi = 'Derive Seed B85' if not is_q1 else 'Derive Seeds (BIP-85)' goto_home() pick_menu_item('Advanced/Tools') pick_menu_item(mi) press_select() # intro story time.sleep(.1) pick_menu_item('12 words') time.sleep(.1) screen = cap_screen() assert 'Index Number' in screen # cancel should pop back to the choices menu, can_cancel=True press_cancel() time.sleep(.2) screen = cap_screen() assert 'Index Number' not in screen # EOF