async def handle_download(self, offset, length, file_number): # let them read from where we store the signed txn # - filenumber can be 0 or 1: uploaded txn, or result from sflash import SF # limiting memory use here, should be MAX_BLK_LEN really length = min(length, MAX_BLK_LEN) assert 0 <= file_number < 2, 'bad fnum' assert 0 <= offset <= MAX_TXN_LEN, "bad offset" assert 1 <= length, 'len' # maintain a running SHA256 over what's sent if offset == 0: self.file_checksum = sha256() pos = (MAX_TXN_LEN * file_number) + offset resp = bytearray(4 + length) resp[0:4] = b'biny' SF.read(pos, memoryview(resp)[4:]) self.file_checksum.update(memoryview(resp)[4:]) return resp
def clean_shutdown(style=0): # wipe SPI flash and shutdown (wiping main memory) import callgate from sflash import SF try: SF.wipe_most() except: pass callgate.show_logout(style)
def count_busy(): from nvstore import SLOTS from sflash import SF busy = 0 b = bytearray(4096) for pos in SLOTS: SF.read(pos, b) if len(set(b)) > 200: busy += 1 return busy
def read_into(self, b): # limitation: this will read past end of file, but not tell the caller actual = min(self.length - self.pos, len(b)) if actual <= 0: return 0 SF.read(self.start + self.pos, b) self.pos += actual return actual
def blank(self): # erase current copy of values in nvram; older ones may exist still # - use when clearing the seed value 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.clear() self.overrides.clear() self.is_dirty = 0 self.capacity = 0
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): SF.block_erase(self.start + i) if i and self.message: from glob import dis dis.progress_bar_show(i / self.max_size) # expect block erase to take up to 2 seconds while SF.is_busy(): await sleep_ms(50)
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 write(self, b): # immediate write, no buffering assert not self.readonly assert self.pos == self.length # "can only append" assert self.pos + len( b ) <= self.max_size # "past end: %r" % [self.pos, len(b), self.max_size] left = len(b) # must perform page-aligned (256) writes, but can start # anywhere in the page, and can write just one byte sofar = 0 while left: if (self.pos + sofar) % 256 != 0: # start is unaligned, do a partial write to align assert sofar == 0 #, (sofar, (self.pos+sofar)) # can only happen on first page runt = min(left, 256 - (self.pos % 256)) here = memoryview(b)[0:runt] assert len(here) == runt else: # write full pages, or final runt here = memoryview(b)[sofar:sofar + 256] assert 1 <= len(here) <= 256 self.wait_writable() SF.write(self.start + self.pos + sofar, here) left -= len(here) sofar += len(here) self.checksum.update(here) assert left >= 0 assert sofar == len(b) self.pos += sofar self.length = self.pos return sofar
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) SF.read(self.start + self.pos, rv) self.pos += ll if self.message and ll > 1: from glob 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 save(self): # render as JSON, encrypt and write it. self.current['_age'] = self.current.get('_age', 1) + 1 pos = self.find_spot(self.my_pos) aes = self.get_aes(pos).cipher with SFFile(pos, max_size=4096, pre_erased=True) as fd: chk = 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(d)) chk.update(d) del d while pad_len > 0: here = min(32, pad_len) pad = bytes(here) fd.write(aes(pad)) chk.update(pad) pad_len -= here fd.write(aes(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
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 options = [s for s in SLOTS if s != not_here] shuffle(options) buf = bytearray(16) for pos in options: SF.read(pos, buf) if set(buf) == {0xff}: # blank return 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 victem
def load(self, dis=None): # Search all slots for any we can read, decrypt that, # and pick the newest one (in unlikely case of dups) # reset self.current.clear() self.overrides.clear() self.my_pos = 0 self.is_dirty = 0 self.capacity = 0 # 4k, but last 32 bytes are a SHA (itself encrypted) global _tmp buf = bytearray(4) empty = 0 for pos in SLOTS: if dis: dis.progress_bar_show( (pos - SLOTS.start) / (SLOTS.stop - SLOTS.start)) 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(pos) chk = aes.copy().cipher(b'{"') if chk != buf[0:2]: # doesn't look like JSON meant for me continue # probably good, read it chk = sha256() aes = aes.cipher expect = None with SFFile(pos, length=4096, pre_erased=True) as fd: for i in range(4096 / 32): b = aes(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. fd = BytesIO(_tmp) d = ujson.load(fd) self.capacity = fd.seek(0, 1) / 4096 # .tell() is missing 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) shuffle(blks) for pos in blks[0:3]: for i in range(0, 4096, 256): h = ngu.random.bytes(256) SF.wait_done() SF.write(pos + i, h)
async def test_sflash(): dis.clear() dis.text(None, 18, 'Serial Flash') dis.show() from sflash import SF from ustruct import pack import ngu 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() # "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} # "not blank" rnd = ngu.hash.sha256s(pack('I', addr)) SF.write(addr, rnd) SF.read(addr, buf) assert buf == rnd # "write failed" dis.progress_bar_show(addr / msize) # check no aliasing, also right size part for addr in range(0, msize, 1024): expect = ngu.hash.sha256s(pack('I', addr)) SF.read(addr, buf) assert buf == expect # "readback failed" dis.progress_bar_show(addr / msize)
# # Unit test for shared/nvstore.py # # this will run on the simulator # run manually with: # execfile('../../testing/devtest/nvram.py') from ubinascii import hexlify as b2a_hex from ubinascii import unhexlify as a2b_hex import ustruct from sflash import SF from nvstore import SLOTS, settings # reset whatever's there SF.chip_erase() settings.load() for v in [123, 'hello', 34.56, dict(a=45)]: settings.set('abc', v) assert settings.get('abc') == v a = settings.get('_age', -1) settings.save() assert settings.get('_age') >= a+1, [settings.get('_age'), a+1] chk = dict(settings.current) settings.load() # some minor differences in values: bytes vs. strings, so just check keys assert sorted(list(chk)) == sorted(list(settings.current)), \
def wait_writable(self): # TODO: timeouts here while SF.is_busy(): pass
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
with SFFile(0, ll) as fd: numpad.inject('y') # for 'success' message await restore_complete_doit(fd, words) assert settings.get('check') == today, \ (settings.get('check'), '!=', today) assert settings.get('chain') == chain, \ (settings.get('chain'), '!=', chain) if version.has_608: assert pa.ls_fetch() == ls if had_policy: assert had_policy == hsm.hsm_policy_available() today += 3 import ux ux.restore_menu() import uasyncio uasyncio.get_event_loop().run_until_complete(test_7z()) # test recovery/reset from sflash import SF SF.chip_erase() settings.load() # EOF
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 # maintain a running SHA256 over what's received if offset == 0: self.file_checksum = 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) # 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 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