diff --git a/services/pddb/src/backend/basis.rs b/services/pddb/src/backend/basis.rs index 29fb9ac9e..d891d7060 100644 --- a/services/pddb/src/backend/basis.rs +++ b/services/pddb/src/backend/basis.rs @@ -160,6 +160,10 @@ impl DerefMut for BasisRoot { } /// A list of open Basis that we can use to search and operate upon. Sort of the "root" data structure of the PDDB. +/// +/// Note to self: it's tempting to integrate the "hw" parameter (the pointer to the PddbOs structure). However, this +/// results in interior mutability problems. I guess we could wrap it in a Rc or RefCell or something like that; but +/// the inconvenience of passing the hw structure around doesn't seem too bad so far... pub(crate) struct BasisCache { /// the cache entries themselves cache: Vec::, @@ -224,8 +228,8 @@ impl BasisCache { index: dict_index, keys: HashMap::::new(), clean: false, - age: 0, - flags: 0, + age: 111, + flags: 222, key_count: 0, }; log::info!("adding dictionary {}", name); @@ -235,6 +239,8 @@ impl BasisCache { basis.dict_sync(hw, name)?; // sync the root basis structure as well, while we're at it... basis.basis_sync(hw); + // finally, sync the page tables. + basis.pt_sync(hw); Ok(()) } else { Err(Error::new(ErrorKind::NotFound, "Requested basis not found, or PDDB not mounted.")) @@ -387,6 +393,17 @@ impl BasisCacheEntry { } } + /// Looks for dirty entries in the page table, and flushes them to disk. + pub(crate) fn pt_sync(&mut self, hw: &mut PddbOs) { + for (&virt, phys) in self.v2p_map.iter_mut() { + if !phys.clean() { + log::info!("syncing dirty pte va: {:x?} pa: {:x?}", virt, phys); + hw.pt_patch_mapping(virt, phys.page_number()); + phys.set_clean(true); + } + } + } + /// This will sync the named Dictionary header + dirty *key descriptors*. It does not /// sync the key data itself. Note this does not mark the surrounding basis structure as clean /// when it exits, even if there are no more dirty entries within. @@ -410,6 +427,8 @@ impl BasisCacheEntry { num_keys: dict.key_count, name: dict_name, }; + log::info!("syncing dict: {:?}", dict_disk); + // log::info!("raw: {:x?}", dict_disk.deref()); // observation: all keys to be flushed to disk will be in the KeyCacheEntry. Some may be clean, // but definitely all the dirty ones are in there (if they aren't, where else would they be??) @@ -447,6 +466,7 @@ impl BasisCacheEntry { // the assumption that the KeyCacheEntry doesn't ever get to a very large N. let next_vpage = VirtAddr::new(cur_vpage.get() + VPAGE_SIZE as u64).unwrap(); for (key_name, key) in dict.keys.iter_mut() { + log::info!("merging in key {}", key_name); if !key.clean { if key.descriptor_vaddr(dict_offset) >= cur_vpage && key.descriptor_vaddr(dict_offset) < next_vpage { @@ -501,7 +521,7 @@ impl BasisCacheEntry { aad: &self.aad, }; let ciphertext = self.cipher.encrypt(&nonce, payload).expect("failed to encrypt DictKeys"); - hw.patch_data(&ciphertext, pp.page_number() * PAGE_SIZE as u32); + hw.patch_data(&[nonce.as_slice(), &ciphertext].concat(), pp.page_number() * PAGE_SIZE as u32); // 4. Check for dirty keys, if there are still some, update vpage_num to target them; otherwise // exit the loop @@ -562,7 +582,7 @@ impl BasisCacheEntry { msg: &block, } ).unwrap(); - hw.patch_data(&ciphertext, pp.page_number() * PAGE_SIZE as u32); + hw.patch_data(&[nonce.as_slice(), &ciphertext].concat(), pp.page_number() * PAGE_SIZE as u32); } } @@ -585,6 +605,7 @@ pub(crate) struct DictCacheEntry { pub(crate) flags: u32, } +#[derive(Debug)] /// On-disk representation of the dictionary header. #[repr(C, align(8))] pub(crate) struct Dictionary { diff --git a/services/pddb/src/backend/fastspace.rs b/services/pddb/src/backend/fastspace.rs index 22a20fc3a..473c6d111 100644 --- a/services/pddb/src/backend/fastspace.rs +++ b/services/pddb/src/backend/fastspace.rs @@ -153,8 +153,9 @@ impl DerefMut for SpaceUpdate { } } - +#[cfg(test)] mod tests { + use super::*; #[test] fn test_fast_space_size() { assert!(core::mem::size_of::() & (PAGE_SIZE - 1) == 0, "FastSpaceInFlash is not exactly a multiple of one page in size"); diff --git a/services/pddb/src/backend/hw.rs b/services/pddb/src/backend/hw.rs index 776c28941..fbaabf3f1 100644 --- a/services/pddb/src/backend/hw.rs +++ b/services/pddb/src/backend/hw.rs @@ -77,6 +77,8 @@ pub(crate) struct PddbOs { fscb_phys_base: PageAlignedPa, data_phys_base: PageAlignedPa, system_basis_key: Option<[u8; AES_KEYSIZE]>, + /// derived cipher for encrypting PTEs -- cache it, so we can save the time cost of constructing the cipher key schedule + cipher_ecb: Option, /// fast space cache fspace_cache: HashSet, /// memoize the location of the fscb log pages @@ -135,6 +137,7 @@ impl PddbOs { fscb_phys_base, data_phys_base: PageAlignedPa::from(fscb_phys_base.as_u32() + FSCB_PAGES as u32 * PAGE_SIZE as u32), system_basis_key: None, + cipher_ecb: None, //v2p_map: HashMap::>::new(), fspace_cache: HashSet::::new(), fspace_log_addrs: Vec::::new(), @@ -158,6 +161,7 @@ impl PddbOs { fscb_phys_base, data_phys_base: PageAlignedPa::from(fscb_phys_base.as_u32() + FSCB_PAGES as u32 * PAGE_SIZE as u32), system_basis_key: None, + cipher_ecb: None, //v2p_map: HashMap::>::new(), fspace_cache: HashSet::::new(), fspace_log_addrs: Vec::::new(), @@ -301,6 +305,20 @@ impl PddbOs { ).expect("couldn't burn fscb"); } + /// Public function that "does the right thing" for patching in a page table entry based on a virtual to + /// physical mapping. Note that the `va` is an *address* (in units of bytes), and the `phy_page_num` is + /// a *page number* (so a physical address divided by the page size). It's a slightly awkward units, but + /// it saves a bit of math going back and forth between the native storage formats of the records. + pub(crate) fn pt_patch_mapping(&mut self, va: VirtAddr, phys_page_num: u32) { + self.syskey_ensure(); + let cipher = self.cipher_ecb.as_ref().expect("Inconsistent internal state - syskey_ensure() failed"); + let mut pte = Pte::new(va, PtFlags::CLEAN, Rc::clone(&self.entropy)); + let mut block = Block::from_mut_slice(pte.deref_mut()); + //log::info!("pte pt: {:x?}", block); + cipher.encrypt_block(&mut block); + //log::info!("pte ct: {:x?}", block); + self.patch_pagetable(&block, phys_page_num * aes::BLOCK_SIZE as u32); + } /// Searches the page table for an MBBB slot. This is currently an O(N) search but /// in practice for Precursor there are only 8 pages, so it's quite fast on average. /// This would want to be optimized or cached for a much larger filesystem. @@ -344,6 +362,7 @@ impl PddbOs { if let Some(pte) = Pte::try_from_slice(block.as_slice()) { let mut pp = PhysPage(0); pp.set_page_number(((page_index * PAGE_SIZE / aes::BLOCK_SIZE) + index) as PhysAddr); + // the state is clean because this entry is, by definition, synchronized with the disk pp.set_clean(true); // handle conflicting journal versions here if let Some(prev_page) = map.get(&pte.vaddr()) { @@ -399,6 +418,7 @@ impl PddbOs { } core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); } + self.cipher_ecb = None; } fn syskey_ensure(&mut self) { if self.system_basis_key.is_none() { @@ -412,6 +432,10 @@ impl PddbOs { } self.system_basis_key = Some(system_key); } + if self.cipher_ecb.is_none() { + let cipher = Aes256::new(GenericArray::from_slice(&self.system_basis_key.unwrap())); + self.cipher_ecb = Some(cipher); + } } fn mbbb_as_slice(&self) -> &[u8] { @@ -821,6 +845,7 @@ impl PddbOs { // the entry will be reclaimed on the next full-space scan. This is a less-bad outcome than filling up // the log with 2x the number of operations to record MaybeUsed and then Used. ppc.set_space_state(SpaceState::Used); + ppc.set_clean(false); // the allocated page is not clean, because it hasn't been written to disk ppc.set_journal(pp.journal() + 1); // this is guaranteed not to overflow because of a check in the "if" clause above // commit the usage to the journal @@ -924,7 +949,7 @@ impl PddbOs { Some(data) }, Err(e) => { - log::info!("Error decrypting page: {:?}", e); + log::trace!("Error decrypting page: {:?}", e); // sometimes this is totally "normal", like when we're testing for valid data. None } } @@ -1151,18 +1176,10 @@ impl PddbOs { } // step 9. generate & write initial page table entries - if let Some(system_key) = self.system_basis_key { - log::info!("encryption key: {:x?}", system_key); - let cipher = Aes256::new(GenericArray::from_slice(&system_key)); - for (&virt, &phys) in basis_v2p_map.iter() { - let mut pte = Pte::new(virt, PtFlags::CLEAN, Rc::clone(&self.entropy)); - let mut block = Block::from_mut_slice(pte.deref_mut()); - //log::info!("pte pt: {:x?}", block); - cipher.encrypt_block(&mut block); - //log::info!("pte ct: {:x?}", block); - self.patch_pagetable(&block, phys.page_number() * aes::BLOCK_SIZE as u32); - //log::info!("pte ct loc: {:x?}", phys.page_number() * aes::BLOCK_SIZE as u32); - } + for (&virt, phys) in basis_v2p_map.iter_mut() { + self.pt_patch_mapping(virt, phys.page_number()); + // mark the entry as clean, as it has been sync'd to disk + phys.set_clean(true); } Ok(()) diff --git a/services/pddb/src/backend/types.rs b/services/pddb/src/backend/types.rs index 25be19071..2836d432a 100644 --- a/services/pddb/src/backend/types.rs +++ b/services/pddb/src/backend/types.rs @@ -176,8 +176,9 @@ impl Add for PageAlignedPa { } } - +#[cfg(test)] mod tests { + use super::*; #[test] /// PAGE_SIZE is required to be a power of two. 0x1000 -> 0x1000 - 1 = 0xFFF, which forms the bitmasks. fn test_page_size() { @@ -190,7 +191,7 @@ mod tests { /// obvious but not explicitly stated fact always remains true. #[test] fn test_bitfield_bool() { - super::bitfield! { + bitfield! { pub struct Test(u8); impl Debug; pub test, set_test: 1; diff --git a/tools/pddbdbg.py b/tools/pddbdbg.py index f52236255..a91e0cd2e 100755 --- a/tools/pddbdbg.py +++ b/tools/pddbdbg.py @@ -10,6 +10,7 @@ import hashlib SYSTEM_BASIS = '.System' PAGE_SIZE = 4096 +VPAGE_SIZE = 4064 MBBB_PAGES = 10 # from https://github.com/wc-duck/pymmh3/blob/master/pymmh3.py @@ -130,6 +131,7 @@ def main(): FSCB_PAGES = 16 KEY_PAGES = 1 global PAGE_SIZE + global VPAGE_SIZE with open(imagefile, 'rb') as img_f: raw_img = img_f.read() @@ -155,26 +157,68 @@ def main(): p2v_table = tables[name][1] basis_data = bytearray() - errors = 0 - for vp in sorted(v2p_table): - pp_start = v2p_table[vp] - # print("pp_start: {}, vp: {}".format(pp_start, vp)) - pp_data = data[pp_start:pp_start + PAGE_SIZE] - try: - cipher = AES_GCM_SIV(key, pp_data[:12]) - pt_data = cipher.decrypt(pp_data[12:], basis_aad(name)) - basis_data.extend(bytearray(pt_data)) - print("decrypted vpage @ {} ppage @ {:x}".format(vp, v2p_table[vp])) - except ValueError: - errors += 1 - print("couldn't decrypt vpage @ {} ppage @ {:x}".format(vp, v2p_table[vp])) - - - if errors == 0: + pp_start = v2p_table[VPAGE_SIZE] + # print("pp_start: {}, vp: {}".format(pp_start, vp)) + pp_data = data[pp_start:pp_start + PAGE_SIZE] + try: + cipher = AES_GCM_SIV(key, pp_data[:12]) + pt_data = cipher.decrypt(pp_data[12:], basis_aad(name)) + basis_data.extend(bytearray(pt_data)) + print("decrypted vpage @ {:x} ppage @ {:x}".format(VPAGE_SIZE, v2p_table[VPAGE_SIZE])) # print([hex(x) for x in basis_data[:256]]) basis = Basis(basis_data) print(basis.as_str()) + basis_dicts = {} + for dict_index in range(basis.num_dicts): + bdict = BasisDicts(dict_index, v2p_table, data, key, name) + basis_dicts[bdict.name] = bdict + + print(" Dictionaries: ") + for bdict in basis_dicts.values(): + print(bdict.as_str()) + + except ValueError: + print("couldn't decrypt basis root vpage @ {:x} ppage @ {:x}".format(VPAGE_SIZE, v2p_table[VPAGE_SIZE])) + + + +class BasisDicts: + DICT_VSTRIDE = 0xFE_0000 + MAX_NAME_LEN = 115 + def __init__(self, index, v2p, disk, key, name): + global PAGE_SIZE + dict_header_vaddr = self.DICT_VSTRIDE * (index + 1) + pp = v2p[dict_header_vaddr] + try: + print('dict decrypt @ pp {:x}, nonce: {}'.format(pp, disk[pp:pp+12].hex())) + cipher = AES_GCM_SIV(key, disk[pp:pp+12]) + pt_data = cipher.decrypt(disk[pp+12:pp+PAGE_SIZE], basis_aad(name)) + # print('raw pt_data: {}'.format(pt_data[:127].hex())) + i = 0 + self.journal = int.from_bytes(pt_data[i:i+4], 'little') + i += 4 + self.flags = int.from_bytes(pt_data[i:i+4], 'little') + i += 4 + self.age = int.from_bytes(pt_data[i:i+4], 'little') + i += 4 + self.num_keys = int.from_bytes(pt_data[i:i+4], 'little') + i += 4 + self.name = pt_data[i:i+BasisDicts.MAX_NAME_LEN].rstrip(b'\x00').decode('utf8', errors='ignore') + i += BasisDicts.MAX_NAME_LEN + print("dict header len: {}".format(i-4)) # subtract 4 because of the journal + except ValueError: + print("basisdicts: couldn't decrypt vpage @ {:x} ppage @ {:x}".format(dict_header_vaddr, v2p[dict_header_vaddr])) + + def as_str(self): + desc = '' + desc += ' Name: {}\n'.format(self.name) + desc += ' Age: {}\n'.format(self.age) + desc += ' Flags: {}\n'.format(self.flags) + desc += ' Key count: {}\n'.format(self.num_keys) + return desc + + class Basis: MAX_NAME_LEN = 64 def __init__(self, i_bytes):