Skip to main content

oc_crypto/
secret.rs

1//! Secret types.
2//!
3//! Each secret has its own type rather than `[u8; 32]`, for two reasons. First:
4//! swapping `SecretA` and `SecretB` in a key-schedule call becomes
5//! a compile error rather than a silent vulnerability. Second: fixed length
6//! is encoded in the type rather than convention, a necessary condition for
7//! correct `HKDF-Extract` combining over concatenation: with variable lengths,
8//! encoding would be ambiguous.
9//!
10//! Every type has redacted `Debug`. A secret in a log or panic
11//! report leaks just as surely as one written to disk.
12
13use core::fmt;
14use rand_core::CryptoRng;
15use zeroize::{Zeroize, ZeroizeOnDrop};
16
17/// Length of all secrets in the schedule.
18pub const SECRET_LEN: usize = 32;
19
20macro_rules! secret_type {
21    ($(#[$meta:meta])* $name:ident) => {
22        $(#[$meta])*
23        #[derive(Clone, Zeroize, ZeroizeOnDrop)]
24        pub struct $name([u8; SECRET_LEN]);
25
26        impl $name {
27            /// Take ownership of existing bytes.
28            pub fn from_bytes(bytes: [u8; SECRET_LEN]) -> Self {
29                Self(bytes)
30            }
31
32            /// Generate using the supplied RNG.
33            ///
34            /// The RNG is passed as an argument, not obtained from the environment:
35            /// otherwise tests cease to be deterministic, and Wycheproof
36            /// vectors cannot exercise our own call sites.
37            pub fn random<R: CryptoRng + ?Sized>(rng: &mut R) -> Self {
38                let mut bytes = [0u8; SECRET_LEN];
39                rng.fill_bytes(&mut bytes);
40                let secret = Self(bytes);
41                // Массив на стеке — вторая копия секрета, и `ZeroizeOnDrop`
42                // самого типа её не покрывает: обёрнут результат, а не
43                // заготовка, из которой он собран.
44                bytes.zeroize();
45                secret
46            }
47
48            /// Expose bytes. Deliberately verbose: the call must stand out
49            /// during code reading and review.
50            pub fn expose(&self) -> &[u8; SECRET_LEN] {
51                &self.0
52            }
53        }
54
55        impl fmt::Debug for $name {
56            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57                write!(f, concat!(stringify!($name), "(<секрет скрыт>)"))
58            }
59        }
60    };
61}
62
63secret_type! {
64    /// Content key. Random, derived from nothing: **wrapped**
65    /// under KEK, not generated by it. This is what allows adding a second
66    /// recipient, recovery key, or rotation later without breaking previously issued
67    /// files.
68    Cek
69}
70
71secret_type! {
72    /// Wrapping key derived from both secrets in the 2-of-2 scheme.
73    Kek
74}
75
76secret_type! {
77    /// Server share. Sealed to the license-server key and stored in the container.
78    SecretA
79}
80
81secret_type! {
82    /// Recipient share. In seamless mode, available to anyone holding the file,
83    /// a deliberate cost of seamlessness documented in the specification.
84    SecretB
85}
86
87secret_type! {
88    /// Payload encryption key derived from CEK and header salt.
89    PayloadKey
90}
91
92secret_type! {
93    /// Mutable-region MAC key.
94    MacKey
95}
96
97secret_type! {
98    /// Session MAC key (K24), separate from the mutable-region key.
99    SessionMacKey
100}
101
102secret_type! {
103    /// Private-metadata key (K5): real filename and informational size.
104    ///
105    /// A distinct type, not [`PayloadKey`], despite identical length and derivation method.
106    /// This module's promise that "swapping arguments in a key-schedule call
107    /// becomes a compile error" did not cover K3/K5: both
108    /// derivations returned `PayloadKey`, silently accepting a metadata key
109    /// where a payload key belonged. Domain separation still held
110    /// (different labels, different values), so no silent vulnerability arose,
111    /// but the claimed protection exceeded the actual protection, exactly the case
112    /// for which these types exist.
113    MetaKey
114}
115
116secret_type! {
117    /// Single-use claim code.
118    ///
119    /// Entropy at least [`crate::MIN_CLAIM_BITS`]. Not a cosmetic
120    /// requirement: XChaCha20-Poly1305 is not key-committing, and a low-entropy
121    /// code is recoverable through a partitioning oracle substantially faster than
122    /// exhaustive search.
123    ClaimSecret
124}
125
126secret_type! {
127    /// X25519 private key for sealing slots.
128    X25519Secret
129}
130
131/// Self-wiping plaintext buffer that never grows.
132///
133/// Not `Zeroizing<Vec<u8>>`; the distinction is fundamental. `Zeroizing` wipes
134/// contents **on destruction**, but cannot intervene on growth: when
135/// `Vec` reallocates, it returns its old buffer to the allocator unchanged, including
136/// all accumulated plaintext. A file decrypted into a growing vector
137/// leaves heap copies at every capacity doubling.
138///
139/// Capacity is therefore set once and never changes, while the array always
140/// has its full length: even the "tail" beyond meaningful data must be wiped,
141/// or remnants of a previous longer chunk would survive a subsequent
142/// shorter write.
143///
144/// This type is deliberately required by [`crate::aead::open_chunk`]'s signature. Previously
145/// it accepted an ordinary `Vec<u8>`, so wiping relied on caller
146/// discipline, meaning it relied on nothing.
147pub struct SecretBuf {
148    /// Length always equals capacity, so wiping covers the unused tail too.
149    bytes: Vec<u8>,
150    /// Number of meaningful bytes from the start.
151    len: usize,
152}
153
154impl SecretBuf {
155    /// Allocate a buffer of the specified capacity. It will not grow.
156    pub fn with_capacity(capacity: usize) -> Self {
157        Self { bytes: vec![0u8; capacity], len: 0 }
158    }
159
160    /// Capacity set at creation.
161    pub fn capacity(&self) -> usize {
162        self.bytes.len()
163    }
164
165    /// Number of meaningful bytes.
166    pub fn len(&self) -> usize {
167        self.len
168    }
169
170    pub fn is_empty(&self) -> bool {
171        self.len == 0
172    }
173
174    /// Meaningful bytes.
175    pub fn as_slice(&self) -> &[u8] {
176        self.bytes.get(..self.len).unwrap_or_default()
177    }
178
179    /// The entire buffer for external writing, for example reading from a file.
180    ///
181    /// The buffer **is wiped before being handed out**. Otherwise "get storage,
182    /// declare length" would expose other bytes: a caller writing
183    /// one hundred bytes but declaring two hundred would get its own hundred plus a hundred
184    /// from previous longer contents. For a buffer carrying
185    /// a decrypted document, that exposes part of an adjacent chunk.
186    ///
187    /// After writing, callers declare length with [`SecretBuf::declare_len`].
188    /// Declaring more than they wrote yields zeroes, not someone else's plaintext.
189    pub fn as_capacity_mut(&mut self) -> &mut [u8] {
190        self.wipe();
191        &mut self.bytes
192    }
193
194    /// Declare the meaningful length. Exceeding capacity fails instead of growing.
195    pub fn declare_len(&mut self, len: usize) -> Result<(), crate::CryptoError> {
196        if len > self.capacity() {
197            return Err(crate::CryptoError::BadLength);
198        }
199        self.len = len;
200        Ok(())
201    }
202
203    /// Writable meaningful bytes for transforming contents IN PLACE.
204    ///
205    /// Differs from [`SecretBuf::as_capacity_mut`] in two essential
206    /// ways: only the declared portion is returned, and the buffer is **not**
207    /// wiped before access. In-place decryption needs exactly this: the buffer
208    /// already contains ciphertext, so wiping before decryption would erase
209    /// the input.
210    ///
211    /// Not added for convenience. Without it, decryption would have to allocate
212    /// a plaintext vector and copy it here, routing every
213    /// decrypted chunk through the ordinary heap, whose pages
214    /// can enter the pagefile. The viewer locks its memory
215    /// (`VirtualLock`); an intermediate copy would defeat that.
216    pub fn as_declared_mut(&mut self) -> &mut [u8] {
217        let len = self.len;
218        self.bytes.get_mut(..len).unwrap_or_default()
219    }
220
221    /// Replace contents with a copy of `src`.
222    pub fn fill_from(&mut self, src: &[u8]) -> Result<(), crate::CryptoError> {
223        // Затирание перед записью, а не только длина: без него хвост от более
224        // длинной предыдущей записи остался бы в буфере и дожил бы до конца
225        // работы, хотя логически его уже нет.
226        self.wipe();
227        let room = self.bytes.get_mut(..src.len()).ok_or(crate::CryptoError::BadLength)?;
228        room.copy_from_slice(src);
229        self.len = src.len();
230        Ok(())
231    }
232
233    /// Wipe all contents, including the unused tail.
234    pub fn wipe(&mut self) {
235        self.bytes.zeroize();
236        // `Vec::zeroize` обнуляет длину, а нам нужна полная — восстанавливаем.
237        self.bytes.resize(self.bytes.capacity(), 0);
238        self.len = 0;
239    }
240}
241
242impl Drop for SecretBuf {
243    fn drop(&mut self) {
244        self.bytes.zeroize();
245    }
246}
247
248impl fmt::Debug for SecretBuf {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        write!(f, "SecretBuf({} байт, содержимое скрыто)", self.len)
251    }
252}
253
254/// Stack bytes below the caller wiped by [`wipe_stack_below`].
255///
256/// Chunk decryption and copying a response to the driver descend a few
257/// kilobytes; measurement C2 found plaintext within 6 KiB below the frame.
258/// An order-of-magnitude margin, no more: a system thread-pool thread reserves 1 MiB of stack.
259pub const STACK_WIPE_BYTES: usize = 64 * 1024;
260
261/// Wipe stack BELOW the current frame, where frames of already returned
262/// calls resided.
263///
264/// Why: `SecretBuf` wipes its memory, but decryption and copying
265/// pass through frames placing plaintext fragments on the stack
266/// (temporary cipher blocks, syscall copies), and return does not clear
267/// the stack. Measurement C2: after view cooldown, broker memory retained
268/// 26 document lines, all on the thread stack that had supplied data to the driver.
269///
270/// How: a local array of the same depth is wiped with writes the
271/// compiler cannot remove (`zeroize`), while the function is not inlined,
272/// or the array would occupy the caller's frame rather than space below it. Call AFTER plaintext
273/// processing, from the same depth that invoked that processing.
274#[inline(never)]
275pub fn wipe_stack_below() {
276    let mut pad = [0u8; STACK_WIPE_BYTES];
277    pad.zeroize();
278    core::hint::black_box(&pad);
279}
280
281#[cfg(test)]
282#[allow(clippy::unwrap_used, clippy::panic)]
283mod tests {
284    use super::*;
285
286    struct SeqRng(u8);
287    impl rand_core::TryRng for SeqRng {
288        type Error = core::convert::Infallible;
289        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
290            Ok(u32::from(self.0))
291        }
292        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
293            Ok(u64::from(self.0))
294        }
295        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
296            for b in dst.iter_mut() {
297                self.0 = self.0.wrapping_add(1);
298                *b = self.0;
299            }
300            Ok(())
301        }
302    }
303    impl rand_core::TryCryptoRng for SeqRng {}
304
305    #[test]
306    fn debug_never_leaks_the_bytes() {
307        // Секрет в логе утекает так же, как записанный на диск.
308        let cek = Cek::from_bytes([0xab; SECRET_LEN]);
309        let rendered = format!("{cek:?}");
310        assert!(!rendered.contains("ab"), "Debug выдал байты секрета: {rendered}");
311        assert!(rendered.contains("скрыт"));
312    }
313
314    #[test]
315    fn random_uses_the_supplied_generator() {
316        let mut rng = SeqRng(0);
317        let a = Cek::random(&mut rng);
318        assert_eq!(a.expose()[0], 1, "генератор должен использоваться, а не подменяться");
319        let b = Cek::random(&mut rng);
320        assert_ne!(a.expose(), b.expose());
321    }
322
323    #[test]
324    fn distinct_secret_types_do_not_interchange() {
325        // Компиляционное свойство, зафиксированное тестом как намерение: перепутать
326        // доли схемы 2-из-2 нельзя, они разных типов.
327        let a = SecretA::from_bytes([1; SECRET_LEN]);
328        let b = SecretB::from_bytes([1; SECRET_LEN]);
329        assert_eq!(a.expose(), b.expose(), "байты совпадают");
330        // `let _: SecretA = b;` не компилируется — в этом и смысл.
331    }
332}