1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-04 17:43:39 +00:00

[PM-26340] Implement encrypted memory store (#16659)

* Extract windows biometrics v2 changes

Co-authored-by: Bernd Schoolmann <mail@quexten.com>

* Address some code review feedback

* cargo fmt

* rely on zeroizing allocator

* Handle TDE edge cases

* Update windows default

* Make windows rust code async and fix restoring focus freezes

* fix formatting

* cleanup native logging

* Add unit test coverage

* Add missing logic to edge case for PIN disable.

* Address code review feedback

* fix test

* code review changes

* fix clippy warning

* Swap to unimplemented on each method

* Implement encrypted memory store

* Make dpapi secure key container pub(super)

* Add comments on sync and send

* Clean up comments

* Clean up

* Fix build

* Add logging and update codeowners

* Run cargo fmt

* Clean up doc

* fix unit tests

* Update apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Handle tampering with re-key and log

* Add docs

* Fix windows build

* Prevent rust flycheck log from being commited to git

* Undo feature flag change

* Add env var override and docs

* Add deps to km owership

---------

Co-authored-by: Thomas Avery <tavery@bitwarden.com>
Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann
2025-10-23 14:04:25 +02:00
committed by GitHub
parent 7e7107f165
commit 7f86f2d0ac
15 changed files with 942 additions and 5 deletions

View File

@@ -54,7 +54,7 @@ impl SecureMemoryStore for DpapiSecretKVStore {
self.map.insert(key, padded_data);
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
fn get(&mut self, key: &str) -> Option<Vec<u8>> {
self.map.get(key).map(|data| {
// A copy is created, that is then mutated by the DPAPI unprotect function.
let mut data = data.clone();

View File

@@ -0,0 +1,105 @@
use tracing::error;
use crate::secure_memory::{
secure_key::{EncryptedMemory, SecureMemoryEncryptionKey},
SecureMemoryStore,
};
/// An encrypted memory store holds a platform protected symmetric encryption key, and uses it
/// to encrypt all items it stores. The ciphertexts for the items are not specially protected. This
/// allows circumventing length and amount limitations on platform specific secure memory APIs since
/// only a single short item needs to be protected.
///
/// The key is briefly in process memory during encryption and decryption, in memory that is protected
/// from swapping to disk via mlock, and then zeroed out immediately after use.
#[allow(unused)]
pub(crate) struct EncryptedMemoryStore {
map: std::collections::HashMap<String, EncryptedMemory>,
memory_encryption_key: SecureMemoryEncryptionKey,
}
impl EncryptedMemoryStore {
#[allow(unused)]
pub(crate) fn new() -> Self {
EncryptedMemoryStore {
map: std::collections::HashMap::new(),
memory_encryption_key: SecureMemoryEncryptionKey::new(),
}
}
}
impl SecureMemoryStore for EncryptedMemoryStore {
fn put(&mut self, key: String, value: &[u8]) {
let encrypted_value = self.memory_encryption_key.encrypt(value);
self.map.insert(key, encrypted_value);
}
fn get(&mut self, key: &str) -> Option<Vec<u8>> {
let encrypted_memory = self.map.get(key);
if let Some(encrypted_memory) = encrypted_memory {
match self.memory_encryption_key.decrypt(encrypted_memory) {
Ok(plaintext) => Some(plaintext),
Err(_) => {
error!("In memory store, decryption failed for key {}. The memory may have been tampered with. re-keying.", key);
self.memory_encryption_key = SecureMemoryEncryptionKey::new();
self.clear();
None
}
}
} else {
None
}
}
fn has(&self, key: &str) -> bool {
self.map.contains_key(key)
}
fn remove(&mut self, key: &str) {
self.map.remove(key);
}
fn clear(&mut self) {
self.map.clear();
}
}
impl Drop for EncryptedMemoryStore {
fn drop(&mut self) {
self.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secret_kv_store_various_sizes() {
let mut store = EncryptedMemoryStore::new();
for size in 0..=2048 {
let key = format!("test_key_{}", size);
let value: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
store.put(key.clone(), &value);
assert!(store.has(&key), "Store should have key for size {}", size);
assert_eq!(
store.get(&key),
Some(value),
"Value mismatch for size {}",
size
);
}
}
#[test]
fn test_crud() {
let mut store = EncryptedMemoryStore::new();
let key = "test_key".to_string();
let value = vec![1, 2, 3, 4, 5];
store.put(key.clone(), &value);
assert!(store.has(&key));
assert_eq!(store.get(&key), Some(value));
store.remove(&key);
assert!(!store.has(&key));
}
}

View File

@@ -1,6 +1,9 @@
#[cfg(target_os = "windows")]
pub(crate) mod dpapi;
mod encrypted_memory_store;
mod secure_key;
/// The secure memory store provides an ephemeral key-value store for sensitive data.
/// Data stored in this store is prevented from being swapped to disk and zeroed out. Additionally,
/// platform-specific protections are applied to prevent memory dumps or debugger access from
@@ -12,7 +15,9 @@ pub(crate) trait SecureMemoryStore {
/// Retrieves a copy of the value associated with the given key from secure memory.
/// This copy does not have additional memory protections applied, and should be zeroed when no
/// longer needed.
fn get(&self, key: &str) -> Option<Vec<u8>>;
///
/// Note: If memory was tampered with, this will re-key the store and return None.
fn get(&mut self, key: &str) -> Option<Vec<u8>>;
/// Checks if a value is stored under the given key.
fn has(&self, key: &str) -> bool;
/// Removes the value associated with the given key from secure memory.

View File

@@ -0,0 +1,96 @@
use std::ptr::NonNull;
use chacha20poly1305::{aead::Aead, Key, KeyInit};
use rand::{rng, Rng};
pub(super) const KEY_SIZE: usize = 32;
pub(super) const NONCE_SIZE: usize = 24;
/// The encryption performed here is xchacha-poly1305. Any tampering with the key or the ciphertexts will result
/// in a decryption failure and panic. The key's memory contents are protected from being swapped to disk
/// via mlock.
pub(super) struct MemoryEncryptionKey(NonNull<[u8]>);
/// An encrypted memory blob that must be decrypted using the same key that it was encrypted with.
pub struct EncryptedMemory {
nonce: [u8; NONCE_SIZE],
ciphertext: Vec<u8>,
}
impl MemoryEncryptionKey {
pub fn new() -> Self {
let mut key = [0u8; KEY_SIZE];
rng().fill(&mut key);
MemoryEncryptionKey::from(&key)
}
/// Encrypts the given plaintext using the key.
#[allow(unused)]
pub(super) fn encrypt(&self, plaintext: &[u8]) -> EncryptedMemory {
let cipher = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(self.as_ref()));
let mut nonce = [0u8; NONCE_SIZE];
rng().fill(&mut nonce);
let ciphertext = cipher
.encrypt(chacha20poly1305::XNonce::from_slice(&nonce), plaintext)
.expect("encryption should not fail");
EncryptedMemory { nonce, ciphertext }
}
/// Decrypts the given encrypted memory using the key. A decryption failure will panic. This is
/// okay because neither the keys nor ciphertexts should ever fail to decrypt, and doing so
/// indicates that the process memory was tampered with.
#[allow(unused)]
pub(super) fn decrypt(&self, encrypted: &EncryptedMemory) -> Result<Vec<u8>, DecryptionError> {
let cipher = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(self.as_ref()));
cipher
.decrypt(
chacha20poly1305::XNonce::from_slice(&encrypted.nonce),
encrypted.ciphertext.as_ref(),
)
.map_err(|_| DecryptionError::CouldNotDecrypt)
}
}
impl Drop for MemoryEncryptionKey {
fn drop(&mut self) {
unsafe {
memsec::free(self.0);
}
}
}
impl From<&[u8; KEY_SIZE]> for MemoryEncryptionKey {
fn from(value: &[u8; KEY_SIZE]) -> Self {
let mut ptr: NonNull<[u8]> =
unsafe { memsec::malloc_sized(KEY_SIZE).expect("malloc_sized should work") };
unsafe {
std::ptr::copy_nonoverlapping(value.as_ptr(), ptr.as_mut().as_mut_ptr(), KEY_SIZE);
}
MemoryEncryptionKey(ptr)
}
}
impl AsRef<[u8]> for MemoryEncryptionKey {
fn as_ref(&self) -> &[u8] {
unsafe { self.0.as_ref() }
}
}
#[derive(Debug)]
pub(crate) enum DecryptionError {
CouldNotDecrypt,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_encryption_key() {
let key = MemoryEncryptionKey::new();
let data = b"Hello, world!";
let encrypted = key.encrypt(data);
let decrypted = key.decrypt(&encrypted).unwrap();
assert_eq!(data.as_ref(), decrypted.as_slice());
}
}

View File

@@ -0,0 +1,93 @@
use super::crypto::{MemoryEncryptionKey, KEY_SIZE};
use super::SecureKeyContainer;
use windows::Win32::Security::Cryptography::{
CryptProtectMemory, CryptUnprotectMemory, CRYPTPROTECTMEMORY_BLOCK_SIZE,
CRYPTPROTECTMEMORY_SAME_PROCESS,
};
/// https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata
/// The DPAPI store encrypts data using the Windows Data Protection API (DPAPI). The key is bound
/// to the current process, and cannot be decrypted by other user-mode processes.
///
/// Note: Admin processes can still decrypt this memory:
/// https://blog.slowerzs.net/posts/cryptdecryptmemory/
pub(super) struct DpapiSecureKeyContainer {
dpapi_encrypted_key: [u8; KEY_SIZE + CRYPTPROTECTMEMORY_BLOCK_SIZE as usize],
}
// SAFETY: The encrypted data is fully owned by this struct, and not exposed outside or cloned,
// and is disposed on drop of this struct.
unsafe impl Send for DpapiSecureKeyContainer {}
// SAFETY: The container is non-mutable and thus safe to share between threads.
unsafe impl Sync for DpapiSecureKeyContainer {}
impl SecureKeyContainer for DpapiSecureKeyContainer {
fn as_key(&self) -> MemoryEncryptionKey {
let mut decrypted_key = self.dpapi_encrypted_key;
unsafe {
CryptUnprotectMemory(
decrypted_key.as_mut_ptr() as *mut core::ffi::c_void,
decrypted_key.len() as u32,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
}
.expect("crypt_unprotect_memory should work");
let mut key = [0u8; KEY_SIZE];
key.copy_from_slice(&decrypted_key[..KEY_SIZE]);
MemoryEncryptionKey::from(&key)
}
fn from_key(key: MemoryEncryptionKey) -> Self {
let mut padded_key = [0u8; KEY_SIZE + CRYPTPROTECTMEMORY_BLOCK_SIZE as usize];
padded_key[..KEY_SIZE].copy_from_slice(key.as_ref());
unsafe {
CryptProtectMemory(
padded_key.as_mut_ptr() as *mut core::ffi::c_void,
padded_key.len() as u32,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
}
.expect("crypt_protect_memory should work");
DpapiSecureKeyContainer {
dpapi_encrypted_key: padded_key,
}
}
fn is_supported() -> bool {
// DPAPI is supported on all Windows versions that we support.
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiple_keys() {
let key1 = MemoryEncryptionKey::new();
let key2 = MemoryEncryptionKey::new();
let container1 = DpapiSecureKeyContainer::from_key(key1);
let container2 = DpapiSecureKeyContainer::from_key(key2);
// Capture at time 1
let data_1_1 = container1.as_key();
let data_2_1 = container2.as_key();
// Capture at time 2
let data_1_2 = container1.as_key();
let data_2_2 = container2.as_key();
// Same keys should be equal
assert_eq!(data_1_1.as_ref(), data_1_2.as_ref());
assert_eq!(data_2_1.as_ref(), data_2_2.as_ref());
// Different keys should be different
assert_ne!(data_1_1.as_ref(), data_2_1.as_ref());
assert_ne!(data_1_2.as_ref(), data_2_2.as_ref());
}
#[test]
fn test_is_supported() {
assert!(DpapiSecureKeyContainer::is_supported());
}
}

View File

@@ -0,0 +1,100 @@
use crate::secure_memory::secure_key::crypto::MemoryEncryptionKey;
use super::crypto::KEY_SIZE;
use super::SecureKeyContainer;
use linux_keyutils::{KeyRing, KeyRingIdentifier};
/// The keys are bound to the process keyring.
const KEY_RING_IDENTIFIER: KeyRingIdentifier = KeyRingIdentifier::Process;
/// This is an atomic global counter used to help generate unique key IDs
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Generates a unique ID for the key in the kernel keyring.
/// SAFETY: This function is safe to call from multiple threads because it uses an atomic counter.
fn make_id() -> String {
let counter = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
// In case multiple processes are running, include the PID in the key ID.
let pid = std::process::id();
format!("bitwarden_desktop_{}_{}", pid, counter)
}
/// A secure key container that uses the Linux kernel keyctl API to store the key.
/// `https://man7.org/linux/man-pages/man1/keyctl.1.html`. The kernel enforces only
/// the correct process can read them, and they do not live in process memory space
/// and cannot be dumped.
pub(super) struct KeyctlSecureKeyContainer {
/// The kernel has an identifier for the key. This is randomly generated on construction.
id: String,
}
// SAFETY: The key id is fully owned by this struct and not exposed or cloned, and cleaned up on drop.
// Further, since we use `KeyRingIdentifier::Process` and not `KeyRingIdentifier::Thread`, the key
// is accessible across threads within the same process bound.
unsafe impl Send for KeyctlSecureKeyContainer {}
// SAFETY: The container is non-mutable and thus safe to share between threads.
unsafe impl Sync for KeyctlSecureKeyContainer {}
impl SecureKeyContainer for KeyctlSecureKeyContainer {
fn as_key(&self) -> MemoryEncryptionKey {
let ring = KeyRing::from_special_id(KEY_RING_IDENTIFIER, false)
.expect("should get process keyring");
let key = ring.search(&self.id).expect("should find key");
let mut buffer = [0u8; KEY_SIZE];
key.read(&mut buffer).expect("should read key");
MemoryEncryptionKey::from(&buffer)
}
fn from_key(data: MemoryEncryptionKey) -> Self {
let ring = KeyRing::from_special_id(KEY_RING_IDENTIFIER, true)
.expect("should get process keyring");
let id = make_id();
ring.add_key(&id, &data).expect("should add key");
KeyctlSecureKeyContainer { id }
}
fn is_supported() -> bool {
KeyRing::from_special_id(KEY_RING_IDENTIFIER, true).is_ok()
}
}
impl Drop for KeyctlSecureKeyContainer {
fn drop(&mut self) {
let ring = KeyRing::from_special_id(KEY_RING_IDENTIFIER, false)
.expect("should get process keyring");
if let Ok(key) = ring.search(&self.id) {
let _ = key.invalidate();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiple_keys() {
let key1 = MemoryEncryptionKey::new();
let key2 = MemoryEncryptionKey::new();
let container1 = KeyctlSecureKeyContainer::from_key(key1);
let container2 = KeyctlSecureKeyContainer::from_key(key2);
// Capture at time 1
let data_1_1 = container1.as_key();
let data_2_1 = container2.as_key();
// Capture at time 2
let data_1_2 = container1.as_key();
let data_2_2 = container2.as_key();
// Same keys should be equal
assert_eq!(data_1_1.as_ref(), data_1_2.as_ref());
assert_eq!(data_2_1.as_ref(), data_2_2.as_ref());
// Different keys should be different
assert_ne!(data_1_1.as_ref(), data_2_1.as_ref());
assert_ne!(data_1_2.as_ref(), data_2_2.as_ref());
}
#[test]
fn test_is_supported() {
assert!(KeyctlSecureKeyContainer::is_supported());
}
}

View File

@@ -0,0 +1,109 @@
use std::{ptr::NonNull, sync::LazyLock};
use super::crypto::MemoryEncryptionKey;
use super::crypto::KEY_SIZE;
use super::SecureKeyContainer;
/// https://man.archlinux.org/man/memfd_secret.2.en
/// The memfd_secret store protects the data using the `memfd_secret` syscall. The
/// data is inaccessible to other user-mode processes, and even to root in most cases.
/// If arbitrary data can be executed in the kernel, the data can still be retrieved:
/// https://github.com/JonathonReinhart/nosecmem
pub(super) struct MemfdSecretSecureKeyContainer {
ptr: NonNull<[u8]>,
}
// SAFETY: The pointers in this struct are allocated by `memfd_secret`, and we have full ownership.
// They are never exposed outside or cloned, and are cleaned up by drop.
unsafe impl Send for MemfdSecretSecureKeyContainer {}
// SAFETY: The container is non-mutable and thus safe to share between threads. Further, memfd-secret
// is accessible across threads within the same process bound.
unsafe impl Sync for MemfdSecretSecureKeyContainer {}
impl SecureKeyContainer for MemfdSecretSecureKeyContainer {
fn as_key(&self) -> MemoryEncryptionKey {
MemoryEncryptionKey::from(
&unsafe { self.ptr.as_ref() }
.try_into()
.expect("slice should be KEY_SIZE"),
)
}
fn from_key(key: MemoryEncryptionKey) -> Self {
let mut ptr: NonNull<[u8]> = unsafe {
memsec::memfd_secret_sized(KEY_SIZE).expect("memfd_secret_sized should work")
};
unsafe {
std::ptr::copy_nonoverlapping(
key.as_ref().as_ptr(),
ptr.as_mut().as_mut_ptr(),
KEY_SIZE,
);
}
MemfdSecretSecureKeyContainer { ptr }
}
/// Note, `memfd_secret` is only available since Linux 6.5, so fallbacks are needed.
fn is_supported() -> bool {
// To test if memfd_secret is supported, we try to allocate a 1 byte and see if that
// succeeds.
static IS_SUPPORTED: LazyLock<bool> = LazyLock::new(|| {
let Some(ptr): Option<NonNull<[u8]>> = (unsafe { memsec::memfd_secret_sized(1) })
else {
return false;
};
// Check that the pointer is readable and writable
let result = unsafe {
let ptr = ptr.as_ptr() as *mut u8;
*ptr = 30;
*ptr += 107;
*ptr == 137
};
unsafe { memsec::free_memfd_secret(ptr) };
result
});
*IS_SUPPORTED
}
}
impl Drop for MemfdSecretSecureKeyContainer {
fn drop(&mut self) {
unsafe {
memsec::free_memfd_secret(self.ptr);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiple_keys() {
let key1 = MemoryEncryptionKey::new();
let key2 = MemoryEncryptionKey::new();
let container1 = MemfdSecretSecureKeyContainer::from_key(key1);
let container2 = MemfdSecretSecureKeyContainer::from_key(key2);
// Capture at time 1
let data_1_1 = container1.as_key();
let data_2_1 = container2.as_key();
// Capture at time 2
let data_1_2 = container1.as_key();
let data_2_2 = container2.as_key();
// Same keys should be equal
assert_eq!(data_1_1.as_ref(), data_1_2.as_ref());
assert_eq!(data_2_1.as_ref(), data_2_2.as_ref());
// Different keys should be different
assert_ne!(data_1_1.as_ref(), data_2_1.as_ref());
assert_ne!(data_1_2.as_ref(), data_2_2.as_ref());
}
#[test]
fn test_is_supported() {
assert!(MemfdSecretSecureKeyContainer::is_supported());
}
}

View File

@@ -0,0 +1,83 @@
use std::ptr::NonNull;
use super::crypto::MemoryEncryptionKey;
use super::crypto::KEY_SIZE;
use super::SecureKeyContainer;
/// A SecureKeyContainer that uses mlock to prevent the memory from being swapped to disk.
/// This does not provide as strong protections as other methods, but is always supported.
pub(super) struct MlockSecureKeyContainer {
ptr: NonNull<[u8]>,
}
// SAFETY: The pointers in this struct are allocated by `malloc_sized`, and we have full ownership.
// They are never exposed outside or cloned, and are cleaned up by drop.
unsafe impl Send for MlockSecureKeyContainer {}
// SAFETY: The container is non-mutable and thus safe to share between threads.
unsafe impl Sync for MlockSecureKeyContainer {}
impl SecureKeyContainer for MlockSecureKeyContainer {
fn as_key(&self) -> MemoryEncryptionKey {
MemoryEncryptionKey::from(
&unsafe { self.ptr.as_ref() }
.try_into()
.expect("slice should be KEY_SIZE"),
)
}
fn from_key(key: MemoryEncryptionKey) -> Self {
let mut ptr: NonNull<[u8]> =
unsafe { memsec::malloc_sized(KEY_SIZE).expect("malloc_sized should work") };
unsafe {
std::ptr::copy_nonoverlapping(
key.as_ref().as_ptr(),
ptr.as_mut().as_mut_ptr(),
KEY_SIZE,
);
}
MlockSecureKeyContainer { ptr }
}
fn is_supported() -> bool {
true
}
}
impl Drop for MlockSecureKeyContainer {
fn drop(&mut self) {
unsafe {
memsec::free(self.ptr);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiple_keys() {
let key1 = MemoryEncryptionKey::new();
let key2 = MemoryEncryptionKey::new();
let container1 = MlockSecureKeyContainer::from_key(key1);
let container2 = MlockSecureKeyContainer::from_key(key2);
// Capture at time 1
let data_1_1 = container1.as_key();
let data_2_1 = container2.as_key();
// Capture at time 2
let data_1_2 = container1.as_key();
let data_2_2 = container2.as_key();
// Same keys should be equal
assert_eq!(data_1_1.as_ref(), data_1_2.as_ref());
assert_eq!(data_2_1.as_ref(), data_2_2.as_ref());
// Different keys should be different
assert_ne!(data_1_1.as_ref(), data_2_1.as_ref());
assert_ne!(data_1_2.as_ref(), data_2_2.as_ref());
}
#[test]
fn test_is_supported() {
assert!(MlockSecureKeyContainer::is_supported());
}
}

View File

@@ -0,0 +1,242 @@
//! This module provides hardened storage for single cryptographic keys. These are meant for encrypting large amounts of memory.
//! Some platforms restrict how many keys can be protected by their APIs, which necessitates this layer of indirection. This significantly
//! reduces the complexity of each platform specific implementation, since all that's needed is implementing protecting a single fixed sized key
//! instead of protecting many arbitrarily sized secrets. This significantly lowers the effort to maintain each implementation.
//!
//! The implementations include DPAPI on Windows, `keyctl` on Linux, and `memfd_secret` on Linux, and a fallback implementation using mlock.
use tracing::info;
mod crypto;
#[cfg(target_os = "windows")]
mod dpapi;
#[cfg(target_os = "linux")]
mod keyctl;
#[cfg(target_os = "linux")]
mod memfd_secret;
mod mlock;
pub use crypto::EncryptedMemory;
use crate::secure_memory::secure_key::crypto::DecryptionError;
/// An ephemeral key that is protected using a platform mechanism. It is generated on construction freshly, and can be used
/// to encrypt and decrypt segments of memory. Since the key is ephemeral, persistent data cannot be encrypted with this key.
/// On Linux and Windows, in most cases the protection mechanisms prevent memory dumps/debuggers from reading the key.
///
/// Note: This can be circumvented if code can be injected into the process and is only effective in combination with the
/// memory isolation provided in `process_isolation`.
/// - https://github.com/zer1t0/keydump
#[allow(unused)]
pub(crate) struct SecureMemoryEncryptionKey(CrossPlatformSecureKeyContainer);
impl SecureMemoryEncryptionKey {
pub fn new() -> Self {
SecureMemoryEncryptionKey(CrossPlatformSecureKeyContainer::from_key(
crypto::MemoryEncryptionKey::new(),
))
}
/// Encrypts the provided plaintext using the contained key, returning an EncryptedMemory blob.
#[allow(unused)]
pub fn encrypt(&self, plaintext: &[u8]) -> crypto::EncryptedMemory {
self.0.as_key().encrypt(plaintext)
}
/// Decrypts the provided EncryptedMemory blob using the contained key, returning the plaintext.
/// If the decryption fails, that means the memory was tampered with, and the function panics.
#[allow(unused)]
pub fn decrypt(&self, encrypted: &crypto::EncryptedMemory) -> Result<Vec<u8>, DecryptionError> {
self.0.as_key().decrypt(encrypted)
}
}
/// A platform specific implementation of a key container that protects a single encryption key
/// from memory attacks.
#[allow(unused)]
trait SecureKeyContainer: Sync + Send {
/// Returns the key as a byte slice. This slice does not have additional memory protections applied.
fn as_key(&self) -> crypto::MemoryEncryptionKey;
/// Creates a new SecureKeyContainer from the provided key.
fn from_key(key: crypto::MemoryEncryptionKey) -> Self;
/// Returns true if this platform supports this secure key container implementation.
fn is_supported() -> bool;
}
#[allow(unused)]
enum CrossPlatformSecureKeyContainer {
#[cfg(target_os = "windows")]
Dpapi(dpapi::DpapiSecureKeyContainer),
#[cfg(target_os = "linux")]
Keyctl(keyctl::KeyctlSecureKeyContainer),
#[cfg(target_os = "linux")]
MemfdSecret(memfd_secret::MemfdSecretSecureKeyContainer),
Mlock(mlock::MlockSecureKeyContainer),
}
impl SecureKeyContainer for CrossPlatformSecureKeyContainer {
fn as_key(&self) -> crypto::MemoryEncryptionKey {
match self {
#[cfg(target_os = "windows")]
CrossPlatformSecureKeyContainer::Dpapi(c) => c.as_key(),
#[cfg(target_os = "linux")]
CrossPlatformSecureKeyContainer::Keyctl(c) => c.as_key(),
#[cfg(target_os = "linux")]
CrossPlatformSecureKeyContainer::MemfdSecret(c) => c.as_key(),
CrossPlatformSecureKeyContainer::Mlock(c) => c.as_key(),
}
}
fn from_key(key: crypto::MemoryEncryptionKey) -> Self {
if let Some(container) = get_env_forced_container() {
return container;
}
#[cfg(target_os = "windows")]
{
if dpapi::DpapiSecureKeyContainer::is_supported() {
info!("Using DPAPI for secure key storage");
return CrossPlatformSecureKeyContainer::Dpapi(
dpapi::DpapiSecureKeyContainer::from_key(key),
);
}
}
#[cfg(target_os = "linux")]
{
// Memfd_secret is slightly better in some cases of the kernel being compromised.
// Note that keyctl may sometimes not be available in e.g. snap. Memfd_secret is
// not available on kernels older than 6.5 while keyctl is supported since 2.6.
//
// Note: This may prevent the system from hibernating but not sleeping. Hibernate
// would write the memory to disk, exposing the keys. If this is an issue,
// the environment variable `SECURE_KEY_CONTAINER_BACKEND` can be used
// to force the use of keyctl or mlock.
if memfd_secret::MemfdSecretSecureKeyContainer::is_supported() {
info!("Using memfd_secret for secure key storage");
return CrossPlatformSecureKeyContainer::MemfdSecret(
memfd_secret::MemfdSecretSecureKeyContainer::from_key(key),
);
}
if keyctl::KeyctlSecureKeyContainer::is_supported() {
info!("Using keyctl for secure key storage");
return CrossPlatformSecureKeyContainer::Keyctl(
keyctl::KeyctlSecureKeyContainer::from_key(key),
);
}
}
// Falling back to mlock means that the key is accessible via memory dumping.
info!("Falling back to mlock for secure key storage");
CrossPlatformSecureKeyContainer::Mlock(mlock::MlockSecureKeyContainer::from_key(key))
}
fn is_supported() -> bool {
// Mlock is always supported as a fallback.
true
}
}
fn get_env_forced_container() -> Option<CrossPlatformSecureKeyContainer> {
let env_var = std::env::var("SECURE_KEY_CONTAINER_BACKEND");
match env_var.as_deref() {
#[cfg(target_os = "windows")]
Ok("dpapi") => {
info!("Forcing DPAPI secure key container via environment variable");
Some(CrossPlatformSecureKeyContainer::Dpapi(
dpapi::DpapiSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::new()),
))
}
#[cfg(target_os = "linux")]
Ok("memfd_secret") => {
info!("Forcing memfd_secret secure key container via environment variable");
Some(CrossPlatformSecureKeyContainer::MemfdSecret(
memfd_secret::MemfdSecretSecureKeyContainer::from_key(
crypto::MemoryEncryptionKey::new(),
),
))
}
#[cfg(target_os = "linux")]
Ok("keyctl") => {
info!("Forcing keyctl secure key container via environment variable");
Some(CrossPlatformSecureKeyContainer::Keyctl(
keyctl::KeyctlSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::new()),
))
}
Ok("mlock") => {
info!("Forcing mlock secure key container via environment variable");
Some(CrossPlatformSecureKeyContainer::Mlock(
mlock::MlockSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::new()),
))
}
_ => {
info!(
"{} is not a valid secure key container backend, using automatic selection",
env_var.unwrap_or_default()
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiple_keys() {
// Create 20 different keys
let original_keys: Vec<crypto::MemoryEncryptionKey> = (0..20)
.map(|_| crypto::MemoryEncryptionKey::new())
.collect();
// Store them in secure containers
let containers: Vec<CrossPlatformSecureKeyContainer> = original_keys
.iter()
.map(|key| {
let key_bytes: &[u8; crypto::KEY_SIZE] = key.as_ref().try_into().unwrap();
CrossPlatformSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::from(
key_bytes,
))
})
.collect();
// Read all keys back and validate they match the originals
for (i, (original_key, container)) in
original_keys.iter().zip(containers.iter()).enumerate()
{
let retrieved_key = container.as_key();
assert_eq!(
original_key.as_ref(),
retrieved_key.as_ref(),
"Key {} should match after storage and retrieval",
i
);
}
// Verify all keys are different from each other
for i in 0..original_keys.len() {
for j in (i + 1)..original_keys.len() {
assert_ne!(
original_keys[i].as_ref(),
original_keys[j].as_ref(),
"Keys {} and {} should be different",
i,
j
);
}
}
// Read keys back a second time to ensure consistency
for (i, (original_key, container)) in
original_keys.iter().zip(containers.iter()).enumerate()
{
let retrieved_key_again = container.as_key();
assert_eq!(
original_key.as_ref(),
retrieved_key_again.as_ref(),
"Key {} should still match on second retrieval",
i
);
}
}
}