async def approve_word_list(seed): # Force the user to write the seeds words down, give a quiz, then save them. # LESSON LEARNED: if the user is writting down the words, as we have # vividly instructed, then it's a big deal to lose those words and have to start # over. So confirm that action, and don't volunteer it. words = bip39.b2a_words(seed).split(' ') assert len(words) == 24 while 1: # show the seed words ch = await show_words(words, escape='46', extra='\n\nPress 4 to add some dice rolls into the mix.') if ch == 'x': # user abort, but confirm it! if await ux_confirm("Throw away those words and stop this process?"): return else: continue if ch == '4': # dice roll mode count, new_seed = await add_dice_rolls(0, seed, False) if count: seed = new_seed words = bip39.b2a_words(seed).split(' ') continue if ch == '6': # wants to skip the quiz (undocumented) if await ux_confirm("Skipping the quiz means you might have " "recorded the seed wrong and will be crying later."): break # Perform a test, to check they wrote them down ch = await word_quiz(words) if ch == 'x': # user abort quiz if await ux_confirm("Throw away those words and stop this process? Press X to see the word list again and restart the quiz."): return # show the words again, but don't change them continue # quiz passed break # Done! set_seed_value(words) # send them to home menu, now with a wallet enabled goto_top_menu()
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)
async def all_done(new_words): # So we have another part, might be done or not. global import_xor_parts assert len(new_words) == 24 import_xor_parts.append(new_words) XORWordNestMenu.pop_all() num_parts = len(import_xor_parts) seed = xor32(*(bip39.a2b_words(w) for w in import_xor_parts)) msg = "You've entered %d parts so far.\n\n" % num_parts if num_parts >= 2: chk_word = bip39.b2a_words(seed).split(' ')[-1] msg += "If you stop now, the 24th word of the XOR-combined seed phrase\nwill be:\n\n" msg += "24: %s\n\n" % chk_word if all((not x) for x in seed): # zero seeds are never right. msg += "ZERO WARNING\nProvided seed works out to all zeros "\ "right now. You may have doubled a part or made some other mistake.\n\n" msg += "Press (1) to enter next list of words, or (2) if done with all words." ch = await ux_show_story(msg, strict_escape=True, escape='12x', sensitive=True) if ch == 'x': # give up import_xor_parts.clear() # concern: we are contaminated w/ secrets return None elif ch == '1': # do another list of words nxt = XORWordNestMenu(num_words=24) the_ux.push(nxt) elif ch == '2': # done; import on temp basis, or be the main secret from pincodes import pa enc = stash.SecretStash.encode(seed_phrase=seed) if pa.is_secret_blank(): # save it since they have no other secret set_seed_value(encoded=enc) # update menu contents now that wallet defined goto_top_menu() else: pa.tmp_secret(enc) await ux_show_story( "New master key in effect until next power down.") return None
def decode(secret, _bip39pw=''): # expecting 72-bytes of secret payload; decode contents into objects # returns: # type, secrets bytes, HDNode(root) # marker = secret[0] hd = ngu.hdnode.HDNode() if marker == 0x01: # xprv => BIP-32 private key values ch, pk = secret[1:33], secret[33:65] assert not _bip39pw hd.from_chaincode_privkey(ch, pk) return 'xprv', ch + pk, hd 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 = bip39.master_secret(bip39.b2a_words(seed_bits), _bip39pw) hd.from_master(ms) return 'words', seed_bits, hd else: # variable-length master secret for BIP-32 vlen = secret[0] assert 16 <= vlen <= 64 assert not _bip39pw ms = secret[1:1 + vlen] hd = hd.from_master(ms) return 'master', ms, hd
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False): # pick a password: like bip39 but no checksum word # b = bytearray(32) while 1: ckcc.rng_bytes(b) words = bip39.b2a_words(b).split(' ')[0:num_pw_words] ch = await seed.show_words( words, prompt="Record this (%d word) backup file password:\n", escape='6') if ch == '6' and not write_sflash: # Secret feature: plaintext mode # - only safe for people living in faraday cages inside locked vaults. if await ux_confirm( "The file will **NOT** be encrypted and " "anyone who finds the file will get all of your money for free!" ): words = [] fname_pattern = 'backup.txt' break continue if ch == 'x': return break if words: # quiz them, but be nice and do a shorter test. ch = await seed.word_quiz(words, limited=(num_pw_words // 3)) if ch == 'x': return return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash)
def test_b2a(): for vector, expect in b39_data.vectors: ans = bip39.b2a_words(vector) assert ans == expect, "(got) %r != (expected) %r " % (ans, expect)
async def xor_split_start(*a): ch = await ux_show_story('''\ Seed XOR Split This feature splits your BIP-39 seed phrase into multiple parts. \ Each part is 24 words and looks and functions as a normal BIP-39 wallet. We recommend spliting into just two parts, but permit up to four. If ANY ONE of the parts is lost, then ALL FUNDS are lost and the original \ seed phrase cannot be reconstructed. Finding a single part does not help an attacker construct the original seed. Press 2, 3 or 4 to select number of parts to split into. ''', strict_escape=True, escape='234x') if ch == 'x': return num_parts = int(ch) ch = await ux_show_story('''\ Split Into {n} Parts On the following screen you will be shown {n} lists of 24-words. \ The new words, when reconstructed, will re-create the seed already \ in use on this Coldcard. The new parts are generated deterministically from your seed, so if you \ repeat this process later, the same {t} words will be shown. If you would prefer a random split using the TRNG, press (2). \ Otherwise, press OK to continue.'''.format(n=num_parts, t=num_parts * 24), escape='2') use_rng = (ch == '2') if ch == 'x': return await ux_dramatic_pause('Generating...', 2) raw_secret = bytes(32) try: with stash.SensitiveValues() as sv: words = None if sv.mode == 'words': words = bip39.b2a_words(sv.raw).split(' ') if not words or len(words) != 24: await ux_show_story("Need 24-seed words for this feature.") return # checksum of target result is useful. chk_word = words[-1] del words # going to need the secret raw_secret = bytearray(sv.raw) assert len(raw_secret) == 32 parts = [] for i in range(num_parts - 1): if use_rng: here = random.bytes(32) assert len(set(here)) > 4 # TRNG failure? mask = ngu.hash.sha256d(here) else: mask = ngu.hash.sha256d(b'Batshitoshi ' + raw_secret + b'%d of %d parts' % (i, num_parts)) parts.append(mask) parts.append(xor32(raw_secret, *parts)) assert xor32(*parts) == raw_secret # selftest finally: stash.blank_object(raw_secret) word_parts = [bip39.b2a_words(p).split(' ') for p in parts] while 1: ch = await show_n_parts(word_parts, chk_word) if ch == 'x': if not use_rng: return if await ux_confirm("Stop and forget those words?"): return continue for ws, part in enumerate(word_parts): ch = await word_quiz(part, title='Word %s%%d is?' % chr(65 + ws)) if ch == 'x': break else: break await ux_show_story('''\ Quiz Passed!\n You have confirmed the details of the new split.''')
def drv_entro_step2(_1, picked, _2): from glob import dis from files import CardSlot, CardMissingError the_ux.pop() index = await ux_enter_number("Index Number?", 9999) if picked in (0,1,2): # BIP-39 seed phrases (we only support English) num_words = (12, 18, 24)[picked] width = (16, 24, 32)[picked] # of bytes path = "m/83696968'/39'/0'/{num_words}'/{index}'".format(num_words=num_words, index=index) s_mode = 'words' elif picked == 3: # HDSeed for Bitcoin Core: but really a WIF of a private key, can be used anywhere s_mode = 'wif' path = "m/83696968'/2'/{index}'".format(index=index) width = 32 elif picked == 4: # New XPRV path = "m/83696968'/32'/{index}'".format(index=index) s_mode = 'xprv' width = 64 elif picked in (5, 6): width = 32 if picked == 5 else 64 path = "m/83696968'/128169'/{width}'/{index}'".format(width=width, index=index) s_mode = 'hex' else: raise ValueError(picked) dis.fullscreen("Working...") encoded = None with stash.SensitiveValues() as sv: node = sv.derive_path(path) entropy = ngu.hmac.hmac_sha512(b'bip-entropy-from-k', node.privkey()) sv.register(entropy) # truncate for this application new_secret = entropy[0:width] # only "new_secret" is interesting past here (node already blanked at this point) del node # Reveal to user! chain = chains.current_chain() if s_mode == 'words': # BIP-39 seed phrase, various lengths words = bip39.b2a_words(new_secret).split(' ') msg = 'Seed words (%d):\n' % len(words) msg += '\n'.join('%2d: %s' % (i+1, w) for i,w in enumerate(words)) encoded = stash.SecretStash.encode(seed_phrase=new_secret) elif s_mode == 'wif': # for Bitcoin Core: a 32-byte of secret exponent, base58 w/ prefix 0x80 # - always "compressed", so has suffix of 0x01 (inside base58) # - we're not checking it's on curve # - we have no way to represent this internally, since we rely on bip32 # append 0x01 to indicate it's a compressed private key pk = new_secret + b'\x01' msg = 'WIF (privkey):\n' + ngu.codecs.b58_encode(chain.b58_privkey + pk) elif s_mode == 'xprv': # Raw XPRV value. ch, pk = new_secret[0:32], new_secret[32:64] master_node = ngu.hdnode.HDNode().from_chaincode_privkey(ch, pk) encoded = stash.SecretStash.encode(xprv=master_node) msg = 'Derived XPRV:\n' + chain.serialize_private(master_node) elif s_mode == 'hex': # Random hex number for whatever purpose msg = ('Hex (%d bytes):\n' % width) + str(b2a_hex(new_secret), 'ascii') stash.blank_object(new_secret) new_secret = None # no need to print it again else: raise ValueError(s_mode) msg += '\n\nPath Used (index=%d):\n %s' % (index, path) if new_secret: msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii') #print(msg) # XXX debug prompt = '\n\nPress 1 to save to MicroSD card' if encoded is not None: prompt += ', 2 to switch to derived secret.' while 1: ch = await ux_show_story(msg+prompt, sensitive=True, escape='12') if ch == '1': # write to SD card: simple text file try: with CardSlot() as card: fname, out_fn = card.pick_filename('drv-%s-idx%d.txt' % (s_mode, index)) with open(fname, 'wt') as fp: fp.write(msg) fp.write('\n') except CardMissingError: await needs_microsd() continue except Exception as e: await ux_show_story('Failed to write!\n\n\n'+str(e)) continue await ux_show_story("Filename is:\n\n%s" % out_fn, title='Saved') else: break if new_secret is not None: stash.blank_object(new_secret) stash.blank_object(msg) if ch == '2' and (encoded is not None): from glob import dis from pincodes import pa # switch over to new secret! dis.fullscreen("Applying...") pa.tmp_secret(encoded) await ux_show_story("New master key in effect until next power down.") if encoded is not None: stash.blank_object(encoded)
def render_backup_contents(): # simple text format: # key = value # or #comments # but value is JSON rv = StringIO() def COMMENT(val=None): if val: rv.write('\n# %s\n' % val) else: rv.write('\n') def ADD(key, val): rv.write('%s = %s\n' % (key, ujson.dumps(val))) rv.write('# Coldcard backup file! DO NOT CHANGE.\n') chain = chains.current_chain() COMMENT('Private key details: ' + chain.name) with stash.SensitiveValues(bypass_pw=True) as sv: if sv.mode == 'words': ADD('mnemonic', bip39.b2a_words(sv.raw)) if sv.mode == 'master': ADD('bip32_master_key', b2a_hex(sv.raw)) ADD('chain', chain.ctype) ADD('xprv', chain.serialize_private(sv.node)) ADD('xpub', chain.serialize_public(sv.node)) # BTW: everything is really a duplicate of this value ADD('raw_secret', b2a_hex(sv.secret).rstrip(b'0')) if pa.has_duress_pin(): COMMENT('Duress Wallet (informational)') dpk = sv.duress_root() 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())) COMMENT('Firmware version (informational)') date, vers, timestamp = version.get_mpy_version()[0:3] ADD('fw_date', date) ADD('fw_version', vers) ADD('fw_timestamp', timestamp) ADD('serial', version.serial_number()) COMMENT('User preferences') # user preferences for k, v in settings.current.items(): if k[0] == '_': continue # debug stuff in simulator if k == 'xpub': continue # redundant, and wrong if bip39pw if k == 'xfp': continue # redundant, and wrong if bip39pw ADD('setting.' + k, v) if version.has_fatram: import hsm if hsm.hsm_policy_available(): ADD('hsm_policy', hsm.capture_backup()) rv.write('\n# EOF\n') return rv.getvalue()