krypteia-quantica 0.1.0

Pure-Rust post-quantum cryptography: FIPS 203 ML-KEM, FIPS 204 ML-DSA, and FIPS 205 SLH-DSA. First-order arithmetic masking, shuffled NTT, FORS recompute-and-compare redundancy, constant-time rejection sampling. Targets embedded (no_std), STM32 M0/M4/M33, ESP32-C3 RISC-V. Zero runtime dependencies.
Documentation
//! Zeroize-on-Drop containers for secret key material.
//!
//! `quantica` exposes secret keys, signing keys and shared secrets
//! through wrapper types that automatically wipe their backing memory
//! when dropped, using the constant-time zeroization primitive from
//! the [`silentops`] crate.
//!
//! Two building blocks live here:
//!
//! - `SecretBytes` — heap-allocated, variable-length zeroizing
//!   container, used as the storage for `DecapsulationKey<P>`,
//!   `SigningKey<P>`, and similar types whose length depends on the
//!   parameter set chosen at runtime.
//! - `SecretArray` — stack-allocated, fixed-size zeroizing
//!   container, used for the 32-byte ML-KEM shared secret.
//!
//! Both types implement [`Deref<Target = [u8]>`](core::ops::Deref) so
//! callers can pass them transparently to any function expecting a
//! `&[u8]`.

use alloc::vec::Vec;
use core::fmt;
use core::ops::{Deref, DerefMut};

/// Heap-allocated, variable-length container that wipes its contents
/// on [`Drop`] using `silentops::ct_zeroize`.
///
/// Used as the storage for the secret-half of every key pair in the
/// crate. The wipe is performed via `write_volatile` + a compiler
/// fence so the optimizer is not allowed to elide it.
#[derive(Clone)]
pub struct SecretBytes {
    bytes: Vec<u8>,
}

impl SecretBytes {
    /// Build a [`SecretBytes`] from an existing `Vec<u8>`.
    ///
    /// The original vector is moved into the wrapper; no copy occurs.
    pub fn from_vec(bytes: Vec<u8>) -> Self {
        Self { bytes }
    }

    /// Build a [`SecretBytes`] by copying the contents of `data`.
    pub fn from_slice(data: &[u8]) -> Self {
        Self { bytes: data.to_vec() }
    }

    /// Borrow the secret as a byte slice.
    pub fn as_bytes(&self) -> &[u8] {
        &self.bytes
    }

    /// Length in bytes.
    pub fn len(&self) -> usize {
        self.bytes.len()
    }

    /// Whether the container is empty.
    pub fn is_empty(&self) -> bool {
        self.bytes.is_empty()
    }
}

impl Drop for SecretBytes {
    fn drop(&mut self) {
        silentops::ct_zeroize(&mut self.bytes);
    }
}

impl Deref for SecretBytes {
    type Target = [u8];
    fn deref(&self) -> &[u8] {
        &self.bytes
    }
}

impl DerefMut for SecretBytes {
    fn deref_mut(&mut self) -> &mut [u8] {
        &mut self.bytes
    }
}

impl AsRef<[u8]> for SecretBytes {
    fn as_ref(&self) -> &[u8] {
        &self.bytes
    }
}

/// Redacted [`Debug`] impl: prints the type and length but never the
/// secret bytes themselves, so accidentally logging a key won't leak
/// it.
impl fmt::Debug for SecretBytes {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "SecretBytes(<redacted; len={}>)", self.bytes.len())
    }
}

/// Stack-allocated, fixed-size byte array that wipes itself on
/// [`Drop`].
///
/// Used for shared secrets and other small secret values whose
/// length is known at compile time. The wipe goes through
/// `silentops::ct_zeroize`, which is `write_volatile` + a compiler
/// fence.
#[derive(Clone)]
pub struct SecretArray<const N: usize> {
    bytes: [u8; N],
}

impl<const N: usize> SecretArray<N> {
    /// Wrap a raw `[u8; N]` array.
    pub fn new(bytes: [u8; N]) -> Self {
        Self { bytes }
    }

    /// Borrow the secret as a byte slice.
    pub fn as_bytes(&self) -> &[u8] {
        &self.bytes
    }

    /// Borrow the secret as a fixed-size array reference.
    pub fn as_array(&self) -> &[u8; N] {
        &self.bytes
    }

    /// Length in bytes (always `N`).
    pub fn len(&self) -> usize {
        N
    }

    /// Constant-time equality with another secret of the same length.
    ///
    /// Wraps `silentops::ct_eq` so the comparison itself does not leak
    /// timing information about which byte first differed.
    pub fn ct_eq(&self, other: &Self) -> bool {
        silentops::ct_eq(&self.bytes, &other.bytes) == 1
    }
}

impl<const N: usize> Drop for SecretArray<N> {
    fn drop(&mut self) {
        silentops::ct_zeroize(&mut self.bytes);
    }
}

impl<const N: usize> Deref for SecretArray<N> {
    type Target = [u8];
    fn deref(&self) -> &[u8] {
        &self.bytes
    }
}

impl<const N: usize> AsRef<[u8]> for SecretArray<N> {
    fn as_ref(&self) -> &[u8] {
        &self.bytes
    }
}

impl<const N: usize> PartialEq for SecretArray<N> {
    /// Constant-time equality (delegates to [`SecretArray::ct_eq`]).
    fn eq(&self, other: &Self) -> bool {
        self.ct_eq(other)
    }
}
impl<const N: usize> Eq for SecretArray<N> {}

/// Redacted [`Debug`] impl: prints the type and length but never the
/// secret bytes, so accidentally logging a shared secret won't leak it.
impl<const N: usize> fmt::Debug for SecretArray<N> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "SecretArray<{}>(<redacted>)", N)
    }
}

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

    #[test]
    fn secret_bytes_zeroes_on_drop() {
        // Use the inner Vec's pointer to peek at the heap region after
        // the wrapper is dropped. We can't dereference it (UAF in
        // practice) but we can construct a fresh allocation likely to
        // land on the same spot, and confirm new contents differ. The
        // real guarantee is that ct_zeroize ran, which we test more
        // directly via the public API on the wrapper before Drop.
        let mut s = SecretBytes::from_slice(&[0xAA; 64]);
        for &b in s.as_bytes() {
            assert_eq!(b, 0xAA);
        }
        // Mutate via DerefMut, then re-read.
        s.fill(0x55);
        for &b in s.as_bytes() {
            assert_eq!(b, 0x55);
        }
        // Drop happens at end of scope; the test simply confirms
        // the API surface compiles and behaves linearly.
    }

    #[test]
    fn secret_array_ct_eq() {
        let a = SecretArray::<8>::new([1, 2, 3, 4, 5, 6, 7, 8]);
        let b = SecretArray::<8>::new([1, 2, 3, 4, 5, 6, 7, 8]);
        let c = SecretArray::<8>::new([1, 2, 3, 4, 5, 6, 7, 9]);
        assert!(a == b);
        assert!(a != c);
        assert!(a.ct_eq(&b));
        assert!(!a.ct_eq(&c));
    }
}