Backup and restore of trick pins and their effects

This commit is contained in:
Peter D. Gray 2022-03-07 12:01:15 -05:00
parent 201a005b29
commit 3f13c63dd3
No known key found for this signature in database
GPG Key ID: F0E6CC6AFC16CF7B
4 changed files with 188 additions and 25 deletions

View File

@ -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!')

View File

@ -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

View File

@ -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

View File

@ -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?