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
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)