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:
scgbckbone 2023-08-11 17:04:05 +02:00 committed by doc-hex
parent 325435b678
commit fe63163c85
17 changed files with 583 additions and 465 deletions

View File

@ -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,

View File

@ -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 = '''\

View File

@ -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])

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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
View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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', [

View File

@ -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

View File

@ -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")