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}