firmware/testing/test_ephemeral.py
scgbckbone f190edb302 fix tests
(cherry picked from commit 697b6e211d)
2024-01-04 11:52:47 -05:00

1403 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
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, need_keypress, 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:
need_keypress("y")
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:
need_keypress("y")
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)
def seed_story_to_words(story: str):
# filter those that starts with space, number and colon --> actual words
# NOTE: will show xprv/tprv in full if we are not storing
# words (ie. BIP-32 loaded as master secret). So just return that string.
if story[1:4] == 'prv':
return story.split()[0]
words = [
line.strip().split(":")[1].strip()
for line in story.split("\n")
if re.search(r"\s\d:", line) or re.search(r"\d{2}:", line)
]
return 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):
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
assert 'can control all funds' in body
need_keypress('y') # skip warning
time.sleep(0.01)
title, story = cap_story()
if nfc:
need_keypress("1") # show QR code
time.sleep(.2)
need_keypress("3") # any QR can be exported via NFC
time.sleep(.2)
str_words = nfc_read_text()
time.sleep(.5)
need_keypress("y") # 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):
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()
ch = "y"
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:
ch = "1"
need_keypress(ch)
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):
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
need_keypress("y")
else:
need_keypress("y") # 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
need_keypress("y")
return expect_xfp
return doit
@pytest.fixture
def seed_vault_delete(pick_menu_item, need_keypress, cap_menu, cap_story,
goto_home):
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:
need_keypress("y")
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, need_keypress, 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):
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()
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
need_keypress("y")
return in_effect_xfp
return doit
@pytest.fixture
def generate_ephemeral_words(goto_eph_seed_menu, pick_menu_item,
need_keypress, cap_story, settings_set,
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 '123456yy':
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
need_keypress("y") # 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):
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 "press (3) to import via NFC" not in story:
pytest.xfail("NFC disabled")
else:
need_keypress("3")
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)
_, story = cap_story()
assert "Select file containing the extended private key" in story
need_keypress("y")
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,
need_keypress, 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):
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 "press (3) to import via NFC" not in story:
pytest.xfail("NFC disabled")
else:
need_keypress("3")
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)
_, story = cap_story()
assert "Pick TAPSIGNER encrypted backup file" in story
need_keypress("y")
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "your TAPSIGNER" in story
assert "back of the card" in story
need_keypress("y") # 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,
need_keypress, reset_seed_words, enter_hex,
tapsigner_encrypted_backup, goto_eph_seed_menu,
microsd_path, ephemeral_seed_disabled, settings_set):
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)
_, story = cap_story()
assert "Pick TAPSIGNER encrypted backup file" in story
need_keypress("y")
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "Press OK to continue X to cancel." in story
need_keypress("y") # 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)
title, story = cap_story()
assert title == "FAILURE"
assert fail_msg in story
need_keypress("x")
need_keypress("x")
@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):
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)
_, story = cap_story()
assert "Pick TAPSIGNER encrypted backup file" in story
need_keypress("y")
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "Press OK to continue X to cancel." in story
need_keypress("y") # 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, need_keypress,
word_menu_entry, settings_set,
seed_vault, seed_vault_enable,
confirm_tmp_seed):
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
need_keypress("y")
@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):
# 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
need_keypress("x")
# rename
pick_menu_item("Rename")
for _ in range(len(xfp) + 1): # [xfp]
need_keypress("x")
# below should yield AAAA
need_keypress("1")
for _ in range(3):
need_keypress("9") # next char
need_keypress("1") # letters
need_keypress("y")
m = cap_menu()
assert m[0] == "AAAA"
# check parnt menu - must be updated too
need_keypress("x")
m = cap_menu()
for item in m:
if "AAAA" in item:
break
else:
assert False
# go back
need_keypress("y")
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
need_keypress("y")
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
need_keypress("y")
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)
need_keypress("x")
def test_seed_vault_captures(request, dev, settings_set, settings_get, pick_menu_item,
need_keypress, cap_story, reset_seed_words, fake_txn,
generate_ephemeral_words, goto_home, get_secrets, master_settings_get,
import_ephemeral_xprv, set_bip39_pw, restore_main_seed,
restore_seed_xor, derive_bip85_secret, activate_bip85_ephemeral,
seed_vault_enable):
# 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):
need_keypress("8") # go down
need_keypress("y")
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
need_keypress("y") # 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):
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
need_keypress("y")
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")
need_keypress("y")
time.sleep(.1)
m = cap_menu()
assert len(m) == 3
# first entry again
need_keypress("y")
pick_menu_item("Rename")
for _ in range(9):
need_keypress("x")
need_keypress("1") # big letters
need_keypress("9")
need_keypress("1")
# name changed to AA
need_keypress("y")
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
need_keypress("x")
# second item
need_keypress("8")
need_keypress("y")
time.sleep(.1)
pick_menu_item("Use This Seed")
title, _ = cap_story()
need_keypress("y") # confirm new eph
time.sleep(.1)
m = cap_menu()
assert m[0] == title
pick_menu_item("Seed Vault")
need_keypress("8")
need_keypress("y")
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(9):
need_keypress("x")
need_keypress("1") # big letters
need_keypress("9")
need_keypress("1")
need_keypress("9")
need_keypress("1")
# name changed to AAA
need_keypress("y")
time.sleep(.1)
m = cap_menu()
assert m[0] == "AAA"
pick_menu_item("Delete")
need_keypress("y")
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
need_keypress("8")
need_keypress("y")
pick_menu_item("Use This Seed")
title, _ = cap_story()
need_keypress("y") # confirm new eph
time.sleep(.1)
m = cap_menu()
assert m[0] == title
pick_menu_item("Seed Vault")
need_keypress("8")
need_keypress("y")
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
need_keypress("y")
# 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, need_keypress, 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
need_keypress("x") # 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,
need_keypress, verify_ephemeral_secret_ui,
seed_vault_enable, refuse):
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:
need_keypress("x")
time.sleep(.1)
m = cap_menu()
assert ADD_MI in m
for mi in m:
assert xfp not in mi
else:
need_keypress("y")
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, need_keypress, 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)
need_keypress('y')
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)
_, story = cap_story()
if "Select file containing the backup" in story:
need_keypress("y")
time.sleep(.1)
pick_menu_item(fname)
word_menu_entry(bk_pw)
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, need_keypress, 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):
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
need_keypress("x")
# 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
need_keypress("y") # 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
need_keypress("x")
# 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")
need_keypress("y") # Select file containing...
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