Backup and restore of trick pins and their effects
This commit is contained in:
parent
201a005b29
commit
3f13c63dd3
@ -61,17 +61,26 @@ def render_backup_contents():
|
||||
# BTW: everything is really a duplicate of this value
|
||||
ADD('raw_secret', b2a_hex(sv.secret).rstrip(b'0'))
|
||||
|
||||
# XXX mk4 a bit different
|
||||
if pa.has_duress_pin():
|
||||
COMMENT('Duress Wallet (informational)')
|
||||
dpk, p = sv.duress_root()
|
||||
COMMENT('path = %s' % p)
|
||||
ADD('duress_xprv', chain.serialize_private(dpk))
|
||||
ADD('duress_xpub', chain.serialize_public(dpk))
|
||||
|
||||
if version.has_608:
|
||||
# save the so-called long-secret
|
||||
ADD('long_secret', b2a_hex(pa.ls_fetch()))
|
||||
|
||||
# Duress wallets (somewhat optional, since derived)
|
||||
if version.mk_num <= 3:
|
||||
if pa.has_duress_pin():
|
||||
COMMENT('Duress Wallet (informational)')
|
||||
dpk, p = sv.duress_root()
|
||||
COMMENT('path = %s' % p)
|
||||
ADD('duress_xprv', chain.serialize_private(dpk))
|
||||
ADD('duress_xpub', chain.serialize_public(dpk))
|
||||
else:
|
||||
from trick_pins import tp
|
||||
for label, path, pairs in tp.backup_duress_wallets(sv):
|
||||
COMMENT()
|
||||
COMMENT(label + ' (informational)')
|
||||
COMMENT(path)
|
||||
for k,v in pairs:
|
||||
ADD(k, v)
|
||||
|
||||
COMMENT('Firmware version (informational)')
|
||||
date, vers, timestamp = version.get_mpy_version()[0:3]
|
||||
@ -98,9 +107,10 @@ def render_backup_contents():
|
||||
|
||||
return rv.getvalue()
|
||||
|
||||
async def restore_from_dict(vals):
|
||||
def restore_from_dict_ll(vals):
|
||||
# Restore from a dict of values. Already JSON decoded.
|
||||
# Reboot on success, return string on failure
|
||||
# Need a Reboot on success, return string on failure
|
||||
# - low-level version, factored out for better testing
|
||||
from glob import dis
|
||||
|
||||
#print("Restoring from: %r" % vals)
|
||||
@ -169,6 +179,16 @@ async def restore_from_dict(vals):
|
||||
|
||||
if k == 'xfp' or k == 'xpub': continue
|
||||
|
||||
if k == 'tp':
|
||||
# restore trick pins, which may involve many ops
|
||||
if version.mk_num >= 4:
|
||||
from trick_pins import tp
|
||||
try:
|
||||
tp.restore_backup(vals[k])
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc)
|
||||
continue
|
||||
|
||||
settings.set(k[8:], vals[k])
|
||||
|
||||
# write out
|
||||
@ -178,6 +198,14 @@ async def restore_from_dict(vals):
|
||||
import hsm
|
||||
hsm.restore_backup(vals['hsm_policy'])
|
||||
|
||||
|
||||
async def restore_from_dict(vals):
|
||||
# Restore from a dict of values. Already JSON decoded (ie. dict object).
|
||||
# Need a Reboot on success, return string on failure
|
||||
|
||||
prob = restore_from_dict_ll(vals)
|
||||
if prob: return prob
|
||||
|
||||
await ux_show_story('Everything has been successfully restored. '
|
||||
'We must now reboot to install the '
|
||||
'updated settings and seed.', title='Success!')
|
||||
|
||||
@ -7,7 +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
|
||||
import version, uctypes, errno, ngu, sys, ckcc, stash, bip39
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from menu import MenuSystem, MenuItem
|
||||
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux, ux_aborted
|
||||
@ -64,6 +64,24 @@ up to last four digits can be different between true PIN and trick.''' % len(rig
|
||||
|
||||
return None, a
|
||||
|
||||
def construct_duress_secret(flags, tc_arg):
|
||||
# is duress wallet required and if so, what are the secret values (32 or 64 bytes)
|
||||
if flags & TC_WORD_WALLET:
|
||||
# derive the secret via BIP-85
|
||||
new_secret, _, _, path = bip85_derive(2, tc_arg)
|
||||
path = "BIP85(words=24, index=%d)" % tc_arg
|
||||
|
||||
elif flags & TC_XPRV_WALLET:
|
||||
# use old method for duress wallets
|
||||
with stash.SensitiveValues() as sv:
|
||||
node, path = sv.duress_root()
|
||||
new_secret = SecretStash.encode(xprv=node)[1:65]
|
||||
assert len(new_secret) == 64
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
return path, new_secret
|
||||
|
||||
def make_slot():
|
||||
b = bytearray(uctypes.sizeof(TRICK_SLOT_LAYOUT))
|
||||
return b, uctypes.struct(uctypes.addressof(b), TRICK_SLOT_LAYOUT)
|
||||
@ -257,6 +275,12 @@ class TrickPinMgmt:
|
||||
if flags & TC_DELTA_MODE:
|
||||
yield k
|
||||
|
||||
def get_deltamode_pins(self):
|
||||
# iterate over all duress wallets
|
||||
for k, (sn,flags,args) in self.tp.items():
|
||||
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
||||
yield k
|
||||
|
||||
def check_new_main_pin(self, pin):
|
||||
# user is trying to change main PIN to new value; check for issues
|
||||
# - dups bad but also: delta mode pin might not work w/ longer main true pin
|
||||
@ -277,6 +301,64 @@ class TrickPinMgmt:
|
||||
assert not prob # see check_new_main_pin() above
|
||||
self.update_slot(d_pin.encode(), tc_arg=arg)
|
||||
|
||||
def backup_duress_wallets(self, sv):
|
||||
# for backup file, yield (label, path, pairs-of-data)
|
||||
done = set()
|
||||
for pin in self.get_deltamode_pins():
|
||||
sn, flags, arg = self.tp[pin]
|
||||
|
||||
if (flags, arg) in done:
|
||||
continue
|
||||
done.add( (flags, arg) )
|
||||
|
||||
if flags & TC_WORD_WALLET:
|
||||
label = "Duress: BIP-85 Derived wallet"
|
||||
path = "BIP85(words=24, index=%d)" % arg
|
||||
b, slot = tp.get_by_pin(pin)
|
||||
words = bip39.b2a_words(slot.xdata[0:32])
|
||||
|
||||
d = [ ('duress_%d_words' % arg, words) ]
|
||||
elif flags & TC_XPRV_WALLET:
|
||||
label = "Duress: XPRV Wallet"
|
||||
node, path = sv.duress_root()
|
||||
path = 'path = ' + path
|
||||
# backwards compat name, but skipping xpub this time
|
||||
d = [ ('duress_xprv', sv.chain.serialize_private(node)) ]
|
||||
|
||||
yield (label, path, d)
|
||||
|
||||
def restore_backup(self, vals):
|
||||
# restoring backup value
|
||||
# - need to re-populate SE2 w/ these values, including duress wallets
|
||||
# - being restored: vals=self.tp
|
||||
# - CAUTION: new true-pin may not match old true-pin; skip any that would
|
||||
# not work w/ new pin (conflicting value, or deltamode issues)
|
||||
from pincodes import pa
|
||||
true_pin = pa.pin.decode()
|
||||
|
||||
for pin in vals:
|
||||
(sn, flags, arg) = vals[pin]
|
||||
|
||||
if pin == true_pin:
|
||||
# drop conflicting trick pin vs. (new) true pin
|
||||
continue
|
||||
|
||||
if flags & TC_DELTA_MODE:
|
||||
prob = validate_delta_pin(true_pin, pin)
|
||||
if prob:
|
||||
# just forget it, no UI here to report issue
|
||||
continue
|
||||
|
||||
try:
|
||||
# might need to construct a BIP-85 or XPRV secret to match
|
||||
path, new_secret = construct_duress_secret(flags, arg)
|
||||
|
||||
b, slot = tp.update_slot(pin.encode(), new=True,
|
||||
tc_flags=flags, tc_arg=arg, secret=new_secret)
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc) # not visible
|
||||
|
||||
|
||||
tp = TrickPinMgmt()
|
||||
|
||||
class TrickPinMenu(MenuSystem):
|
||||
@ -355,18 +437,8 @@ class TrickPinMenu(MenuSystem):
|
||||
|
||||
msg += '\n\n'
|
||||
|
||||
path = None
|
||||
new_secret = None
|
||||
if flags & TC_WORD_WALLET:
|
||||
# derive the secret via BIP-85
|
||||
new_secret, _, _, path = bip85_derive(2, tc_arg)
|
||||
path = "BIP85(words=24, index=%d)" % tc_arg
|
||||
elif flags & TC_XPRV_WALLET:
|
||||
# use old method for duress wallets
|
||||
with stash.SensitiveValues() as sv:
|
||||
node, path = sv.duress_root()
|
||||
new_secret = SecretStash.encode(xprv=node)[1:65]
|
||||
assert len(new_secret) == 64
|
||||
path, new_secret = construct_duress_secret(flags, tc_arg)
|
||||
|
||||
|
||||
if path:
|
||||
msg += "Duress wallet will use path:\n\n%s\n\n" % path
|
||||
|
||||
@ -20,7 +20,7 @@ if 1:
|
||||
blanks = 0
|
||||
checklist = set('mnemonic chain xprv xpub raw_secret fw_date fw_version fw_timestamp serial '
|
||||
'setting.terms_ok setting.idle_to setting.chain'.split(' '))
|
||||
optional = set('setting.pms setting.axi setting.nick setting.lgto setting.usr hsm_policy setting.words long_secret multisig setting.multisig setting.fee_limit setting.tp setting.check'.split(' '))
|
||||
optional = set('setting.pms setting.axi setting.nick setting.lgto setting.usr hsm_policy setting.words long_secret multisig setting.multisig setting.fee_limit setting.tp setting.check duress_xprv duress_xpub duress_1001_words duress_1002_words duress_1003_words'.split(' '))
|
||||
|
||||
for ln in render_backup_contents().split('\n'):
|
||||
ln = ln.strip()
|
||||
@ -137,6 +137,7 @@ else:
|
||||
settings.clear()
|
||||
settings.save()
|
||||
|
||||
print("fully done")
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -304,8 +304,9 @@ def new_pin_confirmed(cap_menu, need_keypress, cap_story, se2_gate):
|
||||
sl = decode_slot(sl)
|
||||
if sl.pin_len:
|
||||
assert sl.pin[0:sl.pin_len].decode('ascii') == new_pin # simulator only
|
||||
assert sl.tc_flags == xflags
|
||||
|
||||
if xflags is not None:
|
||||
assert sl.tc_flags == xflags
|
||||
if xargs is not None:
|
||||
assert sl.tc_arg == xargs
|
||||
|
||||
@ -652,6 +653,67 @@ def test_ux_changing_pins(true_pin, repl, force_main_pin, goto_trick_menu,
|
||||
|
||||
clear_all_tricks()
|
||||
|
||||
def test_trick_backups(goto_trick_menu, clear_all_tricks, repl, unit_test,
|
||||
new_trick_pin, new_pin_confirmed, pick_menu_item, need_keypress):
|
||||
|
||||
clear_all_tricks()
|
||||
|
||||
# - make wallets of all duress types (x2 each)
|
||||
# - plus a few simple ones
|
||||
# - perform a backup and check result
|
||||
|
||||
for n in range(1 or 8):
|
||||
goto_trick_menu()
|
||||
pin = '123-%04d'%n
|
||||
new_trick_pin(pin, 'Duress Wallet', None)
|
||||
item = 'BIP-85 Wallet #%d' % (n%4) if (n%4 != 0) else 'Legacy Wallet'
|
||||
pick_menu_item(item)
|
||||
need_keypress('y')
|
||||
new_pin_confirmed(pin, item, None, None)
|
||||
|
||||
for pin, op_mode, expect, _, xflags in [
|
||||
('11-33', 'Just Reboot', 'Reboot when this PIN', False, TC_REBOOT),
|
||||
('11-55', 'Look Blank', 'Look and act like a freshly', False, TC_BLANK_WALLET),
|
||||
]:
|
||||
new_trick_pin(pin, op_mode, expect)
|
||||
new_pin_confirmed(pin, op_mode, xflags)
|
||||
|
||||
# works, but not the best test
|
||||
#unit_test('devtest/backups.py')
|
||||
|
||||
bk = repl.exec('import backups; RV.write(backups.render_backup_contents())', raw=1)
|
||||
|
||||
assert 'Coldcard backup file' in bk
|
||||
|
||||
def decode_backup(txt):
|
||||
import json
|
||||
vals = dict()
|
||||
for ln in txt.split('\n'):
|
||||
if not ln: continue
|
||||
if ln[0] == '#': continue
|
||||
|
||||
k,v = ln.split(' = ', 1)
|
||||
if k.startswith('duress_'): continue
|
||||
if k.startswith('fw_'): continue
|
||||
vals[k] = json.loads(v)
|
||||
return vals
|
||||
|
||||
# decode it
|
||||
vals = decode_backup(bk)
|
||||
|
||||
unit_test('devtest/clear_seed.py')
|
||||
|
||||
repl.exec(f'import backups; backups.restore_from_dict_ll({vals!r})')
|
||||
|
||||
# recover from recovery
|
||||
repl.exec(f'import backups; pa.setup(pa.pin); pa.login(); from actions import goto_top_menu; goto_top_menu()')
|
||||
|
||||
bk2 = repl.exec('import backups; RV.write(backups.render_backup_contents())', raw=1)
|
||||
assert 'Traceback' not in bk2
|
||||
|
||||
vals2 = decode_backup(bk2)
|
||||
assert vals == vals2
|
||||
|
||||
|
||||
# TODO
|
||||
# - make trick and do login, check arrives right state?
|
||||
|
||||
Loading…
Reference in New Issue
Block a user