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(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 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
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(): 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 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)
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)
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: assert sv.mode == 'words' # protected by menu item predicate 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 await ux_show_story(msg, sensitive=True) stash.blank_object(msg)
def make_msg(start): msg = "Press 1 to save to MicroSD.\n\n" msg += "Addresses %d..%d:\n\n" % (start, start + n - 1) 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) msg += "%s =>\n%s\n\n" % (subpath, chain.address( node, addr_fmt)) 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
def set_key(self, new_secret=None): # System settings (not secrets) are stored in SPI Flash, encrypted with this # key that is derived from main wallet secret. Call this method when the secret # is first loaded, or changes for some reason. from pincodes import pa from stash import blank_object key = None mine = False if not new_secret: if not pa.is_successful() or pa.is_secret_blank(): # simple fixed key allows us to store a few things when logged out key = b'\0' * 32 else: # read secret and use it. new_secret = pa.fetch() mine = True if new_secret: # hash up the secret... without decoding it or similar assert len(new_secret) >= 32 s = sha256(new_secret) for round in range(5): s.update('pad') s = sha256(s.digest()) key = s.digest() if mine: blank_object(new_secret) # for restore from backup case, or when changing (created) the seed self.nvram_key = key
def save_storage_locker(self): # save the "long secret" ... probably only happens first time HSM policy # is activated, because we don't store that original value except here # and in SE. from main import pa # add length half-word to start, and pad to max size tmp = bytearray(AE_LONG_SECRET_LEN) val = self.set_sl.encode('utf8') ustruct.pack_into('H', tmp, 0, len(val)) tmp[2:2 + len(self.set_sl)] = val # write it pa.ls_change(tmp) # memory cleanup blank_object(tmp) blank_object(val) blank_object(self.set_sl) self.set_sl = None
def drv_entro_step2(_1, picked, _2): from main import dis from files import CardSlot, CardMissingError the_ux.pop() index = await ux_enter_number("Index Number?", 9999) if picked in (0,1,2): # BIP39 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 = hmac.HMAC(b'bip-entropy-from-k', node.private_key(), tcc.sha512).digest() 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': # BIP39 seed phrase, various lengths words = tcc.bip39.from_data(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' + tcc.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 = tcc.bip32.HDNode(chain_code=ch, private_key=pk, child_num=0, depth=0, fingerprint=0) 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 main import pa, settings, dis from pincodes import AE_SECRET_LEN # switch over to new secret! dis.fullscreen("Applying...") stash.bip39_passphrase = '' tmp_secret = encoded + bytes(AE_SECRET_LEN - len(encoded)) # monkey-patch to block SE access, and just use new secret pa.fetch = lambda *a, **k: bytearray(tmp_secret) pa.change = lambda *a, **k: None pa.ls_fetch = pa.change pa.ls_change = pa.change # copies system settings to new encrypted-key value, calculates # XFP, XPUB and saves into that, and starts using them. pa.new_main_secret(pa.fetch()) await ux_show_story("New master key in effect until next power down.") if encoded is not None: stash.blank_object(encoded)
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 sign_it(self): # txn is approved. sign all inputs we can sign. add signatures # - hash the txn first # - sign all inputs we have the key for # - inputs might be p2sh, p2pkh and/or segwit style # - save partial inputs somewhere (append?) # - update our state with new partial sigs from main import dis dis.fullscreen('Signing...') with stash.SensitiveValues() as sv: # Double check the change outputs are right. This is slow, but critical because # it detects bad actors, not bugs or mistakes. change_paths = [(n, o.is_change) for n, o in enumerate(self.outputs) if o and o.is_change] if change_paths: for out_idx, (pubkey, subpath) in change_paths: skp = path_to_str(subpath) node = sv.derive_path(skp) # check the pubkey of this BIP32 node pu = node.public_key() if pu != pubkey: raise FraudulentChangeOutput( "Deception regarding change output #%d. " "BIP32 path doesn't match actual address." % out_idx) # Sign individual inputs sigs = 0 success = set() for in_idx, txi in self.input_iter(): dis.progress_bar_show(in_idx / self.num_inputs) inp = self.inputs[in_idx] if not inp.has_utxo(): # maybe they didn't provide the UTXO continue if not inp.required_key: # we don't know the key for this input continue if inp.already_signed and not inp.is_multisig: # for multisig, it's possible I need to add another sig # but in other cases, no more signatures are possible continue which_key = inp.required_key assert not inp.added_sig, "already done??" assert which_key in inp.subpaths, 'unk key' if inp.subpaths[which_key][0] != self.my_xfp: # we don't have the key for this subkey continue txi.scriptSig = inp.scriptSig assert txi.scriptSig, "no scriptsig?" if not inp.is_segwit: # Hash by serializing/blanking various subparts of the transaction digest = self.make_txn_sighash(in_idx, txi, inp.sighash) else: # Hash the inputs and such in totally new ways, based on BIP-143 digest = self.make_txn_segwit_sighash( in_idx, txi, inp.amount, inp.scriptCode, inp.sighash) # Do the ACTUAL signature ... finally!!! skp = path_to_str(inp.subpaths[which_key]) node = sv.derive_path(skp, register=False) pk = node.private_key() # expensive test, but works... and important pu = node.public_key() assert pu == which_key, "Path (%s) led to wrong pubkey for input#%d" % ( skp, in_idx) #print("privkey %s" % b2a_hex(pk).decode('ascii')) #print(" pubkey %s" % b2a_hex(which_key).decode('ascii')) #print(" digest %s" % b2a_hex(digest).decode('ascii')) result = tcc.secp256k1.sign(pk, digest) # private key no longer required stash.blank_object(pk) stash.blank_object(node) del pk, node, pu, skp #print("result %s" % b2a_hex(result).decode('ascii')) # convert signature to DER format assert len(result) == 65 r = result[1:33] s = result[33:65] inp.added_sig = (which_key, ser_sig_der(r, s, inp.sighash)) success.add(in_idx) # memory cleanup del result, r, s gc.collect() if len(success) != self.num_inputs: print("Wasn't able to sign input(s): %s" % ', '.join('#' + str(i) for i in set(range(self.num_inputs)) - success)) # done. dis.progress_bar_show(1)
async def doit(self, *a, have_key=None): # make the wallet. from main import dis try: from chains import current_chain import tcc from serializations import hash160 from stash import blank_object if not have_key: # get some random bytes await ux_dramatic_pause("Picking key...", 2) privkey = tcc.secp256k1.generate_secret() else: # caller must range check this already: 0 < privkey < order privkey = have_key # calculate corresponding public key value pubkey = tcc.secp256k1.publickey(privkey, True) # always compressed style dis.fullscreen("Rendering...") # make payment address digest = hash160(pubkey) ch = current_chain() if self.is_segwit: addr = tcc.codecs.bech32_encode(ch.bech32_hrp, 0, digest) else: addr = tcc.codecs.b58_encode(ch.b58_addr + digest) wif = tcc.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01') if self.can_do_qr(): with imported('uqr') as uqr: # make the QR's now, since it's slow is_alnum = self.is_segwit qr_addr = uqr.make( addr if not is_alnum else addr.upper(), min_version=4, max_version=4, encoding=(uqr.Mode_ALPHANUMERIC if is_alnum else 0)) qr_wif = uqr.make(wif, min_version=4, max_version=4, encoding=uqr.Mode_BYTE) else: qr_addr = None qr_wif = None # Use address as filename. clearly will be unique, but perhaps a bit # awkward to work with. basename = addr dis.fullscreen("Saving...") with CardSlot() as card: fname, nice_txt = card.pick_filename( basename + ('-note.txt' if self.template_fn else '.txt')) with open(fname, 'wt') as fp: self.make_txt(fp, addr, wif, privkey, qr_addr, qr_wif) if self.template_fn: fname, nice_pdf = card.pick_filename(basename + '.pdf') with open(fname, 'wb') as fp: self.make_pdf(fp, addr, wif, qr_addr, qr_wif) else: nice_pdf = '' # Half-hearted attempt to cleanup secrets-contaminated memory # - better would be force user to reboot # - and yet, we just output the WIF to SDCard anyway blank_object(privkey) blank_object(wif) del qr_wif except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n' + str(e)) return await ux_show_story('Done! Created file(s):\n\n%s\n\n%s' % (nice_txt, nice_pdf))
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() qr = None qr_alnum = False if s_mode == 'words': # BIP-39 seed phrase, various lengths words = bip39.b2a_words(new_secret).split(' ') # encode more tightly for QR qr = ' '.join(w[0:4] for w in words) qr_alnum = True 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' qr = ngu.codecs.b58_encode(chain.b58_privkey + pk) msg = 'WIF (privkey):\n' + qr 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) qr = chain.serialize_private(master_node) msg = 'Derived XPRV:\n' + qr elif s_mode == 'hex': # Random hex number for whatever purpose qr = str(b2a_hex(new_secret), 'ascii') msg = ('Hex (%d bytes):\n' % width) + qr qr_alnum = True 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') prompt = '\n\nPress 1 to save to MicroSD card' if encoded is not None: prompt += ', 2 to switch to derived secret' if (qr is not None) and version.has_fatram: prompt += ', 3 to view as QR code.' while 1: ch = await ux_show_story(msg+prompt, sensitive=True, escape='123') 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') elif ch == '3' and version.has_fatram: from ux import show_qr_code await show_qr_code(qr, qr_alnum) continue 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)