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}