mirror of
https://github.com/rosenpass/rosenpass.git
synced 2025-12-20 14:32:36 -08:00
362 lines
11 KiB
Rust
362 lines
11 KiB
Rust
//! Types types for dealing with (secret-) values
|
|
//!
|
|
//! These types use type level coloring to make accidential leackage of secrets extra hard. Both [Secret] and [Public] own their data, but the memory backing
|
|
//! [Secret] is special:
|
|
//! - as it is heap allocated, we can actively zeroize the memory before freeing it.
|
|
//! - guard pages before and after each allocation trap accidential sequential reads that creep towards our secrets
|
|
//! - the memory is mlocked, e.g. it is never swapped
|
|
|
|
use crate::{
|
|
sodium::{rng, zeroize},
|
|
util::{cpy, mutating},
|
|
};
|
|
use lazy_static::lazy_static;
|
|
use libsodium_sys as libsodium;
|
|
use std::{
|
|
collections::HashMap,
|
|
convert::TryInto,
|
|
fmt,
|
|
ops::{Deref, DerefMut},
|
|
os::raw::c_void,
|
|
ptr::null_mut,
|
|
sync::Mutex,
|
|
};
|
|
|
|
// 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…
|
|
lazy_static! {
|
|
static ref SECRET_CACHE: Mutex<SecretMemoryPool> = Mutex::new(SecretMemoryPool::new());
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// Further information about the protection in place can be found in in the
|
|
/// [libsodium documentation](https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations)
|
|
#[derive(Debug)] // TODO check on Debug derive, is that clever
|
|
pub struct SecretMemoryPool {
|
|
pool: HashMap<usize, Vec<*mut c_void>>,
|
|
}
|
|
|
|
impl SecretMemoryPool {
|
|
/// Create a new [SecretMemoryPool]
|
|
#[allow(clippy::new_without_default)]
|
|
pub fn new() -> Self {
|
|
let pool = HashMap::new();
|
|
|
|
Self { pool }
|
|
}
|
|
|
|
/// Return secrete back to the pool for future re-use
|
|
///
|
|
/// This consumes the [Secret], but its memory is re-used.
|
|
pub fn release<const N: usize>(&mut self, mut s: Secret<N>) {
|
|
unsafe {
|
|
self.release_by_ref(&mut s);
|
|
}
|
|
std::mem::forget(s);
|
|
}
|
|
|
|
/// Return secret back to the pool for future re-use, by slice
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// After calling this function on a [Secret], the secret must never be
|
|
/// used again for anything.
|
|
unsafe fn release_by_ref<const N: usize>(&mut self, s: &mut Secret<N>) {
|
|
s.zeroize();
|
|
let Secret { ptr: secret } = s;
|
|
// don't call Secret::drop, that could cause a double free
|
|
self.pool.entry(N).or_default().push(*secret);
|
|
}
|
|
|
|
/// 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
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// This function contains an unsafe call to [libsodium::sodium_malloc].
|
|
/// This call has no known safety invariants, thus nothing can go wrong™.
|
|
/// However, just like normal `malloc()` this can return a null ptr. Thus
|
|
/// the returned pointer is checked for null; causing the program to panic
|
|
/// if it is null.
|
|
pub fn take<const N: usize>(&mut self) -> Secret<N> {
|
|
let entry = self.pool.entry(N).or_default();
|
|
let secret = entry.pop().unwrap_or_else(|| {
|
|
let ptr = unsafe { libsodium::sodium_malloc(N) };
|
|
assert!(
|
|
!ptr.is_null(),
|
|
"libsodium::sodium_mallloc() returned a null ptr"
|
|
);
|
|
ptr
|
|
});
|
|
|
|
let mut s = Secret { ptr: secret };
|
|
s.zeroize();
|
|
s
|
|
}
|
|
}
|
|
|
|
impl Drop for SecretMemoryPool {
|
|
/// # Safety
|
|
///
|
|
/// The drop implementation frees the contained elements using
|
|
/// [libsodium::sodium_free]. This is safe as long as every `*mut c_void`
|
|
/// contained was initialized with a call to [libsodium::sodium_malloc]
|
|
fn drop(&mut self) {
|
|
for ptr in self.pool.drain().flat_map(|(_, x)| x.into_iter()) {
|
|
unsafe {
|
|
libsodium::sodium_free(ptr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// # Safety
|
|
///
|
|
/// No safety implications are known, since the `*mut c_void` in
|
|
/// is essentially used like a `&mut u8` [SecretMemoryPool].
|
|
unsafe impl Send for SecretMemoryPool {}
|
|
|
|
/// Store for a secret
|
|
///
|
|
/// Uses memory allocated with [libsodium::sodium_malloc],
|
|
/// esentially can do the same things as `[u8; N].as_mut_ptr()`.
|
|
pub struct Secret<const N: usize> {
|
|
ptr: *mut c_void,
|
|
}
|
|
|
|
impl<const N: usize> Clone for Secret<N> {
|
|
fn clone(&self) -> Self {
|
|
let mut new = Self::zero();
|
|
new.secret_mut().clone_from_slice(self.secret());
|
|
new
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> Drop for Secret<N> {
|
|
fn drop(&mut self) {
|
|
self.zeroize();
|
|
// the invariant that the [Secret] is not used after the
|
|
// `release_by_ref` call is guaranteed, since this is a drop implementation
|
|
unsafe { SECRET_CACHE.lock().unwrap().release_by_ref(self) };
|
|
self.ptr = null_mut();
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> Secret<N> {
|
|
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 s = SECRET_CACHE.lock().unwrap().take();
|
|
assert_eq!(s.secret(), &[0u8; N]);
|
|
s
|
|
}
|
|
|
|
/// Returns a new [Secret] that is randomized
|
|
pub fn random() -> Self {
|
|
mutating(Self::zero(), |r| r.randomize())
|
|
}
|
|
|
|
/// Sets all data of an existing secret to null bytes
|
|
pub fn zeroize(&mut self) {
|
|
zeroize(self.secret_mut());
|
|
}
|
|
|
|
/// Sets all data an existing secret to random bytes
|
|
pub fn randomize(&mut self) {
|
|
rng(self.secret_mut());
|
|
}
|
|
|
|
/// Borrows the data
|
|
pub fn secret(&self) -> &[u8; N] {
|
|
// - calling `from_raw_parts` is safe, because `ptr` is initalized with
|
|
// as `N` byte allocation from the creation of `Secret` onwards. `ptr`
|
|
// stays valid over the full lifetime of `Secret`
|
|
//
|
|
// - calling uwnrap is safe, because we can guarantee that the slice has
|
|
// exactly the required size `N` to create an array of `N` elements.
|
|
let ptr = self.ptr as *const u8;
|
|
let slice = unsafe { std::slice::from_raw_parts(ptr, N) };
|
|
slice.try_into().unwrap()
|
|
}
|
|
|
|
/// Borrows the data mutably
|
|
pub fn secret_mut(&mut self) -> &mut [u8; N] {
|
|
// the same safety argument as for `secret()` holds
|
|
let ptr = self.ptr as *mut u8;
|
|
let slice = unsafe { std::slice::from_raw_parts_mut(ptr, N) };
|
|
slice.try_into().unwrap()
|
|
}
|
|
}
|
|
|
|
/// The Debug implementation of [Secret] does not reveal the secret data,
|
|
/// instead a placeholder `<SECRET>` is used
|
|
impl<const N: usize> fmt::Debug for Secret<N> {
|
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
|
fmt.write_str("<SECRET>")
|
|
}
|
|
}
|
|
|
|
/// Contains information in the form of a byte array that may be known to the
|
|
/// public
|
|
// TODO: We should get rid of the Public type; just use a normal value
|
|
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
|
#[repr(transparent)]
|
|
pub struct Public<const N: usize> {
|
|
pub value: [u8; N],
|
|
}
|
|
|
|
impl<const N: usize> Public<N> {
|
|
/// Create a new [Public] from a byte slice
|
|
pub fn from_slice(value: &[u8]) -> Self {
|
|
mutating(Self::zero(), |r| cpy(value, &mut r.value))
|
|
}
|
|
|
|
/// Create a new [Public] from a byte array
|
|
pub fn new(value: [u8; N]) -> Self {
|
|
Self { value }
|
|
}
|
|
|
|
/// Create a zero initialized [Public]
|
|
pub fn zero() -> Self {
|
|
Self { value: [0u8; N] }
|
|
}
|
|
|
|
/// Create a random initialized [Public]
|
|
pub fn random() -> Self {
|
|
mutating(Self::zero(), |r| r.randomize())
|
|
}
|
|
|
|
/// Randomize all bytes in an existing [Public]
|
|
pub fn randomize(&mut self) {
|
|
rng(&mut self.value);
|
|
}
|
|
}
|
|
|
|
/// Writes the contents of an `&[u8]` as hexadecimal symbols to a [std::fmt::Formatter]
|
|
pub fn debug_crypto_array(v: &[u8], fmt: &mut fmt::Formatter) -> fmt::Result {
|
|
fmt.write_str("[{}]=")?;
|
|
if v.len() > 64 {
|
|
for byte in &v[..32] {
|
|
std::fmt::LowerHex::fmt(byte, fmt)?;
|
|
}
|
|
fmt.write_str("…")?;
|
|
for byte in &v[v.len() - 32..] {
|
|
std::fmt::LowerHex::fmt(byte, fmt)?;
|
|
}
|
|
} else {
|
|
for byte in v {
|
|
std::fmt::LowerHex::fmt(byte, fmt)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
impl<const N: usize> fmt::Debug for Public<N> {
|
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
|
debug_crypto_array(&self.value, fmt)
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> Deref for Public<N> {
|
|
type Target = [u8; N];
|
|
|
|
fn deref(&self) -> &[u8; N] {
|
|
&self.value
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> DerefMut for Public<N> {
|
|
fn deref_mut(&mut self) -> &mut [u8; N] {
|
|
&mut self.value
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
/// https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations
|
|
/// promises us that allocated memory is initialized with this magic byte
|
|
const SODIUM_MAGIC_BYTE: u8 = 0xdb;
|
|
|
|
/// must be called before any interaction with libsodium
|
|
fn init() {
|
|
unsafe { libsodium_sys::sodium_init() };
|
|
}
|
|
|
|
/// checks that whe can malloc with libsodium
|
|
#[test]
|
|
fn sodium_malloc() {
|
|
init();
|
|
const N: usize = 8;
|
|
let ptr = unsafe { libsodium_sys::sodium_malloc(N) };
|
|
let mem = unsafe { std::slice::from_raw_parts(ptr as *mut u8, N) };
|
|
assert_eq!(mem, &[SODIUM_MAGIC_BYTE; N])
|
|
}
|
|
|
|
/// checks that whe can free with libsodium
|
|
#[test]
|
|
fn sodium_free() {
|
|
init();
|
|
const N: usize = 8;
|
|
let ptr = unsafe { libsodium_sys::sodium_malloc(N) };
|
|
unsafe { libsodium_sys::sodium_free(ptr) }
|
|
}
|
|
|
|
/// check that we can alloc using the magic pool
|
|
#[test]
|
|
fn secret_memory_pool_take() {
|
|
init();
|
|
const N: usize = 0x100;
|
|
let mut pool = SecretMemoryPool::new();
|
|
let secret: Secret<N> = pool.take();
|
|
assert_eq!(secret.secret(), &[0; N]);
|
|
}
|
|
|
|
/// check that a secrete lives, even if its [SecretMemoryPool] is deleted
|
|
#[test]
|
|
fn secret_memory_pool_drop() {
|
|
init();
|
|
const N: usize = 0x100;
|
|
let mut pool = SecretMemoryPool::new();
|
|
let secret: Secret<N> = pool.take();
|
|
std::mem::drop(pool);
|
|
assert_eq!(secret.secret(), &[0; N]);
|
|
}
|
|
|
|
/// check that a secrete can be reborn, freshly initialized with zero
|
|
#[test]
|
|
fn secret_memory_pool_release() {
|
|
init();
|
|
const N: usize = 1;
|
|
let mut pool = SecretMemoryPool::new();
|
|
let mut secret: Secret<N> = pool.take();
|
|
let old_secret_ptr = secret.ptr;
|
|
|
|
secret.secret_mut()[0] = 0x13;
|
|
pool.release(secret);
|
|
|
|
// now check that we get the same ptr
|
|
let new_secret: Secret<N> = pool.take();
|
|
assert_eq!(old_secret_ptr, new_secret.ptr);
|
|
|
|
// and that the secret was zeroized
|
|
assert_eq!(new_secret.secret(), &[0; N]);
|
|
}
|
|
}
|