restore to main se2 secret without reboot; active ephemeral seeds have first home menu item [XFP]; tests reorg - created separate test_backup.py; add ability to remove ephemeral seed settings via Restore Seed
This commit is contained in:
parent
325435b678
commit
fe63163c85
@ -4,6 +4,7 @@
|
||||
- New Feature: `Lock Down Seed` now works with every ephemeral secret (not just BIP39 passphrase)
|
||||
- New Feature: BIP-39 Passphrase can now be added to word based Ephemeral Seeds
|
||||
- New Feature: Add ability to back-up BIP39 Passphrase wallet
|
||||
- New Feature: Restore to main secret from ephemeral without need to reboot the device
|
||||
- Enhancement: Shortcut to `Batch Sign PSBT` via `Ready To Sign` -> `Press (9)`
|
||||
- Enhancement: Old plausible deniability feature on fresh COLDCARD removed.
|
||||
Only needed for Mk 2-3 where SPI flash was external chip,
|
||||
|
||||
@ -792,15 +792,6 @@ async def start_login_sequence():
|
||||
from glob import dis
|
||||
import callgate
|
||||
|
||||
if version.mk_num < 4:
|
||||
# Block very obsolete versions.
|
||||
try:
|
||||
MIN_WATERMARK = b'!\x03)\x19\'"\x00\x00' # b2a_hex('2103291927220000')
|
||||
now = callgate.get_highwater()
|
||||
if now < MIN_WATERMARK:
|
||||
callgate.set_highwater(MIN_WATERMARK)
|
||||
except: pass
|
||||
|
||||
if pa.is_blank():
|
||||
# Blank devices, with no PIN set all, can continue w/o login
|
||||
goto_top_menu()
|
||||
@ -924,26 +915,40 @@ async def start_login_sequence():
|
||||
await ar.interact()
|
||||
except: pass
|
||||
|
||||
if version.mk_num >= 4:
|
||||
if version.has_nfc and settings.get('nfc', 0):
|
||||
# Maybe allow NFC now
|
||||
import nfc
|
||||
nfc.NFCHandler.startup()
|
||||
if version.has_nfc and settings.get('nfc', 0):
|
||||
# Maybe allow NFC now
|
||||
import nfc
|
||||
nfc.NFCHandler.startup()
|
||||
|
||||
if settings.get('vidsk', 0):
|
||||
# Maybe start virtual disk
|
||||
import vdisk
|
||||
vdisk.VirtDisk()
|
||||
if settings.get('vidsk', 0):
|
||||
# Maybe start virtual disk
|
||||
import vdisk
|
||||
vdisk.VirtDisk()
|
||||
|
||||
# Allow USB protocol, now that we are auth'ed
|
||||
if not settings.get('du', 0):
|
||||
from usb import enable_usb
|
||||
enable_usb()
|
||||
|
||||
async def restore_main_secret(*a):
|
||||
ch = await ux_show_story(
|
||||
"Restore main wallet and its settings?\n\n"
|
||||
"Press OK to forget current ephemeral wallet "
|
||||
"settings, or press (1) to save & keep "
|
||||
"those settings for later use.",
|
||||
escape="1"
|
||||
)
|
||||
if ch == "x": return
|
||||
|
||||
from seed import restore_to_main_secret
|
||||
await restore_to_main_secret(False if ch == "y" else True)
|
||||
goto_top_menu()
|
||||
|
||||
def make_top_menu():
|
||||
from menu import MenuSystem
|
||||
from menu import MenuSystem, MenuItem
|
||||
from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu
|
||||
from glob import hsm_active
|
||||
from glob import hsm_active, settings
|
||||
from pincodes import pa
|
||||
|
||||
if hsm_active:
|
||||
from hsm_ux import hsm_ux_obj
|
||||
@ -956,7 +961,18 @@ def make_top_menu():
|
||||
else:
|
||||
assert pa.is_successful(), "nonblank but wrong pin"
|
||||
|
||||
m = MenuSystem(EmptyWallet if pa.is_secret_blank() else NormalSystem)
|
||||
if not pa.is_secret_blank():
|
||||
_cls = NormalSystem[:]
|
||||
if pa.tmp_value:
|
||||
active_xfp = settings.get("xfp", 0)
|
||||
if active_xfp:
|
||||
ui_xfp = "[" + xfp2str(active_xfp) + "]"
|
||||
_cls.insert(0, MenuItem(ui_xfp, f=ready2sign))
|
||||
_cls.append(MenuItem("Restore Seed", f=restore_main_secret))
|
||||
else:
|
||||
_cls = EmptyWallet
|
||||
|
||||
m = MenuSystem(_cls)
|
||||
return m
|
||||
|
||||
def goto_top_menu(first_time=False):
|
||||
@ -1976,12 +1992,11 @@ We strongly recommend all PIN codes used be unique between each other.
|
||||
title="Try Again")
|
||||
continue
|
||||
|
||||
if version.mk_num >= 4:
|
||||
from trick_pins import tp
|
||||
prob = tp.check_new_main_pin(pin)
|
||||
if prob:
|
||||
await ux_show_story(prob, title="Try Again")
|
||||
continue
|
||||
from trick_pins import tp
|
||||
prob = tp.check_new_main_pin(pin)
|
||||
if prob:
|
||||
await ux_show_story(prob, title="Try Again")
|
||||
continue
|
||||
|
||||
break
|
||||
|
||||
@ -2018,10 +2033,9 @@ We strongly recommend all PIN codes used be unique between each other.
|
||||
# we cannot/need not login again
|
||||
pa.login()
|
||||
|
||||
if version.mk_num >= 4:
|
||||
# Deltamode trick pins need to track main pin
|
||||
from trick_pins import tp
|
||||
tp.main_pin_has_changed(pa.pin.decode())
|
||||
# Deltamode trick pins need to track main pin
|
||||
from trick_pins import tp
|
||||
tp.main_pin_has_changed(pa.pin.decode())
|
||||
|
||||
if mode == 'duress':
|
||||
# program the duress secret now... it's derived from real wallet contents
|
||||
@ -2062,7 +2076,7 @@ async def show_version(*a):
|
||||
serial += '\n\nNFC UID:\n' + NFC.get_uid().replace(':', '')
|
||||
|
||||
hw = version.hw_label
|
||||
if not version.has_nfc and version.mk_num >= 4:
|
||||
if not version.has_nfc:
|
||||
hw += ' (no NFC)'
|
||||
|
||||
msg = '''\
|
||||
|
||||
@ -199,12 +199,14 @@ def restore_from_dict_ll(vals):
|
||||
|
||||
if k == 'tp':
|
||||
# restore trick pins, which may involve many ops
|
||||
if version.mk_num >= 4:
|
||||
from trick_pins import tp
|
||||
try:
|
||||
tp.restore_backup(vals[key])
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc)
|
||||
from trick_pins import tp
|
||||
try:
|
||||
tp.restore_backup(vals[key])
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc)
|
||||
|
||||
# continue as `tp.restore_backup` handles
|
||||
# saving into settings
|
||||
continue
|
||||
|
||||
settings.set(k, vals[key])
|
||||
|
||||
@ -400,7 +400,7 @@ class SettingsObject:
|
||||
self.my_pos = pos
|
||||
self.is_dirty = 0
|
||||
|
||||
def blank(self):
|
||||
def blank(self, blank_current=True):
|
||||
# erase current copy of values in nvram; older ones may exist still
|
||||
# - use when clearing the seed value
|
||||
if self.my_pos is not None:
|
||||
@ -408,7 +408,8 @@ class SettingsObject:
|
||||
self.my_pos = 0
|
||||
|
||||
# act blank too, just in case.
|
||||
self.current.clear()
|
||||
if blank_current:
|
||||
self.current.clear()
|
||||
self.is_dirty = 0
|
||||
self.capacity = 0
|
||||
|
||||
|
||||
@ -607,11 +607,7 @@ async def remember_ephemeral_seed():
|
||||
|
||||
dis.fullscreen('Check...')
|
||||
with stash.SensitiveValues() as sv:
|
||||
if sv.mode == "xprv":
|
||||
nv = SecretStash.encode(xprv=sv.node)
|
||||
else:
|
||||
assert sv.mode == "words"
|
||||
nv = SecretStash.encode(seed_phrase=sv.raw)
|
||||
nv = sv.encoded_secret()
|
||||
|
||||
dis.fullscreen('Saving...')
|
||||
pa.change(new_secret=nv)
|
||||
@ -624,6 +620,23 @@ async def remember_ephemeral_seed():
|
||||
pa.reset()
|
||||
pa.login()
|
||||
|
||||
async def restore_to_main_secret(preserve_settings=False):
|
||||
# go back to main se2 secret
|
||||
import stash
|
||||
from glob import settings
|
||||
|
||||
# get main secret
|
||||
with stash.SensitiveValues(bypass_tmp=True) as sv:
|
||||
nv = sv.encoded_secret()
|
||||
# drop ephemeral secret
|
||||
pa.tmp_value = None
|
||||
if not preserve_settings:
|
||||
# we do not blank ram as we want to merge
|
||||
# settings that we want to keep KEEP_SETTINGS (nvstore.py)
|
||||
settings.blank(blank_current=False)
|
||||
|
||||
pa.new_main_secret(nv)
|
||||
|
||||
def clear_seed():
|
||||
from glob import dis
|
||||
import utime, callgate
|
||||
@ -752,6 +765,7 @@ class EphemeralSeedMenu(MenuSystem):
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
async def make_ephemeral_seed_menu(*a):
|
||||
if not pa.tmp_value:
|
||||
# force a warning on them, unless they are already doing it.
|
||||
|
||||
@ -350,4 +350,13 @@ class SensitiveValues:
|
||||
self.register(pk)
|
||||
return pk
|
||||
|
||||
def encoded_secret(self):
|
||||
# we do not support master as secret - only extended keys and mnemonics
|
||||
if self.mode == "xprv":
|
||||
nv = SecretStash.encode(xprv=self.node)
|
||||
else:
|
||||
assert self.mode == "words"
|
||||
nv = SecretStash.encode(seed_phrase=self.raw)
|
||||
return nv
|
||||
|
||||
# EOF
|
||||
|
||||
@ -571,6 +571,9 @@ def goto_home(cap_menu, need_keypress, pick_menu_item):
|
||||
|
||||
if m[0] in { 'New Seed Words', 'Ready To Sign'}:
|
||||
break
|
||||
if (m[1] == "Ready To Sign") and m[0][0] == "[":
|
||||
# ephemeral has XFP as first menu item
|
||||
break
|
||||
else:
|
||||
raise pytest.fail("trapped in a menu")
|
||||
|
||||
@ -674,6 +677,16 @@ def open_microsd(simulator, microsd_path):
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def settings_path(simulator):
|
||||
# open a file from the simulated microsd
|
||||
|
||||
def doit(fn):
|
||||
# could use: ckcc.get_sim_root_dirs() here
|
||||
return '../unix/work/settings/' + fn
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def set_master_key(sim_exec, sim_execfile, simulator, reset_seed_words):
|
||||
# load simulator w/ a specific bip32 master key
|
||||
@ -1752,5 +1765,6 @@ from test_bip39pw import set_bip39_pw
|
||||
from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto_eph_seed_menu
|
||||
from test_ephemeral import ephemeral_seed_disabled_ui
|
||||
from test_ux import enter_complex, pass_word_quiz, word_menu_entry
|
||||
from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed
|
||||
|
||||
# EOF
|
||||
|
||||
@ -129,13 +129,8 @@ print("done")
|
||||
|
||||
|
||||
# test recovery/reset
|
||||
if version.mk_num <= 3:
|
||||
from sflash import SF
|
||||
SF.chip_erase()
|
||||
settings.load()
|
||||
else:
|
||||
settings.clear()
|
||||
settings.save()
|
||||
settings.clear()
|
||||
settings.save()
|
||||
|
||||
print("fully done")
|
||||
|
||||
|
||||
@ -238,16 +238,18 @@ def main():
|
||||
continue
|
||||
print("Started", test_module)
|
||||
if test_module in ["test_bsms.py", "test_address_explorer.py", "test_export.py",
|
||||
"test_multisig.py", "test_ephemeral.py", "test_ux.py"]:
|
||||
"test_multisig.py", "test_ux.py"]:
|
||||
test_args = DEFAULT_SIMULATOR_ARGS + ["--set", "vidsk=1"]
|
||||
if test_module == "test_vdisk.py":
|
||||
test_args = ["--eject"] + DEFAULT_SIMULATOR_ARGS + ["--set", "vidsk=1"]
|
||||
if test_module == "test_bip39pw.py":
|
||||
test_args = []
|
||||
if test_module in ["test_unit.py", "test_se2.py"]:
|
||||
if test_module in ["test_unit.py", "test_se2.py", "test_backup.py"]:
|
||||
# test_nvram_mk4 needs to run without --eff
|
||||
# se2 duress wallet activated as ephemeral seed requires proper `settings.load`
|
||||
test_args = ["--set", "nfc=1"]
|
||||
if test_module == "test_ephemeral.py":
|
||||
test_args = ["--set", "nfc=1", "--set", "vidsk=1"]
|
||||
ec, failed_tests = run_tests_with_simulator(test_module, simulator_args=test_args,
|
||||
pytest_k=args.pytest_k, pdb=args.pdb,
|
||||
failed_first=args.ff, psbt2=args.psbt2)
|
||||
|
||||
381
testing/test_backup.py
Normal file
381
testing/test_backup.py
Normal file
@ -0,0 +1,381 @@
|
||||
import pytest, time
|
||||
from constants import simulator_fixed_words, simulator_fixed_xprv
|
||||
from pycoin.key.BIP32Node import BIP32Node
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
|
||||
@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])
|
||||
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):
|
||||
# Make an encrypted 7z backup, verify it, and even restore it!
|
||||
clear_ms()
|
||||
reset_seed_words()
|
||||
# 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)
|
||||
_, story = cap_story()
|
||||
assert "Above is the master key fingerprint of the current wallet" in story
|
||||
need_keypress("y")
|
||||
assert not get_setting('multisig', None)
|
||||
elif st == "eph":
|
||||
eph_seed = generate_ephemeral_words(num_words=24, dice=False, from_main=True)
|
||||
_, story = cap_story()
|
||||
assert "New ephemeral master key in effect" in story
|
||||
need_keypress("y")
|
||||
|
||||
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
|
||||
|
||||
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 ephemeral 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 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'
|
||||
|
||||
time.sleep(0.1)
|
||||
title, body = cap_story()
|
||||
|
||||
time.sleep(0.1)
|
||||
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)
|
||||
check_and_decrypt_backup(fn, words)
|
||||
|
||||
for i in range(10):
|
||||
need_keypress('x')
|
||||
time.sleep(.01)
|
||||
|
||||
# test verify on device (CRC check)
|
||||
avail_settings = ['multisig'] if multisig else None
|
||||
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)
|
||||
else:
|
||||
sec = import_ephemeral_xprv("sd", from_main=True)
|
||||
|
||||
target = sim_execfile('devtest/get-secrets.py')
|
||||
assert 'Error' not in target
|
||||
need_keypress("y")
|
||||
pick_menu_item("Advanced/Tools")
|
||||
pick_menu_item("Backup")
|
||||
pick_menu_item("Backup System")
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert "An ephemeral 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_xprv 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("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):
|
||||
goto_home()
|
||||
set_bip39_pw(passphrase)
|
||||
target = sim_execfile('devtest/get-secrets.py')
|
||||
assert 'Error' not in target
|
||||
need_keypress("y")
|
||||
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 simulator_fixed_words not in contents
|
||||
assert simulator_fixed_xprv 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
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
@ -9,7 +9,7 @@ from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, CCUserRefused
|
||||
from ckcc_protocol.constants import *
|
||||
import json
|
||||
from mnemonic import Mnemonic
|
||||
from constants import simulator_fixed_xfp, simulator_fixed_words, simulator_fixed_xprv
|
||||
from constants import simulator_fixed_xfp, simulator_fixed_words
|
||||
|
||||
# add the BIP39 test vectors
|
||||
vectors = json.load(open('bip39-vectors.json'))['english']
|
||||
@ -137,7 +137,7 @@ def test_cancel_on_empty_added_numbers(pick_menu_item, goto_home, need_keypress,
|
||||
pick_menu_item('CANCEL')
|
||||
time.sleep(0.1)
|
||||
m = cap_menu()
|
||||
assert m[0] == "Ready To Sign"
|
||||
assert "Ready To Sign" in m[:3]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('stype', ["bip39pw", "words", "xprv", None])
|
||||
@ -234,71 +234,4 @@ def test_bip39pass_on_ephemeral_seed(generate_ephemeral_words, import_ephemeral_
|
||||
assert expect1.fingerprint().hex().upper() == xfp1
|
||||
assert "press (2)" not in story
|
||||
|
||||
|
||||
@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):
|
||||
goto_home()
|
||||
set_bip39_pw(passphrase)
|
||||
target = sim_execfile('devtest/get-secrets.py')
|
||||
assert 'Error' not in target
|
||||
need_keypress("y")
|
||||
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 simulator_fixed_words not in contents
|
||||
assert simulator_fixed_xprv 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)
|
||||
|
||||
# EOF
|
||||
|
||||
@ -7,9 +7,6 @@ from constants import simulator_fixed_xpub
|
||||
from ckcc.protocol import CCProtocolPacker
|
||||
from txn import fake_txn
|
||||
from test_ux import word_menu_entry
|
||||
from constants import simulator_fixed_words, simulator_fixed_xprv
|
||||
from pycoin.key.BIP32Node import BIP32Node
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
|
||||
WORDLISTS = {
|
||||
@ -119,10 +116,55 @@ def goto_eph_seed_menu(goto_home, pick_menu_item, cap_story, need_keypress):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn, try_sign,
|
||||
goto_eph_seed_menu, reset_seed_words, get_identity_story,
|
||||
get_seed_value_ux):
|
||||
def doit(mnemonic=None, xpub=None, expected_xfp=None):
|
||||
def restore_main_seed(goto_home, pick_menu_item, cap_story, cap_menu,
|
||||
need_keypress, settings_path):
|
||||
def list_settings_files():
|
||||
return [fn
|
||||
for fn in os.listdir(settings_path(""))
|
||||
if fn.endswith(".aes")]
|
||||
|
||||
def doit(preserve_settings=False):
|
||||
prev = len(list_settings_files())
|
||||
goto_home()
|
||||
menu = cap_menu()
|
||||
assert menu[-1] == "Restore Seed"
|
||||
assert (menu[0][0] == "[") and (menu[0][-1] == "]")
|
||||
pick_menu_item("Restore Seed")
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
|
||||
assert "Restore main wallet and its settings?\n\n" in story
|
||||
assert "Press OK to forget current ephemeral wallet " in story
|
||||
assert "settings, or press (1) to save & keep " in story
|
||||
assert "those settings for later use." in story
|
||||
|
||||
if preserve_settings:
|
||||
ch = "1"
|
||||
else:
|
||||
ch = "y"
|
||||
|
||||
need_keypress(ch)
|
||||
time.sleep(.3)
|
||||
|
||||
menu = cap_menu()
|
||||
assert menu[-1] != "Restore Seed"
|
||||
assert (menu[0][0] != "[") and (menu[0][-1] != "]")
|
||||
|
||||
after = len(list_settings_files())
|
||||
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 verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn,
|
||||
goto_eph_seed_menu, get_identity_story, try_sign,
|
||||
get_seed_value_ux, pick_menu_item, goto_home,
|
||||
restore_main_seed):
|
||||
def doit(mnemonic=None, xpub=None, expected_xfp=None, preserve_settings=False):
|
||||
time.sleep(0.3)
|
||||
title, story = cap_story()
|
||||
in_effect_xfp = title[1:-1]
|
||||
@ -132,7 +174,10 @@ def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn
|
||||
need_keypress("y") # just confirm new master key message
|
||||
|
||||
menu = cap_menu()
|
||||
assert menu[0] == "Ready To Sign" # returned to main menu
|
||||
|
||||
assert expected_xfp in menu[0] if expected_xfp else True
|
||||
assert menu[1] == "Ready To Sign" # returned to main menu
|
||||
assert menu[-1] == "Restore Seed" # restore main from ephemeral
|
||||
|
||||
ident_story = get_identity_story()
|
||||
assert "Ephemeral seed is in effect" in ident_story
|
||||
@ -158,11 +203,13 @@ def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn
|
||||
# ephemeral seed chosen -> [xfp] will be visible
|
||||
assert menu[0] == f"[{ident_xfp}]"
|
||||
|
||||
reset_seed_words()
|
||||
restore_main_seed(preserve_settings)
|
||||
|
||||
goto_eph_seed_menu()
|
||||
menu = cap_menu()
|
||||
|
||||
assert menu[0] != f"[{ident_xfp}]"
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@ -268,23 +315,29 @@ def import_ephemeral_xprv(microsd_path, virtdisk_path, goto_eph_seed_menu,
|
||||
|
||||
@pytest.mark.parametrize("num_words", [12, 24])
|
||||
@pytest.mark.parametrize("dice", [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,
|
||||
ephemeral_seed_disabled, verify_ephemeral_secret_ui):
|
||||
ephemeral_seed_disabled, verify_ephemeral_secret_ui,
|
||||
preserve_settings):
|
||||
reset_seed_words()
|
||||
goto_eph_seed_menu()
|
||||
ephemeral_seed_disabled()
|
||||
e_seed_words = generate_ephemeral_words(num_words=num_words, dice=dice,
|
||||
from_main=True)
|
||||
verify_ephemeral_secret_ui(mnemonic=e_seed_words)
|
||||
verify_ephemeral_secret_ui(mnemonic=e_seed_words,
|
||||
preserve_settings=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])
|
||||
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):
|
||||
ephemeral_seed_disabled, get_seed_value_ux,
|
||||
preserve_settings):
|
||||
if truncated and not nfc: return
|
||||
|
||||
|
||||
@ -315,7 +368,8 @@ def test_ephemeral_seed_import_words(nfc, truncated, num_words, cap_menu, pick_m
|
||||
|
||||
need_keypress("4") # understand consequences
|
||||
|
||||
verify_ephemeral_secret_ui(mnemonic=words.split(" "), expected_xfp=expect_xfp)
|
||||
verify_ephemeral_secret_ui(mnemonic=words.split(" "), expected_xfp=expect_xfp,
|
||||
preserve_settings=preserve_settings)
|
||||
|
||||
nfc_seed = get_seed_value_ux(nfc=True) # export seed via NFC (always truncated)
|
||||
seed_words = get_seed_value_ux()
|
||||
@ -323,12 +377,13 @@ def test_ephemeral_seed_import_words(nfc, truncated, num_words, cap_menu, pick_m
|
||||
|
||||
|
||||
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"])
|
||||
@pytest.mark.parametrize('retry', range(3))
|
||||
@pytest.mark.parametrize("testnet", [True, False])
|
||||
def test_ephemeral_seed_import_tapsigner(way, retry, testnet, pick_menu_item, cap_story, enter_hex,
|
||||
@pytest.mark.parametrize("preserve_settings", [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):
|
||||
nfc_write_text, tapsigner_encrypted_backup,
|
||||
preserve_settings):
|
||||
reset_seed_words()
|
||||
|
||||
fname, backup_key_hex, node = tapsigner_encrypted_backup(way, testnet=testnet)
|
||||
@ -370,7 +425,7 @@ def test_ephemeral_seed_import_tapsigner(way, retry, testnet, pick_menu_item, ca
|
||||
assert "back of the card" in story
|
||||
need_keypress("y") # yes I have backup key
|
||||
enter_hex(backup_key_hex)
|
||||
verify_ephemeral_secret_ui(xpub=node.hwif())
|
||||
verify_ephemeral_secret_ui(xpub=node.hwif(), preserve_settings=preserve_settings)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fail", ["wrong_key", "key_len", "plaintext", "garbage"])
|
||||
@ -466,15 +521,18 @@ def test_ephemeral_seed_import_tapsigner_real(data, pick_menu_item, cap_story, m
|
||||
|
||||
|
||||
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"])
|
||||
@pytest.mark.parametrize('retry', range(3))
|
||||
@pytest.mark.parametrize("testnet", [True, False])
|
||||
def test_ephemeral_seed_import_xprv(way, retry, testnet, reset_seed_words,
|
||||
@pytest.mark.parametrize("preserve_settings", [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):
|
||||
ephemeral_seed_disabled, import_ephemeral_xprv,
|
||||
preserve_settings):
|
||||
reset_seed_words()
|
||||
goto_eph_seed_menu()
|
||||
ephemeral_seed_disabled()
|
||||
|
||||
node = import_ephemeral_xprv(way=way, testnet=testnet, from_main=True)
|
||||
verify_ephemeral_secret_ui(xpub=node.hwif())
|
||||
verify_ephemeral_secret_ui(xpub=node.hwif(), preserve_settings=preserve_settings)
|
||||
|
||||
|
||||
def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
|
||||
@ -483,8 +541,8 @@ def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
|
||||
word_menu_entry):
|
||||
reset_seed_words()
|
||||
goto_eph_seed_menu()
|
||||
|
||||
ephemeral_seed_disabled()
|
||||
|
||||
words, expected_xfp = WORDLISTS[12]
|
||||
pick_menu_item("Import Words")
|
||||
pick_menu_item(f"12 Words")
|
||||
@ -510,86 +568,4 @@ def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
|
||||
assert already_used_xfp == in_effect_xfp == expected_xfp
|
||||
need_keypress("y")
|
||||
|
||||
|
||||
@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)
|
||||
else:
|
||||
sec = import_ephemeral_xprv("sd", from_main=True)
|
||||
|
||||
target = sim_execfile('devtest/get-secrets.py')
|
||||
assert 'Error' not in target
|
||||
need_keypress("y")
|
||||
pick_menu_item("Advanced/Tools")
|
||||
pick_menu_item("Backup")
|
||||
pick_menu_item("Backup System")
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert "An ephemeral 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_xprv 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)
|
||||
|
||||
# EOF
|
||||
|
||||
@ -733,82 +733,6 @@ def test_ux_changing_pins(true_pin, repl, force_main_pin, goto_trick_menu,
|
||||
|
||||
clear_all_tricks()
|
||||
|
||||
def test_trick_backups(goto_trick_menu, clear_all_tricks, repl, unit_test,
|
||||
new_trick_pin, new_pin_confirmed, pick_menu_item, need_keypress):
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# TODO
|
||||
# - make trick and do login, check arrives right state?
|
||||
# - out of slots
|
||||
|
||||
@ -359,6 +359,7 @@ def test_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, s
|
||||
if hex(got_xfp) != hex(wallet_xfp):
|
||||
raise pytest.xfail("wrong HD master key fingerprint")
|
||||
|
||||
start_sign(psbt, finalize=we_finalize)
|
||||
if mine.txn:
|
||||
# pull out included txn
|
||||
txn2 = B2A(mine.txn)
|
||||
@ -367,8 +368,6 @@ def test_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, s
|
||||
else:
|
||||
assert mine.version == 2
|
||||
|
||||
start_sign(psbt, finalize=we_finalize)
|
||||
|
||||
signed = end_sign(accept=True, finalize=we_finalize)
|
||||
open('debug/vs-signed.psbt', 'wb').write(signed)
|
||||
|
||||
@ -964,6 +963,7 @@ def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind
|
||||
if hex(got_xfp) != hex(wallet_xfp):
|
||||
raise pytest.xfail("wrong HD master key fingerprint")
|
||||
|
||||
start_sign(psbt, finalize=True)
|
||||
if mine.txn:
|
||||
# pull out included txn (only available in PSBTv0)
|
||||
txn2 = B2A(mine.txn)
|
||||
@ -972,8 +972,6 @@ def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind
|
||||
else:
|
||||
assert mine.version == 2
|
||||
|
||||
start_sign(psbt, finalize=True)
|
||||
|
||||
signed_final = end_sign(accept=True, finalize=True)
|
||||
assert signed_final[0:4] != b'psbt', "expecting raw bitcoin txn"
|
||||
open('debug/finalized-by-ckcc.txn', 'wt').write(B2A(signed_final))
|
||||
|
||||
@ -117,157 +117,6 @@ def pass_word_quiz(need_keypress, cap_story):
|
||||
|
||||
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])
|
||||
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):
|
||||
# Make an encrypted 7z backup, verify it, and even restore it!
|
||||
|
||||
# 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)
|
||||
_, story = cap_story()
|
||||
assert "Above is the master key fingerprint of the current wallet" in story
|
||||
need_keypress("y")
|
||||
assert not get_setting('multisig', None)
|
||||
elif st == "eph":
|
||||
eph_seed = generate_ephemeral_words(num_words=24, dice=False, from_main=True)
|
||||
_, story = cap_story()
|
||||
assert "New ephemeral master key in effect" in story
|
||||
need_keypress("y")
|
||||
|
||||
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
|
||||
|
||||
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 ephemeral 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 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'
|
||||
|
||||
time.sleep(0.1)
|
||||
title, body = cap_story()
|
||||
|
||||
time.sleep(0.1)
|
||||
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)
|
||||
check_and_decrypt_backup(fn, words)
|
||||
|
||||
for i in range(10):
|
||||
need_keypress('x')
|
||||
time.sleep(.01)
|
||||
|
||||
# test verify on device (CRC check)
|
||||
avail_settings = ['multisig'] if multisig else None
|
||||
restore_backup_cs(files[0], words, avail_settings=avail_settings)
|
||||
|
||||
reset_seed_words()
|
||||
settings_remove('multisig')
|
||||
|
||||
|
||||
@pytest.mark.qrcode
|
||||
@pytest.mark.parametrize('seed_words, xfp', [
|
||||
|
||||
@ -29,12 +29,11 @@ if '--sflash' not in sys.argv:
|
||||
|
||||
#glob.settings.current = dict(sim_defaults)
|
||||
|
||||
if 1:
|
||||
# Install Mk4 hacks and workarounds
|
||||
import mk4
|
||||
import sim_mk4
|
||||
import sim_psram
|
||||
import sim_vdisk
|
||||
# Install Mk4 hacks and workarounds
|
||||
import mk4
|
||||
import sim_mk4
|
||||
import sim_psram
|
||||
import sim_vdisk
|
||||
|
||||
if sys.argv[-1] != '-q':
|
||||
import main # must be last, does not return
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
# - do not import this file before trick_pins has had a chance to be imported
|
||||
#
|
||||
from binascii import a2b_base64, b2a_base64
|
||||
from binascii import unhexlify as a2b_hex
|
||||
from errno import ENOENT
|
||||
|
||||
# these flags are masked-out from mpy so even it can't tell they happened
|
||||
@ -53,11 +54,16 @@ class SecondSecureElement:
|
||||
from trick_pins import TRICK_SLOT_LAYOUT
|
||||
import uctypes
|
||||
from nvstore import SettingsObject
|
||||
|
||||
from sim_secel import SECRETS
|
||||
|
||||
self.state = {}
|
||||
|
||||
obj = SettingsObject()
|
||||
obj.set_key(a2b_hex(SECRETS["_pin1_secret"]))
|
||||
obj.load()
|
||||
# merging default values as they contain useful nfc,vidsk info
|
||||
obj.merge_previous_active(obj.default_values())
|
||||
obj.save()
|
||||
s = obj.get('_se2', None)
|
||||
if not s:
|
||||
print("no SE2 data")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user