Skip to main content

oc_crypto/
secret.rs

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