def generate_electrum_wallet(is_segwit): # Generate line-by-line JSON details about wallet. # # Much reverse enginerring of Electrum here. It's a complex # legacy file format. from main import settings chain = chains.current_chain() xfp = settings.get('xfp') if is_segwit: derive = "m/84'/{coin_type}'/{account}'".format(account=0, coin_type=chain.b44_cointype) else: derive = "m/44'/{coin_type}'/{account}'".format(account=0, coin_type=chain.b44_cointype) with stash.SensitiveValues() as sv: top = chain.serialize_public(sv.derive_path(derive)) # most values are nicely defaulted, and for max forward compat, don't want to set # anything more than I need to rv = dict(seed_version=17, use_encryption=False, wallet_type='standard') # the important stuff. rv['keystore'] = dict( ckcc_xfp=xfp, ckcc_xpub=settings.get('xpub'), hw_type='coldcard', label='Coldcard Import 0x%08x' % xfp, type='hardware', derivation=derive, xpub=top) return rv
async def remember_bip39_passphrase(): # Compute current xprv and switch to using that as root secret. import stash from main import dis, pa if not stash.bip39_passphrase: if not await ux_confirm( '''You do not have a BIP39 passphrase set right now, so this command does little except forget the seed words. It does not enhance security.''' ): return dis.fullscreen('Check...') with stash.SensitiveValues() as sv: if sv.mode != 'words': # not a BIP39 derived secret, so cannot work. await ux_show_story( '''The wallet secret was not based on a seed phrase, so we cannot add a BIP39 passphrase at this time.''', title='Failed') return nv = SecretStash.encode(xprv=sv.node) dis.fullscreen('Saving...') pa.change(new_secret=nv) # re-read settings since key is now different # - also captures xfp, xpub at this point pa.new_main_secret(nv) # check and reload secret pa.reset() pa.login()
async def interact(self): # Prompt user w/ details and get approval from main import dis ch = await ux_show_story(MSG_SIG_TEMPLATE.format(msg=self.text, addr=self.address, subpath=self.subpath)) if ch != 'y': # they don't want to! self.refused = True else: dis.fullscreen('Signing...', percent=.25) # do the signature itself! with stash.SensitiveValues() as sv: dis.progress_bar_show(.50) node = sv.derive_path(self.subpath) pk = node.private_key() sv.register(pk) digest = sv.chain.hash_message(self.text.encode()) dis.progress_bar_show(.75) self.result = tcc.secp256k1.sign(pk, digest) dis.progress_bar_show(1.0) self.done()
def generate_generic_export(account_num=0): # Generate data that other programers will use to import Coldcard (single-signer) from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH chain = chains.current_chain() rv = dict(chain=chain.ctype, xpub = settings.get('xpub'), xfp = xfp2str(settings.get('xfp')), account = account_num, ) with stash.SensitiveValues() as sv: # each of these paths would have /{change}/{idx} in usage (not hardened) for name, deriv, fmt, atype in [ ( 'bip44', "m/44'/{ct}'/{acc}'", AF_CLASSIC, 'p2pkh' ), ( 'bip49', "m/49'/{ct}'/{acc}'", AF_P2WPKH_P2SH, 'p2sh-p2wpkh' ), # was "p2wpkh-p2sh" ( 'bip84', "m/84'/{ct}'/{acc}'", AF_P2WPKH, 'p2wpkh' ), ]: dd = deriv.format(ct=chain.b44_cointype, acc=account_num) node = sv.derive_path(dd) xfp = xfp2str(swab32(node.my_fp())) xp = chain.serialize_public(node, AF_CLASSIC) zp = chain.serialize_public(node, fmt) if fmt != AF_CLASSIC else None # bonus/check: first non-change address: 0/0 node.derive(0, False).derive(0, False) rv[name] = dict(deriv=dd, xpub=xp, xfp=xfp, first=chain.address(node, fmt), name=atype) if zp: rv[name]['_pub'] = zp return rv
async def xor_restore_start(*a): # shown on import menu when no seed of any kind yet # - or operational system ch = await ux_show_story('''\ To import a seed split using XOR, you must import all the parts. It does not matter the order (A/B/C or C/A/B) and the Coldcard cannot determine when you have all the parts. You may stop at any time and you will have a valid wallet.''') if ch == 'x': return global import_xor_parts import_xor_parts.clear() from pincodes import pa if not pa.is_secret_blank(): msg = "Since you have a seed already on this Coldcard, the reconstructed XOR seed will be temporary and not saved. Wipe the seed first if you want to commit the new value into the secure element." if settings.get('words', True): msg += '''\n Press (1) to include this Coldcard's seed words into the XOR seed set, or OK to continue without.''' ch = await ux_show_story(msg, escape='1') if ch == 'x': return elif ch == '1': with stash.SensitiveValues() as sv: if sv.mode == 'words': words = bip39.b2a_words(sv.raw).split(' ') import_xor_parts.append(words) return XORWordNestMenu(num_words=24)
def make_msg(start): msg = '' if start == 0: msg = "Press 1 to save to MicroSD." if version.has_fatram: msg += " 4 to view QR Codes." msg += '\n\n' msg += "Addresses %d..%d:\n\n" % (start, start + n - 1) addrs = [] chain = chains.current_chain() dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: for idx in range(start, start + n): subpath = path.format(account=0, change=0, idx=idx) node = sv.derive_path(subpath, register=False) addr = chain.address(node, addr_fmt) addrs.append(addr) msg += "%s =>\n%s\n\n" % (subpath, addr) dis.progress_bar_show(idx / n) stash.blank_object(node) msg += "Press 9 to see next group, 7 to go back. X to quit." return msg, addrs
async def remember_bip39_passphrase(): # Compute current xprv and switch to using that as root secret. import stash from common import dis, pa dis.fullscreen('Check...') with stash.SensitiveValues() as sv: if sv.mode != 'words': # not a BIP39 derived secret, so cannot work. await ux_show_story( '''The wallet secret was not based on a seed phrase, so we cannot add a BIP39 passphrase at this time.''', title='Failed') return nv = SecretStash.encode(xprv=sv.node) # Important: won't write new XFP to nvram if pw still set stash.bip39_passphrase = '' dis.fullscreen('Saving...') pa.change(new_secret=nv) # re-read settings since key is now different # - also captures xfp, xpub at this point pa.new_main_secret(nv) # check and reload secret pa.reset() pa.login()
def generate_public_contents(): # Generate public details about wallet. # # simple text format: # key = value # or #comments # but value is JSON from main import settings num_rx = 5 chain = chains.current_chain() with stash.SensitiveValues() as sv: yield ('''\ # Coldcard Wallet Summary File ## Wallet operates on blockchain: {nb} For BIP44, this is coin_type '{ct}', and internally we use symbol {sym} for this blockchain. ## Top-level, 'master' extended public key ('m/'): {xpub} Derived public keys, as may be needed for different systems: '''.format(nb=chain.name, xpub=chain.serialize_public(sv.node), sym=chain.ctype, ct=chain.b44_cointype)) for name, path, addr_fmt in chains.CommonDerivations: if '{coin_type}' in path: path = path.replace('{coin_type}', str(chain.b44_cointype)) yield ('''## For {name}: {path}\n\n'''.format(name=name, path=path)) submaster, kids = path.split('/{', 1) kids = '{' + kids node = sv.derive_path(submaster) yield ("%s => %s\n" % (submaster, chain.serialize_public(node))) yield ( '''\n... first %d receive addresses (account=0, change=0):\n\n''' % num_rx) for i in range(num_rx): subpath = kids.format(account=0, change=0, idx=i) kid = sv.derive_path(subpath, node) yield ('%s/%s => %s\n' % (submaster, subpath, chain.address(kid, addr_fmt))) yield ('\n\n')
def generate_unchained_export(acct_num=0): # They used to rely on our airgapped export file, so this is same style # - for multisig purposes # - BIP-45 style paths for now # - no account numbers (at this level) from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH chain = chains.current_chain() todo = [ ("m/45'", 'p2sh', AF_P2SH), # iff acct_num == 0 ("m/48'/{coin}'/{acct_num}'/1'", 'p2sh_p2wsh', AF_P2WSH_P2SH), ("m/48'/{coin}'/{acct_num}'/2'", 'p2wsh', AF_P2WSH), ] xfp = xfp2str(settings.get('xfp', 0)) rv = dict(account=acct_num, xfp=xfp) with stash.SensitiveValues() as sv: for deriv, name, fmt in todo: if fmt == AF_P2SH and acct_num: continue dd = deriv.format(coin=chain.b44_cointype, acct_num=acct_num) node = sv.derive_path(dd) xp = chain.serialize_public(node, fmt) rv['%s_deriv' % name] = dd rv[name] = xp return rv
def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0): # Produce CSV file contents as a generator if ms_wallet: from ubinascii import hexlify as b2a_hex # For multisig, include redeem script and derivation for each signer yield '"' + '","'.join(['Index', 'Payment Address', 'Redeem Script (%d of %d)' % (ms_wallet.M, ms_wallet.N)] + (['Derivation'] * ms_wallet.N)) + '"\n' for (idx, derivs, addr, script) in ms_wallet.yield_addresses(start, n): ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode()) ln += '","'.join(derivs) ln += '"\n' yield ln return yield '"Index","Payment Address","Derivation"\n' ch = chains.current_chain() with stash.SensitiveValues() as sv: for idx in range(start, start+n): deriv = path.format(account=account_num, change=0, idx=idx) node = sv.derive_path(deriv, register=False) yield '%d,"%s","%s"\n' % (idx, ch.address(node, addr_fmt), deriv) stash.blank_object(node)
def make_msg(start): msg = '' if start == 0: msg = "Press 1 to save to MicroSD." msg += '\n\n' msg += "Addresses %d..%d:\n\n" % (start, start + n - 1) addrs = [] chain = chains.current_chain() dis.fullscreen('Loading...') with stash.SensitiveValues() as sv: for idx in range(start, start + n): subpath = path.format(account=0, change=0, idx=idx) node = sv.derive_path(subpath, register=False) addr = chain.address(node, addr_fmt) addr1 = addr[:16] addr2 = addr[16:] addrs.append(addr) msg += "%s =>\n %s\n %s\n\n" % (subpath, addr1, addr2) dis.progress_bar_show(idx / n) stash.blank_object(node) msg += "Press 9 to see next group.\nPress 7 to see prev. group." return msg, addrs
def generate_wasabi_wallet(): # Generate the data for a JSON file which Wasabi can open directly as a new wallet. from common import settings import ustruct import version # bitcoin (xpub) is used, even for testnet case (ie. no tpub) # - altho, doesn't matter; the wallet operates based on it's own settings for test/mainnet # regardless of the contents of the wallet file btc = chains.BitcoinMain with stash.SensitiveValues() as sv: xpub = btc.serialize_public(sv.derive_path("84'/0'/0'")) xfp = settings.get('xfp') txt_xfp = xfp2str(xfp) chain = chains.current_chain() assert chain.ctype in {'BTC', 'TBTC'}, "Only Bitcoin supported" _, vers, _ = version.get_mpy_version() return dict(MasterFingerprint=txt_xfp, ColdCardFirmwareVersion=vers, ExtPubKey=xpub)
def setup(self, addr_fmt, subpath): self.subpath = subpath self.addr_fmt = addr_fmt with stash.SensitiveValues() as sv: node = sv.derive_path(subpath) self.address = sv.chain.address(node, addr_fmt)
def new_main_secret(self, raw_secret, chain=None): # Main secret has changed: reset the settings+their key, # and capture xfp/xpub from common import settings import stash # capture values we have already old_values = dict(settings.curr_dict) print('old_values = {}'.format(old_values)) settings.set_key(raw_secret) settings.load() print('after load = {}'.format(settings.curr_dict)) # merge in settings, including what chain to use, timeout, etc. settings.merge(old_values) print('after merge = {}'.format(settings.curr_dict)) # Recalculate xfp/xpub values (depends both on secret and chain) with stash.SensitiveValues(raw_secret) as sv: if chain is not None: sv.chain = chain sv.capture_xpub() print('before save = {}'.format(settings.curr_dict)) # Need to save values with new AES key settings.save() print('after save = {}'.format(settings.curr_dict))
async def view_seed_words(*a): import stash, tcc if not await ux_confirm( '''The next screen will show the seed words (and if defined, your BIP39 passphrase).\n\nAnyone with knowledge of those words can control all funds in this wallet.''' ): return with stash.SensitiveValues() as sv: if sv.mode == 'words': words = tcc.bip39.from_data(sv.raw).split(' ') msg = 'Seed words (%d):\n' % len(words) msg += '\n'.join('%2d: %s' % (i + 1, w) for i, w in enumerate(words)) pw = stash.bip39_passphrase if pw: msg += '\n\nBIP39 Passphrase:\n%s' % stash.bip39_passphrase elif sv.mode == 'xprv': import chains msg = chains.current_chain().serialize_private(sv.node) elif sv.mode == 'master': from ubinascii import hexlify as b2a_hex msg = '%d bytes:\n\n' % len(sv.raw) msg += str(b2a_hex(sv.raw), 'ascii') else: raise ValueError(sv.mode) await ux_show_story(msg, sensitive=True) stash.blank_object(msg)
def make_msg(): msg = '' if n > 1: if start == 0: msg = "Press 1 to save to MicroSD." if version.has_fatram and not ms_wallet: msg += " 4 to view QR Codes." msg += '\n\n' msg += "Addresses %d..%d:\n\n" % (start, start + n - 1) else: # single address, from deep path given by user msg += "Showing single address." if version.has_fatram: msg += " Press 4 to view QR Codes." msg += '\n\n' addrs = [] chain = chains.current_chain() dis.fullscreen('Wait...') if ms_wallet: # IMPORTANT safety feature: never show complete address # but show enough they can verify addrs shown elsewhere. # - makes a redeem script # - converts into addr # - assumes 0/0 is first address. for (i, paths, addr, script) in ms_wallet.yield_addresses(start, n): if i == 0 and ms_wallet.N <= 4: msg += '\n'.join(paths) + '\n =>\n' else: msg += '.../0/%d =>\n' % i addrs.append(addr) msg += truncate_address(addr) + '\n\n' dis.progress_bar_show(i/n) else: # single-singer wallets with stash.SensitiveValues() as sv: for idx in range(start, start + n): deriv = path.format(account=self.account_num, change=0, idx=idx) node = sv.derive_path(deriv, register=False) addr = chain.address(node, addr_fmt) addrs.append(addr) msg += "%s =>\n%s\n\n" % (deriv, addr) dis.progress_bar_show(idx/n) stash.blank_object(node) if n > 1: msg += "Press 9 to see next group, 7 to go back. X to quit." return msg, addrs
async def choose_first_address(*a): # Choose from a truncated list of index 0 common addresses, remember # the last address the user selected and use it as the default from main import settings, dis chain = chains.current_chain() dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: def truncate_address(addr): # Truncates address to width of screen, replacing middle chars middle = "-" leftover = SCREEN_CHAR_WIDTH - len(middle) start = addr[0:(leftover + 1) // 2] end = addr[len(addr) - (leftover // 2):] return start + middle + end # Create list of choices (address_index_0, path, addr_fmt) choices = [] for name, path, addr_fmt in chains.CommonDerivations: if '{coin_type}' in path: path = path.replace('{coin_type}', str(chain.b44_cointype)) subpath = path.format(account=0, change=0, idx=0) node = sv.derive_path(subpath, register=False) address = chain.address(node, addr_fmt) choices.append((truncate_address(address), path, addr_fmt)) dis.progress_bar_show(len(choices) / len(chains.CommonDerivations)) stash.blank_object(node) picked = None async def clicked(_1, _2, item): if picked is None: picked = item.arg the_ux.pop() items = [ MenuItem(address, f=clicked, arg=i) for i, (address, path, addr_fmt) in enumerate(choices) ] menu = MenuSystem(items) menu.goto_idx(settings.get('axi', 0)) the_ux.push(menu) await menu.interact() if picked is None: return None # update last clicked address settings.put('axi', picked) address, path, addr_fmt = choices[picked] return (path, addr_fmt)
def __init__(self, subpath, addr_fmt): super().__init__() self.subpath = subpath from main import dis dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: node = sv.derive_path(subpath) self.address = sv.chain.address(node, addr_fmt)
def __init__(self, text, subpath, addr_fmt, approved_cb=None): super().__init__() self.text = text self.subpath = subpath self.approved_cb = approved_cb from main import dis dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: node = sv.derive_path(subpath) self.address = sv.chain.address(node, addr_fmt)
def _calc_key(self, card): # calculate the key to be used. if getattr(self, 'key', None): return try: salt = card.get_id_hash() with stash.SensitiveValues(bypass_pw=True) as sv: self.key = bytearray(sv.encryption_key(salt)) except: self.key = None
def set_chain(idx, text): val = ch[idx][0] assert ch[idx][1] == text settings.set('chain', val) try: # update xpub stored in settings import stash with stash.SensitiveValues() as sv: sv.capture_xpub() except ValueError: # no secrets yet, not an error pass
def generate_address_csv(path, addr_fmt, n): # Produce CSV file contents as a generator yield '"Index","Payment Address","Derivation"\n' ch = chains.current_chain() with stash.SensitiveValues() as sv: for idx in range(n): subpath = path.format(account=0, change=0, idx=idx) node = sv.derive_path(subpath, register=False) yield '%d,"%s","%s"\n' % (idx, ch.address(node, addr_fmt), subpath) stash.blank_object(node)
def generate_bitcoin_core_wallet(example_addrs, account_num): # Generate the data for an RPC command to import keys into Bitcoin Core # - yields dicts for json purposes from descriptor import append_checksum from main import settings import ustruct from public_constants import AF_P2WPKH chain = chains.current_chain() derive = "84'/{coin_type}'/{account}'".format(account=account_num, coin_type=chain.b44_cointype) with stash.SensitiveValues() as sv: prefix = sv.derive_path(derive) xpub = chain.serialize_public(prefix) for i in range(3): sp = '0/%d' % i node = sv.derive_path(sp, master=prefix) a = chain.address(node, AF_P2WPKH) example_addrs.append(('m/%s/%s' % (derive, sp), a)) xfp = settings.get('xfp') txt_xfp = xfp2str(xfp).lower() chain = chains.current_chain() _, vers, _ = version.get_mpy_version() for internal in [False, True]: desc = "wpkh([{fingerprint}/{derive}]{xpub}/{change}/*)".format( derive=derive.replace("'", "h"), fingerprint=txt_xfp, coin_type=chain.b44_cointype, account=0, xpub=xpub, change=(1 if internal else 0)) yield { 'desc': append_checksum(desc), 'range': [0, 1000], 'timestamp': 'now', 'internal': internal, 'keypool': True, 'watchonly': True }
async def handle_mitm_check(self): # Sign the current session key using our master (bitcoin) key. # - proves our identity and that no-one is between # Rate limit and fuzz timing in case we have timing sensitivity await sleep_ms(250 + tcc.random.uniform(1000)) with stash.SensitiveValues() as sv: pk = sv.node.private_key() sv.register(pk) signature = tcc.secp256k1.sign(pk, self.session_key) assert len(signature) == 65 return b'biny' + signature
def generate_electrum_wallet(addr_type, account_num=0): # Generate line-by-line JSON details about wallet. # # Much reverse enginerring of Electrum here. It's a complex # legacy file format. from main import settings from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH chain = chains.current_chain() xfp = settings.get('xfp') # Must get the derivation path, and the SLIP32 version bytes right! if addr_type == AF_CLASSIC: mode = 44 elif addr_type == AF_P2WPKH: mode = 84 elif addr_type == AF_P2WPKH_P2SH: mode = 49 else: raise ValueError(addr_type) derive = "m/{mode}'/{coin_type}'/{account}'".format( mode=mode, account=account_num, coin_type=chain.b44_cointype) with stash.SensitiveValues() as sv: top = chain.serialize_public(sv.derive_path(derive), addr_type) # most values are nicely defaulted, and for max forward compat, don't want to set # anything more than I need to rv = dict(seed_version=17, use_encryption=False, wallet_type='standard') lab = 'Coldcard Import %s' % xfp2str(xfp) if account_num: lab += ' Acct#%d' % account_num # the important stuff. rv['keystore'] = dict(ckcc_xfp=xfp, ckcc_xpub=settings.get('xpub'), hw_type='coldcard', type='hardware', label=lab, derivation=derive, xpub=top) return rv
async def render(self): # Choose from a truncated list of index 0 common addresses, remember # the last address the user selected and use it as the default from glob import dis chain = chains.current_chain() dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: # Create list of choices (address_index_0, path, addr_fmt) choices = [] for name, path, addr_fmt in chains.CommonDerivations: if '{coin_type}' in path: path = path.replace('{coin_type}', str(chain.b44_cointype)) if self.account_num != 0 and '{account}' not in path: # skip derivations that are not affected by account number continue deriv = path.format(account=self.account_num, change=0, idx=0) node = sv.derive_path(deriv, register=False) address = chain.address(node, addr_fmt) choices.append( (truncate_address(address), path, addr_fmt) ) dis.progress_bar_show(len(choices) / len(chains.CommonDerivations)) stash.blank_object(node) items = [MenuItem(address, f=self.pick_single, arg=(path, addr_fmt)) for i, (address, path, addr_fmt) in enumerate(choices)] # some other choices if self.account_num == 0: items.append(MenuItem("Account Number", f=self.change_account)) items.append(MenuItem("Custom Path", menu=self.make_custom)) # if they have MS wallets, add those next for ms in MultisigWallet.iter_wallets(): if not ms.addr_fmt: continue items.append(MenuItem(ms.name, f=self.pick_multisig, arg=ms)) else: items.append(MenuItem("Account: %d" % self.account_num, f=self.change_account)) self.goto_idx(settings.get('axi', 0)) # weak self.replace_items(items)
def set_bip39_passphrase(pw): # apply bip39 passphrase for now (volatile) # takes a bit, so show something from main import dis dis.fullscreen("Working...") # set passphrase import stash stash.bip39_passphrase = pw # capture updated XFP with stash.SensitiveValues() as sv: # can't do it without original seed words (late, but caller has checked) assert sv.mode == 'words' sv.capture_xpub()
def set_bip39_passphrase(pw): # apply bip39 passphrase for now (volatile) # - return None or error msg import stash stash.bip39_passphrase = pw # takes a bit, so show something from common import dis dis.fullscreen("Working...") with stash.SensitiveValues() as sv: if sv.mode != 'words': # can't do it without original seed woods return 'No BIP39 seed words' sv.capture_xpub()
def handle_xpub(self, subpath): # Share the xpub for the indicated subpath. Expects # a text string which is the path derivation. # TODO: might not have a privkey yet from chains import current_chain from utils import cleanup_deriv_path subpath = cleanup_deriv_path(subpath) chain = current_chain() with stash.SensitiveValues() as sv: node = sv.derive_path(subpath) xpub = chain.serialize_public(node) return b'asci' + xpub.encode()
def new_main_secret(self, raw_secret, chain=None): # Main secret has changed: reset the settings+their key, # and capture xfp/xpub from nvstore import settings import stash # 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) # Recalculate xfp/xpub values (depends both on secret and chain) with stash.SensitiveValues(raw_secret) as sv: if chain is not None: sv.chain = chain sv.capture_xpub()