use std::cell::RefCell; use std::collections::HashMap; use std::fmt; use std::ops::{Deref, DerefMut}; use std::path::Path; use anyhow::Context; use rand::{Fill as Randomize, Rng}; use zeroize::{Zeroize, ZeroizeOnDrop}; use rosenpass_util::b64::{b64_decode, b64_encode}; use rosenpass_util::file::{ fopen_r, LoadValue, LoadValueB64, ReadExactToEnd, ReadSliceToEnd, StoreValueB64, StoreValueB64Writer, }; use rosenpass_util::functional::mutating; use crate::alloc::{secret_box, SecretBox, SecretVec}; use crate::file::StoreSecret; use rosenpass_util::file::{fopen_w, Visibility}; use std::io::Write; // This might become a problem in library usage; it's effectively a memory // leak which probably isn't a problem right now because most memory will // be reused… thread_local! { static SECRET_CACHE: RefCell = RefCell::new(SecretMemoryPool::new()); } fn with_secret_memory_pool(mut f: Fn) -> R where Fn: FnMut(Option<&mut SecretMemoryPool>) -> R, { // This acquires the SECRET_CACHE SECRET_CACHE .try_with(|cell| { // And acquires the inner reference cell.try_borrow_mut() .as_deref_mut() // To call the given function .map(|pool| f(Some(pool))) .ok() }) .ok() .flatten() // Failing that, the given function is called with None .unwrap_or_else(|| f(None)) } // Wrapper around SecretBox that applies automatic zeroization #[derive(Debug)] struct ZeroizingSecretBox(Option>); impl ZeroizingSecretBox { fn new(boxed: T) -> Self { ZeroizingSecretBox(Some(secret_box(boxed))) } } impl ZeroizingSecretBox { fn from_secret_box(inner: SecretBox) -> Self { Self(Some(inner)) } fn take(mut self) -> SecretBox { self.0.take().unwrap() } } impl ZeroizeOnDrop for ZeroizingSecretBox {} impl Zeroize for ZeroizingSecretBox { fn zeroize(&mut self) { if let Some(inner) = &mut self.0 { let inner: &mut SecretBox = inner; // type annotation inner.zeroize() } } } impl Drop for ZeroizingSecretBox { fn drop(&mut self) { self.zeroize() } } impl Deref for ZeroizingSecretBox { type Target = T; fn deref(&self) -> &T { self.0.as_ref().unwrap() } } impl DerefMut for ZeroizingSecretBox { fn deref_mut(&mut self) -> &mut T { self.0.as_mut().unwrap() } } /// Pool that stores secret memory allocations /// /// Allocation of secret memory is expensive. Thus, this struct provides a /// pool of secret memory, readily available to yield protected, slices of /// memory. #[derive(Debug)] // TODO check on Debug derive, is that clever struct SecretMemoryPool { pool: HashMap>>, } impl SecretMemoryPool { /// Create a new [SecretMemoryPool] #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { pool: HashMap::new(), } } /// Return secret back to the pool for future re-use pub fn release(&mut self, mut sec: ZeroizingSecretBox<[u8; N]>) { sec.zeroize(); // This conversion sequence is weird but at least it guarantees // that the heap allocation is preserved according to the docs let sec: SecretVec = sec.take().into(); let sec: SecretBox<[u8]> = sec.into(); self.pool .entry(N) .or_default() .push(ZeroizingSecretBox::from_secret_box(sec)); } /// Take protected memory from the pool, allocating new one if no suitable /// chunk is found in the inventory. /// /// The secret is guaranteed to be full of nullbytes pub fn take(&mut self) -> ZeroizingSecretBox<[u8; N]> { let entry = self.pool.entry(N).or_default(); let inner = match entry.pop() { None => secret_box([0u8; N]), Some(sec) => sec.take().try_into().unwrap(), }; ZeroizingSecretBox::from_secret_box(inner) } } /// Storage for secret data pub struct Secret { storage: Option>, } impl Secret { pub fn from_slice(slice: &[u8]) -> Self { let mut new_self = Self::zero(); new_self.secret_mut().copy_from_slice(slice); new_self } /// Returns a new [Secret] that is zero initialized pub fn zero() -> Self { // Using [SecretMemoryPool] here because this operation is expensive, // yet it is used in hot loops let buf = with_secret_memory_pool(|pool| { pool.map(|p| p.take()) .unwrap_or_else(|| ZeroizingSecretBox::new([0u8; N])) }); Self { storage: Some(buf) } } /// Returns a new [Secret] that is randomized pub fn random() -> Self { mutating(Self::zero(), |r| r.randomize()) } /// Sets all data an existing secret to random bytes pub fn randomize(&mut self) { self.try_fill(&mut crate::rand::rng()).unwrap() } /// Borrows the data pub fn secret(&self) -> &[u8; N] { self.storage.as_ref().unwrap() } /// Borrows the data mutably pub fn secret_mut(&mut self) -> &mut [u8; N] { self.storage.as_mut().unwrap() } } impl Randomize for Secret { fn try_fill(&mut self, rng: &mut R) -> Result<(), rand::Error> { // Zeroize self first just to make sure the barriers from the zeroize create take // effect to prevent the compiler from optimizing this away. // We should at some point replace this with our own barriers. self.zeroize(); self.secret_mut().try_fill(rng) } } impl ZeroizeOnDrop for Secret {} impl Zeroize for Secret { fn zeroize(&mut self) { if let Some(inner) = &mut self.storage { inner.zeroize() } } } impl Drop for Secret { fn drop(&mut self) { with_secret_memory_pool(|pool| { if let Some((pool, secret)) = pool.zip(self.storage.take()) { pool.release(secret); } }); // This should be unnecessary: The pool has one item – the inner secret – which // zeroizes itself on drop. Calling it should not do any harm though… self.zeroize() } } impl Clone for Secret { fn clone(&self) -> Self { Self::from_slice(self.secret()) } } /// The Debug implementation of [Secret] does not reveal the secret data, /// instead a placeholder `` is used impl fmt::Debug for Secret { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str("") } } impl LoadValue for Secret { type Error = anyhow::Error; fn load>(path: P) -> anyhow::Result { let mut v = Self::random(); let p = path.as_ref(); fopen_r(p)? .read_exact_to_end(v.secret_mut()) .with_context(|| format!("Could not load file {p:?}"))?; Ok(v) } } impl LoadValueB64 for Secret { type Error = anyhow::Error; fn load_b64>(path: P) -> anyhow::Result { let mut f: Secret = Secret::random(); let mut v = Self::random(); let p = path.as_ref(); let len = fopen_r(p)? .read_slice_to_end(f.secret_mut()) .with_context(|| format!("Could not load file {p:?}"))?; b64_decode(&f.secret()[0..len], v.secret_mut()) .with_context(|| format!("Could not decode base64 file {p:?}"))?; Ok(v) } } impl StoreValueB64 for Secret { type Error = anyhow::Error; fn store_b64>(&self, path: P) -> anyhow::Result<()> { let p = path.as_ref(); let mut f: Secret = Secret::random(); let encoded_str = b64_encode(self.secret(), f.secret_mut()) .with_context(|| format!("Could not encode base64 file {p:?}"))?; fopen_w(p, Visibility::Secret)? .write_all(encoded_str.as_bytes()) .with_context(|| format!("Could not write file {p:?}"))?; f.zeroize(); Ok(()) } } impl StoreValueB64Writer for Secret { type Error = anyhow::Error; fn store_b64_writer(&self, mut writer: W) -> anyhow::Result<()> { let mut f: Secret = Secret::random(); let encoded_str = b64_encode(self.secret(), f.secret_mut()) .with_context(|| "Could not encode secret to base64")?; writer .write_all(encoded_str.as_bytes()) .with_context(|| "Could not write base64 to writer")?; f.zeroize(); Ok(()) } } impl StoreSecret for Secret { type Error = anyhow::Error; fn store_secret>(&self, path: P) -> anyhow::Result<()> { fopen_w(path, Visibility::Secret)?.write_all(self.secret())?; Ok(()) } fn store>(&self, path: P) -> anyhow::Result<()> { fopen_w(path, Visibility::Public)?.write_all(self.secret())?; Ok(()) } } #[cfg(test)] mod test { use crate::test_spawn_process_provided_policies; use super::*; use std::{fs, os::unix::fs::PermissionsExt}; use tempfile::tempdir; procspawn::enable_test_support!(); /// check that we can alloc using the magic pool #[test] fn secret_memory_pool_take() { test_spawn_process_provided_policies!({ const N: usize = 0x100; let mut pool = SecretMemoryPool::new(); let secret: ZeroizingSecretBox<[u8; N]> = pool.take(); assert_eq!(secret.as_ref(), &[0; N]); }); } /// check that a secret lives, even if its [SecretMemoryPool] is deleted #[test] fn secret_memory_pool_drop() { test_spawn_process_provided_policies!({ const N: usize = 0x100; let mut pool = SecretMemoryPool::new(); let secret: ZeroizingSecretBox<[u8; N]> = pool.take(); std::mem::drop(pool); assert_eq!(secret.as_ref(), &[0; N]); }); } /// check that a secret can be reborn, freshly initialized with zero #[test] fn secret_memory_pool_release() { test_spawn_process_provided_policies!({ const N: usize = 1; let mut pool = SecretMemoryPool::new(); let mut secret: ZeroizingSecretBox<[u8; N]> = pool.take(); let old_secret_ptr = secret.as_ref().as_ptr(); secret.as_mut()[0] = 0x13; pool.release(secret); // now check that we get the same ptr let new_secret: ZeroizingSecretBox<[u8; N]> = pool.take(); assert_eq!(old_secret_ptr, new_secret.as_ref().as_ptr()); // and that the secret was zeroized assert_eq!(new_secret.as_ref(), &[0; N]); }); } /// test loading a secret from an example file, and then storing it again in a different file #[test] fn test_secret_load_store() { test_spawn_process_provided_policies!({ const N: usize = 100; // Generate original random bytes let original_bytes: [u8; N] = [rand::random(); N]; // Create a temporary directory let temp_dir = tempdir().unwrap(); // Store the original secret to an example file in the temporary directory let example_file = temp_dir.path().join("example_file"); std::fs::write(&example_file, original_bytes).unwrap(); // Load the secret from the example file let loaded_secret = Secret::load(&example_file).unwrap(); // Check that the loaded secret matches the original bytes assert_eq!(loaded_secret.secret(), &original_bytes); // Store the loaded secret to a different file in the temporary directory let new_file = temp_dir.path().join("new_file"); loaded_secret.store(&new_file).unwrap(); // Read the contents of the new file let new_file_contents = fs::read(&new_file).unwrap(); // Read the contents of the original file let original_file_contents = fs::read(&example_file).unwrap(); // Check that the contents of the new file match the original file assert_eq!(new_file_contents, original_file_contents); }); } /// test loading a base64 encoded secret from an example file, and then storing it again in a different file #[test] fn test_secret_load_store_base64() { test_spawn_process_provided_policies!({ const N: usize = 100; // Generate original random bytes let original_bytes: [u8; N] = [rand::random(); N]; // Create a temporary directory let temp_dir = tempdir().unwrap(); let example_file = temp_dir.path().join("example_file"); let mut encoded_secret = [0u8; N * 2]; let encoded_secret = b64_encode(&original_bytes, &mut encoded_secret).unwrap(); std::fs::write(&example_file, encoded_secret).unwrap(); // Load the secret from the example file let loaded_secret = Secret::load_b64::<{ N * 2 }, _>(&example_file).unwrap(); // Check that the loaded secret matches the original bytes assert_eq!(loaded_secret.secret(), &original_bytes); // Store the loaded secret to a different file in the temporary directory let new_file = temp_dir.path().join("new_file"); loaded_secret.store_b64::<{ N * 2 }, _>(&new_file).unwrap(); // Read the contents of the new file let new_file_contents = fs::read(&new_file).unwrap(); // Read the contents of the original file let original_file_contents = fs::read(&example_file).unwrap(); // Check that the contents of the new file match the original file assert_eq!(new_file_contents, original_file_contents); //Check new file permissions are secret let metadata = fs::metadata(&new_file).unwrap(); assert_eq!(metadata.permissions().mode() & 0o000777, 0o600); // Store the loaded secret to a different file in the temporary directory for a second time let new_file = temp_dir.path().join("new_file_writer"); let new_file_writer = fopen_w(new_file.clone(), Visibility::Secret).unwrap(); loaded_secret .store_b64_writer::<{ N * 2 }, _>(&new_file_writer) .unwrap(); // Read the contents of the new file let new_file_contents = fs::read(&new_file).unwrap(); // Read the contents of the original file let original_file_contents = fs::read(&example_file).unwrap(); // Check that the contents of the new file match the original file assert_eq!(new_file_contents, original_file_contents); //Check new file permissions are secret let metadata = fs::metadata(&new_file).unwrap(); assert_eq!(metadata.permissions().mode() & 0o000777, 0o600); }); } }