firmware/testing/test_ephemeral.py
2024-02-21 09:05:32 -05:00

1419 lines
45 KiB
Python

# (c) Copyright 2022 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Ephemeral Seeds tests
#
import pytest, time, re, os, shutil, pdb, hashlib
from constants import simulator_fixed_tpub, simulator_fixed_xfp, simulator_fixed_xpub
from constants import simulator_fixed_words, simulator_fixed_tprv
from ckcc.protocol import CCProtocolPacker
from txn import fake_txn
from test_ux import word_menu_entry
from pycoin.key.BIP32Node import BIP32Node
from helpers import xfp2str, a2b_hex
from charcodes import KEY_CLEAR, KEY_NFC
WORDLISTS = {
12: ('abandon ' * 11 + 'about', '73C5DA0A'),
18: ('abandon ' * 17 + 'agent', 'E08B8AC3'),
24: ('abandon ' * 23 + 'art', '5436D724'),
}
SEEDVAULT_TEST_DATA = [
("47649253", "344f9dc08e88b8a46d4b8f46c4e6bb6c",
"crowd language ice brown merit fall release impose egg cheese put suit"),
("CC7BB706", "88f53ed897cc371ffe4b715c267206f3286ed2f655ba9d68",
"material prepare renew convince sell morning weird hotel found crime like town manage harvest sun resemble output dolphin"),
("AC39935C", "956f484cc2136178fd1ad45faeb54972c829f65aad0d74eb2541b11984655893",
"nice kid basket loud current round virtual fold garden interest false tortoise little will height payment insane float expire giraffe obscure crawl girl glare"),
('939B32C4',
'017caa3142d48791f837b42fcd7a98662f9fb4101a15ae87cdbc1fecc96f33c11ffcefd8121daaba0625c918a335a0712b8c35c2da60e6fc6eef78b7028f4be02a',
None), # BIP-85 -> BIP-32 -> #23
]
@pytest.fixture
def seed_vault_enable(cap_story, pick_menu_item, press_select, goto_home,
settings_set):
def doit(enable=True):
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Danger Zone")
pick_menu_item("Seed Vault")
time.sleep(.1)
_, story = cap_story()
if "Enable Seed Vault?" in story:
press_select()
if enable:
pick_menu_item("Enable")
else:
pick_menu_item("Default Off")
time.sleep(.2)
_, story = cap_story()
if "Please remove all seeds from the vault" in story:
press_select()
settings_set("seeds", [])
pick_menu_item("Seed Vault")
time.sleep(.1)
pick_menu_item("Default Off")
time.sleep(.1)
return doit
def truncate_seed_words(words):
if isinstance(words, str):
words = words.split(" ")
return ' '.join(w[0:4] for w in words)
@pytest.fixture
def ephemeral_seed_disabled(sim_exec):
def doit():
rv = sim_exec('from pincodes import pa; RV.write(repr(pa.tmp_value))')
assert not eval(rv)
return doit
@pytest.fixture
def ephemeral_seed_disabled_ui(cap_menu):
def doit():
# MUST be in ephemeral seed menu already
time.sleep(0.1)
menu = cap_menu()
# no ephemeral seed chosen (yet)
assert "[" not in menu[0]
return doit
@pytest.fixture
def get_seed_value_ux(goto_home, pick_menu_item, need_keypress, cap_story,
nfc_read_text, seed_story_to_words, press_nfc, press_select):
def doit(nfc=False):
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Danger Zone")
pick_menu_item("Seed Functions")
pick_menu_item('View Seed Words')
time.sleep(.01)
title, body = cap_story()
assert ('Are you SURE' in body) or ('Are you SURE' in title)
assert 'can control all funds' in body
press_select() # skip warning
time.sleep(0.01)
title, story = cap_story()
if nfc:
need_keypress("1") # show QR code
time.sleep(.2)
press_nfc() # any QR can be exported via NFC
time.sleep(.2)
str_words = nfc_read_text()
time.sleep(.5)
press_select() # exit NFC animation
return str_words.split(" ") # always truncated
return seed_story_to_words(story)
return doit
@pytest.fixture
def get_identity_story(goto_home, pick_menu_item, cap_story):
def doit():
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("View Identity")
time.sleep(0.1)
title, story = cap_story()
return story
return doit
@pytest.fixture
def goto_eph_seed_menu(goto_home, pick_menu_item, cap_story, need_keypress):
def _doit():
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Temporary Seed")
title, story = cap_story()
if title == "WARNING":
assert "Temporary seed is a secret completely separate from the master seed" in story
assert "typically held in device RAM" in story
assert "not persisted between reboots in the Secure Element." in story
assert "Enable the Seed Vault feature to store these secrets longer-term." in story
assert "Press (4) to prove you read to the end of this message and accept all consequences." in story
need_keypress("4") # understand consequences
def doit():
try:
_doit()
except:
time.sleep(.1)
_doit()
return doit
@pytest.fixture
def restore_main_seed(goto_home, pick_menu_item, cap_story, cap_menu,
need_keypress, settings_slots, press_select):
def doit(preserve_settings=False, seed_vault=False):
if seed_vault:
preserve_settings = True
prev = len(settings_slots())
goto_home()
menu = cap_menu()
assert menu[-1] == "Restore Master"
assert (menu[0][0] == "[") and (menu[0][-1] == "]")
pick_menu_item("Restore Master")
time.sleep(.1)
title, story = cap_story()
assert "Restore main wallet and its settings?" in story
if seed_vault:
assert "Press OK to forget current temporary seed " not in story
assert "settings, or press (1) to save & keep " not in story
else:
assert "Press OK to forget current temporary seed " in story
assert "settings, or press (1) to save & keep " in story
assert "those settings if same seed is later restored." in story
if preserve_settings and not seed_vault:
need_keypress("1")
else:
press_select()
time.sleep(.3)
menu = cap_menu()
assert menu[-1] != "Restore Master"
assert (menu[0][0] != "[") and (menu[0][-1] != "]")
after = len(settings_slots())
if preserve_settings:
assert prev <= after, "p%d == a%d" % (prev, after)
else:
assert prev > after, "p%d > a%d" % (prev, after)
return doit
@pytest.fixture
def confirm_tmp_seed(need_keypress, cap_story, press_select):
def doit(seedvault=False, expect_xfp=None):
time.sleep(0.3)
title, story = cap_story()
if "Press (1) to store temporary seed into Seed Vault" in story:
if seedvault:
need_keypress("1") # store it
time.sleep(.1)
title, story = cap_story()
assert "Saved to Seed Vault" in story
if expect_xfp is not None:
assert expect_xfp in story
press_select()
else:
press_select() # do not store
time.sleep(.2)
title, story = cap_story()
if expect_xfp is not None:
assert expect_xfp in title
else:
expect_xfp = title[1:-1]
assert "New temporary master key is in effect now." in story
press_select()
return expect_xfp
return doit
@pytest.fixture
def seed_vault_delete(pick_menu_item, need_keypress, cap_menu, cap_story,
goto_home, press_select):
def doit(xfp, wipe=True):
# delete it from records
goto_home()
pick_menu_item("Seed Vault")
time.sleep(.1)
m = cap_menu()
target_sv_mi = None
for mi in m:
if xfp in mi:
pick_menu_item(mi)
target_sv_mi = mi
break
else:
assert False
pick_menu_item("Delete")
time.sleep(.1)
title, story = cap_story()
assert "Remove" in story
assert xfp in title
assert "press (1)" in story
if wipe:
press_select()
else:
# preserve settings - remove just from seed vaul
need_keypress("1")
time.sleep(.1)
goto_home()
pick_menu_item("Seed Vault")
time.sleep(.1)
m = cap_menu()
assert target_sv_mi
assert target_sv_mi not in m
return doit
@pytest.fixture
def verify_ephemeral_secret_ui(cap_story, press_select, cap_menu, dev, fake_txn,
get_identity_story, try_sign, get_seed_value_ux,
pick_menu_item, goto_home):
def doit(mnemonic=None, xpub=None, expected_xfp=None, seed_vault=False,
testnet=True, is_b39pw=False):
goto_home()
menu = cap_menu()
if expected_xfp:
assert expected_xfp in menu[0]
else:
assert menu[0].startswith("[") # ephemeral xfp
in_effect_xfp = menu[0][1:-1]
assert menu[1] == "Ready To Sign" # returned to main menu
assert menu[-1] == "Restore Master" # restore main from ephemeral
if seed_vault:
pick_menu_item("Seed Vault")
time.sleep(.1)
sc_menu = cap_menu()
assert "Restore Master" in sc_menu
item = [i for i in sc_menu if in_effect_xfp in i][0]
pick_menu_item(item)
time.sleep(.1)
m = cap_menu()
assert "Delete" in m
assert "Rename" in m
assert len(m) == 4
assert "Seed In Use" in m
pick_menu_item("Seed In Use") # noop
else:
# Seed Vault disabled
m = cap_menu()
assert "Seed Vault" not in m
ident_story = get_identity_story()
ident_xfp = ident_story.split("\n\n")[1].strip()
if is_b39pw:
assert "BIP-39 passphrase is in effect" in ident_story
else:
assert "Temporary seed is in effect" in ident_story
assert ident_xfp == in_effect_xfp
if mnemonic:
seed_words = get_seed_value_ux()
assert mnemonic == seed_words
e_master_xpub = dev.send_recv(CCProtocolPacker.get_xpub(), timeout=5000)
assert e_master_xpub != (simulator_fixed_tpub if testnet else simulator_fixed_xpub)
if xpub:
assert e_master_xpub == xpub
psbt = fake_txn(2, 2, master_xpub=e_master_xpub, segwit_in=True)
try_sign(psbt, accept=True, finalize=True) # MUST NOT raise
press_select()
return in_effect_xfp
return doit
@pytest.fixture
def generate_ephemeral_words(goto_eph_seed_menu, pick_menu_item, press_select,
need_keypress, cap_story, settings_set, seed_story_to_words,
ephemeral_seed_disabled_ui, confirm_tmp_seed):
def doit(num_words, dice=False, from_main=False, seed_vault=None, testnet=True):
if testnet:
netcode = "XTN"
else:
netcode = "BTC"
settings_set("chain", netcode)
goto_eph_seed_menu()
if from_main:
ephemeral_seed_disabled_ui()
pick_menu_item("Generate Words")
if not dice:
pick_menu_item(f"{num_words} Words")
time.sleep(0.1)
else:
pick_menu_item(f"{num_words} Word Dice Roll")
for ch in '123456\r\r':
need_keypress(ch)
time.sleep(0.2)
title, story = cap_story()
assert f"Record these {num_words} secret words!" in story
assert "Press (6) to skip word quiz" in story
# filter those that starts with space, number and colon --> actual words
e_seed_words = seed_story_to_words(story)
assert len(e_seed_words) == num_words
need_keypress("6") # skip quiz
press_select() # yes - I'm sure
confirm_tmp_seed(seedvault=seed_vault)
return e_seed_words
return doit
@pytest.fixture
def import_ephemeral_xprv(microsd_path, virtdisk_path, goto_eph_seed_menu,
pick_menu_item, need_keypress, cap_story, settings_set,
nfc_write_text, ephemeral_seed_disabled_ui, confirm_tmp_seed,
press_nfc, press_select, is_q1):
def doit(way, extended_key=None, testnet=True, seed_vault=False, from_main=False):
from pycoin.key.BIP32Node import BIP32Node
if testnet:
netcode = "XTN"
else:
netcode = "BTC"
settings_set("chain", netcode)
fname = "ek.txt"
if extended_key is None:
node = BIP32Node.from_master_secret(os.urandom(32), netcode=netcode)
ek = node.hwif(as_private=True) + '\n'
else:
node = BIP32Node.from_wallet_key(extended_key)
assert extended_key == node.hwif(as_private=True)
ek = extended_key
if way == "sd":
fpath = microsd_path(fname)
elif way == "vdisk":
fpath = virtdisk_path(fname)
if way != "nfc":
with open(fpath, "w") as f:
f.write(ek)
if testnet:
assert "tprv" in ek
else:
assert "xprv" in ek
goto_eph_seed_menu()
if from_main:
ephemeral_seed_disabled_ui()
pick_menu_item("Import XPRV")
time.sleep(0.1)
_, story = cap_story()
if way == "sd":
if "Press (1) to import extended private key file from SD Card" in story:
need_keypress("1")
elif way == "nfc":
if f"press {KEY_NFC if is_q1 else '(3)'} to import via NFC" not in story:
pytest.xfail("NFC disabled")
else:
press_nfc()
time.sleep(0.2)
nfc_write_text(ek)
time.sleep(0.3)
else:
# virtual disk
if "press (2) to import from Virtual Disk" not in story:
pytest.xfail("Vdisk disabled")
else:
need_keypress("2")
if way != "nfc":
time.sleep(0.1)
pick_menu_item(fname)
confirm_tmp_seed(expect_xfp=node.fingerprint().hex().upper(),
seedvault=seed_vault)
return node
return doit
@pytest.mark.parametrize("num_words", [12, 24])
@pytest.mark.parametrize("dice", [False, True])
@pytest.mark.parametrize("seed_vault", [False, True])
@pytest.mark.parametrize("preserve_settings", [False, True])
def test_ephemeral_seed_generate(num_words, generate_ephemeral_words, dice,
reset_seed_words, goto_eph_seed_menu, seed_vault,
ephemeral_seed_disabled, verify_ephemeral_secret_ui,
preserve_settings, seed_vault_enable, seed_vault_delete,
restore_main_seed):
reset_seed_words()
goto_eph_seed_menu()
ephemeral_seed_disabled()
seed_vault_enable(seed_vault)
e_seed_words = generate_ephemeral_words(num_words=num_words, dice=dice,
from_main=True, seed_vault=seed_vault)
xfp = verify_ephemeral_secret_ui(mnemonic=e_seed_words, seed_vault=seed_vault)
if seed_vault:
seed_vault_delete(xfp, not preserve_settings)
else:
restore_main_seed(preserve_settings)
@pytest.mark.parametrize("num_words", [12, 18, 24])
@pytest.mark.parametrize("nfc", [False, True])
@pytest.mark.parametrize("truncated", [False, True])
@pytest.mark.parametrize("preserve_settings", [False, True])
@pytest.mark.parametrize("seed_vault", [False, True])
def test_ephemeral_seed_import_words(nfc, truncated, num_words, cap_menu, pick_menu_item,
reset_seed_words, goto_eph_seed_menu,
word_menu_entry, nfc_write_text, verify_ephemeral_secret_ui,
ephemeral_seed_disabled, get_seed_value_ux, seed_vault,
settings_set, cap_story, preserve_settings, seed_vault_enable,
seed_vault_delete, restore_main_seed, confirm_tmp_seed):
if truncated and not nfc: return
words, expect_xfp = WORDLISTS[num_words]
reset_seed_words()
seed_vault_enable(seed_vault)
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Import Words")
if not nfc:
pick_menu_item(f"{num_words} Words")
time.sleep(0.1)
word_menu_entry(words.split())
else:
menu = cap_menu()
if 'Import via NFC' not in menu:
raise pytest.xfail("NFC not enabled")
pick_menu_item('Import via NFC')
if truncated:
truncated_words = truncate_seed_words(words)
nfc_write_text(truncated_words)
else:
nfc_write_text(words)
time.sleep(.5)
confirm_tmp_seed(seedvault=seed_vault)
xfp = verify_ephemeral_secret_ui(mnemonic=words.split(" "), expected_xfp=expect_xfp,
seed_vault=seed_vault)
nfc_seed = get_seed_value_ux(nfc=True) # export seed via NFC (always truncated)
seed_words = get_seed_value_ux()
assert " ".join(nfc_seed) == truncate_seed_words(seed_words)
if seed_vault:
seed_vault_delete(xfp, not preserve_settings)
else:
restore_main_seed(preserve_settings)
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"])
@pytest.mark.parametrize("testnet", [True, False])
@pytest.mark.parametrize("preserve_settings", [False, True])
@pytest.mark.parametrize("seed_vault", [False, True])
def test_ephemeral_seed_import_tapsigner(way, testnet, pick_menu_item, cap_story, enter_hex,
need_keypress, reset_seed_words, goto_eph_seed_menu,
verify_ephemeral_secret_ui, ephemeral_seed_disabled,
nfc_write_text, tapsigner_encrypted_backup, seed_vault,
preserve_settings, seed_vault_enable, settings_set,
seed_vault_delete, restore_main_seed, confirm_tmp_seed,
is_q1, press_select, press_nfc):
reset_seed_words()
if testnet:
netcode = "XTN"
else:
netcode = "BTC"
settings_set("chain", netcode)
seed_vault_enable(seed_vault)
fname, backup_key_hex, node = tapsigner_encrypted_backup(way, testnet=testnet)
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Tapsigner Backup")
time.sleep(0.1)
_, story = cap_story()
if way == "sd":
if "Press (1) to import TAPSIGNER encrypted backup file from SD Card" in story:
need_keypress("1")
elif way == "nfc":
if f"press {KEY_NFC if is_q1 else '(3)'} to import via NFC" not in story:
pytest.xfail("NFC disabled")
else:
press_nfc()
time.sleep(0.2)
nfc_write_text(fname)
time.sleep(0.3)
else:
# virtual disk
if "press (2) to import from Virtual Disk" not in story:
pytest.xfail("Vdisk disabled")
else:
need_keypress("2")
if way != "nfc":
time.sleep(0.1)
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "your TAPSIGNER" in story
assert "back of the card" in story
press_select() # yes I have backup key
enter_hex(backup_key_hex)
confirm_tmp_seed(expect_xfp=node.fingerprint().hex().upper(),
seedvault=seed_vault)
xfp = verify_ephemeral_secret_ui(xpub=node.hwif(), seed_vault=seed_vault,
testnet=testnet)
if seed_vault:
seed_vault_delete(xfp, not preserve_settings)
else:
restore_main_seed(preserve_settings)
@pytest.mark.parametrize("fail", ["wrong_key", "key_len", "plaintext", "garbage"])
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,
settings_set, press_select, press_cancel):
reset_seed_words()
settings_set("seedvault", None)
fail_msg = "Decryption failed - wrong key?"
fname, backup_key_hex, node = tapsigner_encrypted_backup("sd", testnet=False)
if fail == "plaintext":
with open(microsd_path(fname), "w") as f:
f.write(node.hwif(True) + "\n")
if fail == "garbage":
with open(microsd_path(fname), "wb") as f:
f.write(os.urandom(152))
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Tapsigner Backup")
time.sleep(0.1)
_, story = cap_story()
if "Press (1) to import TAPSIGNER encrypted backup file from SD Card" in story:
need_keypress("1")
time.sleep(0.1)
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "Press OK to continue X to cancel." in story
press_select() # yes I have backup key
if fail == "wrong_key":
backup_key_hex = os.urandom(16).hex()
if fail == "key_len":
backup_key_hex = os.urandom(15).hex()
fail_msg = "'Backup Key' length != 32"
enter_hex(backup_key_hex)
time.sleep(0.3)
if fail == "key_len" and is_q1:
assert "Need 32 char" in cap_screen()
press_cancel()
return
title, story = cap_story()
assert title == "FAILURE"
assert fail_msg in story
press_cancel()
press_cancel()
@pytest.mark.parametrize("data", [
(
"backup-4VMI3-2023-02-15T1645.aes",
"cb5bec9ddea4e85558bb54f41dcb1d2e",
"xpub661MyMwAqRbcFkTtUfByC6u46vJtdw6xFHUFhjc2AvA16BJCUPoeuwQcthN6yshHR34WZBT5gsHYVtha2QD9j9QozJf9ENeHS6TDgSAFBeX"
),
(
"backup-O4MZA-2023-02-15T2250.aes",
"578efa5d6803e3c314a98a87d499ce97",
"xpub661MyMwAqRbcGBeMu9h1B222hQmc4XkXasbN4F3mDGTWRJ11UQ5orWv41FPVK7stXsS9UtR5DBTArBvcsHPiCE2E1PAdqq1UQiQTYmrEEaa"
),
])
def test_ephemeral_seed_import_tapsigner_real(data, pick_menu_item, cap_story, microsd_path,
need_keypress, reset_seed_words, enter_hex,
goto_eph_seed_menu, verify_ephemeral_secret_ui,
ephemeral_seed_disabled, settings_set,
confirm_tmp_seed, restore_main_seed, press_select):
fname, backup_key_hex, pub = data
fpath = microsd_path(fname)
shutil.copy(f"data/{fname}", fpath)
reset_seed_words()
settings_set("seedvault", None)
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Tapsigner Backup")
time.sleep(0.1)
_, story = cap_story()
if "Press (1) to import TAPSIGNER encrypted backup file from SD Card" in story:
need_keypress("1")
time.sleep(0.1)
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "Press OK to continue X to cancel." in story
press_select() # yes I have backup key
enter_hex(backup_key_hex)
confirm_tmp_seed(seedvault=False)
verify_ephemeral_secret_ui(xpub=pub)
os.unlink(fpath)
restore_main_seed(False)
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"])
@pytest.mark.parametrize("testnet", [True, False])
@pytest.mark.parametrize("preserve_settings", [False, True])
@pytest.mark.parametrize("seed_vault", [False, True])
def test_ephemeral_seed_import_xprv(way, testnet, reset_seed_words,
goto_eph_seed_menu, verify_ephemeral_secret_ui,
ephemeral_seed_disabled, import_ephemeral_xprv,
preserve_settings, seed_vault, seed_vault_enable,
seed_vault_delete, restore_main_seed, confirm_tmp_seed):
reset_seed_words()
goto_eph_seed_menu()
seed_vault_enable(seed_vault)
ephemeral_seed_disabled()
node = import_ephemeral_xprv(way=way, testnet=testnet, from_main=True,
seed_vault=seed_vault)
xfp = verify_ephemeral_secret_ui(xpub=node.hwif(), seed_vault=seed_vault,
testnet=testnet)
if seed_vault:
seed_vault_delete(xfp, not preserve_settings)
else:
restore_main_seed(preserve_settings)
@pytest.mark.parametrize("seed_vault", [True, False])
def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
ephemeral_seed_disabled, cap_story,
pick_menu_item, press_select,
word_menu_entry, settings_set,
seed_vault, seed_vault_enable,
confirm_tmp_seed, is_q1):
reset_seed_words()
seed_vault_enable(seed_vault)
goto_eph_seed_menu()
ephemeral_seed_disabled()
words, expected_xfp = WORDLISTS[12]
pick_menu_item("Import Words")
pick_menu_item(f"12 Words")
time.sleep(0.1)
word_menu_entry(words.split())
in_effect_xfp = confirm_tmp_seed(seedvault=seed_vault)
goto_eph_seed_menu()
pick_menu_item("Import Words")
pick_menu_item(f"12 Words")
time.sleep(0.1)
word_menu_entry(words.split())
time.sleep(0.2)
title, story = cap_story()
assert "Temporary master key already in use" in story
assert title == "FAILED"
assert in_effect_xfp == expected_xfp
press_select()
@pytest.mark.parametrize('data', SEEDVAULT_TEST_DATA)
def test_seed_vault_menus(dev, data, settings_set, master_settings_get, pick_menu_item,
need_keypress, cap_story, cap_menu, reset_seed_words,
get_identity_story, get_seed_value_ux, fake_txn, try_sign,
sim_exec, goto_home, seed_vault_enable, is_q1, enter_text,
press_select, press_cancel, press_delete):
# Verify "seed vault" feature works as intended
reset_seed_words()
xfp, entropy, mnemonic = data
# 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
settings_set("seedvault", None)
settings_set("seeds", [(xfp, stored_secret.hex(), f"[{xfp}]", "meta")])
# enable Seed Vault
goto_home()
seed_vault_enable(True)
time.sleep(.1)
pick_menu_item("Seed Vault")
time.sleep(.1)
m = cap_menu()
assert len(m) == 1
assert xfp in m[0]
pick_menu_item(m[0])
# Now in submenu for saved seed
# view details.
pick_menu_item('[%s]' % xfp)
_, story = cap_story()
assert xfp in story
if mnemonic:
assert ('%d words' % (6 * (vlen // 8))) in story
else:
assert 'xprv' in story
press_cancel()
# rename
pick_menu_item("Rename")
if not is_q1:
for _ in range(len(xfp) + 1): # [xfp]
press_delete()
# below should yield AAAA
need_keypress("1")
for _ in range(3):
need_keypress("9") # next char
need_keypress("1") # letters
press_select()
else:
need_keypress(KEY_CLEAR)
enter_text('AAAA')
m = cap_menu()
assert m[0] == "AAAA"
# check parent menu - must be updated too
press_cancel()
m = cap_menu()
for item in m:
if "AAAA" in item:
break
else:
assert False
# go back
press_select()
pick_menu_item("Use This Seed")
time.sleep(.1)
title, story = cap_story()
assert xfp in title
assert 'temporary master key is in effect now' in story
press_select()
active = get_seed_value_ux()
if mnemonic:
assert active == mnemonic.split()
else:
assert active[1:4] == 'prv'
node = BIP32Node.from_hwif(active)
ch, pk = entropy_bytes[1:33], entropy_bytes[33:65]
assert node.chain_code() == ch
assert node.secret_exponent() == int(pk.hex(), 16)
istory = get_identity_story()
assert "Temporary seed is in effect" in istory
ident_xfp = istory.split("\n\n")[1].strip()
assert ident_xfp == xfp
e_master_xpub = dev.send_recv(CCProtocolPacker.get_xpub(), timeout=5000)
assert e_master_xpub != simulator_fixed_tpub
psbt = fake_txn(2, 2, master_xpub=e_master_xpub, segwit_in=True)
try_sign(psbt, accept=True, finalize=True) # MUST NOT raise
press_select()
encoded = sim_exec('from pincodes import pa; RV.write(repr(pa.fetch()))')
assert 'Error' not in encoded
encoded = eval(encoded)
assert len(encoded) == 72
assert encoded[0:len(stored_secret)] == stored_secret
# check rename worked
seeds = master_settings_get("seeds")
assert len(seeds) == 1
entry = seeds[0]
assert len(entry) == 4
assert entry[0] == xfp
assert entry[1] == stored_secret.hex()
assert entry[2] == "AAAA"
reset_seed_words()
time.sleep(.2)
goto_home()
def test_seed_vault_captures(request, dev, settings_set, settings_get, pick_menu_item,
cap_story, reset_seed_words, fake_txn, master_settings_get,
generate_ephemeral_words, goto_home, get_secrets,
import_ephemeral_xprv, set_bip39_pw, restore_main_seed,
restore_seed_xor, derive_bip85_secret, activate_bip85_ephemeral,
seed_vault_enable, is_q1, press_select, press_down):
# Capture seeds by all the different paths and verify correct values are captured.
# - BIP-85 -> 12, 24 words
# - BIP-85 -> xprv (BIP-32)
# - XOR seed restore
# - Ephemeral keys menu: random and import
# - Capture a BIP-39 passphrase into words
# - Trick pin -> duress wallet * 4 options
# Then, verify those can all co-exist and be recalled correctly.
reset_seed_words()
seed_vault_enable(True)
settings_set("seeds", [])
expect_count = 0
if 1:
# BIP39 Passphrase
set_bip39_pw('dogsNcats', seed_vault=True, reset=False)
expect_count += 1
restore_main_seed(seed_vault=True)
if 1:
# Trick Pin -> duress wallet
from test_se2 import build_duress_wallets
expect_count += build_duress_wallets(request, seed_vault=True)
if 1:
# Seed XOR of 12words into 3 parts... not simple, kinda slow
xor_parts, xor_expect = (
['become wool crumble brand camera cement gloom sell stand once connect stage',
'save saddle indicate embrace detail weasel spread life staff mushroom bicycle light',
'unlock damp injury tape enhance pause sheriff onion valley panic finger moon'],
'drama jeans craft mixture filter lamp invest suggest vacant neutral history swim')
restore_seed_xor(xor_parts, xor_expect, incl_self=None, save_to_vault=True)
# check was saved
expect_count += 1
restore_main_seed(seed_vault=True)
if 1:
# Create via BIP-85
for mode in ['12 words', '18 words', '24 words', 'XPRV (BIP-32)']:
do_import, story = derive_bip85_secret(mode, index=74, chain="XTN")
activate_bip85_ephemeral(do_import, reset=False, save_to_vault=True)
expect_count += 1
restore_main_seed(seed_vault=True)
if 1:
# Ephemeral seeds - generated words (behaves same as imported words)
for num_words in [12, 24]:
generate_ephemeral_words(num_words=num_words, seed_vault=True)
expect_count += 1
# Ephemeral seeds - extended keys
import_ephemeral_xprv("sd", seed_vault=True)
expect_count += 1
restore_main_seed(seed_vault=True)
# check all saved okay
seeds = master_settings_get('seeds')
n_seeds = len(seeds)
assert n_seeds == expect_count
# Switch to each one
for i, obj in enumerate(seeds):
xfp, encoded_sec, name, meta = obj
pick_menu_item("Seed Vault")
for _ in range(i):
press_down() # go down
press_select()
pick_menu_item('Use This Seed')
time.sleep(0.1)
title, story = cap_story()
assert 'New temporary master key' in story
assert 'power down' not in story
assert xfp in title
press_select() # confirm activation of ephemeral secret
assert xfp2str(settings_get('xfp')) == xfp
raw = get_secrets()['raw_secret']
if len(raw) % 2:
raw += '0'
assert raw == encoded_sec
if 1:
# cleanup
reset_seed_words()
settings_set("seedvault", None)
settings_set("seeds", [])
def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item,
generate_ephemeral_words, import_ephemeral_xprv,
goto_home, cap_story, cap_menu, restore_main_seed,
need_keypress, seed_vault_enable, is_q1, do_keypresses,
press_select, press_cancel, press_down, press_delete):
reset_seed_words()
seed_vault_enable(True)
settings_set("seeds", [])
generate_ephemeral_words(num_words=24, seed_vault=True)
generate_ephemeral_words(num_words=12, seed_vault=True)
import_ephemeral_xprv("sd", seed_vault=True)
import_ephemeral_xprv("sd", seed_vault=True)
goto_home()
pick_menu_item("Seed Vault")
m = cap_menu()
# 4 entries + Restore Master (as we are in ephemeral)
assert len(m) == 5
# restore to main seed
restore_main_seed(seed_vault=True, preserve_settings=True)
time.sleep(.1)
m = cap_menu()
# no ephemeral xfp at the top
assert m[0] == "Ready To Sign"
pick_menu_item("Seed Vault")
# we are no longer in ephemral
assert "Restore Master" not in m
# first entry in menu
press_select()
m = cap_menu()
assert "Rename" in m
assert "Use This Seed" in m # we are in master - so this must be there
assert "Delete" in m
# delete entry 0
pick_menu_item("Delete")
press_select()
time.sleep(.1)
m = cap_menu()
assert len(m) == 3
# first entry again
press_select()
pick_menu_item("Rename")
for _ in range(11):
press_delete()
if is_q1:
do_keypresses("AA")
else:
need_keypress("1") # big letters
need_keypress("9")
need_keypress("1")
# name changed to AA
press_select()
m = cap_menu()
assert m[0] == "AA"
assert "Rename" in m
assert "Use This Seed" in m # we are in master - so this must be there
assert "Delete" in m
# go back
press_cancel()
# second item
press_down()
press_select()
time.sleep(.1)
pick_menu_item("Use This Seed")
title, _ = cap_story()
press_select() # confirm new eph
time.sleep(.1)
m = cap_menu()
assert m[0] == title
pick_menu_item("Seed Vault")
press_down()
press_select()
time.sleep(.1)
m = cap_menu()
assert "Rename" in m
assert "Seed In Use" in m
assert "Delete" in m
pick_menu_item("Rename")
for _ in range(11):
press_delete()
if is_q1:
do_keypresses("AAA")
else:
need_keypress("1") # big letters
need_keypress("9")
need_keypress("1")
need_keypress("9")
need_keypress("1")
# name changed to AAA
press_select()
time.sleep(.1)
m = cap_menu()
assert m[0] == "AAA"
pick_menu_item("Delete")
press_select()
time.sleep(.1)
m = cap_menu()
# after we delete from seed vault together with its settings
# we're back to master secret
assert m[0] == "Ready To Sign"
pick_menu_item("Seed Vault")
time.sleep(.1)
m = cap_menu()
assert len(m) == 2
press_down()
press_select()
pick_menu_item("Use This Seed")
title, _ = cap_story()
press_select() # confirm new eph
time.sleep(.1)
m = cap_menu()
assert m[0] == title
pick_menu_item("Seed Vault")
press_down()
press_select()
time.sleep(.1)
m = cap_menu()
assert "Rename" in m
assert "Seed In Use" in m
assert "Delete" in m
pick_menu_item("Delete")
need_keypress("1") # only delete from seed vault
time.sleep(.1)
m = cap_menu()
assert len(m) == 3
assert "Add current tmp" in m
press_select()
# this is now different eph - modification not allowed
time.sleep(.1)
m = cap_menu()
assert "Rename" not in m
assert "Delete" not in m
assert "Use This Seed" in m
goto_home()
time.sleep(.1)
m = cap_menu()
# still in ephemeral
assert title == m[0]
def test_xfp_collision(reset_seed_words, settings_set, import_ephemeral_xprv,
cap_story, press_cancel, pick_menu_item, cap_menu,
seed_vault_enable):
node = BIP32Node.from_master_secret(os.urandom(32), netcode="XTN")
xfp = node.fingerprint().hex().upper()
k0 = node.hwif(as_private=True)
# change chain code but presevre public key
node._chain_code = hashlib.sha256(node._chain_code).digest()
k1 = node.hwif(as_private=True)
assert k1 != k0
reset_seed_words()
seed_vault_enable(True)
settings_set("seeds", [])
import_ephemeral_xprv("sd", extended_key=k0, seed_vault=True, from_main=True)
import_ephemeral_xprv("sd", extended_key=k1, seed_vault=True, from_main=False)
pick_menu_item("Seed Vault")
m = cap_menu()
assert len(m) == 3 # two seeds and Restore Master
# same master fingerprints
assert xfp in m[0]
assert xfp in m[1]
# but only second is in use
pick_menu_item(m[1])
time.sleep(.1)
sm = cap_menu()
assert "Seed In Use" in sm
assert "Use This Seed" not in sm
press_cancel() # go back
pick_menu_item(m[0])
time.sleep(.1)
sm = cap_menu()
assert "Seed In Use" not in sm
assert "Use This Seed" in sm
@pytest.mark.parametrize("refuse", [False, True])
def test_add_current_active(reset_seed_words, settings_set, import_ephemeral_xprv,
goto_home, pick_menu_item, cap_story, cap_menu,
press_cancel, verify_ephemeral_secret_ui,
seed_vault_enable, refuse, press_select):
ADD_MI = "Add current tmp"
reset_seed_words()
goto_home()
seed_vault_enable(True)
settings_set("seeds", [])
time.sleep(.2)
# in master - do not offer
pick_menu_item("Seed Vault")
time.sleep(.1)
m = cap_menu()
assert ADD_MI not in m
goto_home()
node = import_ephemeral_xprv("sd", seed_vault=False, from_main=True)
xfp = node.fingerprint().hex().upper()
pick_menu_item("Seed Vault")
m = cap_menu()
assert ADD_MI in m
for mi in m:
assert xfp not in mi
pick_menu_item(ADD_MI)
time.sleep(.1)
title, story = cap_story()
assert xfp in title
assert "Add to Seed Vault?" in story
if refuse:
press_cancel()
time.sleep(.1)
m = cap_menu()
assert ADD_MI in m
for mi in m:
assert xfp not in mi
else:
press_select()
verify_ephemeral_secret_ui(xpub=node.hwif(), seed_vault=True)
@pytest.mark.parametrize('multisig', [False, 'multisig'])
@pytest.mark.parametrize('seedvault', [False, True])
@pytest.mark.parametrize('data', SEEDVAULT_TEST_DATA)
def test_temporary_from_backup(multisig, backup_system, import_ms_wallet, get_setting,
data, press_select, cap_story, set_encoded_secret,
reset_seed_words, check_and_decrypt_backup,
goto_eph_seed_menu, pick_menu_item, word_menu_entry,
verify_ephemeral_secret_ui, seedvault, settings_set,
seed_vault_enable, confirm_tmp_seed, settings_path,
seed_vault_delete, restore_main_seed, set_seed_words):
xfp_str, encoded_str, mnemonic = data
if mnemonic:
set_seed_words(mnemonic)
else:
encoded = a2b_hex(encoded_str)
set_encoded_secret(encoded)
settings_set("chain", "XTN")
if multisig:
import_ms_wallet(15, 15, dev_key=True)
press_select()
time.sleep(.1)
assert len(get_setting('multisig')) == 1
# ACTUAL BACKUP
bk_pw = backup_system()
time.sleep(.1)
title, story = cap_story()
fname = story.split("\n\n")[1]
check_and_decrypt_backup(fname, bk_pw)
# restore fixed simulator
reset_seed_words()
seed_vault_enable(seedvault)
goto_eph_seed_menu()
pick_menu_item("Coldcard Backup")
time.sleep(.1)
pick_menu_item(fname)
word_menu_entry(bk_pw, has_checksum=False)
confirm_tmp_seed(seedvault)
time.sleep(.1)
if mnemonic:
mnemonic = mnemonic.split(" ")
xfp = verify_ephemeral_secret_ui(mnemonic=mnemonic, xpub=None, # xpub veriphy ephemeral secret not tested here
seed_vault=seedvault)
if seedvault:
seed_vault_delete(xfp, not False)
else:
restore_main_seed(False)
def test_tmp_upgrade_disabled(reset_seed_words, pick_menu_item, cap_story,
cap_menu, goto_home, unit_test,
import_ephemeral_xprv):
reset_seed_words()
goto_home()
pick_menu_item("Advanced/Tools")
time.sleep(.1)
m = cap_menu()
assert "Upgrade Firmware" in m
node = BIP32Node.from_master_secret(os.urandom(32), netcode="XTN")
k0 = node.hwif(as_private=True)
import_ephemeral_xprv("sd", extended_key=k0, seed_vault=True, from_main=True)
goto_home()
pick_menu_item("Advanced/Tools")
time.sleep(.1)
m = cap_menu()
assert "Upgrade Firmware" not in m
# Virgin CC
unit_test('devtest/clear_seed.py')
m = cap_menu()
assert m[0] == 'New Seed Words'
goto_home()
pick_menu_item("Advanced/Tools")
time.sleep(.1)
m = cap_menu()
assert "Upgrade Firmware" in m
import_ephemeral_xprv("sd", extended_key=k0, seed_vault=True, from_main=True)
goto_home()
pick_menu_item("Advanced/Tools")
time.sleep(.1)
m = cap_menu()
assert "Upgrade Firmware" not in m
def test_import_master_as_tmp(reset_seed_words, goto_eph_seed_menu, cap_story,
ephemeral_seed_disabled, pick_menu_item, goto_home,
need_keypress, word_menu_entry, settings_set,
confirm_tmp_seed, cap_menu, microsd_path,
restore_main_seed, get_identity_story, press_select,
press_cancel):
reset_seed_words()
goto_eph_seed_menu()
ephemeral_seed_disabled()
# try import same seed as current simulator master
words, expected_xfp = simulator_fixed_words, simulator_fixed_xfp
xfp_str = xfp2str(expected_xfp)
pick_menu_item("Import Words")
pick_menu_item(f"24 Words")
time.sleep(0.1)
word_menu_entry(words.split())
time.sleep(.1)
title, story = cap_story()
assert "FAILED" == title
assert 'Cannot use master seed as temporary.' in story
press_cancel()
# go to ephemeral seed and then try to create new ephemeral seed from master
# when in different temporary seed whatsoever
goto_eph_seed_menu()
# random temporary seed
pick_menu_item("Generate Words")
pick_menu_item(f"12 Words")
need_keypress("6") # skip quiz
press_select() # yes - I'm sure
confirm_tmp_seed(seedvault=False)
goto_home()
time.sleep(0.1)
menu = cap_menu()
# ephemeral seed chosen
assert "[" in menu[0]
goto_eph_seed_menu()
pick_menu_item("Import Words")
pick_menu_item(f"24 Words")
time.sleep(0.1)
word_menu_entry(words.split())
time.sleep(.1)
title, story = cap_story()
assert "FAILED" == title
assert 'Cannot use master seed as temporary.' in story
press_cancel()
# now import same seed but represented as master extended key
# this works and does not delete master settings as encoded
# secret is different and therefore nvram_key too
fname = "ek_sim.txt"
with open(microsd_path(fname), "w") as f:
f.write(simulator_fixed_tprv)
goto_eph_seed_menu()
pick_menu_item("Import XPRV")
title, story = cap_story()
if "Press (1)" in story:
need_keypress("1")
pick_menu_item(fname)
confirm_tmp_seed(seedvault=False) # allowed
# verify we are in temporary seed
goto_home()
time.sleep(0.1)
menu = cap_menu()
# ephemeral seed chosen
assert "[" in menu[0]
assert xfp_str in menu[0]
restore_main_seed(preserve_settings=False, seed_vault=False)
story = get_identity_story()
assert "00000000" not in story
assert xfp_str in story
# EOF