# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # # SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard # and is covered by GPLv3 license found in COPYING. # # stash.py - encoding the ultrasecrets: bip39 seeds and words # # references: # - # - # - zero values: # - 'abandon' * 23 + 'art' # - 'abandon' * 17 + 'agent' # - 'abandon' * 11 + 'about' # import trezorcrypto, uctypes, gc from pincodes import SE_SECRET_LEN def blank_object(item): # Use/abuse uctypes to blank objects until python. Will likely # even work on immutable types, so be careful. Also works # well to kill references to sensitive values (but not copies). # if isinstance(item, (bytearray, bytes, str)): addr, ln = uctypes.addressof(item), len(item) buf = uctypes.bytearray_at(addr, ln) for i in range(ln): buf[i] = 0 elif isinstance(item, trezorcrypto.bip32.HDNode): pass # item.blank() # node.blank() elsewhere else: raise TypeError(item) # Chip can hold 72-bytes as a secret: we need to store either # a list of seed words (packed), of various lengths, or maybe # a raw master secret, and so on class SecretStash: @staticmethod def encode(seed_bits=None, master_secret=None, xprv=None): nv = bytearray(SE_SECRET_LEN) if seed_bits: # typical: seed bits without checksum bits vlen = len(seed_bits) # TODO: Do we support all of these?s assert vlen in [16, 24, 32] nv[0] = 0x80 | ((vlen // 8) - 2) nv[1:1+vlen] = seed_bits elif master_secret: # between 128 and 512 bits of master secret for BIP32 key derivation vlen = len(master_secret) assert 16 <= vlen <= 64 nv[0] = vlen nv[1:1+vlen] = master_secret elif xprv: # master xprivkey, which could be a subkey of something we don't know # - we record only the minimum assert isinstance(xprv, trezorcrypto.bip32.HDNode) nv[0] = 0x01 nv[1:33] = xprv.chain_code() nv[33:65] = xprv.private_key() return nv @staticmethod def decode(secret, _bip39pw=''): # expecting 72-bytes of secret payload; decode meaning # returns: # type, secrets bytes, HDNode(root) # marker = secret[0] if marker == 0x01: # xprv => BIP32 private key values ch, pk = secret[1:33], secret[33:65] assert not _bip39pw return 'xprv', ch+pk, trezorcrypto.bip32.HDNode(chain_code=ch, private_key=pk, child_num=0, depth=0, fingerprint=0) if marker & 0x80: # seed phrase ll = ((marker & 0x3) + 2) * 8 # note: # - byte length > number of words # - not storing checksum assert ll in [16, 24, 32] # make master secret, using the memonic words, and passphrase (or empty string) seed_bits = secret[1:1+ll] ms = trezorcrypto.bip39.seed(trezorcrypto.bip39.from_data(seed_bits), _bip39pw) hd = trezorcrypto.bip32.from_seed(ms, 'secp256k1') return 'words', seed_bits, hd else: # variable-length master secret for BIP32 vlen = secret[0] assert 16 <= vlen <= 64 assert not _bip39pw ms = secret[1:1+vlen] hd = trezorcrypto.bip32.from_seed(ms, 'secp256k1') return 'master', ms, hd # optional global value: user-supplied passphrase to salt BIP39 seed process bip39_passphrase = '' bip39_hash = '' class SensitiveValues: # be a context manager, and holder to secrets in-memory def __init__(self, secret=None, for_backup=False): from common import system if secret is None: # fetch the secret from bootloader/atecc508a from common import pa if pa.is_secret_blank(): raise ValueError('no secrets yet') self.secret = pa.fetch() self.spots = [ self.secret ] else: # sometimes we already know it # assert set(secret) != {0} self.secret = secret self.spots = [] # backup during volatile bip39 encryption: do not use passphrase self._bip39pw = '' if for_backup else str(bip39_passphrase) # print('self._bip39pw={}'.format(self._bip39pw)) def __enter__(self): import chains self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw) self.spots.append(self.node) self.spots.append(self.raw) self.chain = chains.current_chain() return self def __exit__(self, exc_type, exc_val, exc_tb): # Clear secrets from memory ... yes, they could have been # copied elsewhere, but in normal case, at least we blanked them. for item in self.spots: blank_object(item) if hasattr(self, 'secret'): # will be blanked from above del self.secret if hasattr(self, 'node'): # specialized blanking code already above del self.node # just in case this holds some pointers? del self.spots # .. and some GC will help too! gc.collect() if exc_val: # An exception happened, but we've done cleanup already now, so # not a big deal. Cause it be raised again. return False return True def get_xfp(self): return self.node.my_fingerprint() def capture_xpub(self): # track my xpubkey fingerprint & value in settings (not sensitive really) # - we share these on any USB connection import common from common import settings # # Set the master values if no account selected yet # if common.active_account: # # Derive xfp and xpub based on the current active account # # The BIP39 passphrase is already taken into account by SensitiveValues # # print('deriving from path: {}'.format(common.active_account.deriv_path)) # if not common.active_account.deriv_path: # return # # node = self.derive_path(common.active_account.deriv_path) # # xfp = node.my_fingerprint() # print('capture_xpub(): xfp={}'.format(hex(xfp))) # xpub = self.chain.serialize_public(node, common.active_account.addr_type) # print('capture_xpub(): xpub={}'.format(xpub)) # else: xfp = self.node.my_fingerprint() # print('capture_xpub(): xfp={}'.format(hex(xfp))) xpub = self.chain.serialize_public(self.node) # print('capture_xpub(): xpub={}'.format(xpub)) # Always store these volatile - Takes less than 1 second to recreate, and it will change whenever # a passphrase is entered, so no need to waste flash cycles on storing it. settings.set_volatile('xfp', xfp) settings.set_volatile('xpub', xpub) settings.set('chain', self.chain.ctype) settings.set('words', (self.mode == 'words')) def register(self, item): # Caller can add his own sensitive (derived?) data to our wiper # typically would be byte arrays or byte strings, but also # supports bip32 nodes self.spots.append(item) def derive_path(self, path, master=None, register=True): # Given a string path, derive the related subkey rv = (master or self.node).clone() if register: self.register(rv) for i in path.split('/'): if i == 'm': continue if not i: continue # trailing or duplicated slashes if i[-1] == "'": assert len(i) >= 2, i here = int(i[:-1]) assert 0 <= here < 0x80000000, here here |= 0x80000000 else: here = int(i) assert 0 <= here < 0x80000000, here rv.derive(here) return rv # EOF