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 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 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 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 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)
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)
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)
# everything should be encrypted now assert count_busy() == len(SLOTS) # check we hide initial values SF.chip_erase() settings.load() settings.save() assert count_busy() == 4 # check checksum/age stuff works settings.set('wrecked', 768) settings.save() b = bytearray(4096) SF.read(settings.my_pos, b) was_age = settings.get('_age') settings.set('wrecked', 123) settings.save() assert settings.get('_age') == was_age+1 was_pos = settings.my_pos # write old data everywhere else for pos in SLOTS: if pos != was_pos: for i in range(0, 4096, 256): SF.write(pos+i, b[i:i+256]) settings.load() assert was_pos == settings.my_pos