entropy-auth 2026.7.31

Authentication and authorization for Entropy Softworks server and API projects
//! Secure memory clearing for secret material.
//!
//! Provides [`zeroize`] to overwrite byte buffers with zeros, and
//! [`Zeroizing`] — a wrapper that automatically clears its contents when
//! dropped. Together they ensure that secrets (keys, tokens, passwords)
//! do not linger in process memory after use.
//!
//! # Security rationale
//!
//! A naive `buf.fill(0)` can be elided by the compiler when the buffer
//! is never read again. We use [`std::ptr::write_volatile`] to guarantee
//! the write is emitted. This [`zeroize`] function is the crate's only
//! `unsafe` primitive; the SHA-1/SHA-2 compression-state scrubbing reuses the
//! same `write_volatile` pattern, and no other production code contains
//! `unsafe`.
//!
//! # Approach
//!
//! Types that hold secret bytes implement [`ZeroizeOnDrop`], which
//! [`Zeroizing<T>`] calls during [`Drop`]. This keeps the `unsafe`
//! surface area minimal — only the [`zeroize`] function itself.

use std::fmt;
use std::ops::{Deref, DerefMut};

// ---------------------------------------------------------------------------
// Core zeroize function
// ---------------------------------------------------------------------------

/// Overwrites every byte in `buf` with `0x00`.
///
/// Uses [`std::ptr::write_volatile`] to prevent the compiler from
/// eliding the write when `buf` is not read afterwards. This is
/// critical for erasing secret material from memory.
///
/// # Examples
///
/// ```
/// use entropy_auth::crypto::zeroize::zeroize;
///
/// let mut key = vec![0xAB; 32];
/// zeroize(&mut key);
/// assert!(key.iter().all(|&b| b == 0));
/// ```
#[allow(unsafe_code)]
pub fn zeroize(buf: &mut [u8]) {
    for byte in buf.iter_mut() {
        // SAFETY: `byte` is a valid, aligned, dereferenceable pointer
        // obtained from a mutable slice reference. `write_volatile`
        // guarantees the store is not optimised away.
        unsafe {
            std::ptr::write_volatile(byte, 0);
        }
    }
}

// ---------------------------------------------------------------------------
// ZeroizeOnDrop trait
// ---------------------------------------------------------------------------

/// Trait for types whose backing memory should be zeroized on drop.
///
/// Implement this for any type that can hold secret bytes so that
/// [`Zeroizing<T>`] can clear them automatically.
///
/// # Security
///
/// For the growable `Vec<u8>` / `String` implementations the entire current
/// allocation is cleared — both the initialized length **and** any spare
/// capacity, since a secret written then truncated (or an over-allocating
/// `push`) can leave residue in the slack between `len` and `capacity`. The
/// one residual hazard this cannot cover is *reallocation*: if a buffer is
/// grown (and thus reallocated) *after* secret bytes are written, the
/// previous allocation is freed without being scrubbed. Size sensitive
/// buffers to their final length before writing secrets into them.
///
/// As a side effect of scrubbing the spare capacity, the `Vec<u8>`
/// implementation grows `len` to `capacity`, and the `String` implementation
/// empties `self` (its buffer is moved out, scrubbed, and dropped). The value
/// is being dropped, so this is invisible in the normal [`Zeroizing`] flow.
pub trait ZeroizeOnDrop {
    /// Overwrites the secret bytes held by this value — including spare
    /// capacity — with zeros.
    fn zeroize_contents(&mut self);
}

impl ZeroizeOnDrop for Vec<u8> {
    fn zeroize_contents(&mut self) {
        // Extend `len` to cover the full allocation so the volatile zeroize
        // also scrubs spare capacity. `resize` to exactly `capacity()` cannot
        // reallocate (no orphaned allocation is left behind), and `0u8` needs
        // no drop. This is plain-safe — no `unsafe` needed for `Vec<u8>`.
        let cap = self.capacity();
        self.resize(cap, 0);
        zeroize(self.as_mut_slice());
    }
}

impl ZeroizeOnDrop for String {
    fn zeroize_contents(&mut self) {
        // Move the backing buffer out — `into_bytes` reinterprets the same
        // allocation as a `Vec<u8>` with no copy or reallocation — and scrub
        // it through the `Vec<u8>` impl above (which also covers spare
        // capacity). `self` is left as an empty `String`. Routing through the
        // safe `Vec<u8>` path keeps `unsafe` out of the `String` scrub.
        let mut bytes = std::mem::take(self).into_bytes();
        bytes.zeroize_contents();
    }
}

// ---------------------------------------------------------------------------
// Zeroizing<T> wrapper
// ---------------------------------------------------------------------------

/// A wrapper that calls [`ZeroizeOnDrop::zeroize_contents`] on the
/// inner value when it is dropped.
///
/// Use this for any value that holds secret material (keys, tokens,
/// passwords) to ensure it is cleared from memory deterministically.
///
/// `Deref` and `DerefMut` are implemented so the inner value is
/// accessible transparently. `Display` is deliberately **not**
/// implemented — secrets should not be printable via `{}`.
///
/// # Examples
///
/// ```
/// use entropy_auth::crypto::zeroize::Zeroizing;
///
/// let secret = Zeroizing::new(vec![0xFFu8; 16]);
/// assert_eq!(secret.len(), 16);
/// // secret is zeroized when it goes out of scope
/// ```
pub struct Zeroizing<T: ZeroizeOnDrop> {
    inner: T,
}

impl<T: ZeroizeOnDrop> Zeroizing<T> {
    /// Wraps `value` so that it will be zeroized on drop.
    #[must_use]
    pub fn new(value: T) -> Self {
        Self { inner: value }
    }
}

// SECURITY: `Debug` is redacted to prevent accidental logging of secrets.
impl<T: ZeroizeOnDrop> fmt::Debug for Zeroizing<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("Zeroizing([REDACTED])")
    }
}

impl<T: ZeroizeOnDrop> Deref for Zeroizing<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.inner
    }
}

impl<T: ZeroizeOnDrop> DerefMut for Zeroizing<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.inner
    }
}

impl<T: ZeroizeOnDrop + Clone> Clone for Zeroizing<T> {
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
        }
    }
}

// SECURITY: PartialEq is deliberately NOT implemented for Zeroizing<T>.
// A non-constant-time comparison on secret material would create a timing
// side-channel. All secret comparisons must go through constant_time_eq
// on the raw bytes instead.

impl<T: ZeroizeOnDrop> Drop for Zeroizing<T> {
    fn drop(&mut self) {
        self.inner.zeroize_contents();
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    // --- Zeroize Function ---

    #[test]
    fn zeroize_clears_buffer() {
        let mut buf = vec![0xDE, 0xAD, 0xBE, 0xEF];
        zeroize(&mut buf);
        assert!(buf.iter().all(|&b| b == 0), "all bytes should be zero");
    }

    #[test]
    fn zeroize_empty_buffer() {
        let mut buf: Vec<u8> = Vec::new();
        zeroize(&mut buf); // must not panic
        assert!(buf.is_empty());
    }

    // --- Zeroizing Wrapper ---

    #[test]
    fn zeroize_contents_clears_vec_bytes() {
        let mut vec = vec![0xFFu8; 64];
        assert!(vec.iter().all(|&b| b == 0xFF));

        vec.zeroize_contents();

        assert!(
            vec.iter().all(|&b| b == 0),
            "vec contents should be zeroed after zeroize_contents",
        );
    }

    #[test]
    fn zeroize_contents_clears_string_bytes() {
        // `String::zeroize_contents` moves the backing buffer into a `Vec<u8>`,
        // scrubs it via the (separately tested) `Vec` impl, then drops it —
        // leaving `self` empty. The empty post-state is directly observable.
        let mut s = String::from("super-secret-password");
        assert!(!s.is_empty());

        s.zeroize_contents();
        assert!(
            s.is_empty(),
            "string is emptied once its buffer is scrubbed"
        );

        // The byte-scrubbing guarantee, exercised on a buffer obtained exactly
        // as the `String` impl obtains it (`into_bytes` — same allocation).
        let mut bytes = String::from("super-secret-password").into_bytes();
        bytes.zeroize_contents();
        assert!(
            bytes.iter().all(|&b| b == 0),
            "string-backed buffer must be zeroed after zeroize_contents",
        );
    }

    #[test]
    fn zeroize_contents_scrubs_spare_capacity() {
        // A secret written then truncated leaves residue in the slack between
        // len and capacity; zeroize_contents must scrub the whole allocation.
        let mut vec = Vec::with_capacity(64);
        vec.extend_from_slice(&[0xAAu8; 64]);
        vec.truncate(8); // bytes 8..64 are now slack still holding 0xAA
        let cap = vec.capacity();

        vec.zeroize_contents();

        // After scrubbing, len grows to capacity and every byte (incl. the
        // former slack) is zero.
        assert_eq!(vec.len(), cap);
        assert!(
            vec.iter().all(|&b| b == 0),
            "spare capacity must be scrubbed"
        );
    }

    #[test]
    fn drop_invokes_zeroize_contents() {
        // The load-bearing guarantee: dropping a `Zeroizing<T>` actually calls
        // `zeroize_contents`. A spy type records the call during its own drop
        // path (driven by `Zeroizing::drop`), which we observe afterwards via
        // a shared flag — no use-after-free.
        use std::cell::Cell;
        use std::rc::Rc;

        struct Spy {
            zeroized: Rc<Cell<bool>>,
        }
        impl ZeroizeOnDrop for Spy {
            fn zeroize_contents(&mut self) {
                self.zeroized.set(true);
            }
        }

        let flag = Rc::new(Cell::new(false));
        {
            let _z = Zeroizing::new(Spy {
                zeroized: Rc::clone(&flag),
            });
            assert!(!flag.get(), "not zeroized before drop");
        } // _z dropped here
        assert!(flag.get(), "Zeroizing::drop must call zeroize_contents");
    }

    // --- Debug Redaction ---

    #[test]
    fn debug_redacts_contents() {
        let secret = Zeroizing::new(vec![1, 2, 3]);
        let debug_output = format!("{secret:?}");
        assert_eq!(debug_output, "Zeroizing([REDACTED])");
        assert!(
            !debug_output.contains('1'),
            "debug output must not leak contents",
        );
    }

    // --- Deref / Clone / PartialEq ---

    #[test]
    fn deref_provides_access() {
        let secret = Zeroizing::new(vec![10u8, 20, 30]);
        // Deref to &Vec<u8>
        assert_eq!(secret.len(), 3);
        assert_eq!(secret[0], 10);

        let mut secret_mut = Zeroizing::new(vec![0u8; 4]);
        // DerefMut to &mut Vec<u8>
        secret_mut[2] = 42;
        assert_eq!(secret_mut[2], 42);
    }

    #[test]
    fn clone_produces_independent_copy() {
        let original = Zeroizing::new(vec![0xAA; 8]);
        let mut cloned = original.clone();

        // Mutating the clone must not affect the original.
        cloned[0] = 0x00;
        assert_eq!(original[0], 0xAA);
        assert_eq!(cloned[0], 0x00);
    }

    // --- PartialEq deliberately NOT implemented ---
    //
    // SECURITY: Zeroizing<T> intentionally does not implement PartialEq
    // to prevent accidental non-constant-time comparison of secrets.
    // All secret comparisons must use constant_time_eq on the raw bytes.
    #[test]
    fn zeroizing_is_not_partial_eq() {
        // Verify that Zeroizing<Vec<u8>> does NOT implement PartialEq
        // by confirming we can still compare the inner contents via Deref
        // when needed (in tests only, through constant_time_eq in prod).
        let a = Zeroizing::new(vec![1, 2, 3]);
        let b = Zeroizing::new(vec![1, 2, 3]);
        // Use Deref to access inner values for test-only comparison.
        assert_eq!(&*a, &*b, "inner values should be equal via Deref");
    }
}