From c52afa459546096bdc9de12c3fe11edb723c32bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 20 Jan 2025 16:49:56 +0100 Subject: [PATCH] Add `mlock` and `memfd_secret` implementations --- Cargo.lock | 12 + crates/bitwarden-crypto/Cargo.toml | 7 + .../custom_slice/linux_memfd_secret.rs | 111 +++ .../implementation/custom_slice/mod.rs | 747 ++++++++++++++++++ .../implementation/custom_slice/rust.rs | 111 +++ .../src/store/backend/implementation/mod.rs | 12 +- 6 files changed, 998 insertions(+), 2 deletions(-) create mode 100644 crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/linux_memfd_secret.rs create mode 100644 crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/mod.rs create mode 100644 crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/rust.rs diff --git a/Cargo.lock b/Cargo.lock index 4c61bf967..374c88db1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,6 +426,7 @@ dependencies = [ "generic-array", "hkdf", "hmac", + "memsec", "num-bigint", "num-traits", "pbkdf2", @@ -2367,6 +2368,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "memsec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" +dependencies = [ + "getrandom", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "mime" version = "0.3.17" diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index 7bf326152..bec6334eb 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -50,6 +50,9 @@ wasm-bindgen = { workspace = true, optional = true } zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] } zeroizing-alloc = ">=0.1.0, <0.2" +[target.'cfg(all(not(target_arch = "wasm32"), not(windows)))'.dependencies] +memsec = { version = "0.7.0", features = ["alloc_ext"] } + [dev-dependencies] criterion = "0.5.1" rand_chacha = "0.3.1" @@ -67,3 +70,7 @@ required-features = ["no-memory-hardening"] [lints] workspace = true + +[package.metadata.cargo-udeps.ignore] +# This is unused when using --all-features, as that disables memory-hardening +normal = ["memsec"] diff --git a/crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/linux_memfd_secret.rs b/crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/linux_memfd_secret.rs new file mode 100644 index 000000000..9c2bd6d61 --- /dev/null +++ b/crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/linux_memfd_secret.rs @@ -0,0 +1,111 @@ +use std::{mem::MaybeUninit, ptr::NonNull, sync::OnceLock}; + +use super::{KeyId, SliceBackend, SliceLike}; + +// This is an in-memory key store that is protected by memfd_secret on Linux 5.14+. +// This should be secure against memory dumps from anything except a malicious kernel driver. +// Note that not all 5.14+ systems have support for memfd_secret enabled, so +// LinuxMemfdSecretKeyStore::new returns an Option. +pub(crate) type LinuxMemfdSecretBackend = SliceBackend; + +pub(crate) struct MemfdSecretImplKeyData { + ptr: std::ptr::NonNull<[u8]>, + capacity: usize, +} + +// For Send+Sync to be safe, we need to ensure that the memory is only accessed mutably from one +// thread. To do this, we have to make sure that any funcion in `MemfdSecretImplKeyData` that +// accesses the pointer mutably is defined as &mut self, and that the pointer is never copied or +// moved outside the struct. +unsafe impl Send for MemfdSecretImplKeyData {} +unsafe impl Sync for MemfdSecretImplKeyData {} + +impl Drop for MemfdSecretImplKeyData { + fn drop(&mut self) { + unsafe { + memsec::free_memfd_secret(self.ptr); + } + } +} + +impl SliceLike for MemfdSecretImplKeyData { + fn is_available() -> bool { + static IS_SUPPORTED: OnceLock = OnceLock::new(); + + *IS_SUPPORTED.get_or_init(|| unsafe { + let Some(ptr) = memsec::memfd_secret_sized(1) else { + return false; + }; + memsec::free_memfd_secret(ptr); + true + }) + } + + fn with_capacity(capacity: usize) -> Self { + let entry_size = std::mem::size_of::>(); + + unsafe { + let ptr: NonNull<[u8]> = memsec::memfd_secret_sized(capacity * entry_size) + .expect("memfd_secret_sized failed"); + + // Initialize the array with Nones using MaybeUninit + let uninit_slice: &mut [MaybeUninit<_>] = std::slice::from_raw_parts_mut( + ptr.as_ptr() as *mut MaybeUninit>, + capacity, + ); + for elem in uninit_slice { + elem.write(None); + } + + MemfdSecretImplKeyData { ptr, capacity } + } + } + + fn get_key_data(&self) -> &[Option<(Key, Key::KeyValue)>] { + let ptr = self.ptr.as_ptr() as *const Option<(Key, Key::KeyValue)>; + // SAFETY: The pointer is valid and points to a valid slice of the correct size. + // This function is &self so it only takes a immutable *const pointer. + unsafe { std::slice::from_raw_parts(ptr, self.capacity) } + } + + fn get_key_data_mut(&mut self) -> &mut [Option<(Key, Key::KeyValue)>] { + let ptr = self.ptr.as_ptr() as *mut Option<(Key, Key::KeyValue)>; + // SAFETY: The pointer is valid and points to a valid slice of the correct size. + // This function is &mut self so it can take a mutable *mut pointer. + unsafe { std::slice::from_raw_parts_mut(ptr, self.capacity) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::store::backend::{ + implementation::custom_slice::tests::{TestKey, TestKeyValue}, + StoreBackend as _, + }; + + #[test] + fn test_resize() { + let mut store = LinuxMemfdSecretBackend::::with_capacity(1).unwrap(); + + for (idx, key) in [ + TestKey::A, + TestKey::B(10), + TestKey::C, + TestKey::B(7), + TestKey::A, + TestKey::C, + ] + .into_iter() + .enumerate() + { + store.upsert(key, TestKeyValue::new(idx)); + } + + assert_eq!(store.get(TestKey::A), Some(&TestKeyValue::new(4))); + assert_eq!(store.get(TestKey::B(10)), Some(&TestKeyValue::new(1))); + assert_eq!(store.get(TestKey::C), Some(&TestKeyValue::new(5))); + assert_eq!(store.get(TestKey::B(7)), Some(&TestKeyValue::new(3))); + assert_eq!(store.get(TestKey::B(20)), None); + } +} diff --git a/crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/mod.rs b/crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/mod.rs new file mode 100644 index 000000000..f816eb481 --- /dev/null +++ b/crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/mod.rs @@ -0,0 +1,747 @@ +use std::marker::PhantomData; + +use zeroize::ZeroizeOnDrop; + +use super::StoreBackend; +use crate::KeyId; + +#[cfg(all(target_os = "linux", not(feature = "no-memory-hardening")))] +pub(crate) mod linux_memfd_secret; +pub(crate) mod rust; + +/// This trait represents some data stored sequentially in memory that can be interpreted as a +/// slice. +/// Implementations of this trait should ensure that the initialized data is protected +/// as much as possible. The data is already Zeroized on Drop, so implementations should +/// only need to worry about removing any protections they've added, or releasing any resources. +#[allow(drop_bounds)] +pub(crate) trait SliceLike: Send + Sync + Sized + Drop { + /// Check if the data store is available on this platform. + fn is_available() -> bool; + + /// Initialize a new data store with the given capacity. + /// The data MUST be initialized to all None values, and + /// it's capacity must be equal or greater than the provided value. + fn with_capacity(capacity: usize) -> Self; + + /// Return an immutable slice of the data. It must return the full allocated capacity, with no + /// uninitialized values. + fn get_key_data(&self) -> &[Option<(Key, Key::KeyValue)>]; + + /// Return an mutable slice of the data. It must return the full allocated capacity, with no + /// uninitialized values. + fn get_key_data_mut(&mut self) -> &mut [Option<(Key, Key::KeyValue)>]; +} + +/// This represents a key store over an arbitrary fixed size slice. +/// This is meant to abstract over the different ways to store keys in memory, whether we're +/// using a Box<[u8]> or a NonNull, regardless of if that memory was allocated by Rust or not. +/// +/// Internally this is represented as a slice of `Option<(Key, Key::KeyValue)>` +/// and elements are sorted based on Key for performance. In essence, this is almost a homegrown +/// `HashMap`. +/// +/// The reason why we're not using a Rust collection like `Vec` or `HashMap` is that those types +/// expect their memory to be allocated by Rust, and they will deallocate/reallocate it as needed. +/// That will not work for our usecases where we want to have control over allocations/deallocations +/// and where some of our strategies rely on using system-allocated secure memory for the storage, +/// like the Linux-only `memfd_secret` API. +pub(crate) struct SliceBackend> { + // This represents the number of elements in the container, it's always less than or equal to + // the length of `data`. + length: usize, + + // This represents the maximum number of elements that can be stored in the container. + // This is always equal to the length of `data`, but we store it to avoid recomputing it. + capacity: usize, + + // This is the actual data that stores the keys, optional as we can have it not yet + // uninitialized + slice: Option, + + _key: PhantomData, +} + +impl> ZeroizeOnDrop for SliceBackend {} + +impl> Drop for SliceBackend { + fn drop(&mut self) { + self.clear(); + } +} + +impl> StoreBackend for SliceBackend { + fn upsert(&mut self, key_ref: Key, key: Key::KeyValue) { + match self.find_by_key_ref(&key_ref) { + Ok(idx) => { + // Key already exists, we just need to replace the value + let slice = self.get_key_data_mut(); + slice[idx] = Some((key_ref, key)); + } + Err(idx) => { + // Make sure that we have enough capacity, and resize if needed + self.ensure_capacity(1); + + let len = self.length; + let slice = self.get_key_data_mut(); + if idx < len { + // If we're not right at the end, we have to shift all the following elements + // one position to the right + slice[idx..=len].rotate_right(1); + } + slice[idx] = Some((key_ref, key)); + self.length += 1; + } + } + } + + fn get(&self, key_ref: Key) -> Option<&Key::KeyValue> { + self.find_by_key_ref(&key_ref) + .ok() + .and_then(|idx| self.get_key_data().get(idx)) + .and_then(|f| f.as_ref().map(|f| &f.1)) + } + + fn remove(&mut self, key_ref: Key) { + if let Ok(idx) = self.find_by_key_ref(&key_ref) { + let len = self.length; + let slice = self.get_key_data_mut(); + slice[idx] = None; + slice[idx..len].rotate_left(1); + self.length -= 1; + } + } + + fn clear(&mut self) { + let len = self.length; + self.get_key_data_mut()[0..len].fill_with(|| None); + self.length = 0; + } + + fn retain(&mut self, f: fn(Key) -> bool) { + let len = self.length; + let slice = self.get_key_data_mut(); + + let mut removed_elements = 0; + + for value in slice.iter_mut().take(len) { + let key = value + .as_ref() + .map(|e| e.0) + .expect("Values in a slice are always Some"); + + if !f(key) { + *value = None; + removed_elements += 1; + } + } + + // If we haven't removed any elements, we don't need to compact the slice + if removed_elements == 0 { + return; + } + + // Remove all the None values from the middle of the slice + + for idx in 0..len { + if slice[idx].is_none() { + slice[idx..len].rotate_left(1); + } + } + + self.length -= removed_elements; + } +} + +impl> SliceBackend { + pub(crate) fn new() -> Option { + Self::with_capacity(0) + } + + pub(crate) fn with_capacity(capacity: usize) -> Option { + if !Data::is_available() { + return None; + } + + // If the capacity is 0, we don't need to allocate any memory. + // This allows us to initialize the container lazily. + let slice = (capacity != 0).then(|| Data::with_capacity(capacity)); + Some(Self { + length: 0, + capacity, + slice, + _key: PhantomData, + }) + } + + /// Check if the container has enough capacity to store `new_elements` more elements. + /// If the result is Ok, the container has enough capacity. + /// If it's Err, the container needs to be resized. + /// The error value returns a suggested new capacity. + fn check_capacity(&self, new_elements: usize) -> Result<(), usize> { + let new_size = self.length + new_elements; + + // We still have enough capacity + if new_size <= self.capacity { + Ok(()) + + // This is the first allocation + } else if self.capacity == 0 { + const PAGE_SIZE: usize = 4096; + let entry_size = std::mem::size_of::>(); + + // We're using mlock APIs to protect the memory, which lock at the page level. + // To avoid wasting memory, we want to allocate at least a page. + let entries_per_page = PAGE_SIZE / entry_size; + Err(entries_per_page) + + // We need to resize the container + } else { + // We want to increase the capacity by a multiple to be mostly aligned with page size, + // we also need to make sure that we have enough space for the new elements, so we round + // up + let increase_factor = usize::div_ceil(new_size, self.capacity); + Err(self.capacity * increase_factor) + } + } + + fn ensure_capacity(&mut self, new_elements: usize) { + if let Err(new_capacity) = self.check_capacity(new_elements) { + // Create a new store with the correct capacity and replace self with it + let mut new_self = + Self::with_capacity(new_capacity).expect("Could not allocate new store"); + new_self.copy_from(self); + *self = new_self; + } + } + + // These two are just helper functions to avoid having to deal with the optional Data + // When Data is None we just return empty slices, which don't allow any operations + fn get_key_data(&self) -> &[Option<(Key, Key::KeyValue)>] { + self.slice.as_ref().map(|d| d.get_key_data()).unwrap_or(&[]) + } + fn get_key_data_mut(&mut self) -> &mut [Option<(Key, Key::KeyValue)>] { + self.slice + .as_mut() + .map(|d| d.get_key_data_mut()) + .unwrap_or(&mut []) + } + + fn find_by_key_ref(&self, key_ref: &Key) -> Result { + // Because we know all the None's are at the end and all the Some values are at the + // beginning, we only need to search for the key in the first `size` elements. + let slice = &self.get_key_data()[..self.length]; + + // This structure is almost always used for reads instead of writes, so we can use a binary + // search to optimize for the read case. + slice.binary_search_by(|k| { + debug_assert!( + k.is_some(), + "We should never have a None value in the middle of the slice" + ); + + match k { + Some((k, _)) => k.cmp(key_ref), + None => std::cmp::Ordering::Greater, + } + }) + } + + pub(crate) fn copy_from(&mut self, other: &mut Self) -> bool { + if other.capacity > self.capacity { + return false; + } + + // Empty the current container + self.clear(); + + let new_length = other.length; + + // Move the data from the other container + let this = self.get_key_data_mut(); + let that = other.get_key_data_mut(); + for idx in 0..new_length { + std::mem::swap(&mut this[idx], &mut that[idx]); + } + + // Update the length + self.length = new_length; + + true + } +} + +#[cfg(test)] +pub(crate) mod tests { + use zeroize::Zeroize; + + use super::{rust::RustBackend, *}; + use crate::{CryptoKey, KeyId}; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum TestKey { + A, + B(u8), + C, + } + #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] + pub struct TestKeyValue([u8; 16]); + impl zeroize::ZeroizeOnDrop for TestKeyValue {} + impl CryptoKey for TestKeyValue {} + impl TestKeyValue { + pub fn new(value: usize) -> Self { + // Just fill the array with some values + let mut key = [0; 16]; + key[0..8].copy_from_slice(&value.to_le_bytes()); + key[8..16].copy_from_slice(&value.to_be_bytes()); + Self(key) + } + } + + impl Drop for TestKeyValue { + fn drop(&mut self) { + self.0.as_mut().zeroize(); + } + } + + impl KeyId for TestKey { + type KeyValue = TestKeyValue; + + fn is_local(&self) -> bool { + false + } + } + + #[test] + fn test_slice_container_insertion() { + let mut container = RustBackend::::with_capacity(5).unwrap(); + + assert_eq!(container.get_key_data(), [None, None, None, None, None]); + + // Insert one key, which should be at the beginning + container.upsert(TestKey::B(10), TestKeyValue::new(110)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + None, + None, + None, + None + ] + ); + + // Insert a key that should be right after the first one + container.upsert(TestKey::C, TestKeyValue::new(1000)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::C, TestKeyValue::new(1000))), + None, + None, + None + ] + ); + + // Insert a key in the middle + container.upsert(TestKey::B(20), TestKeyValue::new(210)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::C, TestKeyValue::new(1000))), + None, + None + ] + ); + + // Insert a key right at the start + container.upsert(TestKey::A, TestKeyValue::new(0)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(0))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::C, TestKeyValue::new(1000))), + None + ] + ); + + // Insert a key in the middle, which fills the container + container.upsert(TestKey::B(30), TestKeyValue::new(310)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(0))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + + // Replacing an existing value at the start + container.upsert(TestKey::A, TestKeyValue::new(1)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + + // Replacing an existing value at the middle + container.upsert(TestKey::B(20), TestKeyValue::new(211)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(211))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + + // Replacing an existing value at the end + container.upsert(TestKey::C, TestKeyValue::new(1001)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(211))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1001))), + ] + ); + } + + #[test] + fn test_slice_container_get() { + let mut container = RustBackend::::with_capacity(5).unwrap(); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + container.upsert(key, value); + } + + assert_eq!(container.get(TestKey::A), Some(&TestKeyValue::new(1))); + assert_eq!(container.get(TestKey::B(10)), Some(&TestKeyValue::new(110))); + assert_eq!(container.get(TestKey::B(20)), None); + assert_eq!(container.get(TestKey::C), Some(&TestKeyValue::new(1000))); + } + + #[test] + fn test_slice_container_clear() { + let mut container = RustBackend::::with_capacity(5).unwrap(); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + container.upsert(key, value); + } + + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + + container.clear(); + + assert_eq!(container.get_key_data(), [None, None, None, None, None]); + } + + #[test] + fn test_slice_container_ensure_capacity() { + let mut container = RustBackend::::with_capacity(5).unwrap(); + + assert_eq!(container.capacity, 5); + assert_eq!(container.length, 0); + + assert_eq!(container.check_capacity(0), Ok(())); + assert_eq!(container.check_capacity(6), Err(10)); + assert_eq!(container.check_capacity(10), Err(10)); + assert_eq!(container.check_capacity(11), Err(15)); + assert_eq!(container.check_capacity(51), Err(55)); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + container.upsert(key, value); + } + + assert_eq!(container.check_capacity(0), Ok(())); + assert_eq!(container.check_capacity(6), Err(15)); + assert_eq!(container.check_capacity(10), Err(15)); + assert_eq!(container.check_capacity(11), Err(20)); + assert_eq!(container.check_capacity(51), Err(60)); + } + + #[test] + fn test_slice_container_removal() { + let mut container = RustBackend::::with_capacity(5).unwrap(); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + container.upsert(key, value); + } + + // Remove the last element + container.remove(TestKey::C); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + ] + ); + + // Remove the first element + container.remove(TestKey::A); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None + ] + ); + + // Remove a non-existing element + container.remove(TestKey::A); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None + ] + ); + + // Remove an element in the middle + container.remove(TestKey::B(20)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None, + None + ] + ); + + // Remove all the remaining elements + container.remove(TestKey::B(30)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + None, + None, + None, + None + ] + ); + container.remove(TestKey::B(10)); + assert_eq!(container.get_key_data(), [None, None, None, None, None]); + + // Remove from an empty container + container.remove(TestKey::B(10)); + assert_eq!(container.get_key_data(), [None, None, None, None, None]); + } + + #[test] + fn test_slice_container_retain_removes_one() { + let mut container = RustBackend::::with_capacity(5).unwrap(); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + container.upsert(key, value); + } + + // Remove the last element + container.retain(|k| k != TestKey::C); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + ] + ); + + // Remove the first element + container.retain(|k| k != TestKey::A); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None + ] + ); + + // Remove a non-existing element + container.retain(|k| k != TestKey::A); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None + ] + ); + + // Remove an element in the middle + container.retain(|k| k != TestKey::B(20)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None, + None + ] + ); + + // Remove all the remaining elements + container.retain(|k| k != TestKey::B(30)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + None, + None, + None, + None + ] + ); + container.retain(|k| k != TestKey::B(10)); + assert_eq!(container.get_key_data(), [None, None, None, None, None]); + + // Remove from an empty container + container.retain(|k| k != TestKey::B(10)); + assert_eq!(container.get_key_data(), [None, None, None, None, None]); + } + + #[test] + fn test_slice_container_retain_removes_none() { + let mut container = RustBackend::::with_capacity(5).unwrap(); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + container.upsert(key, value); + } + + container.retain(|_k| true); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + } + + #[test] + fn test_slice_container_retain_removes_some() { + let mut container = RustBackend::::with_capacity(5).unwrap(); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + container.upsert(key, value); + } + + container.retain(|k| matches!(k, TestKey::A | TestKey::B(20) | TestKey::C)); + assert_eq!( + container.get_key_data(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::C, TestKeyValue::new(1000))), + None, + None, + ] + ); + } + + #[test] + fn test_slice_container_retain_removes_all() { + let mut container = RustBackend::::with_capacity(5).unwrap(); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + container.upsert(key, value); + } + + container.retain(|_k| false); + assert_eq!(container.get_key_data(), [None, None, None, None, None]); + } +} diff --git a/crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/rust.rs b/crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/rust.rs new file mode 100644 index 000000000..7f4415fe5 --- /dev/null +++ b/crates/bitwarden-crypto/src/store/backend/implementation/custom_slice/rust.rs @@ -0,0 +1,111 @@ +use super::{KeyId, SliceBackend, SliceLike}; + +// This is a basic in-memory key store for the cases where we don't have a secure key store +// available. We still make use mlock to protect the memory from being swapped to disk, and we +// zeroize the values when dropped. +pub(crate) type RustBackend = SliceBackend>; + +pub(crate) struct RustBackendImpl { + #[allow(clippy::type_complexity)] + data: Box<[Option<(Key, Key::KeyValue)>]>, +} + +impl Drop for RustBackendImpl { + fn drop(&mut self) { + munlock_data(self.data.as_mut()); + } +} + +impl SliceLike for RustBackendImpl { + fn is_available() -> bool { + true + } + + fn with_capacity(capacity: usize) -> Self { + #[allow(unused_mut)] + let mut data: Box<_> = std::iter::repeat_with(|| None).take(capacity).collect(); + mlock_data(data.as_mut()); + RustBackendImpl { data } + } + + fn get_key_data(&self) -> &[Option<(Key, Key::KeyValue)>] { + self.data.as_ref() + } + + fn get_key_data_mut(&mut self) -> &mut [Option<(Key, Key::KeyValue)>] { + self.data.as_mut() + } +} + +#[allow(unused_variables)] +fn mlock_data(data: &mut [T]) { + #[cfg(all( + not(target_arch = "wasm32"), + not(windows), + not(feature = "no-memory-hardening") + ))] + { + unsafe { + memsec::mlock(data.as_mut_ptr() as *mut u8, std::mem::size_of_val(data)); + } + } +} + +#[allow(unused_variables)] +fn munlock_data(data: &mut [T]) { + #[cfg(all( + not(target_arch = "wasm32"), + not(windows), + not(feature = "no-memory-hardening") + ))] + { + use std::mem::MaybeUninit; + unsafe { + memsec::munlock(data.as_mut_ptr() as *mut u8, std::mem::size_of_val(data)); + + // Note: munlock is zeroing the memory, which leaves the data in an undefined + // state, so we set it to None/Default again to avoid UB in the Drop implementation. + let uninit_slice: &mut [MaybeUninit<_>] = std::slice::from_raw_parts_mut( + data.as_mut_ptr() as *mut MaybeUninit, + data.len(), + ); + for elem in uninit_slice { + elem.write(T::default()); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::store::backend::{ + implementation::custom_slice::tests::{TestKey, TestKeyValue}, + StoreBackend as _, + }; + + #[test] + fn test_resize() { + let mut store = RustBackend::::with_capacity(1).unwrap(); + + for (idx, key) in [ + TestKey::A, + TestKey::B(10), + TestKey::C, + TestKey::B(7), + TestKey::A, + TestKey::C, + ] + .into_iter() + .enumerate() + { + store.upsert(key, TestKeyValue::new(idx)); + } + + assert_eq!(store.get(TestKey::A), Some(&TestKeyValue::new(4))); + assert_eq!(store.get(TestKey::B(10)), Some(&TestKeyValue::new(1))); + assert_eq!(store.get(TestKey::C), Some(&TestKeyValue::new(5))); + assert_eq!(store.get(TestKey::B(7)), Some(&TestKeyValue::new(3))); + assert_eq!(store.get(TestKey::B(20)), None); + } +} diff --git a/crates/bitwarden-crypto/src/store/backend/implementation/mod.rs b/crates/bitwarden-crypto/src/store/backend/implementation/mod.rs index 5ae4f8ef6..07d932278 100644 --- a/crates/bitwarden-crypto/src/store/backend/implementation/mod.rs +++ b/crates/bitwarden-crypto/src/store/backend/implementation/mod.rs @@ -1,11 +1,19 @@ use super::StoreBackend; use crate::store::KeyId; -mod basic; +mod custom_slice; /// Initializes a key store backend with the best available implementation for the current platform pub fn create_store() -> Box> { - Box::new(basic::BasicBackend::::new()) + #[cfg(all(target_os = "linux", not(feature = "no-memory-hardening")))] + if let Some(key_store) = custom_slice::linux_memfd_secret::LinuxMemfdSecretBackend::::new() + { + return Box::new(key_store); + } + + Box::new( + custom_slice::rust::RustBackend::new().expect("RustKeyStore should always be available"), + ) } #[cfg(test)]