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:
parent
cd068f7bbc
commit
79ce4ae115
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
102
shared/flow.py
102
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 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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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()')
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user