def handle_bag_number(self, bag_num): import version, callgate from glob import dis from pincodes import pa if version.is_factory_mode and bag_num: # check state first assert settings.get('tested', False) assert pa.is_blank() assert 8 <= len(bag_num) < 32 # do the change failed = callgate.set_bag_number(bag_num) assert not failed callgate.set_rdp_level(2 if not is_devmode else 0) pa.greenlight_firmware() dis.fullscreen(bytes(bag_num).decode()) self.call_after(callgate.show_logout, 1) # always report the existing/new value val = callgate.get_bag_number() or b'' return b'asci' + val
async def append(self, xfp, bip39pw): # encrypt and save; always appends. from ux import ux_dramatic_pause from glob import dis from actions import needs_microsd while 1: dis.fullscreen('Saving...') try: with CardSlot() as card: self._calc_key(card) data = self._read(card) if self.key else [] data.append(dict(xfp=xfp, pw=bip39pw)) encrypt = ngu.aes.CTR(self.key) msg = encrypt.cipher(ujson.dumps(data)) with open(self.filename(card), 'wb') as fd: fd.write(msg) await ux_dramatic_pause("Saved.", 1) return except CardMissingError: ch = await needs_microsd() if ch == 'x': # undocumented, but needs escape route break
async def make_json_wallet(label, generator, fname_pattern='new-wallet.json'): # Record **public** values and helpful data into a JSON file from glob import dis from files import CardSlot, CardMissingError from actions import needs_microsd dis.fullscreen('Generating...') body = generator() # choose a filename try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wt') as fd: ujson.dump(body, fd) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n'+str(e)) return msg = '''%s file written:\n\n%s''' % (label, nice) await ux_show_story(msg)
def __init__(self, *args): super().__init__() from glob import dis dis.fullscreen('Wait...') # this must set self.address and do other slow setup self.setup(*args)
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 make_summary_file(fname_pattern='public.txt'): from glob import dis # record **public** values and helpful data into a text file dis.fullscreen('Generating...') # generator function: body = generate_public_contents() await write_text_file(fname_pattern, body, 'Summary')
async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.txt'): from glob import dis import ustruct xfp = xfp2str(settings.get('xfp')) dis.fullscreen('Generating...') # make the data examples = [] imp_multi = [] imp_desc = [] for a, b in generate_bitcoin_core_wallet(account_num, examples): imp_multi.append(a) imp_desc.append(b) imp_multi = ujson.dumps(imp_multi) imp_desc = ujson.dumps(imp_desc) body = '''\ # Bitcoin Core Wallet Import File https://github.com/Coldcard/firmware/blob/master/docs/bitcoin-core-usage.md ## For wallet with master key fingerprint: {xfp} Wallet operates on blockchain: {nb} ## Bitcoin Core RPC The following command can be entered after opening Window -> Console in Bitcoin Core, or using bitcoin-cli: importdescriptors '{imp_desc}' ### Bitcoin Core before v0.21.0 This command can be used on older versions, but it is not as robust and "importdescriptors" should be prefered if possible: importmulti '{imp_multi}' ## Resulting Addresses (first 3) '''.format(imp_multi=imp_multi, imp_desc=imp_desc, xfp=xfp, nb=chains.current_chain().name) body += '\n'.join('%s => %s' % t for t in examples) body += '\n' await write_text_file(fname_pattern, body, 'Bitcoin Core')
async def ux_dramatic_pause(msg, seconds): from glob import dis, hsm_active if hsm_active: return # show a full-screen msg, with a dramatic pause + progress bar n = seconds * 8 dis.fullscreen(msg) for i in range(n): dis.progress_bar_show(i / n) await sleep_ms(125) ux_clear_keys()
def wipe_most(self): # erase everything except settings: takes 5 seconds at least from nvstore import SLOTS end = SLOTS[0] from glob import dis dis.fullscreen("Cleanup...") for addr in range(0, end, self.BLOCK_SIZE): self.block_erase(addr) dis.progress_bar_show(addr / end) while self.is_busy(): pass
def __init__(self, text, subpath, addr_fmt, approved_cb=None): super().__init__() self.text = text self.subpath = subpath self.approved_cb = approved_cb from glob import dis dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: node = sv.derive_path(subpath) self.address = sv.chain.address(node, addr_fmt) dis.progress_bar_show(1)
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 glob 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 sign_message_digest(digest, subpath, prompt): # do the signature itself! from glob import dis if prompt: dis.fullscreen(prompt, percent=.25) with stash.SensitiveValues() as sv: dis.progress_bar_show(.50) node = sv.derive_path(subpath) pk = node.privkey() sv.register(pk) dis.progress_bar_show(.75) rv = ngu.secp256k1.sign(pk, digest, 0).to_bytes() dis.progress_bar_show(1) return rv
async def interact(self): from version import decode_firmware_header from sflash import SF date, version, _ = decode_firmware_header(self.hdr) msg = '''\ Install this new firmware? {version} {built} Binary checksum and signature will be further verified before any changes are made. '''.format(version=version, built=date) try: ch = await ux_show_story(msg) if ch == 'y': # Accepted: # - write final file header, so bootloader will see it # - reboot to start process from glob import dis import callgate SF.write(self.length, self.hdr) dis.fullscreen('Upgrading...', percent=1) callgate.show_logout(2) else: # they don't want to! self.refused = True SF.block_erase(0) # just in case, but not required await ux_dramatic_pause("Refused.", 2) except BaseException as exc: self.failed = "Exception" sys.print_exception(exc) finally: UserAuthorizedAction.cleanup() # because no results to store self.pop_menu()
def wipe_microsd_card(): # Erase and re-format SD card. Not secure erase, because that is too slow. import ckcc, pyb from glob import dis try: os.umount('/sd') except: pass sd = pyb.SDCard() assert sd if not sd.present(): return # power cycle so card details (like size) are re-read from current card sd.power(0) sd.power(1) dis.fullscreen('Part Erase...') cutoff = 1024 # arbitrary blk = bytearray(512) for bnum in range(cutoff): ckcc.rng_bytes(blk) sd.writeblocks(bnum, blk) dis.progress_bar_show(bnum/cutoff) dis.fullscreen('Formatting...') # remount, with newfs option os.mount(sd, '/sd', readonly=0, mkfs=1) # done, cleanup os.umount('/sd') # important: turn off power sd = pyb.SDCard() sd.power(0)
def wipe_flash_filesystem(): # erase and re-format the flash filesystem (/flash/) import ckcc, pyb from glob import dis dis.fullscreen('Erasing...') os.umount('/flash') # from extmod/vfs.h BP_IOCTL_SEC_COUNT = (4) BP_IOCTL_SEC_SIZE = (5) # block-level erase fl = pyb.Flash() bsize = fl.ioctl(BP_IOCTL_SEC_SIZE, 0) assert bsize == 512 bcount = fl.ioctl(BP_IOCTL_SEC_COUNT, 0) blk = bytearray(bsize) ckcc.rng_bytes(blk) # trickiness: actual flash blocks are offset by 0x100 (FLASH_PART1_START_BLOCK) # so fake MBR can be inserted. Count also inflated by 2X, but not from ioctl above. for n in range(bcount): fl.writeblocks(n + 0x100, blk) ckcc.rng_bytes(blk) dis.progress_bar_show(n*2/bcount) # rebuild and mount /flash dis.fullscreen('Rebuilding...') ckcc.wipe_fs() # re-store current settings from nvstore import settings settings.save() # remount it os.mount(fl, '/flash')
def set_seed_value(words=None, encoded=None): # Save the seed words into secure element, and reboot. BIP-39 password # is not set at this point (empty string) if words: bip39.a2b_words(words) # checksum check # map words to bip39 wordlist indices data = [bip39.wordlist_en.index(w) for w in words] # map to packed binary representation. val = 0 for v in data: val <<= 11 val |= v # remove the checksum part vlen = (len(words) * 4) // 3 val >>= (len(words) // 3) # convert to bytes seed = val.to_bytes(vlen, 'big') assert len(seed) == vlen # encode it for our limited secret space nv = SecretStash.encode(seed_phrase=seed) else: nv = encoded from glob import dis dis.fullscreen('Applying...') 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 make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.txt'): from glob import dis import ustruct xfp = xfp2str(settings.get('xfp')) dis.fullscreen('Generating...') # make the data examples = [] payload = ujson.dumps( list(generate_bitcoin_core_wallet(examples, account_num))) body = '''\ # Bitcoin Core Wallet Import File https://github.com/Coldcard/firmware/blob/master/docs/bitcoin-core-usage.md ## For wallet with master key fingerprint: {xfp} Wallet operates on blockchain: {nb} ## Bitcoin Core RPC The following command can be entered after opening Window -> Console in Bitcoin Core, or using bitcoin-cli: importmulti '{payload}' ## Resulting Addresses (first 3) '''.format(payload=payload, xfp=xfp, nb=chains.current_chain().name) body += '\n'.join('%s => %s' % t for t in examples) body += '\n' await write_text_file(fname_pattern, body, 'Bitcoin Core')
async def remember_bip39_passphrase(): # Compute current xprv and switch to using that as root secret. import stash from glob import dis dis.fullscreen('Check...') with stash.SensitiveValues() as sv: 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()
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num, count=250): # write addresses into a text file on the MicroSD from glob import dis from files import CardSlot, CardMissingError from actions import needs_microsd # simple: always set number of addresses. # - takes 60 seconds to write 250 addresses on actual hardware dis.fullscreen('Saving 0-%d' % count) fname_pattern='addresses.csv' # generator function body = generate_address_csv(path, addr_fmt, ms_wallet, account_num, count) # pick filename and write try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wb') as fd: for idx, part in enumerate(body): fd.write(part.encode()) if idx % 5 == 0: dis.progress_bar_show(idx / count) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n'+str(e)) return msg = '''Address summary file written:\n\n%s''' % nice await ux_show_story(msg)
def clear_seed(): from glob import dis import utime, version dis.fullscreen('Clearing...') # clear settings associated with this key, since it will be no more settings.blank() # save a blank secret (all zeros is a special case, detected by bootloader) nv = bytes(AE_SECRET_LEN) pa.change(new_secret=nv) if version.has_608: # wipe the long secret too nv = bytes(AE_LONG_SECRET_LEN) pa.ls_change(nv) dis.fullscreen('Reboot...') utime.sleep(1) # security: need to reboot to really be sure to clear the secrets from main memory. from machine import reset reset()
async def doit(self, *a, have_key=None): # make the wallet. from glob import dis try: import ngu from chains import current_chain from serializations import hash160 from stash import blank_object if not have_key: # get some random bytes await ux_dramatic_pause("Picking key...", 2) pair = ngu.secp256k1.keypair() else: # caller must range check this already: 0 < privkey < order # - actually libsecp256k1 will check it again anyway pair = ngu.secp256k1.keypair(have_key) # pull out binary versions (serialized) as we need privkey = pair.privkey() pubkey = pair.pubkey().to_bytes(False) # always compressed style dis.fullscreen("Rendering...") # make payment address digest = hash160(pubkey) ch = current_chain() if self.is_segwit: addr = ngu.codecs.segwit_encode(ch.bech32_hrp, 0, digest) else: addr = ngu.codecs.b58_encode(ch.b58_addr + digest) wif = ngu.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))
async def restore_from_dict(vals): # Restore from a dict of values. Already JSON decoded. # Reboot on success, return string on failure from glob import dis #print("Restoring from: %r" % vals) # step1: the private key # - prefer raw_secret over other values # - TODO: fail back to other values try: chain = chains.get_chain(vals.get('chain', 'BTC')) assert 'raw_secret' in vals raw = bytearray(AE_SECRET_LEN) rs = vals.pop('raw_secret') if len(rs) % 2: rs += '0' x = a2b_hex(rs) raw[0:len(x)] = x # check we can decode this right (might be different firmare) opmode, bits, node = stash.SecretStash.decode(raw) assert node # verify against xprv value (if we have it) if 'xprv' in vals: check_xprv = chain.serialize_private(node) assert check_xprv == vals['xprv'], 'xprv mismatch' except Exception as e: return ('Unable to decode raw_secret and ' 'restore the seed value!\n\n\n' + str(e)) ls = None if ('long_secret' in vals) and version.has_608: try: ls = a2b_hex(vals.pop('long_secret')) except Exception as exc: sys.print_exception(exc) # but keep going. dis.fullscreen("Saving...") dis.progress_bar_show(.25) # clear (in-memory) settings and change also nvram key # - also captures xfp, xpub at this point pa.change(new_secret=raw) # force the right chain pa.new_main_secret(raw, chain) # updates xfp/xpub # NOTE: don't fail after this point... they can muddle thru w/ just right seed if ls is not None: try: pa.ls_change(ls) except Exception as exc: sys.print_exception(exc) # but keep going # restore settings from backup file for idx, k in enumerate(vals): dis.progress_bar_show(idx / len(vals)) if not k.startswith('setting.'): continue if k == 'xfp' or k == 'xpub': continue settings.set(k[8:], vals[k]) # write out settings.save() if version.has_fatram and ('hsm_policy' in vals): import hsm hsm.restore_backup(vals['hsm_policy']) await ux_show_story( 'Everything has been successfully restored. ' 'We must now reboot to install the ' 'updated settings and seed.', title='Success!') from machine import reset reset()
async def handle_upload(self, offset, total_size, data): from sflash import SF from glob import dis, hsm_active from utils import check_firmware_hdr from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC # maintain a running SHA256 over what's received if offset == 0: self.file_checksum = sha256() self.is_fw_upgrade = False assert offset % 256 == 0, 'alignment' assert offset + len(data) <= total_size <= MAX_UPLOAD_LEN, 'long' if hsm_active: # additional restrictions in HSM mode assert offset + len(data) <= total_size <= MAX_TXN_LEN, 'psbt' if offset == 0: assert data[0:5] == b'psbt\xff', 'psbt' for pos in range(offset, offset + len(data), 256): if pos % 4096 == 0: # erase here dis.fullscreen("Receiving...", offset / total_size) SF.sector_erase(pos) # expect 10-22 ms delay here await sleep_ms(12) while SF.is_busy(): await sleep_ms(2) # write up to 256 bytes here = data[pos - offset:pos - offset + 256] self.file_checksum.update(here) # Very special case for firmware upgrades: intercept and modify # header contents on the fly, and also fail faster if wouldn't work # on this specific hardware. # - workaround: ckcc-protocol upgrade process understates the file # length and appends hdr, but that's kinda a bug, so support both is_trailer = (pos == (total_size - FW_HEADER_SIZE) or pos == total_size) if pos == (FW_HEADER_OFFSET & ~255): hdr = memoryview(here)[-128:] magic, = unpack_from('<I', hdr[0:4]) if magic == FW_HEADER_MAGIC: self.is_fw_upgrade = bytes(hdr) prob = check_firmware_hdr(hdr, total_size) if prob: raise ValueError(prob) if is_trailer and self.is_fw_upgrade: # expect the trailer to exactly match the original one assert len(here) == 128 # == FW_HEADER_SIZE hdr = memoryview(here)[-128:] assert hdr == self.is_fw_upgrade # indicates hacking # but don't write it, instead offer user a chance to abort from auth import authorize_upgrade authorize_upgrade(self.is_fw_upgrade, pos) # pretend we wrote it, so ckcc-protocol or whatever gives normal feedback return offset SF.write(pos, here) # full page write: 0.6 to 3ms while SF.is_busy(): await sleep_ms(1) if offset + len(data) >= total_size and not hsm_active: # probably done dis.progress_bar_show(1.0) ux.restore_menu() return offset
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)
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None): # Open file, read it, maybe decrypt it; return string if any error # - some errors will be shown, None return in that case # - no return if successful (due to reboot) from glob import dis from files import CardSlot, CardMissingError from actions import needs_microsd # build password password = '******'.join(words) prob = None try: with CardSlot() as card: # filename already picked, taste it and maybe consider using its data. try: fd = open(fname_or_fd, 'rb') if isinstance( fname_or_fd, str) else fname_or_fd except: return 'Unable to open backup file.\n\n' + str(fname_or_fd) try: if not words: contents = fd.read() else: try: compat7z.check_file_headers(fd) except Exception as e: return 'Unable to read backup file. Has it been touched?\n\nError: ' \ + str(e) dis.fullscreen("Decrypting...") try: zz = compat7z.Builder() fname, contents = zz.read_file( fd, password, MAX_BACKUP_FILE_SIZE, progress_fcn=dis.progress_bar_show) # simple quick sanity checks assert fname.endswith( '.txt') # was == 'ckcc-backup.txt' assert contents[0:1] == b'#' and contents[-1:] == b'\n' except Exception as e: # assume everything here is "password wrong" errors #print("pw wrong? %s" % e) return ( 'Unable to decrypt backup file. Incorrect password?' '\n\nTried:\n\n' + password) finally: fd.close() if file_cleanup: file_cleanup(fname_or_fd) except CardMissingError: await needs_microsd() return vals = {} for line in contents.decode().split('\n'): if not line: continue if line[0] == '#': continue try: k, v = line.split(' = ', 1) #print("%s = %s" % (k, v)) vals[k] = ujson.loads(v) except: print("unable to decode line: %r" % line) # but keep going! # this leads to reboot if it works, else errors shown, etc. return await restore_from_dict(vals)
def sign_psbt_file(filename): # sign a PSBT file found on a MicroSD card from files import CardSlot, CardMissingError, securely_blank_file from glob import dis from sram2 import tmp_buf from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer UserAuthorizedAction.cleanup() #print("sign: %s" % filename) # copy file into our spiflash # - can't work in-place on the card because we want to support writing out to different card # - accepts hex or base64 encoding, but binary prefered with CardSlot() as card: with open(filename, 'rb') as fd: dis.fullscreen('Reading...') # see how long it is psbt_len = fd.seek(0, 2) fd.seek(0) # determine encoding used, altho we prefer binary taste = fd.read(10) fd.seek(0) if taste[0:5] == b'psbt\xff': decoder = None output_encoder = lambda x: x elif taste[0:10] == b'70736274ff': decoder = HexStreamer() output_encoder = HexWriter psbt_len //= 2 elif taste[0:6] == b'cHNidP': decoder = Base64Streamer() output_encoder = Base64Writer psbt_len = (psbt_len * 3 // 4) + 10 total = 0 with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out: # blank flash await out.erase() while 1: n = fd.readinto(tmp_buf) if not n: break if n == len(tmp_buf): abuf = tmp_buf else: abuf = memoryview(tmp_buf)[0:n] if not decoder: out.write(abuf) total += n else: for here in decoder.more(abuf): out.write(here) total += len(here) dis.progress_bar_show(total / psbt_len) # might have been whitespace inflating initial estimate of PSBT size assert total <= psbt_len psbt_len = total async def done(psbt): orig_path, basename = filename.rsplit('/', 1) orig_path += '/' base = basename.rsplit('.', 1)[0] out2_fn = None out_fn = None txid = None from nvstore import settings import os del_after = settings.get('del', 0) while 1: # try to put back into same spot, but also do top-of-card is_comp = psbt.is_complete() if not is_comp: # keep the filename under control during multiple passes target_fname = base.replace('-part', '')+'-part.psbt' else: # add -signed to end. We won't offer to sign again. target_fname = base+'-signed.psbt' for path in [orig_path, None]: try: with CardSlot() as card: out_full, out_fn = card.pick_filename(target_fname, path) out_path = path if out_full: break except CardMissingError: prob = 'Missing card.\n\n' out_fn = None if not out_fn: # need them to insert a card prob = '' else: # attempt write-out try: with CardSlot() as card: if is_comp and del_after: # don't write signed PSBT if we'd just delete it anyway out_fn = None else: with output_encoder(open(out_full, 'wb')) as fd: # save as updated PSBT psbt.serialize(fd) if is_comp: # write out as hex too, if it's final out2_full, out2_fn = card.pick_filename( base+'-final.txn' if not del_after else 'tmp.txn', out_path) if out2_full: with HexWriter(open(out2_full, 'w+t')) as fd: # save transaction, in hex txid = psbt.finalize(fd) if del_after: # rename it now that we know the txid after_full, out2_fn = card.pick_filename( txid+'.txn', out_path, overwrite=True) os.rename(out2_full, after_full) if del_after: # this can do nothing if they swapped SDCard between steps, which is ok, # but if the original file is still there, this blows it away. # - if not yet final, the foo-part.psbt file stays try: securely_blank_file(filename) except: pass # success and done! break except OSError as exc: prob = 'Failed to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? ch = await ux_show_story(prob+"Please insert an SDCard to receive signed transaction, " "and press OK.", title="Need Card") if ch == 'x': await ux_aborted() return # done. if out_fn: msg = "Updated PSBT is:\n\n%s" % out_fn if out2_fn: msg += '\n\n' else: # del_after is probably set msg = '' if out2_fn: msg += 'Finalized transaction (ready for broadcast):\n\n%s' % out2_fn if txid and not del_after: msg += '\n\nFinal TXID:\n'+txid await ux_show_story(msg, title='PSBT Signed') UserAuthorizedAction.cleanup() UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, approved_cb=done) # kill any menu stack, and put our thing at the top abort_and_goto(UserAuthorizedAction.active_request)
def __enter__(self): if self.message: from glob import dis dis.fullscreen(self.message) return self
async def interact(self): # Prompt user w/ details and get approval from glob import dis, hsm_active # step 1: parse PSBT from sflash into in-memory objects. try: dis.fullscreen("Reading...") with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len) as fd: self.psbt = psbtObject.read_psbt(fd) except BaseException as exc: if isinstance(exc, MemoryError): msg = "Transaction is too complex" exc = None else: msg = "PSBT parse failed" return await self.failure(msg, exc) dis.fullscreen("Validating...") # Do some analysis/ validation try: await self.psbt.validate() # might do UX: accept multisig import self.psbt.consider_inputs() self.psbt.consider_keys() self.psbt.consider_outputs() except FraudulentChangeOutput as exc: print('FraudulentChangeOutput: ' + exc.args[0]) return await self.failure(exc.args[0], title='Change Fraud') except FatalPSBTIssue as exc: print('FatalPSBTIssue: ' + exc.args[0]) return await self.failure(exc.args[0]) except BaseException as exc: del self.psbt gc.collect() if isinstance(exc, MemoryError): msg = "Transaction is too complex" exc = None else: msg = "Invalid PSBT" return await self.failure(msg, exc) # step 2: figure out what we are approving, so we can get sign-off # - outputs, amounts # - fee # # notes: # - try to handle lots of outputs # - cannot calc fee as sat/byte, only as percent # - somethings are 'warnings': # - fee too big # - inputs we can't sign (no key) # try: msg = uio.StringIO() # mention warning at top wl= len(self.psbt.warnings) if wl == 1: msg.write('(1 warning below)\n\n') elif wl >= 2: msg.write('(%d warnings below)\n\n' % wl) self.output_summary_text(msg) gc.collect() fee = self.psbt.calculate_fee() if fee is not None: msg.write("\nNetwork fee:\n%s %s\n" % self.chain.render_value(fee)) # NEW: show where all the change outputs are going self.output_change_text(msg) gc.collect() if self.psbt.warnings: msg.write('\n---WARNING---\n\n') for label, m in self.psbt.warnings: msg.write('- %s: %s\n\n' % (label, m)) if self.do_visualize: # stop here and just return the text of approval message itself self.result = await self.save_visualization(msg, (self.stxn_flags & STXN_SIGNED)) del self.psbt self.done() return if not hsm_active: msg.write("\nPress OK to approve and sign transaction. X to abort.") ch = await ux_show_story(msg, title="OK TO SEND?") else: ch = await hsm_active.approve_transaction(self.psbt, self.psbt_sha, msg.getvalue()) dis.progress_bar(1) # finish the Validating... except MemoryError: # recovery? maybe. try: del self.psbt del msg except: pass # might be NameError since we don't know how far we got gc.collect() msg = "Transaction is too complex" return await self.failure(msg) if ch != 'y': # they don't want to! self.refused = True await ux_dramatic_pause("Refused.", 1) del self.psbt self.done() return # do the actual signing. try: dis.fullscreen('Wait...') gc.collect() # visible delay causes by this but also sign_it() below self.psbt.sign_it() except FraudulentChangeOutput as exc: return await self.failure(exc.args[0], title='Change Fraud') except MemoryError: msg = "Transaction is too complex" return await self.failure(msg) except BaseException as exc: return await self.failure("Signing failed late", exc) if self.approved_cb: # for micro sd case await self.approved_cb(self.psbt) self.done() return txid = None try: # re-serialize the PSBT back out with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd: await fd.erase() if self.do_finalize: txid = self.psbt.finalize(fd) else: self.psbt.serialize(fd) self.result = (fd.tell(), fd.checksum.digest()) self.done(redraw=(not txid)) except BaseException as exc: return await self.failure("PSBT output failed", exc) if self.do_finalize and txid and not hsm_active: # Show txid when we can; advisory # - maybe even as QR, hex-encoded in alnum mode tmsg = txid if version.has_fatram: tmsg += '\n\nPress 1 for QR Code of TXID.' ch = await ux_show_story(tmsg, "Final TXID", escape='1') if version.has_fatram and ch=='1': await show_qr_code(txid, True)
async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_copies=True): # Just do the writing from glob import dis from files import CardSlot, CardMissingError # Show progress: dis.fullscreen('Encrypting...' if words else 'Generating...') body = render_backup_contents().encode() gc.collect() if words: # NOTE: Takes a few seconds to do the key-streching, but little actual # time to do the encryption. pw = ' '.join(words) zz = compat7z.Builder(password=pw, progress_fcn=dis.progress_bar_show) zz.add_data(body) # pick random filename, but ending in .txt word = bip39.wordlist_en[ngu.random.uniform(2048)] num = ngu.random.uniform(1000) fname = '%s%d.txt' % (word, num) hdr, footer = zz.save(fname) filesize = len(body) + MAX_BACKUP_FILE_SIZE del body gc.collect() else: # cleartext dump zz = None filesize = len(body) + 10 if write_sflash: # for use over USB and unit testing: commit file into SPI flash from sffile import SFFile with SFFile(0, max_size=filesize, message='Saving...') as fd: await fd.erase() if zz: fd.write(hdr) fd.write(zz.body) fd.write(footer) else: fd.write(body) return fd.tell(), fd.checksum.digest() for copy in range(25): # choose a filename try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wb') as fd: if zz: fd.write(hdr) fd.write(zz.body) fd.write(footer) else: fd.write(body) except Exception as e: # includes CardMissingError import sys sys.print_exception(e) # catch any error ch = await ux_show_story( 'Failed to write! Please insert formated MicroSD card, ' 'and press OK to try again.\n\nX to cancel.\n\n\n' + str(e)) if ch == 'x': break continue if not allow_copies: return if copy == 0: while 1: msg = '''Backup file written:\n\n%s\n\n\ To view or restore the file, you must have the full password.\n\n\ Insert another SD card and press 2 to make another copy.''' % (nice) ch = await ux_show_story(msg, escape='2') if ch == 'y': return if ch == '2': break else: ch = await ux_show_story('''File (#%d) written:\n\n%s\n\n\ Press OK for another copy, or press X to stop.''' % (copy + 1, nice), escape='2') if ch == 'x': break