Do NOT inherit linked and prelogin settings, do NOT backup words length setting; goto_top_menu after activating bip85 as ephemeral secret

This commit is contained in:
scgbckbone 2023-07-13 11:13:09 +02:00 committed by doc-hex
parent cd068f7bbc
commit 79ce4ae115
24 changed files with 274 additions and 452 deletions

View File

@ -6,6 +6,8 @@
[BIP-0380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions)
was `(m=0F056943)/m/48'/1'/0'/2'/0/0` now `[0F056943/48'/1'/0'/2'/0/0]`
- Enhancement: Address explorer UX cosmetics, now with arrows and dots.
- Enhancement: Linked settings (multisig, trick pins, backup password, hsm users and utxo cache)
separation for new main secret.
- Rename `Unchained Capital` to `Unchained`
- Bugfix: Correct `scriptPubkey` parsing for segwit v1-v16
- Bugfix: Do not infer segwit just by availability of `PSBT_IN_WITNESS_UTXO` in PSBT.
@ -14,6 +16,7 @@
- Bugfix: Empty number during BIP-39 passphrase entry could cause crash.
- Bugfix: Signing with BIP39 Passphrase showed master fingerprint as integer. Fixed to show hex.
- Bugfix: Fixed inability to generate paper wallet without secrets
- Bugfix: Activating trick pin duress wallet copied multisig settings from main wallet
- Bugfix: SD2FA setting is cleared when seed is wiped after failed login due to policy SD2FA enforce.
Prevents infinite seed wipe loop when restoring backup after 2FA MicroSD lost or damaged.
SD2FA is not backed up and also not restored from older backups. If SD2FA is set up,

View File

@ -2187,20 +2187,18 @@ async def usb_keyboard_emulation(enable):
async def change_nfc_enable(enable):
# NFC enable / disable
import glob
from glob import NFC
import nfc
if not enable:
if glob.NFC:
glob.NFC.shutdown()
if NFC:
NFC.shutdown()
else:
nfc.NFCHandler.startup()
async def change_virtdisk_enable(enable):
# NOTE: enable can be 0,1,2
import glob, vdisk
from usb import enable_usb, disable_usb
if bool(enable) == bool(glob.VD):
# not a change in state, do nothing
@ -2231,7 +2229,9 @@ async def microsd_2fa(*a):
from pwsave import MicroSD2FA
if not settings.get('sd2fa'):
ch = await ux_show_story('''When enabled, this feature requires a specially prepared MicroSD card to be inserted during login process. After correct PIN is provided, if card slot is empty or unknown card present, the seed is wiped.''')
ch = await ux_show_story('When enabled, this feature requires a specially prepared MicroSD card '
'to be inserted during login process. After correct PIN is provided, '
'if card slot is empty or unknown card present, the seed is wiped.')
if ch != 'y':
return

View File

@ -72,7 +72,7 @@ class KeypathMenu(MenuSystem):
dis.clear_rect(0, y, dis.WIDTH, 8)
dis.text(-1, y+4, self.prefix, FontTiny, invert=False)
def done(self, _1, menu_idx, item):
async def done(self, _1, menu_idx, item):
final_path = item.arg or item.label
self.chosen = menu_idx
self.show()
@ -89,7 +89,7 @@ class KeypathMenu(MenuSystem):
return PickAddrFmtMenu(final_path, top)
def deeper(self, _1, _2, item):
async def deeper(self, _1, _2, item):
val = item.arg or item.label
assert val.endswith('/..')
cpath = val[:-3]
@ -110,7 +110,7 @@ class PickAddrFmtMenu(MenuSystem):
if path.startswith("m/49'"):
self.goto_idx(2)
def done(self, _1, _2, item):
async def done(self, _1, _2, item):
the_ux.pop()
await self.parent.got_custom_path(*item.arg)
@ -126,7 +126,7 @@ class ApplicationsMenu(MenuSystem):
]
super().__init__(items)
def done(self, _1, _2, item):
async def done(self, _1, _2, item):
path = item.arg[0]
addr_fmt = item.arg[1]
await self.parent.show_n_addresses(path, addr_fmt, None, n=10, allow_change=True)

View File

@ -5,7 +5,6 @@
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from utils import imported, xfp2str
from ux import ux_show_story, ux_confirm, ux_dramatic_pause
import version, ujson
from uio import StringIO
@ -98,6 +97,7 @@ def render_backup_contents():
if k == 'xfp': continue # redundant, and wrong if bip39pw
if k == 'bkpw': continue # confusing/circular
if k == 'sd2fa': continue # do NOT backup SD 2FA (card can be lost or damaged)
if k == 'words': continue # words length is recalculated from secret
ADD('setting.' + k, v)
if version.has_fatram:
@ -292,7 +292,7 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_copies=True):
# Just do the writing
from glob import dis
from files import CardSlot, CardMissingError
from files import CardSlot
# Show progress:
dis.fullscreen('Encrypting...' if words else 'Generating...')
@ -374,11 +374,11 @@ async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_
while 1:
msg = '''Backup file written:\n\n%s\n\n\
To view or restore the file, you must have the full password.\n\n\
Insert another SD card and press 2 to make another copy.''' % (nice)
Insert another SD card and press 2 to make another copy.''' % nice
ch = await ux_show_story(msg, escape='2')
if ch == 'y': return
if ch in 'xy': return
if ch == '2': break
else:

View File

@ -114,13 +114,14 @@ def check_file_headers(f):
raise ValueError("Second header too big")
# capture this spot
# TODO 'data_start' unused
data_start = f.tell() # expect 0x20
try:
f.seek(sh.offset, 1)
th = f.read(sh.size)
if len(th) != sh.size:
raise IndexError("Truncated file? %s" % e.message)
raise IndexError("Truncated file?")
# Look for properties about compression. this could be
# faked-out but good enough for now

View File

@ -4,8 +4,6 @@
#
from ucollections import OrderedDict
from nvstore import SettingsObject
from menu import MenuItem
from ux import ux_show_story, ux_dramatic_pause
# Login countdown length, stored in minutes
#
@ -53,81 +51,5 @@ def real_countdown_chooser(tag, offset, def_to):
def countdown_chooser():
return real_countdown_chooser('lgto', 0, 0)
def cd_countdown_chooser():
return real_countdown_chooser('cd_lgto', 1, 60)
# Mk3 only
async def set_countdown_pin(_1, _2, menu_item):
# Accept a new PIN to be used to enable this feature
from login import LoginUX
lll = LoginUX()
lll.reset()
lll.subtitle = "Countdown PIN"
pin = await lll.get_new_pin(None, allow_clear=True) # a string
s = SettingsObject()
from pincodes import pa
if pin == pa.pin.decode():
# can't compare to others like duress/brickme but will override them
await ux_show_story("Must be a unique PIN value!")
return
elif not pin:
# X on first screen does this (better than CLEAR_PIN thing)
s.remove_key('cd_pin')
msg = 'PIN Cleared.'
menu_item.label = "Enable Feature"
else:
s.set('cd_pin', pin)
msg = 'PIN Set.'
menu_item.label = "PIN is Set!"
s.save()
await ux_dramatic_pause(msg, 3)
# Mk3 only
def set_countdown_pin_mode():
# cd_mode = various harm levels
s = SettingsObject()
which = s.get('cd_mode', 0) # default is brick
del s
ch = ['Brick', 'Final PIN', 'Test Mode']
def set(idx, text):
# save it, but "outside" of login PIN
s = SettingsObject()
s.set('cd_mode', idx)
s.save()
del s
return which, ch, set
# Mk3 only
async def countdown_pin_submenu(*a):
# Background and settings for duress-countdown pin
s = SettingsObject()
pin_set = bool(s.get('cd_pin', 0))
if not pin_set:
ok = await ux_show_story('''\
This special PIN will immediately and silently brick the Coldcard, \
but as it does that, it shows a normal-looking countdown timer for login. \
At the end of the countdown, the Coldcard crashes with a vague error. \
Instead of complete brick, you may select a test mode (no harm done) or \
to consume all but the final PIN attempt.\
''')
if not ok: return
return [
MenuItem('PIN is Set!' if pin_set else 'Enable Feature', f=set_countdown_pin),
MenuItem('Countdown Time', chooser=cd_countdown_chooser),
MenuItem('Brick Mode', chooser=set_countdown_pin_mode),
]
# EOF

View File

@ -2,12 +2,10 @@
#
# display.py - OLED rendering
#
import machine, ssd1306, uzlib, ckcc, utime
import machine, uzlib, ckcc, utime
from ssd1306 import SSD1306_SPI
from version import is_devmode
import framebuf
import uasyncio
from uasyncio import sleep_ms
from graphics import Graphics
from sram2 import display2_buf

View File

@ -246,12 +246,11 @@ async def drv_entro_step2(_1, picked, _2):
stash.blank_object(msg)
if ch == '2' and (encoded is not None):
from glob import dis
from pincodes import pa
# switch over to new secret!
from actions import goto_top_menu
dis.fullscreen("Applying...")
await seed.set_ephemeral_seed(encoded)
goto_top_menu()
if encoded is not None:
stash.blank_object(encoded)

View File

@ -3,11 +3,11 @@
# flow.py - Menu structure
#
from menu import MenuItem, ToggleMenuItem
import version
from glob import settings
from actions import *
from choosers import *
from mk4 import dev_enable_repl
from multisig import make_multisig_menu, import_multisig_nfc
from seed import make_ephemeral_seed_menu
from address_explorer import address_explore
@ -15,63 +15,19 @@ from users import make_users_menu
from drv_entro import drv_entro_start, password_entry
from backups import clone_start, clone_write_data
from xor_seed import xor_split_start, xor_restore_start
from countdowns import countdown_pin_submenu, countdown_chooser
from countdowns import countdown_chooser
from hsm import hsm_policy_available
from paper import make_paper_wallet
from trick_pins import TrickPinMenu
# Optional feature: HSM
if version.has_fatram:
from hsm import hsm_policy_available
else:
hsm_policy_available = lambda: False
# Optional feature: Paper Wallets
try:
from paper import make_paper_wallet
except:
make_paper_wallet = None
if version.mk_num >= 4:
from trick_pins import TrickPinMenu
trick_pin_menu = TrickPinMenu.make_menu
else:
trick_pin_menu = None
trick_pin_menu = TrickPinMenu.make_menu
#
# NOTE: "Always In Title Case"
#
# - try to keep harmless things as first item: so double-tap of OK does no harm
# Mk3 and earlier: see Trick Pins for Mk4
PinChangesMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Change Main PIN', f=pin_changer, arg='main'),
MenuItem('Second Wallet', f=pin_changer, arg='secondary',
predicate=lambda: not version.has_608),
MenuItem('Duress PIN', f=pin_changer, arg='duress'),
MenuItem('Brick Me PIN', f=pin_changer, arg='brickme'),
MenuItem('Countdown PIN', menu=countdown_pin_submenu, predicate=lambda: version.has_608),
MenuItem('Login Now', f=login_now, arg=1),
]
# Not reachable on Mark3 hardware
if not version.has_608:
SecondaryPinChangesMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Second Wallet', f=pin_changer, arg='secondary'),
MenuItem('Duress PIN', f=pin_changer, arg='duress'),
MenuItem('Countdown PIN', menu=countdown_pin_submenu),
MenuItem('Login Now', f=login_now, arg=1),
]
async def which_pin_menu(_1,_2, item):
assert version.mk_num < 4
if version.has_608:
# mk3
return PinChangesMenu
else:
# mk2 only
from pincodes import pa
return PinChangesMenu if not pa.is_secondary else SecondaryPinChangesMenu
#
# Predicates
#
@ -89,7 +45,7 @@ def vdisk_enabled():
def se2_and_real_secret():
from pincodes import pa
return version.has_se2 and (not pa.is_secret_blank()) and (not pa.tmp_value)
return (not pa.is_secret_blank()) and (not pa.tmp_value)
HWTogglesMenu = [
@ -97,7 +53,7 @@ HWTogglesMenu = [
on_change=change_usb_disable, story='''\
Blocks any data over USB port. Useful when your plan is air-gap usage.'''),
ToggleMenuItem('Virtual Disk', 'vidsk', ['Default Off', 'Enable', 'Enable & Auto'],
predicate=lambda: version.has_psram, on_change=change_virtdisk_enable,
on_change=change_virtdisk_enable,
story='''Coldcard can emulate a virtual disk drive (4MB) where new PSBT files \
can be saved. Signed PSBT files (transactions) will also be saved here. \n\
In "auto" mode, selects PSBT as soon as written.'''),
@ -112,11 +68,10 @@ with the Coldcard.''',
LoginPrefsMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Change Main PIN', f=pin_changer, arg='main'),
MenuItem('PIN Options', predicate=lambda: not version.has_se2, menu=which_pin_menu),
MenuItem('Trick PINs', predicate=lambda: version.has_se2, menu=trick_pin_menu),
MenuItem('Trick PINs', menu=trick_pin_menu),
MenuItem('Set Nickname', f=pick_nickname),
MenuItem('Scramble Keypad', f=pick_scramble),
MenuItem('Kill Key', f=pick_killkey, predicate=lambda: version.has_se2),
MenuItem('Kill Key', f=pick_killkey),
MenuItem('Login Countdown', chooser=countdown_chooser),
MenuItem('MicroSD 2FA', menu=microsd_2fa, predicate=se2_and_real_secret),
MenuItem('Test Login Now', f=login_now, arg=1),
@ -199,26 +154,13 @@ UpgradeMenu = [
MenuItem('Bless Firmware', f=bless_flash),
]
if version.mk_num < 4:
DevelopersMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Normal USB Mode", f=dev_enable_protocol),
MenuItem("Enable USB REPL", f=dev_enable_vcp),
MenuItem("Enable USB Disk", f=dev_enable_disk),
MenuItem("Wipe Patch Area", f=wipe_filesystem), # needs better label
MenuItem('Warm Reset', f=reset_self),
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
]
else:
# Mk4 and later
from mk4 import dev_enable_repl
DevelopersMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Serial REPL", f=dev_enable_repl),
MenuItem("Wipe LFS", f=wipe_filesystem), # kills settings, HSM stuff
MenuItem('Warm Reset', f=reset_self),
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
]
DevelopersMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Serial REPL", f=dev_enable_repl),
MenuItem("Wipe LFS", f=wipe_filesystem), # kills settings, HSM stuff
MenuItem('Warm Reset', f=reset_self),
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
]
AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
# xxxxxxxxxxxxxxxx
@ -285,7 +227,7 @@ Keep blocked unless you intend to sign special transactions.'''),
story="Testnet must only be used by developers because \
correctly- crafted transactions signed on Testnet could be broadcast on Mainnet."),
MenuItem('Settings Space', f=show_settings_space),
MenuItem('MCU Key Slots', predicate=lambda: version.has_se2, f=show_mcu_keys_left),
MenuItem('MCU Key Slots', f=show_mcu_keys_left),
]
BackupStuffMenu = [
@ -316,9 +258,8 @@ AdvancedNormalMenu = [
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
ToggleMenuItem('Enable HSM', 'hsmcmd', ['Default Off', 'Enable'],
story="Enable HSM? Enables all user management commands, and other HSM-only USB commands. \
By default these commands are disabled.",
predicate=lambda: version.has_fatram),
MenuItem('User Management', menu=make_users_menu, predicate=lambda: version.has_fatram),
By default these commands are disabled."),
MenuItem('User Management', menu=make_users_menu),
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu),
MenuItem("Danger Zone", menu=DangerZoneMenu),
]
@ -344,7 +285,6 @@ ImportWallet = [
MenuItem("Seed XOR", f=xor_restore_start),
]
NewSeedMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("24 Word (default)", f=pick_new_seed, arg=24),
@ -363,7 +303,6 @@ EmptyWallet = [
MenuItem('Settings', menu=SettingsMenu),
]
# In operation, normal system, after a good PIN received.
NormalSystem = [
# xxxxxxxxxxxxxxxx
@ -377,7 +316,6 @@ NormalSystem = [
MenuItem('Settings', menu=SettingsMenu),
]
# Shown until unit is put into a numbered bag
FactoryMenu = [
MenuItem('Bag Me Now'), # nice to have NOP at top of menu

View File

@ -757,7 +757,7 @@ class MultisigWallet:
# HACK but there is no difference extended_keys - just bech32 hrp
assert chain.ctype == "XTN"
else:
assert chain.ctype == expect_chain # 'wrong chain'
assert chain.ctype == expect_chain, 'wrong chain'
depth = node.depth()

View File

@ -18,13 +18,11 @@
# - SHA-256 check on decrypted data
# - (Mk4) each slot is a file on /flash/settings
#
import os, sys, ujson, ustruct, ckcc, gc, ngu, aes256ctr
from uio import BytesIO
import os, ujson, ustruct, ckcc, gc, ngu, aes256ctr
from uhashlib import sha256
from random import shuffle, randbelow
from random import randbelow
from utils import call_later_ms
from version import mk_num, is_devmode
from glob import PSRAM
# TODO fs.sync
@ -36,7 +34,6 @@ from glob import PSRAM
# b39skip = (bool) skip discussion about use of BIP-39 passphrase
# idle_to = idle timeout period (seconds)
# _age = internal verison number for data (see below)
# terms_ok = customer has signed-off on the terms of sale
# tested = selftest has been completed successfully
# multisig = list of defined multisig wallets (complex)
# pms = trust/import/distrust xpubs found in PSBT files
@ -58,35 +55,40 @@ from glob import PSRAM
# sd2fa = (list of strings): track which SD card is needed for login
# bkpw = (string): last backup password, so can be re-used easily
# sighshchk = (bool) set if sighash checks are disabled
# Stored w/ key=00 for access before login
# _skip_pin = hard code a PIN value (dangerous, only for debug)
# nick = optional nickname for this coldcard (personalization)
# rngk = randomize keypad for PIN entry
# delay_left = [REMOVED-obsolete] seconds remaining on login countdown, if defined
# lgto = (minutes) how long to wait for Login Countdown feature [in v4.0.2+]
# cd_lgto = [<=mk3] minutes to show in countdown (in countdown-to-brick mode)
# cd_mode = [<=mk3] set to enable some less-destructive modes
# cd_pin = [<=mk3] pin code which enables "countdown to brick" mode
# kbtn = (1 char) '1'-'9' that will wipe seed during login process (mk4+)
# terms_ok = customer has signed-off on the terms of sale
# settings linked to seed
# LINKED_SETTINGS = ["multisig", "tp", "ovc", "xfp", "xpub", "words"]
# settings that does not make sense to copy to ephemeral secret
# LINKED_SETTINGS += ["sd2fa", "usr", "axi", "hsmcmd"]
# prelogin settings - do not need to be part of other saved settings
# PRELOGIN_SETTINGS = ["_skip_pin", "nick", "rngk", "lgto", "kbtn", "terms_ok"]
# settings that need to be copied to any newly loaded settings as they describe state as is (a.k.a current state)
KEEP_SETTINGS = ["du", "nfc", "vidsk"]
# keep these settings only if unspecified on the other end
KEEP_IF_BLANK_SETTINGS = ["bkpw", "wa", "sighshchk", "emu", "rz",
"axskip", "del", "pms", "idle_to", "b39skip"]
if mk_num <= 3:
# where in SPI Flash we work (last 128k)
SLOTS = range((1024-128)*1024, 1024*1024, 4096)
NUM_SLOTS = 32
NUM_SLOTS = const(64)
SLOTS = range(NUM_SLOTS)
MK4_WORKDIR = '/flash/settings/'
from sffile import SFFile
from sflash import SF
else:
# work in LFS2 filesystem instead, but same terminology
SLOTS = range(0, 64)
NUM_SLOTS = 64
MK4_WORKDIR = '/flash/settings/'
# for mk4: we store binary files on LFS2 filesystem
def MK4_FILENAME(slot):
return MK4_WORKDIR + ('%03x.aes' % slot)
# for mk4: we store binary files on LFS2 filesystem
def MK4_FILENAME(slot):
return MK4_WORKDIR + ('%03x.aes' % slot)
class SettingsObject:
@ -147,10 +149,6 @@ class SettingsObject:
self.nvram_key = key
def get_capacity(self):
# percent space used (0.0=>empty)
if mk_num <= 3:
return self.capacity
# could use whole filesystem, so use that as imprecise proxy
_, _, blocks, bfree, *_ = os.statvfs(MK4_WORKDIR)
@ -162,10 +160,6 @@ class SettingsObject:
def _slot_is_blank(self, pos, buf):
# read a few bytes from start of slot
if mk_num <= 3:
SF.read(pos, buf)
return (buf[0] == buf[1] == buf[2] == buf[3] == 0xff)
try:
with self._open_file(pos) as fd:
fd.readinto(buf)
@ -175,73 +169,38 @@ class SettingsObject:
def _wipe_slot(self, pos):
# blank out a slot
if mk_num <= 3:
SF.wait_done()
SF.sector_erase(pos)
SF.wait_done()
else:
fn = MK4_FILENAME(pos)
try:
os.remove(fn)
except BaseException as exc:
# Error (ENOENT) expected here when saving first time, because the
# "old" slot was not in use
pass
fn = MK4_FILENAME(pos)
try:
os.remove(fn)
except:
# Error (ENOENT) expected here when saving first time, because the
# "old" slot was not in use
pass
def _deny_slot(self, pos):
# write garbage to look legit in a slot
if mk_num <= 3:
with self._open_file(pos, 'wb') as fd:
for i in range(0, 4096, 256):
h = ngu.random.bytes(256)
SF.wait_done()
SF.write(pos+i, h)
else:
with self._open_file(pos, 'wb') as fd:
for i in range(0, 4096, 256):
h = ngu.random.bytes(256)
fd.write(h)
fd.write(h)
def _read_slot(self, pos, decryptor):
# return decrypted (json text) and last 32-bytes which is SHA-256 over that
if mk_num <= 3:
chk = sha256()
# Mk4 is just reading a binary file and decrypt as we go.
with self._open_file(pos) as fd:
# missing ftell(), so emulate
ln = fd.seek(0, 2)
fd.seek(0, 0)
from sram2 import nvstore_buf as _tmp
buf = fd.read(ln - 32)
assert len(buf) == ln-32
with SFFile(pos, length=4096, pre_erased=True) as fd:
for i in range(4096/32):
b = decryptor(fd.read(32))
if i != 127:
_tmp[i*32:(i*32)+32] = b
chk.update(b)
else:
expect = b
rv = decryptor(buf)
digest = ngu.hash.sha256s(rv)
# check how much space was used for encoded JSON
try:
end = bytes(_tmp).index(b'\0')
self.capacity = end / (4096-32)
except ValueError:
self.capacity = 1.0
expect = decryptor(fd.read(32))
assert len(expect) == 32
return _tmp, expect, chk.digest()
else:
# Mk4 is just reading a binary file and decrypt as we go.
with self._open_file(pos) as fd:
# missing ftell(), so emulate
ln = fd.seek(0, 2)
fd.seek(0, 0)
buf = fd.read(ln - 32)
assert len(buf) == ln-32
rv = decryptor(buf)
digest = ngu.hash.sha256s(rv)
expect = decryptor(fd.read(32))
assert len(expect) == 32
return rv, expect, digest
return rv, expect, digest
def _write_slot(self, pos, aes):
# SHA-256 over plaintext
@ -250,51 +209,26 @@ class SettingsObject:
# serialize the data into JSON
d = ujson.dumps(self.current)
if mk_num <= 3:
with SFFile(pos, max_size=4096, pre_erased=True) as fd:
# pad w/ zeros
dat_len = len(d)
pad_len = (4096-32) - dat_len
assert pad_len >= 0, 'too big'
with self._open_file(pos, 'wb') as fd:
# pad w/ zeros at least to 4k, but allow larger
dat_len = len(d)
pad_len = (4096-32) - dat_len
self.capacity = dat_len / 4096
fd.write(aes(d))
assert fd.tell() == dat_len
chk.update(d)
del d
fd.write(aes(d))
chk.update(d)
del d
while pad_len > 0:
here = min(32, pad_len)
while pad_len > 0:
here = min(32, pad_len)
pad = bytes(here)
fd.write(aes(pad))
chk.update(pad)
pad = bytes(here)
fd.write(aes(pad))
chk.update(pad)
pad_len -= here
pad_len -= here
fd.write(aes(chk.digest()))
assert fd.tell() == 4096
else:
with self._open_file(pos, 'wb') as fd:
# pad w/ zeros at least to 4k, but allow larger
dat_len = len(d)
pad_len = (4096-32) - dat_len
fd.write(aes(d))
assert fd.tell() == dat_len
chk.update(d)
del d
while pad_len > 0:
here = min(32, pad_len)
pad = bytes(here)
fd.write(aes(pad))
chk.update(pad)
pad_len -= here
fd.write(aes(chk.digest()))
fd.write(aes(chk.digest()))
def _used_slots(self):
# mk4: faster list of slots in use; doesn't open them
@ -304,35 +238,19 @@ class SettingsObject:
def _nonempty_slots(self, dis=None):
# generate slots that are non-empty
taste = bytearray(4)
# use directory listing
files = self._used_slots()
self.num_empty = NUM_SLOTS - len(files)
if mk_num <= 3:
self.num_empty = 0
for i, pos in enumerate(files):
if dis:
dis.progress_bar_show(i / len(files))
for pos in SLOTS:
if dis:
dis.progress_bar_show((pos-SLOTS.start) / (SLOTS.stop-SLOTS.start))
gc.collect()
if self._slot_is_blank(pos, taste):
# unlikely case, but easy to handle
continue
if self._slot_is_blank(pos, taste):
# erased (probably)
self.num_empty += 1
continue
yield pos, taste
else:
# use directory listing
files = self._used_slots()
self.num_empty = NUM_SLOTS - len(files)
for i, pos in enumerate(files):
if dis:
dis.progress_bar_show(i / len(files))
if self._slot_is_blank(pos, taste):
# unlikely case, but easy to handle
continue
yield pos, taste
yield pos, taste
def load(self, dis=None):
# Search all slots for any we can read, decrypt that,
@ -358,7 +276,6 @@ class SettingsObject:
# probably good, read it
aes = aes.cipher
json_data, expect, actual = self._read_slot(pos, aes)
try:
# verify checksum in last 32 bytes
assert expect == actual
@ -373,10 +290,8 @@ class SettingsObject:
# likely winner
self.current = d
self.my_pos = pos
#print("NV: data @ 0x%x w/ age=%d" % (pos, got_age))
else:
# stale data seen; clean it up.
#print("NV: cleanup @ 0x%x" % pos)
assert self.current['_age'] > 0
self._wipe_slot(pos)
@ -385,15 +300,13 @@ class SettingsObject:
# done, if we found something
if self.my_pos is not None:
#print("NV: load done")
return
return
# nothing found, use defaults
self.current = self.default_values()
# pick a (new) random home
self.my_pos = self.find_spot(-1)
#print("NV: empty")
if is_devmode:
self.current['chain'] = 'XTN'
@ -434,10 +347,23 @@ class SettingsObject:
self.current.pop(kn, None)
self.changed()
def merge_previous_active(self, previous):
for k in KEEP_SETTINGS:
if k not in previous:
self.current.pop(k, None)
else:
self.current[k] = previous[k]
for k in KEEP_IF_BLANK_SETTINGS:
if previous.get(k, None) and not self.current.get(k, None):
self.current[k] = previous[k]
self.changed()
def clear(self):
# could be just:
# self.current = {}
# but accomidating the simulator here
# but accommodating the simulator here
rk = [k for k in self.current if k[0] != '_']
for k in rk:
del self.current[k]
@ -459,43 +385,26 @@ class SettingsObject:
call_later_ms(250, self.write_out)
def find_spot(self, not_here=0):
# search for a blank sector to use
# search for a blank sector to use
# - check randomly and pick first blank one (wear leveling, deniability)
# - we will write and then erase old slot
# - if "full", blow away a random one
if mk_num <= 3:
options = [s for s in SLOTS if s != not_here]
shuffle(options)
# on mk4, use the filesystem to see what's already taken
avail = set(SLOTS) - set(self._used_slots())
avail.discard(not_here)
buf = bytearray(4)
for pos in options:
if self._slot_is_blank(pos, buf):
# found a blank area
return pos
if avail:
return avail.pop()
# No-where to write! (probably a bug because we have lots of slots)
# ... so pick a random slot and kill what it had
victim = options[0]
else:
# on mk4, use the filesystem to see what's already taken
avail = set(SLOTS) - set(self._used_slots())
avail.discard(not_here)
if avail:
return avail.pop()
victim = randbelow(NUM_SLOTS)
#print("ERROR: nvram full")
# TODO destructive
victim = randbelow(NUM_SLOTS)
self._wipe_slot(victim)
return victim
def save(self):
# render as JSON, encrypt and write it.
self.current['_age'] = self.current.get('_age', 1) + 1
pos = self.find_spot(self.my_pos)
aes = self.get_aes(pos).cipher
@ -509,10 +418,6 @@ class SettingsObject:
self.my_pos = pos
self.is_dirty = 0
def merge(self, prev):
# take a dict of previous values and merge them into what we have
self.current.update(prev)
def blank(self):
# erase current copy of values in nvram; older ones may exist still
# - use when clearing the seed value

View File

@ -390,15 +390,8 @@ class PinAttempt:
if self.tmp_value:
return bytes(AE_LONG_SECRET_LEN)
if version.mk_num < 4:
secret = b''
for n in range(13):
secret += self.roundtrip(6, ls_offset=n)[0:32]
return secret
else:
# faster method for Mk4
return self.roundtrip(8, after_buf=bytes(AE_LONG_SECRET_LEN))
# faster method for Mk4
return self.roundtrip(8, after_buf=bytes(AE_LONG_SECRET_LEN))
def ls_change(self, new_long_secret):
# set the "long secret"
@ -423,18 +416,15 @@ class PinAttempt:
def new_main_secret(self, raw_secret, chain=None):
# Main secret has changed: reset the settings+their key,
# and capture xfp/xpub
from glob import settings
from glob import settings, NFC
import stash
stash.SensitiveValues.clear_cache()
# capture values we have already
old_values = dict(settings.current)
settings.set_key(raw_secret)
settings.load()
# merge in settings, including what chain to use, timeout, etc.
settings.merge(old_values)
settings.merge_previous_active(old_values)
# Recalculate xfp/xpub values (depends both on secret and chain)
with stash.SensitiveValues(raw_secret) as sv:
@ -446,7 +436,11 @@ class PinAttempt:
def tmp_secret(self, encoded, chain=None):
# Use indicated secret and stop using the SE; operate like this until reboot
self.tmp_value = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
if self.tmp_value == val:
return False
self.tmp_value = val
# We're no longer blank. hard to say about duress secret and stuff tho
self.state_flags = PA_SUCCESSFUL
@ -459,6 +453,7 @@ class PinAttempt:
# 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)
return True
def trick_request(self, method_num, data):
# send/recv a trick-pin related request (mk4 only)

View File

@ -55,6 +55,7 @@ class PSRAMWrapper:
def wipe_all(self):
# works, but code in bootrom is much faster and better (rng values used)
from glob import dis
z = bytes(16384)
for pos in range(0, self.length, len(z)):

View File

@ -14,13 +14,12 @@ from menu import MenuItem, MenuSystem
from utils import xfp2str, parse_extended_key
import ngu, uctypes, bip39, random, version
from uhashlib import sha256
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm, show_qr_code
from ux import PressRelease, ux_input_numbers, ux_input_text
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm
from ux import PressRelease, ux_input_numbers, ux_input_text, show_qr_code
from pincodes import AE_SECRET_LEN, AE_LONG_SECRET_LEN
from actions import goto_top_menu
from stash import SecretStash, SensitiveValues
from stash import SecretStash
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from pwsave import PassphraseSaver
from glob import settings, dis
from pincodes import pa
@ -294,6 +293,7 @@ async def show_words(words, prompt=None, escape=None, extra='', ephemeral=False)
return ch
async def add_dice_rolls(count, seed, judge_them, nwords=None, enforce=False):
from glob import dis
from display import FontTiny, FontLarge
@ -408,12 +408,16 @@ async def new_from_dice(nwords):
goto_top_menu(first_time=True)
async def set_ephemeral_seed(encoded, chain=None):
pa.tmp_secret(encoded, chain=chain)
applied = pa.tmp_secret(encoded, chain=chain)
dis.progress_bar_show(1)
xfp = settings.get("xfp", "")
if xfp:
xfp = "[" + xfp2str(xfp) + "]\n"
await ux_show_story("%sNew ephemeral master key in effect until next power down.\n\nIt is NOT stored anywhere." % xfp)
if not applied:
await ux_show_story("%sEphemeral master key already in use." % xfp)
return
await ux_show_story("%sNew ephemeral master key in effect until next power down." % xfp)
async def set_ephemeral_seed_words(words):
dis.progress_bar_show(0.1)

View File

@ -557,6 +557,7 @@ class CTransaction(object):
self.hash = b2a_hex(bytes(tmp[i] for i in range(len(tmp)-1, -1, -1)))
def is_valid(self):
COIN = 100000000
self.calc_sha256()
for tout in self.vout:
if tout.nValue < 0 or tout.nValue > 21000000 * COIN:

View File

@ -7,8 +7,7 @@
# - replaces old "duress wallet" and "brickme" features
# - changes require knowledge of real PIN code (it is checked)
#
import version, uctypes, errno, ngu, sys, ckcc, stash, bip39
from ubinascii import hexlify as b2a_hex
import uctypes, errno, ngu, sys, stash, bip39
from menu import MenuSystem, MenuItem
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux, ux_aborted
from stash import SecretStash
@ -764,10 +763,8 @@ normal operation.''')
# switch over to new secret!
dis.fullscreen("Applying...")
pa.tmp_secret(encoded)
tp.reload()
await ux_show_story("New master key in effect until next power down.")
from seed import set_ephemeral_seed
await set_ephemeral_seed(encoded)
from actions import goto_top_menu
goto_top_menu()

View File

@ -7,7 +7,7 @@
#
import stash, ngu, bip39, random
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause
from seed import word_quiz, WordNestMenu, set_seed_value
from seed import word_quiz, WordNestMenu, set_seed_value, set_ephemeral_seed
from glob import settings
from actions import goto_top_menu
@ -169,8 +169,8 @@ class XORWordNestMenu(WordNestMenu):
# update menu contents now that wallet defined
goto_top_menu(first_time=True)
else:
pa.tmp_secret(enc)
await ux_show_story("New master key in effect until next power down.")
await set_ephemeral_seed(enc)
goto_top_menu()
return None

View File

@ -762,8 +762,8 @@ def set_seed_words(sim_exec, sim_execfile, simulator, reset_seed_words):
# load simulator w/ a specific bip32 master key
def doit(words):
sim_exec('import main; main.WORDS = %r; ' % words.split())
cmd = 'import main; main.WORDS = %r;' % words.split()
sim_exec(cmd)
rv = sim_execfile('devtest/set_seed.py')
if rv: pytest.fail(rv)
@ -785,8 +785,8 @@ def reset_seed_words(sim_exec, sim_execfile, simulator):
def doit():
words = simulator_fixed_words
sim_exec('import main; main.WORDS = %r; ' % words.split())
cmd = 'import main; main.WORDS = %r;' % words.split()
sim_exec(cmd)
rv = sim_execfile('devtest/set_seed.py')
if rv: pytest.fail(rv)

View File

@ -3,32 +3,30 @@
# load up the simulator w/ indicated list of seed words
from sim_settings import sim_defaults
import stash, chains
from h import b2a_hex
from pincodes import pa
from glob import settings
import stash
from seed import set_seed_value
from utils import xfp2str
from actions import goto_top_menu
tn = chains.BitcoinTestnet
if 1:
stash.bip39_passphrase = ''
settings.current = sim_defaults
settings.overrides.clear()
settings.set('chain', 'XTN')
settings.set('words', True)
settings.set('terms_ok', True)
settings.set('idle_to', 0)
stash.bip39_passphrase = ''
settings.current = sim_defaults
settings.overrides.clear()
settings.set('chain', 'XTN')
settings.set('words', True)
settings.set('terms_ok', True)
settings.set('idle_to', 0)
import main
pa.tmp_value = None
set_seed_value(main.WORDS)
import main
pa.tmp_value = None
set_seed_value(main.WORDS)
print("New key in effect: %s" % settings.get('xpub', 'MISSING'))
print("Fingerprint: %s" % xfp2str(settings.get('xfp', 0)))
print("New key in effect: %s" % settings.get('xpub', 'MISSING'))
print("Fingerprint: %s" % xfp2str(settings.get('xfp', 0)))
# impt: if going from xprv => seed words, main menu needs updating
from actions import goto_top_menu
goto_top_menu()
# impt: if going from xprv => seed words, main menu needs updating
goto_top_menu()

View File

@ -60,7 +60,7 @@ def set_bip39_pw(dev, need_keypress, reset_seed_words, cap_story):
def doit(pw):
# reset from previous runs
words = reset_seed_words()
# optimization
if pw == '':
return simulator_fixed_xfp

View File

@ -3,13 +3,19 @@
# Ephemeral Seeds tests
#
import pytest, time, re, os, shutil
from constants import simulator_fixed_xpub
from ckcc.protocol import CCProtocolPacker
from txn import fake_txn
from test_ux import word_menu_entry
WORDLISTS = {
12: ('abandon ' * 11 + 'about', '73C5DA0A'),
18: ('abandon ' * 17 + 'agent', 'E08B8AC3'),
24: ('abandon ' * 23 + 'art', '5436D724'),
}
def truncate_seed_words(words):
if isinstance(words, str):
words = words.split(" ")
@ -93,13 +99,15 @@ def goto_eph_seed_menu(goto_home, pick_menu_item, cap_story, need_keypress):
@pytest.fixture
def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, goto_home, pick_menu_item,
fake_txn, try_sign, goto_eph_seed_menu, reset_seed_words,
get_identity_story, get_seed_value_ux):
def doit(mnemonic=None, xpub=None):
def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn, try_sign,
goto_eph_seed_menu, reset_seed_words, get_identity_story,
get_seed_value_ux):
def doit(mnemonic=None, xpub=None, expected_xfp=None):
time.sleep(0.3)
_, story = cap_story()
in_effect_xfp = story[1:9]
if expected_xfp is not None:
assert in_effect_xfp == expected_xfp
assert "key in effect until next power down." in story
need_keypress("y") # just confirm new master key message
@ -140,9 +148,9 @@ def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, goto_hom
@pytest.mark.parametrize("num_words", [12, 24])
@pytest.mark.parametrize("dice", [False, True])
def test_ephemeral_seed_generate(num_words, pick_menu_item, goto_home, cap_story, need_keypress,
reset_seed_words, goto_eph_seed_menu, dice, ephemeral_seed_disabled,
verify_ephemeral_secret_ui):
def test_ephemeral_seed_generate(num_words, pick_menu_item, cap_story, need_keypress,
reset_seed_words, goto_eph_seed_menu, dice,
ephemeral_seed_disabled, verify_ephemeral_secret_ui):
reset_seed_words()
try:
@ -180,18 +188,14 @@ def test_ephemeral_seed_generate(num_words, pick_menu_item, goto_home, cap_story
@pytest.mark.parametrize("num_words", [12, 18, 24])
@pytest.mark.parametrize("nfc", [False, True])
@pytest.mark.parametrize("truncated", [False, True])
def test_ephemeral_seed_import_words(nfc, truncated, num_words, cap_menu, pick_menu_item, goto_home,
cap_story, need_keypress, reset_seed_words, goto_eph_seed_menu,
def test_ephemeral_seed_import_words(nfc, truncated, num_words, cap_menu, pick_menu_item,
need_keypress, reset_seed_words, goto_eph_seed_menu,
word_menu_entry, nfc_write_text, verify_ephemeral_secret_ui,
ephemeral_seed_disabled, get_seed_value_ux):
if truncated and not nfc: return
wordlists = {
12: ( 'abandon ' * 11 + 'about', 0x0adac573),
18: ( 'abandon ' * 17 + 'agent', 0xc38a8be0),
24: ( 'abandon ' * 23 + 'art', 0x24d73654 ),
}
words, expect_xfp = wordlists[num_words]
words, expect_xfp = WORDLISTS[num_words]
reset_seed_words()
try:
@ -222,7 +226,7 @@ def test_ephemeral_seed_import_words(nfc, truncated, num_words, cap_menu, pick_m
need_keypress("4") # understand consequences
verify_ephemeral_secret_ui(mnemonic=words.split(" "))
verify_ephemeral_secret_ui(mnemonic=words.split(" "), expected_xfp=expect_xfp)
nfc_seed = get_seed_value_ux(nfc=True) # export seed via NFC (always truncated)
seed_words = get_seed_value_ux()
@ -285,7 +289,7 @@ def test_ephemeral_seed_import_tapsigner(way, retry, testnet, pick_menu_item, ca
@pytest.mark.parametrize("fail", ["wrong_key", "key_len", "plaintext", "garbage"])
def test_ephemeral_seed_import_tapsigner_fail(cap_menu, pick_menu_item, goto_home, cap_story, fail,
def test_ephemeral_seed_import_tapsigner_fail(pick_menu_item, cap_story, fail,
need_keypress, reset_seed_words, enter_hex,
tapsigner_encrypted_backup, goto_eph_seed_menu,
microsd_path, ephemeral_seed_disabled):
@ -347,8 +351,8 @@ def test_ephemeral_seed_import_tapsigner_fail(cap_menu, pick_menu_item, goto_hom
"xpub661MyMwAqRbcGBeMu9h1B222hQmc4XkXasbN4F3mDGTWRJ11UQ5orWv41FPVK7stXsS9UtR5DBTArBvcsHPiCE2E1PAdqq1UQiQTYmrEEaa"
),
])
def test_ephemeral_seed_import_tapsigner_real(data, cap_menu, pick_menu_item, goto_home, cap_story,
need_keypress, reset_seed_words, enter_hex, microsd_path,
def test_ephemeral_seed_import_tapsigner_real(data, pick_menu_item, cap_story, microsd_path,
need_keypress, reset_seed_words, enter_hex,
goto_eph_seed_menu, verify_ephemeral_secret_ui,
ephemeral_seed_disabled):
fname, backup_key_hex, pub = data
@ -386,10 +390,11 @@ def test_ephemeral_seed_import_tapsigner_real(data, cap_menu, pick_menu_item, go
@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, cap_menu, pick_menu_item, goto_home,
cap_story, need_keypress, reset_seed_words, goto_eph_seed_menu,
nfc_write_text, enter_hex, microsd_path, virtdisk_path,
verify_ephemeral_secret_ui, ephemeral_seed_disabled):
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):
reset_seed_words()
fname = "ek.txt"
from pycoin.key.BIP32Node import BIP32Node
@ -444,4 +449,46 @@ def test_ephemeral_seed_import_xprv(way, retry, testnet, cap_menu, pick_menu_ite
verify_ephemeral_secret_ui(xpub=node.hwif())
def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
ephemeral_seed_disabled, cap_story,
pick_menu_item, need_keypress,
word_menu_entry):
reset_seed_words()
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
ephemeral_seed_disabled()
words, expected_xfp = WORDLISTS[12]
pick_menu_item("Import Words")
pick_menu_item(f"12 Words")
time.sleep(0.1)
word_menu_entry(words.split())
time.sleep(0.3)
_, story = cap_story()
assert "key in effect until next power down." in story
in_effect_xfp = story[1:9]
need_keypress("y")
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
pick_menu_item("Import Words")
pick_menu_item(f"12 Words")
time.sleep(0.1)
word_menu_entry(words.split())
time.sleep(0.3)
_, story = cap_story()
assert "Ephemeral master key already in use" in story
already_used_xfp = story[1:9]
assert already_used_xfp == in_effect_xfp == expected_xfp
need_keypress("y")
# EOF

View File

@ -1090,11 +1090,15 @@ def make_myself_wallet(dev, set_bip39_pw, offer_ms_import, need_keypress, clear_
def select_wallet(idx):
# select to specific pw
xfp = set_bip39_pw(passwords[idx])
if do_import:
offer_ms_import(config)
time.sleep(.1)
need_keypress('y')
assert xfp == keys[idx][0]
return (keys, select_wallet)
yield doit
yield doit
set_bip39_pw('')
@ -1273,7 +1277,7 @@ def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev
open(f'debug/myself-before.psbt', 'w').write(b64encode(psbt).decode())
for idx in range(M):
select_wallet(idx)
_, updated = try_sign(psbt, accept_ms_import=(incl_xpubs and (idx==0)))
_, updated = try_sign(psbt, accept_ms_import=incl_xpubs)
open(f'debug/myself-after.psbt', 'w').write(b64encode(updated).decode())
assert updated != psbt

View File

@ -5,8 +5,6 @@
# - use 'simulator.py --eff' for these
#
import pytest, struct, time
from helpers import B2A
from binascii import b2a_hex, a2b_hex
from collections import namedtuple
from mnemonic import Mnemonic
@ -53,7 +51,6 @@ TRICK_FMT = 'IHH64s16sII32s'
TRICK_FMT_FLDS = 'slot_num tc_flags tc_arg xdata pin pin_len blank_slots spare'
assert struct.calcsize(TRICK_FMT) == 128
from collections import namedtuple
SlotInfo = namedtuple('SlotInfo', TRICK_FMT_FLDS)
def make_slot(**kws):
@ -432,9 +429,16 @@ def test_ux_wipe_choices_1(subchoice, expect, xflags,
# ( 'Blank Coldcard', 'freshly wiped Coldcard', TC_WIPE|TC_BLANK_WALLET, 0 ),
])
def test_ux_duress_choices(with_wipe, subchoice, expect, xflags, xargs,
reset_seed_words, repl, clear_all_tricks,
reset_seed_words, repl, clear_all_tricks, import_ms_wallet, get_setting, clear_ms,
new_trick_pin, new_pin_confirmed, cap_menu, pick_menu_item, cap_story, need_keypress):
# import multisig
clear_ms()
import_ms_wallet(2, 2)
need_keypress('y')
time.sleep(.1)
assert len(get_setting('multisig')) == 1
# after Wipe Seed -> Wipe->Wallet choice, another level
clear_all_tricks()
@ -502,12 +506,17 @@ def test_ux_duress_choices(with_wipe, subchoice, expect, xflags, xargs,
need_keypress('y')
time.sleep(.1)
_, story = cap_story()
assert 'New master key in effect' in story
assert 'New ephemeral master key in effect' in story
xp = repl.eval("settings.get('xpub')")
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

@ -148,8 +148,8 @@ def test_import_xor(incl_self, parts, expect, goto_home, pick_menu_item, cap_sto
need_keypress('2')
time.sleep(0.01)
title, body = cap_story()
assert 'New master key in effect' in body
title, story = cap_story()
assert 'New ephemeral master key in effect' in story
assert get_secrets()['mnemonic'] == expect
reset_seed_words()