oc-crypto 0.0.2

Cryptographic primitives and key schemes for Open Crate containers
Documentation
//! Secret types.
//!
//! Each secret has its own type rather than `[u8; 32]`, for two reasons. First:
//! swapping `SecretA` and `SecretB` in a key-schedule call becomes
//! a compile error rather than a silent vulnerability. Second: fixed length
//! is encoded in the type rather than convention, a necessary condition for
//! correct `HKDF-Extract` combining over concatenation: with variable lengths,
//! encoding would be ambiguous.
//!
//! Every type has redacted `Debug`. A secret in a log or panic
//! report leaks just as surely as one written to disk.

use core::fmt;
use rand_core::CryptoRng;
use zeroize::{Zeroize, ZeroizeOnDrop};

/// Length of all secrets in the schedule.
pub const SECRET_LEN: usize = 32;

macro_rules! secret_type {
    ($(#[$meta:meta])* $name:ident) => {
        $(#[$meta])*
        #[derive(Clone, Zeroize, ZeroizeOnDrop)]
        pub struct $name([u8; SECRET_LEN]);

        impl $name {
            /// Take ownership of existing bytes.
            pub fn from_bytes(bytes: [u8; SECRET_LEN]) -> Self {
                Self(bytes)
            }

            /// Generate using the supplied RNG.
            ///
            /// The RNG is passed as an argument, not obtained from the environment:
            /// otherwise tests cease to be deterministic, and Wycheproof
            /// vectors cannot exercise our own call sites.
            pub fn random<R: CryptoRng + ?Sized>(rng: &mut R) -> Self {
                let mut bytes = [0u8; SECRET_LEN];
                rng.fill_bytes(&mut bytes);
                let secret = Self(bytes);
                // Массив на стеке — вторая копия секрета, и `ZeroizeOnDrop`
                // самого типа её не покрывает: обёрнут результат, а не
                // заготовка, из которой он собран.
                bytes.zeroize();
                secret
            }

            /// Expose bytes. Deliberately verbose: the call must stand out
            /// during code reading and review.
            pub fn expose(&self) -> &[u8; SECRET_LEN] {
                &self.0
            }
        }

        impl fmt::Debug for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, concat!(stringify!($name), "(<секрет скрыт>)"))
            }
        }
    };
}

secret_type! {
    /// Content key. Random, derived from nothing: **wrapped**
    /// under KEK, not generated by it. This is what allows adding a second
    /// recipient, recovery key, or rotation later without breaking previously issued
    /// files.
    Cek
}

secret_type! {
    /// Wrapping key derived from both secrets in the 2-of-2 scheme.
    Kek
}

secret_type! {
    /// Server share. Sealed to the license-server key and stored in the container.
    SecretA
}

secret_type! {
    /// Recipient share. In seamless mode, available to anyone holding the file,
    /// a deliberate cost of seamlessness documented in the specification.
    SecretB
}

secret_type! {
    /// Payload encryption key derived from CEK and header salt.
    PayloadKey
}

secret_type! {
    /// Mutable-region MAC key.
    MacKey
}

secret_type! {
    /// Session MAC key (K24), separate from the mutable-region key.
    SessionMacKey
}

secret_type! {
    /// Private-metadata key (K5): real filename and informational size.
    ///
    /// A distinct type, not [`PayloadKey`], despite identical length and derivation method.
    /// This module's promise that "swapping arguments in a key-schedule call
    /// becomes a compile error" did not cover K3/K5: both
    /// derivations returned `PayloadKey`, silently accepting a metadata key
    /// where a payload key belonged. Domain separation still held
    /// (different labels, different values), so no silent vulnerability arose,
    /// but the claimed protection exceeded the actual protection, exactly the case
    /// for which these types exist.
    MetaKey
}

secret_type! {
    /// Single-use claim code.
    ///
    /// Entropy at least [`crate::MIN_CLAIM_BITS`]. Not a cosmetic
    /// requirement: XChaCha20-Poly1305 is not key-committing, and a low-entropy
    /// code is recoverable through a partitioning oracle substantially faster than
    /// exhaustive search.
    ClaimSecret
}

secret_type! {
    /// X25519 private key for sealing slots.
    X25519Secret
}

/// Self-wiping plaintext buffer that never grows.
///
/// Not `Zeroizing<Vec<u8>>`; the distinction is fundamental. `Zeroizing` wipes
/// contents **on destruction**, but cannot intervene on growth: when
/// `Vec` reallocates, it returns its old buffer to the allocator unchanged, including
/// all accumulated plaintext. A file decrypted into a growing vector
/// leaves heap copies at every capacity doubling.
///
/// Capacity is therefore set once and never changes, while the array always
/// has its full length: even the "tail" beyond meaningful data must be wiped,
/// or remnants of a previous longer chunk would survive a subsequent
/// shorter write.
///
/// This type is deliberately required by [`crate::aead::open_chunk`]'s signature. Previously
/// it accepted an ordinary `Vec<u8>`, so wiping relied on caller
/// discipline, meaning it relied on nothing.
pub struct SecretBuf {
    /// Length always equals capacity, so wiping covers the unused tail too.
    bytes: Vec<u8>,
    /// Number of meaningful bytes from the start.
    len: usize,
}

impl SecretBuf {
    /// Allocate a buffer of the specified capacity. It will not grow.
    pub fn with_capacity(capacity: usize) -> Self {
        Self { bytes: vec![0u8; capacity], len: 0 }
    }

    /// Capacity set at creation.
    pub fn capacity(&self) -> usize {
        self.bytes.len()
    }

    /// Number of meaningful bytes.
    pub fn len(&self) -> usize {
        self.len
    }

    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    /// Meaningful bytes.
    pub fn as_slice(&self) -> &[u8] {
        self.bytes.get(..self.len).unwrap_or_default()
    }

    /// The entire buffer for external writing, for example reading from a file.
    ///
    /// The buffer **is wiped before being handed out**. Otherwise "get storage,
    /// declare length" would expose other bytes: a caller writing
    /// one hundred bytes but declaring two hundred would get its own hundred plus a hundred
    /// from previous longer contents. For a buffer carrying
    /// a decrypted document, that exposes part of an adjacent chunk.
    ///
    /// After writing, callers declare length with [`SecretBuf::declare_len`].
    /// Declaring more than they wrote yields zeroes, not someone else's plaintext.
    pub fn as_capacity_mut(&mut self) -> &mut [u8] {
        self.wipe();
        &mut self.bytes
    }

    /// Declare the meaningful length. Exceeding capacity fails instead of growing.
    pub fn declare_len(&mut self, len: usize) -> Result<(), crate::CryptoError> {
        if len > self.capacity() {
            return Err(crate::CryptoError::BadLength);
        }
        self.len = len;
        Ok(())
    }

    /// Writable meaningful bytes for transforming contents IN PLACE.
    ///
    /// Differs from [`SecretBuf::as_capacity_mut`] in two essential
    /// ways: only the declared portion is returned, and the buffer is **not**
    /// wiped before access. In-place decryption needs exactly this: the buffer
    /// already contains ciphertext, so wiping before decryption would erase
    /// the input.
    ///
    /// Not added for convenience. Without it, decryption would have to allocate
    /// a plaintext vector and copy it here, routing every
    /// decrypted chunk through the ordinary heap, whose pages
    /// can enter the pagefile. The viewer locks its memory
    /// (`VirtualLock`); an intermediate copy would defeat that.
    pub fn as_declared_mut(&mut self) -> &mut [u8] {
        let len = self.len;
        self.bytes.get_mut(..len).unwrap_or_default()
    }

    /// Replace contents with a copy of `src`.
    pub fn fill_from(&mut self, src: &[u8]) -> Result<(), crate::CryptoError> {
        // Затирание перед записью, а не только длина: без него хвост от более
        // длинной предыдущей записи остался бы в буфере и дожил бы до конца
        // работы, хотя логически его уже нет.
        self.wipe();
        let room = self.bytes.get_mut(..src.len()).ok_or(crate::CryptoError::BadLength)?;
        room.copy_from_slice(src);
        self.len = src.len();
        Ok(())
    }

    /// Wipe all contents, including the unused tail.
    pub fn wipe(&mut self) {
        self.bytes.zeroize();
        // `Vec::zeroize` обнуляет длину, а нам нужна полная — восстанавливаем.
        self.bytes.resize(self.bytes.capacity(), 0);
        self.len = 0;
    }
}

impl Drop for SecretBuf {
    fn drop(&mut self) {
        self.bytes.zeroize();
    }
}

impl fmt::Debug for SecretBuf {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "SecretBuf({} байт, содержимое скрыто)", self.len)
    }
}

/// Stack bytes below the caller wiped by [`wipe_stack_below`].
///
/// Chunk decryption and copying a response to the driver descend a few
/// kilobytes; measurement C2 found plaintext within 6 KiB below the frame.
/// An order-of-magnitude margin, no more: a system thread-pool thread reserves 1 MiB of stack.
pub const STACK_WIPE_BYTES: usize = 64 * 1024;

/// Wipe stack BELOW the current frame, where frames of already returned
/// calls resided.
///
/// Why: `SecretBuf` wipes its memory, but decryption and copying
/// pass through frames placing plaintext fragments on the stack
/// (temporary cipher blocks, syscall copies), and return does not clear
/// the stack. Measurement C2: after view cooldown, broker memory retained
/// 26 document lines, all on the thread stack that had supplied data to the driver.
///
/// How: a local array of the same depth is wiped with writes the
/// compiler cannot remove (`zeroize`), while the function is not inlined,
/// or the array would occupy the caller's frame rather than space below it. Call AFTER plaintext
/// processing, from the same depth that invoked that processing.
#[inline(never)]
pub fn wipe_stack_below() {
    let mut pad = [0u8; STACK_WIPE_BYTES];
    pad.zeroize();
    core::hint::black_box(&pad);
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
    use super::*;

    struct SeqRng(u8);
    impl rand_core::TryRng for SeqRng {
        type Error = core::convert::Infallible;
        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
            Ok(u32::from(self.0))
        }
        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
            Ok(u64::from(self.0))
        }
        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
            for b in dst.iter_mut() {
                self.0 = self.0.wrapping_add(1);
                *b = self.0;
            }
            Ok(())
        }
    }
    impl rand_core::TryCryptoRng for SeqRng {}

    #[test]
    fn debug_never_leaks_the_bytes() {
        // Секрет в логе утекает так же, как записанный на диск.
        let cek = Cek::from_bytes([0xab; SECRET_LEN]);
        let rendered = format!("{cek:?}");
        assert!(!rendered.contains("ab"), "Debug выдал байты секрета: {rendered}");
        assert!(rendered.contains("скрыт"));
    }

    #[test]
    fn random_uses_the_supplied_generator() {
        let mut rng = SeqRng(0);
        let a = Cek::random(&mut rng);
        assert_eq!(a.expose()[0], 1, "генератор должен использоваться, а не подменяться");
        let b = Cek::random(&mut rng);
        assert_ne!(a.expose(), b.expose());
    }

    #[test]
    fn distinct_secret_types_do_not_interchange() {
        // Компиляционное свойство, зафиксированное тестом как намерение: перепутать
        // доли схемы 2-из-2 нельзя, они разных типов.
        let a = SecretA::from_bytes([1; SECRET_LEN]);
        let b = SecretB::from_bytes([1; SECRET_LEN]);
        assert_eq!(a.expose(), b.expose(), "байты совпадают");
        // `let _: SecretA = b;` не компилируется — в этом и смысл.
    }
}