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 find_spot(self, not_here=0): # search for a blank sector to use # - check randomly and pick first blank one (wear leveling, deniability) # - we will write and then erase old slot # - if "full", blow away a random one from main import sf options = [s for s in SLOTS if s != not_here] tcc.random.shuffle(options) buf = bytearray(16) for pos in options: sf.read(pos, buf) if set(buf) == {0xff}: # blank return sf, pos # No where to write! (probably a bug because we have lots of slots) # ... so pick a random slot and kill what it had #print("ERROR: nvram full?") victem = options[0] sf.sector_erase(victem) sf.wait_done() return sf, victem
def blank(self): # erase current copy of values in nvram; older ones may exist still # - use when clearing the seed value from main import sf if self.my_pos: sf.wait_done() sf.sector_erase(self.my_pos) self.my_pos = 0 # act blank too, just in case. self.current = {} self.is_dirty = 0
def save(self): # render as JSON, encrypt and write it. self.current['_age'] = self.current.get('_age', 1) + 1 sf, pos = self.find_spot(self.my_pos) aes = self.get_aes(tcc.AES.Encrypt, pos) with SFFile(pos, max_size=4096, pre_erased=True) as fd: chk = tcc.sha256() # first the json data d = ujson.dumps(self.current) # pad w/ zeros dat_len = len(d) pad_len = (4096 - 32) - dat_len assert pad_len >= 0, 'too big' self.capacity = dat_len / 4096 fd.write(aes.update(d)) chk.update(d) del d while pad_len > 0: here = min(32, pad_len) pad = bytes(here) fd.write(aes.update(pad)) chk.update(pad) pad_len -= here fd.write(aes.update(chk.digest())) assert fd.tell() == 4096 # erase old copy of data if self.my_pos and self.my_pos != pos: sf.wait_done() sf.sector_erase(self.my_pos) sf.wait_done() self.my_pos = pos self.is_dirty = 0
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
def load(self): # Search all slots for any we can read, decrypt that, # and pick the newest one (in unlikely case of dups) from main import sf # reset self.current = {} self.my_pos = 0 self.is_dirty = 0 # 4k, but last 32 bytes are a SHA (itself encrypted) global _tmp buf = bytearray(4) empty = 0 for pos in SLOTS: gc.collect() sf.read(pos, buf) if buf[0] == buf[1] == buf[2] == buf[3] == 0xff: # erased (probably) empty += 1 continue # check if first 2 bytes makes sense for JSON aes = self.get_aes(tcc.AES.Encrypt, pos) chk = aes.update(b'{"') if chk != buf[0:2]: # doesn't look like JSON meant for me continue # probably good, read it aes = self.get_aes(tcc.AES.Encrypt, pos) chk = tcc.sha256() expect = None with SFFile(pos, length=4096, pre_erased=True) as fd: for i in range(4096 / 32): b = aes.update(fd.read(32)) if i != 127: _tmp[i * 32:(i * 32) + 32] = b chk.update(b) else: expect = b try: # verify checksum in last 32 bytes assert expect == chk.digest() # loads() can't work from a byte array, and converting to # bytes here would copy it; better to use file emulation. d = ujson.load(BytesIO(_tmp)) except: # One in 65k or so chance to come here w/ garbage decoded, so # not an error. continue got_age = d.get('_age', 0) if got_age > self.current.get('_age', -1): # likely winner self.current = d self.my_pos = pos #print("NV: data @ %d w/ age=%d" % (pos, got_age)) else: # stale data seen; clean it up. assert self.current['_age'] > 0 print("NV: cleanup @ %d" % pos) sf.sector_erase(pos) sf.wait_done() # 4k is a large object, sigh, for us right now. cleanup gc.collect() # done, if we found something if self.my_pos: return # nothing found. self.my_pos = 0 self.current = self.default_values() if empty == len(SLOTS): # Whole thing is blank. Bad for plausible deniability. Write 3 slots # with garbage. They will be wasted space until it fills. blks = list(SLOTS) tcc.random.shuffle(blks) for pos in blks[0:3]: for i in range(0, 4096, 256): h = tcc.random.bytes(256) sf.wait_done() sf.write(pos + i, h)
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()
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()