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:
parent
342af8f78e
commit
22fd6b4010
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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.")
|
||||
|
||||
|
||||
@ -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)',
|
||||
|
||||
@ -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()),
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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!')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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()')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user