async def make_summary_file(fname_pattern='public.txt'): # record **public** values and helpful data into a text file from main import dis, pa, settings from files import CardSlot, CardMissingError from actions import needs_microsd dis.fullscreen('Generating...') # generator function: body = generate_public_contents() total_parts = 72 # need not be precise # choose a filename 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): dis.progress_bar_show(idx / total_parts) fd.write(part.encode()) except CardMissingError: await needs_microsd() return except Exception as e: await ux_show_story('Failed to write!\n\n\n'+str(e)) return msg = '''Summary file written:\n\n%s''' % nice await ux_show_story(msg)
def wipe_microsd_card(): import ckcc, pyb from main 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('Formating...') # remount, with newfs option os.mount(sd, '/sd', readonly=0, mkfs=1)
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 write_text_file(fname_pattern, body, title, total_parts=72): # - total_parts does need not be precise from main import dis, pa, settings from files import CardSlot, CardMissingError from actions import needs_microsd # choose a filename 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): dis.progress_bar_show(idx / total_parts) fd.write(part.encode()) 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''' % (title, nice) await ux_show_story(msg)
async def handle_upload(self, offset, total_size, data): from main import dis, sf, hsm_active from utils import check_firmware_hdr from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE # maintain a running SHA256 over what's received if offset == 0: self.file_checksum = tcc.sha256() 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) while sf.is_busy(): await sleep_ms(10) # 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 if (pos == (FW_HEADER_OFFSET & ~255) or pos == (total_size - FW_HEADER_SIZE) or pos == total_size): prob = check_firmware_hdr(memoryview(here)[-128:], None, bad_magic_ok=True) if prob: raise ValueError(prob) 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 wipe_flash_filesystem(): # erase and re-format the flash filesystem (/flash/) import ckcc, pyb from main import dis, settings 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 settings settings.save()
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)
async def ux_dramatic_pause(msg, seconds): from main import dis # 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 __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) dis.progress_bar_show(1)
def wipe_most(self): # erase everything except settings: takes 5 seconds at least from nvstore import SLOTS end = SLOTS[0] from main 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
async def erase(self): # must be used by caller before writing any bytes assert not self.readonly assert self.length == 0 # 'already wrote?' for i in range(0, self.max_size, blksize): self.sf.block_erase(self.start + i) if i and self.message: from main import dis dis.progress_bar_show(i/self.max_size) # expect block erase to take up to 2 seconds while self.sf.is_busy(): await sleep_ms(50)
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 main import settings, 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 test_sflash(): dis.clear() dis.text(None, 18, 'Serial Flash') dis.show() #if ckcc.is_simulator(): return from main import sf from ustruct import pack import tcc msize = 1024 * 1024 sf.chip_erase() for phase in [0, 1]: steps = 7 * 4 for i in range(steps): dis.progress_bar(i / steps) dis.show() await sleep_ms(250) if not sf.is_busy(): break assert not sf.is_busy(), "sflash erase didn't finish" # leave chip blank if phase == 1: break buf = bytearray(32) for addr in range(0, msize, 1024): sf.read(addr, buf) assert set(buf) == {255}, "sflash not blank:" + repr(buf) rnd = tcc.sha256(pack('I', addr)).digest() sf.write(addr, rnd) sf.read(addr, buf) assert buf == rnd, "sflash write failed" dis.progress_bar_show(addr / msize) # check no aliasing, also right size part for addr in range(0, msize, 1024): expect = tcc.sha256(pack('I', addr)).digest() sf.read(addr, buf) assert buf == expect, "sflash readback failed" dis.progress_bar_show(addr / msize)
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()
async def handle_upload(self, offset, total_size, data): from main import dis, sf # maintain a running SHA256 over what's received if offset == 0: self.file_checksum = tcc.sha256() assert offset % 256 == 0, 'alignment' assert offset + len(data) <= total_size <= MAX_UPLOAD_LEN, 'long' rb = bytearray(256) for pos in range(offset, offset + len(data), 256): if pos % 4096 == 0: # erase here sf.sector_erase(pos) dis.fullscreen("Receiving...") dis.progress_bar_show(offset / total_size) while sf.is_busy(): await sleep_ms(10) # write up to 256 bytes here = data[pos - offset:pos - offset + 256] sf.write(pos, here) # full page write: 0.6 to 3ms while sf.is_busy(): await sleep_ms(1) # use actual read back for verify sf.read(pos, rb) self.file_checksum.update(rb[0:len(here)]) if offset + len(data) >= total_size: # probably done dis.progress_bar_show(1.0) ux.restore_menu() return offset
async def make_address_summary_file(path, addr_fmt, fname_pattern='addresses.txt'): # write addresses into a text file on the MicroSD from main 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 count = 250 dis.fullscreen('Saving 0-%d' % count) # generator function body = generate_address_csv(path, addr_fmt, 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 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 read(self, ll=None): if ll == 0: return b'' elif ll is None: ll = self.length - self.pos else: ll = min(ll, self.length - self.pos) if ll <= 0: # at EOF return b'' rv = bytearray(ll) self.sf.read(self.start + self.pos, rv) self.pos += ll if self.message and ll > 1: from main import dis dis.progress_bar_show(self.pos / self.length) # altho tempting to return a bytearray (which we already have) many # callers expect return to be bytes and have those methods, like "find" return bytes(rv)
def sign_message_digest(digest, subpath, prompt): # do the signature itself! from main 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.private_key() sv.register(pk) dis.progress_bar_show(.75) rv = tcc.secp256k1.sign(pk, digest) dis.progress_bar_show(1) return rv
def sign_psbt_file(filename): # sign a PSBT file found on a MicroSD card from files import CardSlot, CardMissingError, securely_blank_file from main 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 main 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)
async def restore_from_dict(vals): # Restore from a dict of values. Already JSON decoded. # Reboot on success, return string on failure from main import pa, dis, settings from pincodes import AE_SECRET_LEN #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/or seed.', title='Success!') from machine import reset reset()
async def test_microsd(): if ckcc.is_simulator(): return from main import numpad numpad.stop() try: import pyb sd = pyb.SDCard() sd.power(0) # test presence switch for ph in range(7): want = not sd.present() dis.clear() dis.text(None, 10, 'MicroSD Card:') dis.text(None, 34, 'Remove' if sd.present() else 'Insert', font=FontLarge) dis.show() while 1: if want == sd.present(): break await sleep_ms(100) if ux_poll_once(): raise RuntimeError("MicroSD test aborted") if ph >= 2 and sd.present(): # debounce await sleep_ms(100) if sd.present(): break if ux_poll_once(): raise RuntimeError("MicroSD test aborted") dis.clear() dis.text(None, 10, 'MicroSD Card:') dis.text(None, 34, 'Testing', font=FontLarge) dis.show() # card inserted assert sd.present(), "SD not present?" # power up? sd.power(1) await sleep_ms(100) try: blks, bsize, ctype = sd.info() assert bsize == 512, "wrong block size" except: assert 0, "unable to get card info" # just read it a bit, writing would prove little buf = bytearray(512) msize = 1024 * 1024 for addr in range(0, msize, 1024): sd.readblocks(addr, buf) dis.progress_bar_show(addr / msize) if addr == 0: assert buf[-2:] == b'\x55\xaa', "Bad read" finally: # CRTICAL: power it back down sd.power(0) numpad.start()
async def test_microsd(): if ckcc.is_simulator(): return async def wait_til_state(want): dis.clear() dis.text(None, 10, 'MicroSD Card:') dis.text(None, 34, 'Remove' if sd.present() else 'Insert', font=FontLarge) dis.show() while 1: if want == sd.present(): return await sleep_ms(100) if ux_poll_once(): raise RuntimeError("MicroSD test aborted") try: import pyb sd = pyb.SDCard() sd.power(0) # test presence switch for ph in range(7): await wait_til_state(not sd.present()) if ph >= 2 and sd.present(): # debounce await sleep_ms(100) if sd.present(): break if ux_poll_once(): raise RuntimeError("MicroSD test aborted") dis.clear() dis.text(None, 10, 'MicroSD Card:') dis.text(None, 34, 'Testing', font=FontLarge) dis.show() # card inserted assert sd.present() #, "SD not present?" # power up? sd.power(1) await sleep_ms(100) try: blks, bsize, *unused = sd.info() assert bsize == 512 except: assert 0 # , "card info" # just read it a bit, writing would prove little buf = bytearray(512) msize = 256*1024 for addr in range(0, msize, 1024): sd.readblocks(addr, buf) dis.progress_bar_show(addr/msize) if addr == 0: assert buf[-2:] == b'\x55\xaa' # "Bad read" # force removal, so cards don't get stuck in finished units await wait_til_state(False) finally: # CRTICAL: power it back down sd.power(0)
def sign_psbt_file(filename): # sign a PSBT file found on a MicroSD card from files import CardSlot, CardMissingError from main import dis from sram2 import tmp_buf global active_request 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 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) 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): out.write(tmp_buf) else: out.write(memoryview(tmp_buf)[0:n]) total += n dis.progress_bar_show(total / psbt_len) assert total == psbt_len, repr([total, psbt_len]) async def done(psbt): orig_path, basename = filename.rsplit('/', 1) orig_path += '/' base = basename.rsplit('.', 1)[0] out2_fn = None out_fn = None 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: with 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', out_path) if out2_full: with HexWriter(open(out2_full, 'wt')) as fd: # save transaction, in hex psbt.finalize(fd) # 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. msg = "Updated PSBT is:\n\n%s" % out_fn if out2_fn: msg += '\n\nFinalized transaction (ready for broadcast):\n\n%s' % out2_fn await ux_show_story(msg, title='PSBT Signed') UserAuthorizedAction.cleanup() active_request = ApproveTransaction(psbt_len, approved_cb=done) # kill any menu stack, and put our thing at the top abort_and_goto(active_request)
def __exit__(self, exc_type, exc_val, exc_tb): if self.message: from main import dis dis.progress_bar_show(1) return False
async def microsd_upgrade(*a): # Upgrade vis MicroSD card # - search for a particular file # - verify it lightly # - erase serial flash # - copy it over (slow) # - reboot into bootloader, which finishes install fn = await file_picker('Pick firmware image to use (.DFU)', suffix='.dfu', min_size=0x7800) if not fn: return failed = None with CardSlot() as card: with open(fn, 'rb') as fp: from main import sf, dis from files import dfu_parse from utils import check_firmware_hdr offset, size = dfu_parse(fp) # we also put a copy of special signed heaer at the end of the flash from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE # read just the signature header hdr = bytearray(FW_HEADER_SIZE) fp.seek(offset + FW_HEADER_OFFSET) rv = fp.readinto(hdr) assert rv == FW_HEADER_SIZE # check header values failed = check_firmware_hdr(hdr, size) if not failed: patched = 0 # copy binary into serial flash fp.seek(offset) buf = bytearray(256) # must be flash page size pos = 0 dis.fullscreen("Loading...") while pos <= size + FW_HEADER_SIZE: dis.progress_bar_show(pos/size) if pos == size: # save an extra copy of the header (also means we got done) buf = hdr patched += 1 else: here = fp.readinto(buf) if not here: break if pos == (FW_HEADER_OFFSET & ~255): # update w/ our patched version of hdr buf[128:FW_HEADER_SIZE+128] = hdr patched += 1 if pos % 4096 == 0: # erase here sf.sector_erase(pos) while sf.is_busy(): await sleep_ms(10) sf.write(pos, buf) # full page write: 0.6 to 3ms while sf.is_busy(): await sleep_ms(1) pos += here assert patched == 2 if failed: await ux_show_story(failed, title='Sorry!') return # continue process... import machine machine.reset()
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...') # 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: with stash.SensitiveValues() as sv: 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) 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!!! with stash.SensitiveValues() as sv: skp = path_to_str(inp.subpaths[which_key]) node = sv.derive_path(skp) pk = node.private_key() sv.register(pk) # 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) #print("result %s" % b2a_hex(result).decode('ascii')) # convert to DER format assert len(result) == 65 r = result[1:33] s = result[33:65] assert len(r) == 32 assert len(s) == 32 inp.added_sig = (which_key, ser_sig_der(r, s, inp.sighash)) success.add(in_idx) 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 microsd_upgrade(*a): # Upgrade vis MicroSD card # - search for a particular file # - verify it lightly # - erase serial flash # - copy it over (slow) # - reboot into bootloader, which finishes install fn = await file_picker('Pick firmware image to use (.DFU)', suffix='.dfu', min_size=0x7800) if not fn: return failed = None with CardSlot() as card: with open(fn, 'rb') as fp: from main import sf, dis from files import dfu_parse from ustruct import unpack_from offset, size = dfu_parse(fp) # get a copy of special signed heaer at the end of the flash as well from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC, FWH_PY_FORMAT hdr = bytearray(FW_HEADER_SIZE) fp.seek(offset + FW_HEADER_OFFSET) # basic checks only: for confused customers, not attackers. try: rv = fp.readinto(hdr) assert rv == FW_HEADER_SIZE magic_value, timestamp, version_string, pk, fw_size = \ unpack_from(FWH_PY_FORMAT, hdr)[0:5] assert magic_value == FW_HEADER_MAGIC assert fw_size == size # TODO: maybe show the version string? Warn them that downgrade doesn't work? except Exception as exc: failed = "Sorry! That does not look like a firmware " \ "file we would want to use.\n\n\n%s" % exc if not failed: # copy binary into serial flash fp.seek(offset) buf = bytearray(256) # must be flash page size pos = 0 dis.fullscreen("Loading...") while pos <= size + FW_HEADER_SIZE: dis.progress_bar_show(pos / size) if pos == size: # save an extra copy of the header (also means we got done) buf = hdr else: here = fp.readinto(buf) if not here: break if pos % 4096 == 0: # erase here sf.sector_erase(pos) while sf.is_busy(): await sleep_ms(10) sf.write(pos, buf) # full page write: 0.6 to 3ms while sf.is_busy(): await sleep_ms(1) pos += here if failed: await ux_show_story(failed, title='Corrupt') return # continue process... import machine machine.reset()