diff --git a/releases/ChangeLog.md b/releases/ChangeLog.md index a2a9a45f..3ac4c60a 100644 --- a/releases/ChangeLog.md +++ b/releases/ChangeLog.md @@ -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, diff --git a/shared/actions.py b/shared/actions.py index 50f50c67..845fd31e 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -2194,20 +2194,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 @@ -2238,7 +2236,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 diff --git a/shared/address_explorer.py b/shared/address_explorer.py index d528f798..cfd2a4d6 100644 --- a/shared/address_explorer.py +++ b/shared/address_explorer.py @@ -68,7 +68,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() @@ -85,7 +85,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] @@ -107,7 +107,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) @@ -123,7 +123,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) diff --git a/shared/backups.py b/shared/backups.py index f769f9db..c14abe41 100644 --- a/shared/backups.py +++ b/shared/backups.py @@ -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: diff --git a/shared/compat7z.py b/shared/compat7z.py index 414ba050..e475c4f6 100644 --- a/shared/compat7z.py +++ b/shared/compat7z.py @@ -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 diff --git a/shared/countdowns.py b/shared/countdowns.py index 3794b36a..469b38a4 100644 --- a/shared/countdowns.py +++ b/shared/countdowns.py @@ -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 diff --git a/shared/display.py b/shared/display.py index f2eef2d7..82acacf9 100644 --- a/shared/display.py +++ b/shared/display.py @@ -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, is_edge import framebuf -import uasyncio -from uasyncio import sleep_ms from graphics import Graphics from sram2 import display2_buf diff --git a/shared/drv_entro.py b/shared/drv_entro.py index 6b768ff0..82ac2585 100644 --- a/shared/drv_entro.py +++ b/shared/drv_entro.py @@ -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) diff --git a/shared/flow.py b/shared/flow.py index 6c2ca799..0c5cc78a 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -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 miniscript import make_miniscript_menu from seed import make_ephemeral_seed_menu @@ -16,63 +16,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 # @@ -90,7 +46,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 = [ @@ -98,7 +54,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.'''), @@ -113,11 +69,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), @@ -202,26 +157,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 @@ -288,7 +230,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 = [ @@ -319,9 +261,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), ] @@ -347,7 +288,6 @@ ImportWallet = [ MenuItem("Seed XOR", f=xor_restore_start), ] - NewSeedMenu = [ # xxxxxxxxxxxxxxxx MenuItem("24 Word (default)", f=pick_new_seed, arg=24), @@ -366,7 +306,6 @@ EmptyWallet = [ MenuItem('Settings', menu=SettingsMenu), ] - # In operation, normal system, after a good PIN received. NormalSystem = [ # xxxxxxxxxxxxxxxx @@ -380,7 +319,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 diff --git a/shared/multisig.py b/shared/multisig.py index 1fa60e54..62baf25a 100644 --- a/shared/multisig.py +++ b/shared/multisig.py @@ -1080,7 +1080,6 @@ Addresses: # concern: the order of keys here is non-deterministic # or order is taken from descriptor order (multi) but we do not support it # or order is determined by BIP (sortedmulti) - # concern: the order of keys here is non-deterministic for idx, (xfp, deriv, xpub) in enumerate(self.xpubs): if idx: msg.write('\n---===---\n\n') diff --git a/shared/nvstore.py b/shared/nvstore.py index e27915af..83876327 100644 --- a/shared/nvstore.py +++ b/shared/nvstore.py @@ -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) # miniscript = list of defined miniscript wallets (complex) @@ -59,35 +56,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: @@ -148,10 +150,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) @@ -163,10 +161,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) @@ -176,73 +170,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 @@ -251,51 +210,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 @@ -305,35 +239,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, @@ -359,7 +277,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 @@ -374,10 +291,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) @@ -386,15 +301,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' @@ -435,10 +348,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] @@ -460,43 +386,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 @@ -510,10 +419,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 diff --git a/shared/pincodes.py b/shared/pincodes.py index 877e10c7..fa92c49e 100644 --- a/shared/pincodes.py +++ b/shared/pincodes.py @@ -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) diff --git a/shared/psram.py b/shared/psram.py index 2783da66..5debb1e6 100644 --- a/shared/psram.py +++ b/shared/psram.py @@ -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)): diff --git a/shared/seed.py b/shared/seed.py index 03559d89..28de6847 100644 --- a/shared/seed.py +++ b/shared/seed.py @@ -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) diff --git a/shared/serializations.py b/shared/serializations.py index 3fc1656d..9a91acc4 100755 --- a/shared/serializations.py +++ b/shared/serializations.py @@ -569,6 +569,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: diff --git a/shared/trick_pins.py b/shared/trick_pins.py index 7cb33bf3..8c673643 100644 --- a/shared/trick_pins.py +++ b/shared/trick_pins.py @@ -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() diff --git a/shared/xor_seed.py b/shared/xor_seed.py index 27387f8e..08ed4355 100644 --- a/shared/xor_seed.py +++ b/shared/xor_seed.py @@ -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 diff --git a/testing/conftest.py b/testing/conftest.py index 9da01e51..44ef5099 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -775,8 +775,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) @@ -798,8 +798,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) diff --git a/testing/devtest/set_seed.py b/testing/devtest/set_seed.py index e8556412..e97e2ef8 100644 --- a/testing/devtest/set_seed.py +++ b/testing/devtest/set_seed.py @@ -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() diff --git a/testing/test_bip39pw.py b/testing/test_bip39pw.py index 490c9a32..a08cf5b4 100644 --- a/testing/test_bip39pw.py +++ b/testing/test_bip39pw.py @@ -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 diff --git a/testing/test_ephemeral.py b/testing/test_ephemeral.py index acdac070..daef8ea6 100644 --- a/testing/test_ephemeral.py +++ b/testing/test_ephemeral.py @@ -3,13 +3,19 @@ # Ephemeral Seeds tests # import pytest, time, re, os, shutil - from constants import simulator_fixed_tpub 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 diff --git a/testing/test_multisig.py b/testing/test_multisig.py index 7d47fd14..0ddd63c6 100644 --- a/testing/test_multisig.py +++ b/testing/test_multisig.py @@ -1089,11 +1089,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('') @@ -1272,7 +1276,7 @@ def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev open(f'debug/myself-before.psbt', 'w').write(base64.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(base64.b64encode(updated).decode()) assert updated != psbt diff --git a/testing/test_se2.py b/testing/test_se2.py index 32b95df3..a023b516 100644 --- a/testing/test_se2.py +++ b/testing/test_se2.py @@ -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()') diff --git a/testing/test_seed_xor.py b/testing/test_seed_xor.py index dd8e5d89..ab90cbd0 100644 --- a/testing/test_seed_xor.py +++ b/testing/test_seed_xor.py @@ -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()