BIP39 passphrase as ephemeral seed; Lock Down Seed for all ephemeral; BIP-39 wallet backup

(cherry picked from commit e5d1782b9d)
This commit is contained in:
scgbckbone 2023-07-13 11:13:09 +02:00 committed by doc-hex
parent 342af8f78e
commit 22fd6b4010
25 changed files with 782 additions and 394 deletions

View File

@ -38,6 +38,17 @@ a single file, which is a simple text file and
easy to read. Before version 4.0.0, this text file was always
called `ckcc-backup.txt`, but the filename is now picked randomly.
## BIP39 Passphrase
If BIP39 passphrase is active the default behavior is to back-up
main wallet - not BIP39 passphrase wallet. From version `5.2.0`
users can choose to back-up also BIP39 passphrase wallet.
## Ephemeral Seeds
If ephemeral seed is active the default behavior is to always
back-up ephemeral wallet instead of the main wallet.
## Limitations
- The archive file names are not encrypted. You can see there is a single

View File

@ -1,11 +1,19 @@
## 5.2.0 - 2023-09-21
- 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
- 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,
easily observed, but now that's different. New simpler and less storage
wasteful plausible deniability.
- Enhancement: Remove obsolete Mk2/Mk3 code-paths from master branch
- Enhancement: BIP39 Passphrase is now internally handled as an ephemeral secret.
Ability to see BIP-39 Passphrase after wallet is active via `View Seed Words`
was removed as a consequence of this change.
- Enhancement: Showing secrets now also display extended private key for BIP-39
passphrase wallets.
## 5.1.4 - 2023-09-08

View File

@ -108,8 +108,7 @@ Extended Master Key:
if stash.bip39_passphrase:
msg += '\nBIP-39 passphrase is in effect.\n'
if pa.tmp_value:
elif pa.tmp_value:
msg += '\nEphemeral seed is in effect.\n'
bn = callgate.get_bag_number()
@ -555,22 +554,42 @@ def new_from_dice(menu, label, item):
import seed
return seed.new_from_dice(item.arg)
async def convert_bip39_to_bip32(*a):
import seed, stash
async def convert_ephemeral_to_master(*a):
import seed
from pincodes import pa
from stash import bip39_passphrase
if not await ux_confirm('''This operation computes the extended master private key using your BIP-39 seed words and passphrase, and then saves the resulting value (xprv) as the wallet secret.
if not pa.tmp_value:
await ux_show_story('You do not have an active ephemeral seed (including BIP-39 passphrase)'
' right now, so this command does little except forget the seed words.'
' It does not enhance security in any way.')
return
The seed words themselves are erased forever, but effectively there is no other change. If a BIP-39 passphrase is currently in effect, its value is captured during this process and will be 'in effect' going forward, but the passphrase itself is erased and unrecoverable. The resulting wallet cannot be used with any other passphrase.
words = settings.get("words", True)
msg = 'Convert currently used '
msg += 'BIP-39 passphrase ' if bip39_passphrase else 'ephemeral seed '
msg += 'to main seed. '
if words or bip39_passphrase:
msg += 'Main seed words themselves are erased forever, '
else:
msg += 'Main seed is erased forever, '
msg += 'but effectively there is no other change. '
if bip39_passphrase:
msg += ('BIP-39 passphrase is currently in effect, its value '
'is captured during this process and will be in effect '
'going forward, but the passphrase itself is erased '
'and unrecoverable. ')
if not words:
msg += 'The resulting wallet cannot be used with any other passphrase. '
msg += 'A reboot is part of this process. PIN code, and funds are not affected.'
if not await ux_confirm(msg):
A reboot is part of this process. PIN code, and funds are not affected.
'''):
return await ux_aborted()
if not stash.bip39_passphrase:
if not await ux_confirm('''You do not have a BIP-39 passphrase set right now, so this command does little except forget the seed words. It does not enhance security.'''):
return
await seed.remember_bip39_passphrase()
await seed.remember_ephemeral_seed()
settings.save()
@ -612,8 +631,9 @@ consequences.''', escape='4')
def render_master_secrets(mode, raw, node):
# Render list of words, or XPRV / master secret to text.
import stash
import stash, chains
c = chains.current_chain()
qr_alnum = False
if mode == 'words':
@ -628,12 +648,14 @@ def render_master_secrets(mode, raw, node):
msg = 'Seed words (%d):\n' % len(words)
msg += '\n'.join('%2d: %s' % (i+1, w) for i,w in enumerate(words))
pw = stash.bip39_passphrase
if pw:
msg += '\n\nBIP-39 Passphrase:\n%s' % pw
if stash.bip39_passphrase:
msg += '\n\nBIP-39 Passphrase:\n *****'
if node:
msg += '\n\nSeed+Passphrase:\n%s' % c.serialize_private(node)
elif mode == 'xprv':
import chains
msg = chains.current_chain().serialize_private(node)
msg = c.serialize_private(node)
qr = msg
elif mode == 'master':
@ -648,21 +670,40 @@ def render_master_secrets(mode, raw, node):
async def view_seed_words(*a):
import stash
if not await ux_confirm('''The next screen will show the seed words (and if defined, your BIP-39 passphrase).\n\nAnyone with knowledge of those words can control all funds in this wallet.''' ):
if not await ux_confirm('The next screen will show the seed words'
' (and if defined, your BIP-39 passphrase).'
'\n\nAnyone with knowledge of those words '
'can control all funds in this wallet.'):
return
from glob import dis
dis.fullscreen("Wait...")
dis.busy_bar(True)
with stash.SensitiveValues() as sv:
# preserve old UI where we show words + passphrase
# instead of just calculated seed + passphrase = extended privkey
# new: calculated xprv is now also shown for BIP39 passphrase wallet
raw = mode = None
if stash.bip39_passphrase:
# get main secret - bypass tmp
with stash.SensitiveValues(bypass_tmp=True) as sv:
if not sv.deltamode:
assert sv.mode == "words"
raw = sv.raw[:]
mode = sv.mode
stash.SensitiveValues.clear_cache()
with stash.SensitiveValues(bypass_tmp=False) as sv:
if sv.deltamode:
# give up and wipe self rather than show true seed values.
import callgate
callgate.fast_wipe()
dis.busy_bar(False)
msg, qr, qr_alnum = render_master_secrets(sv.mode, sv.raw, sv.node)
msg, qr, qr_alnum = render_master_secrets(mode or sv.mode,
raw or sv.raw,
sv.node)
msg += '\n\nPress (1) to view as QR Code.'
@ -674,8 +715,9 @@ async def view_seed_words(*a):
continue
break
stash.blank_object(qr)
stash.blank_object(msg)
stash.blank_object(qr)
stash.blank_object(msg)
stash.blank_object(raw)
async def damage_myself():
# called when it's time to disable ourselves due to various
@ -1709,13 +1751,14 @@ async def ready2sign(*a):
# - if no card, check virtual disk for PSBT
# - if still nothing, then talk about USB connection
import stash
from pincodes import pa
from glob import NFC
# just check if we have candidates, no UI
choices = await file_picker(None, suffix='psbt', min_size=50,
max_size=MAX_TXN_LEN, taster=is_psbt)
if stash.bip39_passphrase:
if pa.tmp_value:
title = '[%s]' % xfp2str(settings.get('xfp'))
else:
title = None

View File

@ -1220,7 +1220,7 @@ Press (2) to view the provided passphrase.\n\nOK to continue, X to cancel.''' %
from seed import set_bip39_passphrase
# full screen message shown: "Working..."
set_bip39_passphrase(self._pw)
await set_bip39_passphrase(self._pw, summarize_ux=False)
self.result = settings.get('xpub')
@ -1233,8 +1233,9 @@ Press (2) to view the provided passphrase.\n\nOK to continue, X to cancel.''' %
if self.result:
new_xfp = settings.get('xfp')
await ux_show_story('''Above is the master key fingerprint of the current wallet.''',
title="[%s]" % xfp2str(new_xfp))
await ux_show_story('Above is the master key fingerprint '
'of the current wallet.',
title="[%s]" % xfp2str(new_xfp))
def start_bip39_passphrase(pw):

View File

@ -18,12 +18,12 @@ num_pw_words = const(12)
# max size we expect for a backup data file (encrypted or cleartext)
MAX_BACKUP_FILE_SIZE = const(10000) # bytes
def render_backup_contents():
def render_backup_contents(bypass_tmp=False):
# simple text format:
# key = value
# or #comments
# but value is JSON
current_tmp = None
rv = StringIO()
def COMMENT(val=None):
@ -41,7 +41,7 @@ def render_backup_contents():
COMMENT('Private key details: ' + chain.name)
with stash.SensitiveValues(bypass_pw=True) as sv:
with stash.SensitiveValues(bypass_tmp=bypass_tmp) as sv:
if sv.deltamode:
# die rather than give up our secrets
import callgate
@ -72,6 +72,15 @@ def render_backup_contents():
COMMENT(path)
for k,v in pairs:
ADD(k, v)
if bypass_tmp:
current_tmp = pa.tmp_value[:]
pa.tmp_value = None
# we also need correct settings from main seed
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
settings.set_key(nv)
settings.load()
stash.blank_object(nv)
COMMENT('Firmware version (informational)')
date, vers, timestamp = version.get_mpy_version()[0:3]
@ -98,6 +107,13 @@ def render_backup_contents():
rv.write('\n# EOF\n')
if bypass_tmp:
# go back to tmp secret and its settings
stash.SensitiveValues.clear_cache()
pa.tmp_value = current_tmp
settings.set_key()
settings.load()
return rv.getvalue()
def restore_from_dict_ll(vals):
@ -217,11 +233,26 @@ async def restore_from_dict(vals):
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
from stash import bip39_passphrase
words = None
skip_quiz = False
bypass_tmp = False
if pa.tmp_value:
if not await ux_confirm("An ephemeral seed is in effect, so backup will be of that seed."):
if bip39_passphrase and pa.tmp_value:
# this is a BIP39 password ephemeral wallet
msg = ("BIP39 passphrase is in effect. Backup ignores passphrases "
"and produces backup of main seed. Press OK to back-up main wallet,"
" press (2) to back-up BIP39 passphrase wallet "
"(extended private key created via seed + pass)")
ch = await ux_show_story(msg, escape="2")
if ch == "x": return
if ch == "y":
bypass_tmp = True
elif pa.tmp_value:
if not await ux_confirm("An ephemeral seed is in effect, "
"so backup will be of that seed."):
return
stored_words = settings.get('bkpw', None)
@ -278,16 +309,18 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
settings.remove_key('bkpw')
settings.save()
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash)
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash,
bypass_tmp=bypass_tmp)
async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_copies=True):
async def write_complete_backup(words, fname_pattern, write_sflash=False,
allow_copies=True, bypass_tmp=False):
# Just do the writing
from glob import dis
from files import CardSlot
# Show progress:
dis.fullscreen('Encrypting...' if words else 'Generating...')
body = render_backup_contents().encode()
body = render_backup_contents(bypass_tmp=bypass_tmp).encode()
gc.collect()
@ -637,7 +670,7 @@ async def clone_write_data(*a):
fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z'
await write_complete_backup(words, fname, allow_copies=False)
await write_complete_backup(words, fname, allow_copies=False, bypass_tmp=True)
await ux_show_story("Done.\n\nTake this MicroSD card back to other Coldcard and continue from there.")

View File

@ -18,6 +18,7 @@ from utils import chunk_writer
BIP85_PWD_LEN = 21
async def drv_entro_start(*a):
from pincodes import pa
# UX entry
ch = await ux_show_story('''\
@ -36,8 +37,14 @@ so the other wallet is effectively segregated from the Coldcard and yet \
still backed-up.''')
if ch != 'y': return
if stash.bip39_passphrase:
if not await ux_confirm('''You have a BIP-39 passphrase set right now and so that will become wrapped into the new secret.'''):
if pa.tmp_value:
if stash.bip39_passphrase:
msg = ('You have a BIP-39 passphrase set right now '
'and so it will be wrapped into the new secret.')
else:
msg = 'You have an ephemeral seed - deriving from ephemeral.'
if not await ux_confirm(msg):
return
choices = [ '12 words', '18 words', '24 words', 'WIF (privkey)',

View File

@ -48,6 +48,11 @@ def se2_and_real_secret():
from pincodes import pa
return (not pa.is_secret_blank()) and (not pa.tmp_value)
def bip39_passphrase_active():
from stash import bip39_passphrase
from pincodes import pa
return settings.get('words', True) or (bip39_passphrase and pa.tmp_value)
HWTogglesMenu = [
ToggleMenuItem('USB Port', 'du', ['Default On', 'Disable USB'], invert=True,
@ -206,7 +211,7 @@ SeedFunctionsMenu = [
MenuItem('View Seed Words', f=view_seed_words), # text is a little wrong sometimes, rare
MenuItem('Seed XOR', menu=SeedXORMenu),
MenuItem("Destroy Seed", f=clear_seed),
MenuItem('Lock Down Seed', f=convert_bip39_to_bip32),
MenuItem('Lock Down Seed', f=convert_ephemeral_to_master),
]
DangerZoneMenu = [
@ -310,7 +315,7 @@ EmptyWallet = [
NormalSystem = [
# xxxxxxxxxxxxxxxx
MenuItem('Ready To Sign', f=ready2sign),
MenuItem('Passphrase', f=start_b39_pw, predicate=lambda: settings.get('words', True)),
MenuItem('Passphrase', f=start_b39_pw, predicate=bip39_passphrase_active),
MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available),
MenuItem("Address Explorer", f=address_explore),
MenuItem('Type Passwords', f=password_entry, predicate=lambda: settings.get("emu", False) and has_secrets()),

View File

@ -98,7 +98,6 @@ class SettingsObject:
self.nvram_key = b'\0'*32
self.capacity = 0
self.current = self.default_values()
self.overrides = {} # volatile overide values
self.load(dis)
@ -256,7 +255,6 @@ class SettingsObject:
# and pick the newest one (in unlikely case of dups)
# reset
self.current.clear()
self.overrides.clear()
self.my_pos = None
self.is_dirty = 0
self.capacity = 0
@ -311,10 +309,7 @@ class SettingsObject:
self.current['chain'] = 'XTN'
def get(self, kn, default=None):
if kn in self.overrides:
return self.overrides.get(kn)
else:
return self.current.get(kn, default)
return self.current.get(kn, default)
def changed(self):
self.is_dirty += 1
@ -330,9 +325,6 @@ class SettingsObject:
self.current[kn] = v
self.changed()
def put_volatile(self, kn, v):
self.overrides[kn] = v
set = put
def remove_key(self, kn):
@ -359,8 +351,7 @@ class SettingsObject:
rk = [k for k in self.current if k[0] != '_']
for k in rk:
del self.current[k]
self.overrides.clear()
self.changed()
async def write_out(self):
@ -419,7 +410,6 @@ class SettingsObject:
# act blank too, just in case.
self.current.clear()
self.overrides.clear()
self.is_dirty = 0
self.capacity = 0

View File

@ -369,8 +369,8 @@ class PinAttempt:
# - call new_main_secret() when main secret changes!
# - is_secret_blank and is_successful may be wrong now, re-login to get again
def fetch(self, duress_pin=None, spare_num=0):
if self.tmp_value:
def fetch(self, duress_pin=None, spare_num=0, bypass_tmp=False):
if self.tmp_value and not bypass_tmp:
# must make a copy here, and must be mutable instance so not reused
if spare_num:
return bytearray(AE_SECRET_LEN)
@ -413,12 +413,15 @@ class PinAttempt:
self.roundtrip(7, fw_upgrade=(start, length))
# not-reached
def new_main_secret(self, raw_secret, chain=None):
def new_main_secret(self, raw_secret, chain=None, bip39pw=''):
# Main secret has changed: reset the settings+their key,
# and capture xfp/xpub
from glob import settings, NFC
from glob import settings
import stash
stash.SensitiveValues.clear_cache()
stash.bip39_passphrase = bool(bip39pw)
# capture values we have already
old_values = dict(settings.current)
@ -434,7 +437,7 @@ class PinAttempt:
# does not call settings.save() but caller should!
def tmp_secret(self, encoded, chain=None):
def tmp_secret(self, encoded, chain=None, bip39pw=''):
# Use indicated secret and stop using the SE; operate like this until reboot
val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
if self.tmp_value == val:
@ -445,14 +448,9 @@ class PinAttempt:
# We're no longer blank. hard to say about duress secret and stuff tho
self.state_flags = PA_SUCCESSFUL
# Clear bip-39 secret, not applicable anymore.
import stash
stash.bip39_passphrase = ''
stash.SensitiveValues.clear_cache()
# Copies system settings to new encrypted-key value, calculates
# XFP, XPUB and saves into that, and starts using them.
self.new_main_secret(self.tmp_value, chain=chain)
self.new_main_secret(self.tmp_value, chain=chain, bip39pw=bip39pw)
return True
def trick_request(self, method_num, data):

View File

@ -25,7 +25,7 @@ class PassphraseSaver:
try:
salt = card.get_id_hash()
with stash.SensitiveValues(bypass_pw=True) as sv:
with stash.SensitiveValues(bypass_tmp=True) as sv:
self.key = bytearray(sv.encryption_key(salt))
except:
@ -132,7 +132,7 @@ class PassphraseSaver:
async def doit(menu, idx, item):
# apply the password immediately and drop them at top menu
pw, expect_xfp = item.arg
set_bip39_passphrase(pw)
await set_bip39_passphrase(pw)
from glob import settings
from utils import xfp2str

View File

@ -11,7 +11,7 @@
# - 'abandon' * 11 + 'about'
#
from menu import MenuItem, MenuSystem
from utils import xfp2str, parse_extended_key
from utils import xfp2str, parse_extended_key, swab32
import ngu, uctypes, bip39, random, version
from uhashlib import sha256
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm
@ -405,17 +405,18 @@ async def new_from_dice(nwords):
# send them to home menu, now with a wallet enabled
goto_top_menu(first_time=True)
async def set_ephemeral_seed(encoded, chain=None):
applied = pa.tmp_secret(encoded, chain=chain)
async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw=''):
applied = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw)
dis.progress_bar_show(1)
xfp = settings.get("xfp", "")
xfp = settings.get("xfp", None)
if xfp:
xfp = "[" + xfp2str(xfp) + "]\n"
xfp = "[" + xfp2str(xfp) + "]"
if not applied:
await ux_show_story("%sEphemeral master key already in use." % xfp)
await ux_show_story(title=xfp, msg="Ephemeral master key already in use.")
return
await ux_show_story("%sNew ephemeral master key in effect until next power down." % xfp)
if summarize_ux:
await ux_show_story(title=xfp, msg="New ephemeral master key in effect until next power down.")
async def set_ephemeral_seed_words(words):
dis.progress_bar_show(0.1)
@ -579,40 +580,38 @@ def set_seed_value(words=None, encoded=None, chain=None):
finally:
dis.busy_bar(False)
def set_bip39_passphrase(pw):
# apply bip39 passphrase for now (volatile)
# takes a bit, so show something
async def calc_bip39_passphrase(pw, bypass_tmp=False):
from glob import dis
dis.fullscreen("Working...")
# set passphrase
import stash
stash.bip39_passphrase = pw
# capture updated XFP
with stash.SensitiveValues() as sv:
# generate secret
with stash.SensitiveValues(bip39pw=pw, bypass_tmp=bypass_tmp) as sv:
# can't do it without original seed words (late, but caller has checked)
assert sv.mode == 'words'
nv = SecretStash.encode(xprv=sv.node)
xfp = swab32(sv.node.my_fp())
return nv, xfp
sv.capture_xpub()
async def set_bip39_passphrase(pw, summarize_ux=True):
nv, _ = await calc_bip39_passphrase(pw)
await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw)
# Might need to bounce the USB connection, because our pubkey has changed,
# altho if they have already picked a shared session key, no need, and
# would only affect MitM test, which has already been done.
async def remember_bip39_passphrase():
async def remember_ephemeral_seed():
# Compute current xprv and switch to using that as root secret.
import stash
from glob import dis
dis.fullscreen('Check...')
with stash.SensitiveValues() as sv:
nv = SecretStash.encode(xprv=sv.node)
# Important: won't write new XFP to nvram if pw still set
stash.bip39_passphrase = ''
if sv.mode == "xprv":
nv = SecretStash.encode(xprv=sv.node)
else:
assert sv.mode == "words"
nv = SecretStash.encode(seed_phrase=sv.raw)
dis.fullscreen('Saving...')
pa.change(new_secret=nv)
@ -866,23 +865,33 @@ class PassphraseMenu(MenuSystem):
async def done_apply(self, *a):
# apply the passphrase.
# - important to work on empty string here too.
from stash import bip39_passphrase
old_pw = str(bip39_passphrase)
import stash
from glob import settings
from pincodes import pa
set_bip39_passphrase(pp_sofar)
nv, xfp = await calc_bip39_passphrase(pp_sofar, bypass_tmp=True)
xfp = settings.get('xfp')
msg = ('Above is the master key fingerprint of the new wallet. '
'Press X to abort and keep editing passphrase, '
'OK to use the new wallet, (1) to use and save to MicroSD')
msg = '''Above is the master key fingerprint of the new wallet.
msg1 = ""
if pa.tmp_value and settings.get("words", True):
# we have ephemeral seed but can add passphrase to it as it is word based
msg1 = (", or press (2) to add passphrase to the current "
"active ephemeral seed instead of the main seed.")
Press X to abort and keep editing passphrase, OK to use the new wallet, or 1 to use and save to MicroSD'''
ch = await ux_show_story(msg, title="[%s]" % xfp2str(xfp), escape='1')
ch = await ux_show_story(msg + msg1, title="[%s]" % xfp2str(xfp), escape='12')
if ch == 'x':
# go back!
set_bip39_passphrase(old_pw)
return
if ch == "2":
stash.SensitiveValues.clear_cache()
nv, xfp = await calc_bip39_passphrase(pp_sofar, bypass_tmp=False)
ch = await ux_show_story(msg, title="[%s]" % xfp2str(xfp), escape='1')
if ch == "x": return
await set_ephemeral_seed(nv, summarize_ux=False, bip39pw=pp_sofar)
if ch == '1':
await PassphraseSaver().append(xfp, pp_sofar)

View File

@ -128,7 +128,8 @@ class SecretStash:
return 'master', ms, hd
# optional global value: user-supplied passphrase to salt BIP-39 seed process
bip39_passphrase = ''
# just a boolean flag from version 5.2.0
bip39_passphrase = False
CACHE_CHECK_RATE = const(10*1000) # 10 seconds
CACHE_MAX_LIFE = const(60*1000) # one minute
@ -136,16 +137,14 @@ CACHE_MAX_LIFE = const(60*1000) # one minute
class SensitiveValues:
# be a context manager, and holder of secrets in-memory
# class-level cache, key is bip39 pass
_cache = {}
# class-level cache
_cache_secret = None
_cache_used = None
def __init__(self, secret=None, bypass_pw=False):
def __init__(self, secret=None, bip39pw='', bypass_tmp=False):
self.spots = []
# backup during volatile bip39 encryption: do not use passphrase
self._bip39pw = '' if bypass_pw else str(bip39_passphrase)
self._bip39pw = bip39pw
if secret is not None:
# sometimes we already know the secret
@ -162,24 +161,20 @@ class SensitiveValues:
raise ValueError('no secrets yet')
self.deltamode = pa.is_deltamode()
if self._bip39pw in self._cache:
# cache hit
if self._cache_secret and not bypass_tmp:
# they are using new BIP39 passphrase but we already have raw secret
self.secret = bytearray(self._cache_secret)
self.mode, r, n = self._cache[self._bip39pw]
self.raw = bytearray(r)
self.node = n.copy()
self.__class__._cache_used = utime.ticks_ms()
else:
if self._cache_secret:
# they are using new BIP39 passphrase but we already have raw secret
self.secret = bytearray(self._cache_secret)
else:
# slow: read from secure element(s)
self.secret = pa.fetch()
# slow: read from secure element(s)
self.secret = pa.fetch(bypass_tmp=bypass_tmp)
# slow: do bip39 key stretching (typically)
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
# slow: do bip39 key stretching (typically)
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
if not bypass_tmp:
# DO NOT save to cache if we are bypassing tmp
# we mostly just need it for some specific
# operation after which we go back to tmp
self.save_to_cache()
self.spots.append(self.secret)
@ -197,12 +192,6 @@ class SensitiveValues:
# - will be called after 2 minutes of idle keypad
blank_object(cls._cache_secret)
cls._cache_secret = None
for _,raw,node in cls._cache.values():
blank_object(raw)
blank_object(node)
cls._cache.clear()
cls._cache_used = None
def save_to_cache(self):
@ -212,7 +201,6 @@ class SensitiveValues:
else:
assert SensitiveValues._cache_secret == self.secret
SensitiveValues._cache[self._bip39pw] = ( self.mode, bytearray(self.raw), self.node.copy() )
SensitiveValues._cache_used = utime.ticks_ms()
call_later_ms(CACHE_CHECK_RATE, self.cache_check)
@ -290,14 +278,8 @@ class SensitiveValues:
xfp = swab32(self.node.my_fp())
xpub = self.chain.serialize_public(self.node)
if self._bip39pw:
settings.put_volatile('xfp', xfp)
settings.put_volatile('xpub', xpub)
else:
settings.overrides.clear()
settings.put('xfp', xfp)
settings.put('xpub', xpub)
settings.put('xfp', xfp)
settings.put('xpub', xpub)
settings.put('chain', self.chain.ctype)
# calc num words in seed, or zero

View File

@ -1,6 +1,7 @@
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, bech32
from subprocess import check_output
from ckcc.protocol import CCProtocolPacker
from helpers import B2A, U2SAT, prandom, taptweak
from msg import verify_message
@ -706,7 +707,7 @@ def set_master_key(sim_exec, sim_execfile, simulator, reset_seed_words):
reset_seed_words()
@pytest.fixture(scope="function")
def set_xfp(sim_exec, sim_execfile, simulator, reset_seed_words):
def set_xfp(sim_exec):
# set the XFP, without really knowing the private keys
# - won't be able to sign, but should accept PSBT for signing
@ -716,11 +717,12 @@ def set_xfp(sim_exec, sim_execfile, simulator, reset_seed_words):
import struct
need_xfp, = struct.unpack("<I", a2b_hex(xfp))
sim_exec('from main import settings; settings.put_volatile("xfp", 0x%x);' % need_xfp)
sim_exec('from main import settings; settings.set("xfp", 0x%x);' % need_xfp)
yield doit
sim_exec('from main import settings; settings.overrides.clear();')
sim_exec('from main import settings; settings.set("xfp", 0x%x);' % simulator_fixed_xfp)
@pytest.fixture(scope="function")
def set_encoded_secret(sim_exec, sim_execfile, simulator, reset_seed_words):
@ -1699,10 +1701,97 @@ def validate_address():
return doit
@pytest.fixture
def verify_backup_file(goto_home, pick_menu_item, cap_story, need_keypress):
def doit(fn):
# Check on-device verify UX works.
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('Backup')
pick_menu_item('Verify Backup')
time.sleep(0.1)
title, body = cap_story()
assert "Select file" in body
need_keypress('y')
time.sleep(0.1)
pick_menu_item(os.path.basename(fn))
time.sleep(0.1)
title, body = cap_story()
assert "Backup file CRC checks out okay" in body
return doit
@pytest.fixture
def check_and_decrypt_backup(microsd_path):
def doit(fn, passphrase):
# List contents using unix tools
pn = microsd_path(fn)
out = check_output(['7z', 'l', pn], encoding='utf8')
xfname, = re.findall('[a-z0-9]{4,30}.txt', out)
print(f"Filename inside 7z: {xfname}")
assert xfname in out
assert 'Method = 7zAES' in out
xfn_path = microsd_path(xfname)
if os.path.exists(xfn_path):
os.remove(xfn_path)
# does decryption; at least for CRC purposes
args = ['7z', 'e', '-p' + ' '.join(passphrase), pn, xfname, '-o' + '../unix/work/MicroSD',]
out = check_output(args, encoding='utf8')
assert "Extracting archive" in out, out
assert "Everything is Ok" in out, out
with open(xfn_path, "r") as f:
res = f.read()
return res
return doit
@pytest.fixture
def restore_backup_cs(unit_test, pick_menu_item, cap_story, cap_menu,
need_keypress, word_menu_entry, get_setting):
# restore backup with clear seed as first step
def doit(fn, passphrase, avail_settings=None):
unit_test('devtest/clear_seed.py')
m = cap_menu()
assert m[0] == 'New Seed Words'
pick_menu_item('Import Existing')
pick_menu_item('Restore Backup')
# skip
title, body = cap_story()
if ('files to pick from' in body) or ("only one file to pick from" in body):
need_keypress('y')
time.sleep(.01)
pick_menu_item(fn)
time.sleep(.1)
word_menu_entry(passphrase)
title, body = cap_story()
assert title == 'Success!'
assert 'has been successfully restored' in body
if avail_settings:
for key in avail_settings:
assert get_setting(key)
# avoid simulator reboot; restore normal state
unit_test('devtest/abort_ux.py')
return doit
# useful fixtures related to multisig
from test_multisig import (import_ms_wallet, make_multisig, offer_ms_import, fake_ms_txn,
make_ms_address, clear_ms, make_myself_wallet)
from test_bip39pw import set_bip39_pw, clear_bip39_pw
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
# EOF

View File

@ -10,7 +10,7 @@ from sim_settings import sim_defaults
if not pa.is_secret_blank():
# clear settings associated with this key, since it will be no more
settings.current = dict(sim_defaults)
settings.overrides.clear()
pa.tmp_value = None
# save a blank secret (all zeros is a special case, detected by bootloader)
dis.fullscreen('Wipe Seed!')

View File

@ -10,7 +10,6 @@ from stash import SecretStash, SensitiveValues
from utils import xfp2str
settings.current = dict(sim_defaults)
settings.overrides.clear()
import main
raw = main.ENCODED_SECRET

View File

@ -15,7 +15,6 @@ print("New raw secret: %s" % b2a_hex(rs))
if 1:
settings.current = dict(sim_defaults)
settings.overrides.clear()
settings.set('chain', 'XTN')
pa.change(new_secret=rs)

View File

@ -14,7 +14,6 @@ tn = chains.BitcoinTestnet
stash.bip39_passphrase = ''
settings.current = sim_defaults
settings.overrides.clear()
settings.set('chain', 'XTN')
settings.set('words', True)
settings.set('terms_ok', True)

View File

@ -25,7 +25,6 @@ if settings.get('xfp') == swab32(node.my_fp()):
else:
settings.current = sim_defaults
settings.overrides.clear()
settings.set('chain', 'XTN')
raw = SecretStash.encode(xprv=node)

View File

@ -4,13 +4,12 @@
#
import pytest, time, struct
from pycoin.key.BIP32Node import BIP32Node
from base64 import b64encode
from binascii import b2a_hex, a2b_hex
from binascii import a2b_hex
from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, CCUserRefused
from ckcc_protocol.constants import *
import json
from mnemonic import Mnemonic
from conftest import simulator_fixed_xfp
from constants import simulator_fixed_xfp, simulator_fixed_words, simulator_fixed_xprv
# add the BIP39 test vectors
vectors = json.load(open('bip39-vectors.json'))['english']
@ -50,16 +49,18 @@ def test_b9p_basic(pw, set_bip39_pw):
@pytest.fixture()
def clear_bip39_pw(sim_exec, reset_seed_words):
# faster?
reset_seed_words()
def set_bip39_pw(dev, need_keypress, reset_seed_words, cap_story, sim_execfile):
@pytest.fixture()
def set_bip39_pw(dev, need_keypress, reset_seed_words, cap_story):
def doit(pw):
def doit(pw, reset=True):
# reset from previous runs
words = reset_seed_words()
if reset:
words = reset_seed_words()
else:
conts = sim_execfile('devtest/get-secrets.py')
assert 'mnemonic' in conts
for l in conts.split("\n"):
if l.startswith("mnemonic ="):
words = l.split("=")[-1].strip().replace('"', '')
# optimization
if pw == '':
@ -139,12 +140,20 @@ def test_cancel_on_empty_added_numbers(pick_menu_item, goto_home, need_keypress,
assert m[0] == "Ready To Sign"
@pytest.mark.parametrize('haz', [ False, True ])
def test_lockdown(dev, haz, cap_menu, pick_menu_item, set_bip39_pw, goto_home, cap_story, need_keypress, sim_exec, sim_eval, get_settings, reset_seed_words, get_setting):
@pytest.mark.parametrize('stype', ["bip39pw", "words", "xprv", None])
def test_lockdown(stype, pick_menu_item, set_bip39_pw, goto_home, cap_story,
need_keypress, sim_exec, get_settings, reset_seed_words,
get_setting, generate_ephemeral_words, import_ephemeral_xprv):
# test UX and operation of the 'seed lockdown' option
if haz:
xfp = set_bip39_pw('test')
if stype:
if stype == "bip39pw":
set_bip39_pw('test')
elif stype == "words":
generate_ephemeral_words(24)
elif stype == "xprv":
import_ephemeral_xprv("sd")
xfp = get_setting("xfp")
assert xfp != simulator_fixed_xfp
goto_home()
@ -156,26 +165,13 @@ def test_lockdown(dev, haz, cap_menu, pick_menu_item, set_bip39_pw, goto_home, c
time.sleep(0.1)
title, story = cap_story()
assert 'Are you SURE' in story
assert 'computes' in story
if not haz:
need_keypress('y')
time.sleep(0.1)
title, story = cap_story()
if stype:
assert 'Are you SURE' in story
assert 'do not have a BIP-39 passphrase' in story
else:
assert 'do not have an active ephemeral seed' in story
need_keypress('x')
return
# before saving, xfp should be in-memory only
nv = get_settings()
mem_xfp = get_setting('xfp')
assert hex(mem_xfp) == hex(xfp), "XFP or key correct b4 save"
assert nv['xfp'] != mem_xfp, "in-memory xfp not different from saved value"
# real code does reboot, which is poorly simulated; avoid that
sim_exec('import callgate; callgate.show_logout = lambda x:0')
@ -191,7 +187,118 @@ def test_lockdown(dev, haz, cap_menu, pick_menu_item, set_bip39_pw, goto_home, c
assert nv['xfp'] == mem_xfp, "in-memory xfp different from saved value"
# not 100% sure this reset is complete enough
goto_home()
# goto_home()
reset_seed_words()
@pytest.mark.parametrize("stype", ["words", "xprv"])
@pytest.mark.parametrize("passphrase", ["@coinkite rulez!!", "!@#!@", "AAAAAAAAAAA"])
def test_bip39pass_on_ephemeral_seed(generate_ephemeral_words, import_ephemeral_xprv,
need_keypress, pick_menu_item, goto_home,
reset_seed_words, goto_eph_seed_menu, stype,
enter_complex, cap_story, cap_menu, passphrase):
reset_seed_words()
goto_eph_seed_menu()
if stype == "words":
# words
sec = generate_ephemeral_words(24, from_main=True)
else:
# node
sec = import_ephemeral_xprv("sd", from_main=True)
goto_home()
if stype == "xprv":
# cannot add passphrase on top of extended key - only words
m = cap_menu()
assert "Passphrase" not in m
return
pick_menu_item("Passphrase")
need_keypress("y")
enter_complex(passphrase)
pick_menu_item("APPLY")
time.sleep(.1)
title, story = cap_story()
# title is xfp = simulator fixed words + pass (as first iteration is always from main seed)
xfp0 = title[1:-1]
seed0 = Mnemonic.to_seed(simulator_fixed_words, passphrase=passphrase)
expect0 = BIP32Node.from_master_secret(seed0)
assert expect0.fingerprint().hex().upper() == xfp0
assert "press (2) to add passphrase to the current active ephemeral seed" in story
need_keypress("2")
time.sleep(.5)
title, story = cap_story()
xfp1 = title[1:-1]
seed1 = Mnemonic.to_seed(" ".join(sec), passphrase=passphrase)
expect1 = BIP32Node.from_master_secret(seed1)
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,6 +7,9 @@ from constants import simulator_fixed_tpub
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 = {
@ -33,8 +36,17 @@ def seed_story_to_words(story: str):
@pytest.fixture
def ephemeral_seed_disabled(cap_menu):
def ephemeral_seed_disabled(sim_exec):
def doit():
rv = sim_exec('from pincodes import pa; RV.write(repr(pa.tmp_value))')
assert not eval(rv)
return doit
@pytest.fixture
def ephemeral_seed_disabled_ui(cap_menu):
def doit():
# MUST be in ephemeral seed menu already
time.sleep(0.1)
menu = cap_menu()
# no ephemeral seed chosen (yet)
@ -85,7 +97,7 @@ def get_identity_story(goto_home, pick_menu_item, cap_story):
@pytest.fixture
def goto_eph_seed_menu(goto_home, pick_menu_item, cap_story, need_keypress):
def doit():
def _doit():
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Ephemeral Seed")
@ -95,6 +107,14 @@ def goto_eph_seed_menu(goto_home, pick_menu_item, cap_story, need_keypress):
assert "temporary secret stored solely in device RAM" in story
assert "Press (4) to prove you read to the end of this message and accept all consequences." in story
need_keypress("4") # understand consequences
def doit():
try:
_doit()
except:
time.sleep(.1)
_doit()
return doit
@ -104,8 +124,8 @@ def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn
get_seed_value_ux):
def doit(mnemonic=None, xpub=None, expected_xfp=None):
time.sleep(0.3)
_, story = cap_story()
in_effect_xfp = story[1:9]
title, story = cap_story()
in_effect_xfp = title[1:-1]
if expected_xfp is not None:
assert in_effect_xfp == expected_xfp
assert "key in effect until next power down." in story
@ -146,42 +166,115 @@ def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn
return doit
@pytest.fixture
def generate_ephemeral_words(goto_eph_seed_menu, pick_menu_item,
need_keypress, cap_story,
ephemeral_seed_disabled_ui):
def doit(num_words, dice=False, from_main=False):
goto_eph_seed_menu()
if from_main:
ephemeral_seed_disabled_ui()
pick_menu_item("Generate Words")
if not dice:
pick_menu_item(f"{num_words} Words")
time.sleep(0.1)
else:
pick_menu_item(f"{num_words} Word Dice Roll")
for ch in '123456yy':
need_keypress(ch)
time.sleep(0.2)
title, story = cap_story()
assert f"Record these {num_words} secret words!" in story
assert "Press (6) to skip word quiz" in story
# filter those that starts with space, number and colon --> actual words
e_seed_words = seed_story_to_words(story)
assert len(e_seed_words) == num_words
need_keypress("6") # skip quiz
need_keypress("y") # yes - I'm sure
time.sleep(0.1)
need_keypress("4") # understand consequences
return e_seed_words
return doit
@pytest.fixture
def import_ephemeral_xprv(microsd_path, virtdisk_path, goto_eph_seed_menu,
pick_menu_item, need_keypress, cap_story,
nfc_write_text, ephemeral_seed_disabled_ui):
def doit(way, extended_key=None, testnet=True, from_main=False):
from pycoin.key.BIP32Node import BIP32Node
fname = "ek.txt"
if extended_key is None:
node = BIP32Node.from_master_secret(os.urandom(32), netcode="XTN" if testnet else "BTC")
ek = node.hwif(as_private=True) + '\n'
if way == "sd":
fpath = microsd_path(fname)
elif way == "vdisk":
fpath = virtdisk_path(fname)
if way != "nfc":
with open(fpath, "w") as f:
f.write(ek)
else:
node = BIP32Node.from_wallet_key(extended_key)
assert extended_key == node.hwif(as_private=True)
ek = extended_key
if testnet:
assert "tprv" in ek
else:
assert "xprv" in ek
goto_eph_seed_menu()
if from_main:
ephemeral_seed_disabled_ui()
pick_menu_item("Import XPRV")
time.sleep(0.1)
_, story = cap_story()
if way == "sd":
if "Press (1) to import extended private key file from SD Card" in story:
need_keypress("1")
elif way == "nfc":
if "press (3) to import via NFC" not in story:
pytest.xfail("NFC disabled")
else:
need_keypress("3")
time.sleep(0.2)
nfc_write_text(ek)
time.sleep(0.3)
else:
# virtual disk
if "press (2) to import from Virtual Disk" not in story:
pytest.xfail("Vdisk disabled")
else:
need_keypress("2")
if way != "nfc":
time.sleep(0.1)
_, story = cap_story()
assert "Select file containing the extended private key" in story
need_keypress("y")
pick_menu_item(fname)
return node
return doit
@pytest.mark.parametrize("num_words", [12, 24])
@pytest.mark.parametrize("dice", [False, True])
def test_ephemeral_seed_generate(num_words, pick_menu_item, cap_story, need_keypress,
reset_seed_words, goto_eph_seed_menu, dice,
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):
reset_seed_words()
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Generate Words")
if not dice:
pick_menu_item(f"{num_words} Words")
time.sleep(0.1)
else:
pick_menu_item(f"{num_words} Word Dice Roll")
for ch in '123456yy':
need_keypress(ch)
time.sleep(0.2)
title, story = cap_story()
assert f"Record these {num_words} secret words!" in story
assert "Press (6) to skip word quiz" in story
# filter those that starts with space, number and colon --> actual words
e_seed_words = seed_story_to_words(story)
assert len(e_seed_words) == num_words
need_keypress("6") # skip quiz
need_keypress("y") # yes - I'm sure
time.sleep(0.1)
need_keypress("4") # understand consequences
e_seed_words = generate_ephemeral_words(num_words=num_words, dice=dice,
from_main=True)
verify_ephemeral_secret_ui(mnemonic=e_seed_words)
@ -198,11 +291,7 @@ def test_ephemeral_seed_import_words(nfc, truncated, num_words, cap_menu, pick_m
words, expect_xfp = WORDLISTS[num_words]
reset_seed_words()
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Import Words")
@ -244,11 +333,7 @@ def test_ephemeral_seed_import_tapsigner(way, retry, testnet, pick_menu_item, ca
fname, backup_key_hex, node = tapsigner_encrypted_backup(way, testnet=testnet)
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Tapsigner Backup")
@ -302,11 +387,8 @@ def test_ephemeral_seed_import_tapsigner_fail(pick_menu_item, cap_story, fail,
if fail == "garbage":
with open(microsd_path(fname), "wb") as f:
f.write(os.urandom(152))
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Tapsigner Backup")
@ -359,11 +441,7 @@ def test_ephemeral_seed_import_tapsigner_real(data, pick_menu_item, cap_story, m
fpath = microsd_path(fname)
shutil.copy(f"data/{fname}", fpath)
reset_seed_words()
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Tapsigner Backup")
@ -390,63 +468,12 @@ 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, pick_menu_item,
cap_story, need_keypress, reset_seed_words,
goto_eph_seed_menu, nfc_write_text, microsd_path,
virtdisk_path, verify_ephemeral_secret_ui,
ephemeral_seed_disabled):
def test_ephemeral_seed_import_xprv(way, retry, testnet, reset_seed_words,
goto_eph_seed_menu, verify_ephemeral_secret_ui,
ephemeral_seed_disabled, import_ephemeral_xprv):
reset_seed_words()
fname = "ek.txt"
from pycoin.key.BIP32Node import BIP32Node
node = BIP32Node.from_master_secret(os.urandom(32), netcode="XTN" if testnet else "BTC")
ek = node.hwif(as_private=True) + '\n'
if way =="sd":
fpath = microsd_path(fname)
elif way == "vdisk":
fpath = virtdisk_path(fname)
if way != "nfc":
with open(fpath, "w") as f:
f.write(ek)
if testnet:
assert "tprv" in ek
else:
assert "xprv" in ek
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Import XPRV")
time.sleep(0.1)
_, story = cap_story()
if way == "sd":
if "Press (1) to import extended private key file from SD Card" in story:
need_keypress("1")
elif way == "nfc":
if "press (3) to import via NFC" not in story:
pytest.xfail("NFC disabled")
else:
need_keypress("3")
time.sleep(0.2)
nfc_write_text(ek)
time.sleep(0.3)
else:
# virtual disk
if "press (2) to import from Virtual Disk" not in story:
pytest.xfail("Vdisk disabled")
else:
need_keypress("2")
if way != "nfc":
time.sleep(0.1)
_, story = cap_story()
assert "Select file containing the extended private key" in story
need_keypress("y")
pick_menu_item(fname)
node = import_ephemeral_xprv(way=way, testnet=testnet, from_main=True)
verify_ephemeral_secret_ui(xpub=node.hwif())
@ -455,11 +482,7 @@ def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
pick_menu_item, need_keypress,
word_menu_entry):
reset_seed_words()
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
goto_eph_seed_menu()
ephemeral_seed_disabled()
words, expected_xfp = WORDLISTS[12]
@ -469,15 +492,11 @@ def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
word_menu_entry(words.split())
time.sleep(0.3)
_, story = cap_story()
title, story = cap_story()
assert "key in effect until next power down." in story
in_effect_xfp = story[1:9]
in_effect_xfp = title[1:-1]
need_keypress("y")
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
goto_eph_seed_menu()
pick_menu_item("Import Words")
pick_menu_item(f"12 Words")
@ -485,10 +504,92 @@ def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
word_menu_entry(words.split())
time.sleep(0.3)
_, story = cap_story()
title, story = cap_story()
assert "Ephemeral master key already in use" in story
already_used_xfp = story[1:9]
already_used_xfp = title[1:-1]
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

@ -6,7 +6,7 @@
#
# py.test test_multisig.py -m ms_danger --ms-danger
#
import time, pytest, os, random, json, shutil, pdb, io, base64
import time, pytest, os, random, json, shutil, pdb, io, base64, struct
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
from ckcc.protocol import CCProtocolPacker, MAX_TXN_LEN
from pprint import pprint
@ -87,14 +87,14 @@ def clear_ms(unit_test):
unit_test('devtest/wipe_ms.py')
return doit
@pytest.fixture()
def make_multisig():
@pytest.fixture
def make_multisig(dev, sim_execfile):
# make a multsig wallet, always with simulator as an element
# default is BIP-45: m/45'/... (but no co-signer idx)
# - but can provide str format for deriviation, use {idx} for cosigner idx
def doit(M, N, unique=0, deriv=None, chain="XTN"):
def doit(M, N, unique=0, deriv=None, dev_key=False, chain="XTN"):
keys = []
for i in range(N-1):
@ -114,7 +114,14 @@ def make_multisig():
keys.append((xfp, pk, sub))
pk = BIP32Node.from_wallet_key(simulator_fixed_tprv if chain == "XTN" else simulator_fixed_xprv)
if dev_key:
sk = sim_execfile('devtest/dump_private.py').strip()
pk = BIP32Node.from_wallet_key(sk)
xfp_bytes = pk.fingerprint()
xfp = swab32(struct.unpack('>I', xfp_bytes)[0])
else:
pk = BIP32Node.from_wallet_key(simulator_fixed_tprv if chain == "XTN" else simulator_fixed_xprv)
xfp = simulator_fixed_xfp
if not deriv:
sub = pk.subkey(45, is_hardened=True, as_private=True)
@ -126,7 +133,7 @@ def make_multisig():
# some test cases are using bogus paths
sub = pk
keys.append((simulator_fixed_xfp, pk, sub))
keys.append((xfp, pk, sub))
return keys
@ -153,9 +160,12 @@ def offer_ms_import(cap_story, dev, need_keypress):
@pytest.fixture
def import_ms_wallet(dev, make_multisig, offer_ms_import, need_keypress):
def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None, keys=None, do_import=True, derivs=None,
descriptor=False, int_ext_desc=False, chain="XTN"):
keys = keys or make_multisig(M, N, unique=unique, deriv=common or (derivs[0] if derivs else None), chain=chain)
def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None,
keys=None, do_import=True, derivs=None, descriptor=False,
int_ext_desc=False, dev_key=False, chain="XTN"):
keys = keys or make_multisig(M, N, unique=unique, dev_key=dev_key,
deriv=common or (derivs[0] if derivs else None),
chain=chain)
name = name or f'test-{M}-{N}'
if not do_import:

View File

@ -46,8 +46,8 @@ def get_to_pwmenu(cap_story, need_keypress, goto_home, pick_menu_item):
'1aaa2aaa',
'ab'*25,
])
def test_first_time(pws, need_keypress, cap_story, pick_menu_item, goto_home, enter_complex, cap_menu, get_to_pwmenu):
def test_first_time(pws, need_keypress, cap_story, pick_menu_item, enter_complex,
cap_menu, get_to_pwmenu, reset_seed_words):
try: os.unlink(SIM_FNAME)
except: pass
@ -75,11 +75,11 @@ def test_first_time(pws, need_keypress, cap_story, pick_menu_item, goto_home, en
time.sleep(.01)
title, story = cap_story()
xfp = title[1:-1]
assert '1 to use and save to MicroSD' in story
assert '(1) to use and save to MicroSD' in story
need_keypress('1')
xfps[pw] = xfp
reset_seed_words()
for n, pw in enumerate(uniq):
get_to_pwmenu()
@ -103,7 +103,8 @@ def test_first_time(pws, need_keypress, cap_story, pick_menu_item, goto_home, en
xfp = title[1:-1]
assert xfp == xfps[uniq[n]]
need_keypress('y');
need_keypress('y')
reset_seed_words()
def test_crypto_unittest(sim_eval, sim_exec, simulator):

View File

@ -2,7 +2,7 @@
#
# Mk4 SE2 (second secure element) test cases and fixtures.
#
# - use 'simulator.py --eff' for these
# - use 'simulator.py' without '--eff' for these
#
import pytest, struct, time
from collections import namedtuple
@ -497,7 +497,6 @@ def test_ux_duress_choices(with_wipe, subchoice, expect, xflags, xargs,
need_keypress('x')
time.sleep(.1)
pick_menu_item('Activate Wallet')
time.sleep(.1)
_, story = cap_story()
@ -512,9 +511,7 @@ def test_ux_duress_choices(with_wipe, subchoice, expect, xflags, xargs,
assert xp == wallet.hwif(as_private=False)
assert not get_setting('multisig') # multisig is not copied
assert not get_setting('tp') # trick pins are not copied
assert not get_setting('bkpw') # backup password is not copied
assert not get_setting('usr') # hsm users are not copied
# re-login to recover normal seed
reset_seed_words()
repl.exec('pa.tmp_value=False; pa.setup(pa.pin); pa.login()')

View File

@ -2345,7 +2345,7 @@ def test_batch_sign(num_tx, ui_path, action, fake_txn, need_keypress,
pytest.skip("classic sign")
_, story = cap_story()
assert "Press (9) to use Batch Sign" in story
assert "Press (9) to select all files for potential signing" in story
need_keypress("9")
time.sleep(.1)

View File

@ -2,7 +2,9 @@
#
import pytest, time, os, re, hashlib
from helpers import xfp2str, prandom
from constants import AF_CLASSIC
from constants import AF_CLASSIC, simulator_fixed_words
from mnemonic import Mnemonic
from pycoin.key.BIP32Node import BIP32Node
def test_get_secrets(get_secrets, master_xpub):
@ -127,17 +129,43 @@ def pass_word_quiz(need_keypress, cap_story):
@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, 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):
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!
if multisig:
# 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:
@ -149,6 +177,18 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
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
@ -181,7 +221,7 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
assert "Press (1) to save" in body
if save_pw:
need_keypress('1')
time.sleep(.01)
time.sleep(.1)
assert get_setting('bkpw') == ' '.join(words)
else:
@ -193,6 +233,12 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
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):
@ -218,68 +264,17 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
need_keypress('x')
time.sleep(.01)
# Check on-device verify UX works.
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('Backup')
pick_menu_item('Verify Backup')
time.sleep(0.1)
title, body = cap_story()
assert "Select file" in body
need_keypress('y')
time.sleep(0.1)
pick_menu_item(os.path.basename(fn))
time.sleep(0.1)
title, body = cap_story()
assert "Backup file CRC checks out okay" in body
# List contents using unix tools
from subprocess import check_output
import re
pn = microsd_path(files[0])
out = check_output(['7z', 'l', pn], encoding='utf8')
xfname, = re.findall('[a-z0-9]{4,30}.txt', out)
print(f"Filename inside 7z: {xfname}")
assert xfname in out
assert 'Method = 7zAES' in out
# does decryption; at least for CRC purposes
out = check_output(['7z', 't', '-p'+' '.join(words), pn, xfname], encoding='utf8')
assert "Everything is Ok" in out, out
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)
# try decrypt on microptyhon
unit_test('devtest/clear_seed.py')
avail_settings = ['multisig'] if multisig else None
restore_backup_cs(files[0], words, avail_settings=avail_settings)
m = cap_menu()
assert m[0] == 'New Seed Words'
pick_menu_item('Import Existing')
pick_menu_item('Restore Backup')
# skip
title, body = cap_story()
assert 'files to pick from' in body
need_keypress('y'); time.sleep(.01)
pick_menu_item(files[0])
word_menu_entry(words)
title, body = cap_story()
assert title == 'Success!'
assert 'has been successfully restored' in body
if multisig:
assert len(get_setting('multisig')) == 1
# avoid simulator reboot; restore normal state
unit_test('devtest/abort_ux.py')
reset_seed_words()
settings_remove('multisig')
@ -489,7 +484,6 @@ def test_import_prv(way, testnet, pick_menu_item, cap_story, need_keypress, unit
unit_test('devtest/clear_seed.py')
from pycoin.key.BIP32Node import BIP32Node
node = BIP32Node.from_master_secret(os.urandom(32), netcode=netcode)
prv = node.hwif(as_private=True)+'\n'
if testnet:
@ -756,15 +750,15 @@ def test_bip39_complex(target, goto_home, pick_menu_item, cap_story,
@pytest.mark.qrcode
@pytest.mark.parametrize('mode', ['words', 'xprv', 'ms'])
@pytest.mark.parametrize('b39_word', ['', 'AbcZz1203'])
def test_show_seed(mode, b39_word, goto_home, pick_menu_item, cap_story, need_keypress, sim_exec,
cap_menu, get_pp_sofar, get_secrets, cap_screen_qr, set_encoded_secret, qr_quality_check):
def test_show_seed(mode, b39_word, goto_home, pick_menu_item, cap_story, need_keypress,
sim_exec, cap_menu, get_pp_sofar, get_secrets, cap_screen_qr,
set_encoded_secret, qr_quality_check, reset_seed_words, set_bip39_pw):
reset_seed_words()
if mode == 'words':
# Check the seed words are displayed correctly: the new "View Seed Words" feature
sim_exec("import stash; stash.bip39_passphrase = '%s'" % b39_word)
set_bip39_pw(b39_word, reset=False)
words = simulator_fixed_words.split(" ")
v = get_secrets()
words = v['mnemonic'].split(' ')
else:
if b39_word: return
@ -785,11 +779,11 @@ def test_show_seed(mode, b39_word, goto_home, pick_menu_item, cap_story, need_ke
pick_menu_item('Danger Zone')
pick_menu_item('Seed Functions')
pick_menu_item('View Seed Words')
time.sleep(.01);
time.sleep(.01)
title, body = cap_story()
assert 'Are you SURE' in body
assert 'can control all funds' in body
need_keypress('y'); # skip warning
need_keypress('y') # skip warning
time.sleep(0.01)
title, body = cap_story()
@ -803,9 +797,15 @@ def test_show_seed(mode, b39_word, goto_home, pick_menu_item, cap_story, need_ke
if b39_word:
assert lines[26] == 'BIP-39 Passphrase:'
assert b39_word in lines[27]
sim_exec("import stash; stash.bip39_passphrase = ''")
assert "*" in lines[27]
assert "Seed+Passphrase" in lines[29]
ek = lines[30]
seed = Mnemonic.to_seed(simulator_fixed_words, passphrase=b39_word)
expect = BIP32Node.from_master_secret(seed, netcode="XTN")
esk = expect.hwif(as_private=True)
assert esk == ek
else:
assert "BIP-39 Passphrase" not in body
qr_expect = ' '.join(w[0:4].upper() for w in words)
@ -983,7 +983,7 @@ def test_bip39_pw_signing_xfp_ux(goto_home, pick_menu_item, need_keypress, cap_s
time.sleep(0.3)
title, _ = cap_story()
assert title == "[0C9DC99D]"
need_keypress("y") # confirm new wallet
need_keypress("y") # confirm passphrase
pick_menu_item("Ready To Sign")
time.sleep(0.1)
title_sign, _ = cap_story()