# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # trick_pins.py - manage the "trick" PIN codes, which can do anything but let you in! # # - mk4+ only # - uses SE2 to store PIN codes (hashed) and what actions to perform for each # - replaces old "duress wallet" and "brickme" features # - changes require knowledge of real PIN code (it is checked) # import uctypes, errno, ngu, sys, stash, bip39, version from menu import MenuSystem, MenuItem from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux from stash import SecretStash from drv_entro import bip85_derive from utils import node_from_privkey # see from mk4-bootloader/se2.h NUM_TRICKS = const(14) TRICK_SLOT_LAYOUT = { "slot_num": 0 | uctypes.INT32, "tc_flags": 4 | uctypes.UINT16, "tc_arg": 6 | uctypes.UINT16, "xdata": (8 | uctypes.ARRAY, 64 | uctypes.UINT8), "pin": (8+64 | uctypes.ARRAY, 16 | uctypes.UINT8), "pin_len": (8+64+16) | uctypes.INT32, "blank_slots": (8+64+16+4) | uctypes.UINT32, "spare": ((8+64+16+4+4) | uctypes.ARRAY, 8|uctypes.INT32), } TC_WIPE = const(0x8000) TC_BRICK = const(0x4000) TC_FAKE_OUT = const(0x2000) TC_WORD_WALLET = const(0x1000) TC_XPRV_WALLET = const(0x0800) TC_DELTA_MODE = const(0x0400) TC_REBOOT = const(0x0200) TC_FW_DEFINED = const(0x0100) # for our use, not implemented in bootrom TC_BLANK_WALLET = const(0x0080) TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay # tc_args encoding: # TC_WORD_WALLET -> BIP-85 index, 1001..1003 for 24 words, 2001..2003 for 12-words # If TC_FW_DEFINED is true, then we can do anything with this PIN at the firmware # level. First application is to unlock spending stuff. TCA_SP_UNLOCK = const(0x0001) # spending policy unlock # special "pin" used as catch-all for wrong pins WRONG_PIN_CODE = '!p' def validate_delta_pin(true_pin, proposed_delta_pin): # Check delta pin proposal works w/ limitations and # provide error msg, and/or calc required tc_arg value. right = true_pin.replace('-', '') fake = proposed_delta_pin.replace('-', '') if (len(right) != len(fake)) or (right[0:-4] != fake[0:-4]): prob = '''\ Trick PIN must be same length (%d) as true PIN and \ up to last four digits can be different between true PIN and trick.''' % len(right) return prob, 0 a = 0 for i in range(4): dx = -(1+i) if right[dx] == fake[dx]: # no need to reveal this digit to SE2 hacker if same a |= 0xf << (i*4) else: a |= (ord(right[-(1+i)]) - 0x30) << (i*4) 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 nwords = 24 if (tc_arg//1000 == 1) else 12 mmode = 0 if (nwords == 12) else 2 # weak: based on menu design new_secret, _, _, path = bip85_derive(mmode, tc_arg) path = "BIP85(words=%d, index=%d)" % (nwords, 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) class TrickPinMgmt: def __init__(self): assert uctypes.sizeof(TRICK_SLOT_LAYOUT) == 128 self.reload() def reload(self): # we track known PINS as a dictionary: # pin (in ascii) => (slot_num, tc_flags, arg) from glob import settings self.tp = settings.get('tp', {}) def save_record(self): # commit changes back to settings from glob import settings if self.tp: settings.set('tp', self.tp) else: settings.remove_key('tp') settings.save() def roundtrip(self, method_num, slot_buf=None): from pincodes import pa if slot_buf is not None: arg = slot_buf else: # use zeros assert method_num == 0 arg = bytes(uctypes.sizeof(TRICK_SLOT_LAYOUT)) rc, data = pa.trick_request(method_num, arg) if slot_buf is not None: # overwrite request w/ result (works inplace) slot_buf[:] = data return rc def clear_all(self): # get rid of them all self.roundtrip(0) self.tp = {} self.save_record() def forget_pin(self, pin): # forget about settings for a PIN self.tp.pop(pin, None) self.save_record() def restore_pin(self, new_pin): # remember/restore PIN that we "forgot", return T if worked b, slot = tp.get_by_pin(new_pin) if slot is None: return False record = (slot.slot_num, slot.tc_flags, 0xffff if slot.tc_flags & TC_DELTA_MODE else slot.tc_arg) self.tp[new_pin] = record self.save_record() return True def clear_slots(self, slot_nums): # remove some slots, not all b, slot = make_slot() slot.blank_slots = sum(1<