diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 7569c1b0fc956474b933ba90cf10e2549af0914a..565b0b99bb4f3b39fbc532c02226e93ee5e1392e 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -198,6 +198,7 @@ impl PersistentStore { .insert(StoreEntry { tag: MASTER_KEYS, data: &master_keys, + sensitive: true, }) .unwrap(); } @@ -206,6 +207,7 @@ impl PersistentStore { .insert(StoreEntry { tag: PIN_RETRIES, data: &[MAX_PIN_RETRIES], + sensitive: false, }) .unwrap(); } @@ -245,6 +247,7 @@ impl PersistentStore { let new_entry = StoreEntry { tag: TAG_CREDENTIAL, data: &credential, + sensitive: true, }; match old_entry { None => self.store.insert(new_entry)?, @@ -299,6 +302,7 @@ impl PersistentStore { .insert(StoreEntry { tag: GLOBAL_SIGNATURE_COUNTER, data: &buffer, + sensitive: false, }) .unwrap(); } @@ -312,6 +316,7 @@ impl PersistentStore { StoreEntry { tag: GLOBAL_SIGNATURE_COUNTER, data: &buffer, + sensitive: false, }, ) .unwrap(); @@ -339,6 +344,7 @@ impl PersistentStore { let entry = StoreEntry { tag: PIN_HASH, data: pin_hash, + sensitive: true, }; match self.store.find_one(&Key::PinHash) { None => self.store.insert(entry).unwrap(), @@ -368,6 +374,7 @@ impl PersistentStore { StoreEntry { tag: PIN_RETRIES, data: &[new_value], + sensitive: false, }, ) .unwrap(); @@ -381,6 +388,7 @@ impl PersistentStore { StoreEntry { tag: PIN_RETRIES, data: &[MAX_PIN_RETRIES], + sensitive: false, }, ) .unwrap(); @@ -465,9 +473,9 @@ mod test { let storage = Storage::new(store, options); let store = embedded_flash::Store::new(storage, Config).unwrap(); // We can replace 3 bytes with minimal overhead. - assert_eq!(store.replace_len(0), 2 * WORD_SIZE); - assert_eq!(store.replace_len(3), 2 * WORD_SIZE); - assert_eq!(store.replace_len(4), 3 * WORD_SIZE); + assert_eq!(store.replace_len(false, 0), 2 * WORD_SIZE); + assert_eq!(store.replace_len(false, 3), 3 * WORD_SIZE); + assert_eq!(store.replace_len(false, 4), 3 * WORD_SIZE); } #[test] diff --git a/src/embedded_flash/store/bitfield.rs b/src/embedded_flash/store/bitfield.rs index 60c6f869b583e4cf2fb6d4ff35040fe579dc0efb..797c78bd2b2cd12554c2248318e98d68e49f3c33 100644 --- a/src/embedded_flash/store/bitfield.rs +++ b/src/embedded_flash/store/bitfield.rs @@ -55,6 +55,11 @@ impl ByteGap { bit + 8 * self.length } } + + /// Returns the slice of `data` corresponding to the gap. + pub fn slice(self, data: &[u8]) -> &[u8] { + &data[self.start..self.start + self.length] + } } /// Returns whether a bit is set in a sequence of bits. diff --git a/src/embedded_flash/store/format.rs b/src/embedded_flash/store/format.rs index cbef61f5e5981cae5b257bedc919ace462ed9449..8308c4a95f69494b35b522c4259dfa1e17e913b0 100644 --- a/src/embedded_flash/store/format.rs +++ b/src/embedded_flash/store/format.rs @@ -59,6 +59,14 @@ pub struct Format { /// - 1 for insert entries. replace_bit: usize, + /// Whether a user entry has sensitive data. + /// + /// When a user entry with sensitive data is deleted, the data is overwritten with zeroes. + /// + /// - 0 for sensitive data. + /// - 1 for non-sensitive data. + sensitive_bit: usize, + /// The data length of a user entry. length_range: bitfield::BitRange, @@ -138,8 +146,9 @@ impl Format { let deleted_bit = present_bit + 1; let internal_bit = deleted_bit + 1; let replace_bit = internal_bit + 1; + let sensitive_bit = replace_bit + 1; let length_range = bitfield::BitRange { - start: replace_bit + 1, + start: sensitive_bit + 1, length: byte_bits, }; let tag_range = bitfield::BitRange { @@ -182,6 +191,7 @@ impl Format { deleted_bit, internal_bit, replace_bit, + sensitive_bit, length_range, tag_range, replace_page_range, @@ -196,10 +206,11 @@ impl Format { // Make sure all the following conditions hold: // - The page header is one word. // - The internal entry is one word. - // - The entry header fits in one word. + // - The entry header fits in one word (which is equivalent to the entry header size being + // exactly one word for sensitive entries). if format.page_header_size() != word_size || format.internal_entry_size() != word_size - || format.header_size() > word_size + || format.header_size(true) != word_size { return None; } @@ -220,28 +231,46 @@ impl Format { /// Returns the entry header length in bytes. /// /// This is the smallest number of bytes necessary to store all fields of the entry info up to - /// and including `length`. - pub fn header_size(&self) -> usize { - self.bits_to_bytes(self.length_range.end()) + /// and including `length`. For sensitive entries, the result is word-aligned. + pub fn header_size(&self, sensitive: bool) -> usize { + let mut size = self.bits_to_bytes(self.length_range.end()); + if sensitive { + // We need to align to the next word boundary so that wiping the user data will not + // count as a write to the header. + size = self.align_word(size); + } + size + } + + /// Returns the entry header length in bytes. + /// + /// This is a convenience function for `header_size` above. + fn header_offset(&self, entry: &[u8]) -> usize { + self.header_size(self.is_sensitive(entry)) } /// Returns the entry info length in bytes. /// /// This is the number of bytes necessary to store all fields of the entry info. This also - /// includes the internal padding to protect the `committed` bit from the `deleted` bit. - fn info_size(&self, is_replace: IsReplace) -> usize { + /// includes the internal padding to protect the `committed` bit from the `deleted` bit and to + /// protect the entry info from the user data for sensitive entries. + fn info_size(&self, is_replace: IsReplace, sensitive: bool) -> usize { let suffix_bits = 2; // committed + complete let info_bits = match is_replace { IsReplace::Replace => self.replace_byte_range.end() + suffix_bits, IsReplace::Insert => self.tag_range.end() + suffix_bits, }; - let info_size = self.bits_to_bytes(info_bits); + let mut info_size = self.bits_to_bytes(info_bits); // If the suffix bits would end up in the header, we need to add one byte for them. - if info_size == self.header_size() { - info_size + 1 - } else { - info_size + let header_size = self.header_size(sensitive); + if info_size <= header_size { + info_size = header_size + 1; + } + // If the entry is sensitive, we need to align to the next word boundary. + if sensitive { + info_size = self.align_word(info_size); } + info_size } /// Returns the length in bytes of an entry. @@ -249,8 +278,8 @@ impl Format { /// This depends on the length of the user data and whether the entry replaces an old entry or /// is an insertion. This also includes the internal padding to protect the `committed` bit from /// the `deleted` bit. - pub fn entry_size(&self, is_replace: IsReplace, length: usize) -> usize { - let mut entry_size = length + self.info_size(is_replace); + pub fn entry_size(&self, is_replace: IsReplace, sensitive: bool, length: usize) -> usize { + let mut entry_size = length + self.info_size(is_replace, sensitive); let word_size = self.word_size; entry_size = self.align_word(entry_size); // The entry must be at least 2 words such that the `committed` and `deleted` bits are on @@ -308,6 +337,14 @@ impl Format { bitfield::set_zero(self.replace_bit, header, bitfield::NO_GAP) } + pub fn is_sensitive(&self, header: &[u8]) -> bool { + bitfield::is_zero(self.sensitive_bit, header, bitfield::NO_GAP) + } + + pub fn set_sensitive(&self, header: &mut [u8]) { + bitfield::set_zero(self.sensitive_bit, header, bitfield::NO_GAP) + } + pub fn get_length(&self, header: &[u8]) -> usize { bitfield::get_range(self.length_range, header, bitfield::NO_GAP) } @@ -317,16 +354,19 @@ impl Format { } pub fn get_data<'a>(&self, entry: &'a [u8]) -> &'a [u8] { - &entry[self.header_size()..][..self.get_length(entry)] + &entry[self.header_offset(entry)..][..self.get_length(entry)] } /// Returns the span of user data in an entry. /// /// The complement of this gap in the entry is exactly the entry info. The header is before the /// gap and the footer is after the gap. - fn entry_gap(&self, entry: &[u8]) -> bitfield::ByteGap { - let start = self.header_size(); - let length = self.get_length(entry); + pub fn entry_gap(&self, entry: &[u8]) -> bitfield::ByteGap { + let start = self.header_offset(entry); + let mut length = self.get_length(entry); + if self.is_sensitive(entry) { + length = self.align_word(length); + } bitfield::ByteGap { start, length } } @@ -406,16 +446,23 @@ impl Format { /// Builds an entry for replace or insert operations. pub fn build_entry(&self, replace: Option<Index>, user_entry: StoreEntry) -> Vec<u8> { - let StoreEntry { tag, data } = user_entry; + let StoreEntry { + tag, + data, + sensitive, + } = user_entry; let is_replace = match replace { None => IsReplace::Insert, Some(_) => IsReplace::Replace, }; - let entry_len = self.entry_size(is_replace, data.len()); + let entry_len = self.entry_size(is_replace, sensitive, data.len()); let mut entry = Vec::with_capacity(entry_len); // Build the header. - entry.resize(self.header_size(), 0xff); + entry.resize(self.header_size(sensitive), 0xff); self.set_present(&mut entry[..]); + if sensitive { + self.set_sensitive(&mut entry[..]); + } self.set_length(&mut entry[..], data.len()); // Add the data. entry.extend_from_slice(data); diff --git a/src/embedded_flash/store/mod.rs b/src/embedded_flash/store/mod.rs index 0431073009286219699fcce75307c66b1a957e5d..4111068a9e426dec4369745ab38f3e4141aba808 100644 --- a/src/embedded_flash/store/mod.rs +++ b/src/embedded_flash/store/mod.rs @@ -57,7 +57,7 @@ //! new_page:page_bits //! Padding(word) //! Entry := Header Data Footer -//! // Let X be the byte following `length` in `Info`. +//! // Let X be the byte (word-aligned for sensitive queries) following `length` in `Info`. //! Header := Info[..X] // must fit in one word //! Footer := Info[X..] // must fit in one word //! Info := @@ -65,6 +65,7 @@ //! deleted:1 //! internal=1 //! replace:1 +//! sensitive:1 //! length:byte_bits //! tag:tag_bits //! [ // present if `replace` is 0 @@ -109,15 +110,16 @@ //! 0.1 deleted //! 0.2 internal //! 0.3 replace -//! 0.4 length (9 bits) -//! 1.5 tag (least significant 3 bits out of 5) +//! 0.4 sensitive +//! 0.5 length (9 bits) +//! 1.6 tag (least significant 2 bits out of 5) //! (the header ends at the first byte boundary after `length`) //! 2.0 <user data> (2 bytes in this example) //! (the footer starts immediately after the user data) -//! 4.0 tag (most significant 2 bits out of 5) -//! 4.2 replace_page (6 bits) -//! 5.0 replace_byte (9 bits) -//! 6.1 padding (make sure the 2 properties below hold) +//! 4.0 tag (most significant 3 bits out of 5) +//! 4.3 replace_page (6 bits) +//! 5.1 replace_byte (9 bits) +//! 6.2 padding (make sure the 2 properties below hold) //! 7.6 committed //! 7.7 complete (on a different word than `present`) //! 8.0 <end> (word-aligned) @@ -203,6 +205,11 @@ pub struct StoreEntry<'a> { /// The data of the entry. pub data: &'a [u8], + + /// Whether the data is sensitive. + /// + /// Sensitive data is overwritten with zeroes when the entry is deleted. + pub sensitive: bool, } /// Implements a configurable multi-set on top of any storage. @@ -262,6 +269,7 @@ impl<S: Storage, C: StoreConfig> Store<S, C> { StoreEntry { tag: self.format.get_tag(entry), data: self.format.get_data(entry), + sensitive: self.format.is_sensitive(entry), }, )) } else { @@ -326,7 +334,7 @@ impl<S: Storage, C: StoreConfig> Store<S, C> { self.format.validate_entry(new)?; let mut old_index = old.index; // Find a slot. - let entry_len = self.replace_len(new.data.len()); + let entry_len = self.replace_len(new.sensitive, new.data.len()); let index = self.find_slot_for_write(entry_len, Some(&mut old_index))?; // Build a new entry replacing the old one. let entry = self.format.build_entry(Some(old_index), new); @@ -360,17 +368,20 @@ impl<S: Storage, C: StoreConfig> Store<S, C> { /// Returns the byte cost of a replace operation. /// /// Computes the length in bytes that would be used in the storage if a replace operation is - /// executed provided the data of the new entry has `length` bytes. - pub fn replace_len(&self, length: usize) -> usize { - self.format.entry_size(IsReplace::Replace, length) + /// executed provided the data of the new entry has `length` bytes and whether this data is + /// sensitive. + pub fn replace_len(&self, sensitive: bool, length: usize) -> usize { + self.format + .entry_size(IsReplace::Replace, sensitive, length) } /// Returns the byte cost of an insert operation. /// /// Computes the length in bytes that would be used in the storage if an insert operation is - /// executed provided the data of the inserted entry has `length` bytes. - pub fn insert_len(&self, length: usize) -> usize { - self.format.entry_size(IsReplace::Insert, length) + /// executed provided the data of the inserted entry has `length` bytes and whether this data is + /// sensitive. + pub fn insert_len(&self, sensitive: bool, length: usize) -> usize { + self.format.entry_size(IsReplace::Insert, sensitive, length) } /// Returns the erase count of all pages. @@ -410,8 +421,11 @@ impl<S: Storage, C: StoreConfig> Store<S, C> { let entry_index = index; let entry = self.read_entry(index); index.byte += entry.len(); - if !self.format.is_alive(entry) { - // Skip deleted entries (or the page padding). + if !self.format.is_present(entry) { + // Reached the end of the page. + } else if self.format.is_deleted(entry) { + // Wipe sensitive data if needed. + self.wipe_sensitive_data(entry_index); } else if self.format.is_internal(entry) { // Finish page compaction. self.erase_page(entry_index); @@ -449,6 +463,31 @@ impl<S: Storage, C: StoreConfig> Store<S, C> { /// The provided index must point to the beginning of an entry. fn delete_index(&mut self, index: Index) { self.update_word(index, |format, word| format.set_deleted(word)); + self.wipe_sensitive_data(index); + } + + /// Wipes the data of a sensitive entry. + /// + /// If the entry at the provided index is sensitive, overwrites the data with zeroes. Otherwise, + /// does nothing. + fn wipe_sensitive_data(&mut self, mut index: Index) { + let entry = self.read_entry(index); + debug_assert!(self.format.is_present(entry)); + debug_assert!(self.format.is_deleted(entry)); + if self.format.is_internal(entry) || !self.format.is_sensitive(entry) { + // No need to wipe the data. + return; + } + let gap = self.format.entry_gap(entry); + let data = gap.slice(entry); + if data.iter().all(|&byte| byte == 0x00) { + // The data is already wiped. + return; + } + index.byte += gap.start; + self.storage + .write_slice(index, &vec![0; gap.length]) + .unwrap(); } /// Finds a page with enough free space. @@ -555,10 +594,13 @@ impl<S: Storage, C: StoreConfig> Store<S, C> { } else if self.format.is_internal(first_byte) { self.format.internal_entry_size() } else { - let header = self.read_slice(index, self.format.header_size()); + // We don't know if the entry is sensitive or not, but it doesn't matter here. We just + // need to read the replace, sensitive, and length fields. + let header = self.read_slice(index, self.format.header_size(false)); let replace = self.format.is_replace(header); + let sensitive = self.format.is_sensitive(header); let length = self.format.get_length(header); - self.format.entry_size(replace, length) + self.format.entry_size(replace, sensitive, length) }; // Truncate the length to fit the page. This can only happen in case of corruption or // partial writes. @@ -673,7 +715,7 @@ impl<S: Storage, C: StoreConfig> Store<S, C> { // Save the old page index and erase count to the new page. let erase_index = new_index; let erase_entry = self.format.build_erase_entry(old_page, erase_count); - self.storage.write_slice(new_index, &erase_entry).unwrap(); + self.write_entry(new_index, &erase_entry); // Erase the page. self.erase_page(erase_index); // Increase generation. @@ -728,6 +770,22 @@ impl<C: StoreConfig> Store<BufferStorage, C> { pub fn set_erase_count(&mut self, page: usize, erase_count: usize) { self.initialize_page(page, erase_count); } + + /// Checks whether all deleted sensitive entries have been wiped. + pub fn check_wiped(&self) { + for (_, entry) in Iter::new(self) { + if !self.format.is_present(entry) + || !self.format.is_deleted(entry) + || self.format.is_internal(entry) + || !self.format.is_sensitive(entry) + { + continue; + } + let gap = self.format.entry_gap(entry); + let data = gap.slice(entry); + assert!(data.iter().all(|&byte| byte == 0x00)); + } + } } /// Maps an index from an old page to a new page if needed. @@ -843,7 +901,27 @@ mod tests { let tag = 0; let key = 1; let data = &[key, 2]; - let entry = StoreEntry { tag, data }; + let entry = StoreEntry { + tag, + data, + sensitive: false, + }; + store.insert(entry).unwrap(); + assert_eq!(store.iter().count(), 1); + assert_eq!(store.find_one(&key).unwrap().1, entry); + } + + #[test] + fn insert_sensitive_ok() { + let mut store = new_store(); + let tag = 0; + let key = 1; + let data = &[key, 4]; + let entry = StoreEntry { + tag, + data, + sensitive: true, + }; store.insert(entry).unwrap(); assert_eq!(store.iter().count(), 1); assert_eq!(store.find_one(&key).unwrap().1, entry); @@ -857,6 +935,7 @@ mod tests { let entry = StoreEntry { tag, data: &[key, 2], + sensitive: false, }; store.insert(entry).unwrap(); assert_eq!(store.find_all(&key).count(), 1); @@ -866,6 +945,25 @@ mod tests { assert_eq!(store.iter().count(), 0); } + #[test] + fn delete_sensitive_ok() { + let mut store = new_store(); + let tag = 0; + let key = 1; + let entry = StoreEntry { + tag, + data: &[key, 2], + sensitive: true, + }; + store.insert(entry).unwrap(); + assert_eq!(store.find_all(&key).count(), 1); + let (index, _) = store.find_one(&key).unwrap(); + store.delete(index).unwrap(); + assert_eq!(store.find_all(&key).count(), 0); + assert_eq!(store.iter().count(), 0); + store.check_wiped(); + } + #[test] fn insert_until_full() { let mut store = new_store(); @@ -875,6 +973,7 @@ mod tests { .insert(StoreEntry { tag, data: &[key, 0], + sensitive: false, }) .is_ok() { @@ -892,6 +991,7 @@ mod tests { .insert(StoreEntry { tag, data: &[key, 0], + sensitive: false, }) .is_ok() { @@ -903,6 +1003,7 @@ mod tests { .insert(StoreEntry { tag: 0, data: &[key, 0], + sensitive: false, }) .unwrap(); for k in 1..=key { @@ -916,7 +1017,11 @@ mod tests { let tag = 0; let key = 1; let data = &[key, 2]; - let entry = StoreEntry { tag, data }; + let entry = StoreEntry { + tag, + data, + sensitive: false, + }; store.insert(entry).unwrap(); // Reboot the store. @@ -934,10 +1039,12 @@ mod tests { let old_entry = StoreEntry { tag, data: &[key, 2, 3, 4, 5, 6], + sensitive: false, }; let new_entry = StoreEntry { tag, data: &[key, 7, 8, 9], + sensitive: false, }; let mut delay = 0; loop { @@ -973,6 +1080,7 @@ mod tests { .insert(StoreEntry { tag, data: &[key, 0], + sensitive: false, }) .is_ok() { @@ -983,7 +1091,14 @@ mod tests { let (index, _) = store.find_one(&1).unwrap(); store.arm_snapshot(delay); store - .replace(index, StoreEntry { tag, data: &[1, 1] }) + .replace( + index, + StoreEntry { + tag, + data: &[1, 1], + sensitive: false, + }, + ) .unwrap(); let (complete, store) = match store.get_snapshot() { Err(_) => (true, store.get_storage()), @@ -995,7 +1110,11 @@ mod tests { assert_eq!(store.find_all(&k).count(), 1); assert_eq!( store.find_one(&k).unwrap().1, - StoreEntry { tag, data: &[k, 0] } + StoreEntry { + tag, + data: &[k, 0], + sensitive: false, + } ); } assert_eq!(store.find_all(&1).count(), 1); @@ -1012,7 +1131,11 @@ mod tests { #[test] fn invalid_tag() { let mut store = new_store(); - let entry = StoreEntry { tag: 1, data: &[] }; + let entry = StoreEntry { + tag: 1, + data: &[], + sensitive: false, + }; assert_eq!(store.insert(entry), Err(StoreError::InvalidTag)); } @@ -1022,6 +1145,7 @@ mod tests { let entry = StoreEntry { tag: 0, data: &[0; PAGE_SIZE], + sensitive: false, }; assert_eq!(store.insert(entry), Err(StoreError::StoreFull)); }