secure_gate/sealed/prekey.rs
1//! The pre-key: 16 KiB of random bytes every sealing key is derived from.
2
3use alloc::sync::Arc;
4
5use chacha20poly1305::aead::AeadInOut;
6use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce};
7use guarded_block::GuardedBlock;
8use rand::{TryRng, rngs::SysRng};
9use sha2::{Digest, Sha256};
10use zeroize::Zeroize;
11
12use crate::{Fixed, RevealSecret};
13
14/// Size of the pre-key in bytes: four 4 KiB pages, 131,072 bits.
15pub(crate) const PREKEY_LEN: usize = 16 * 1024;
16
17/// Size of the detached ChaCha20-Poly1305 tag stored beside each ciphertext.
18pub(crate) const TAG_LEN: usize = 16;
19
20/// Size of the random salt drawn for every seal.
21pub(crate) const SALT_LEN: usize = 32;
22
23/// Why the pre-key's pages are not locked in RAM. Returned by [`Prekey::lock_status`].
24///
25/// Not fatal: the sealed types work the same either way. What is lost is the guarantee
26/// that the pre-key never reaches the swap file. On Windows the usual cause is the
27/// process working-set quota, which every `VirtualLock` in the process shares; on Unix it
28/// is `RLIMIT_MEMLOCK`.
29///
30/// It is also returned, without asking the operating system, for a pre-key made with
31/// [`Prekey::new_idle_noaccess`] on Windows, where a no-access page cannot stay locked, and
32/// in a `fork()` child of the process that made the pre-key, which does not inherit its lock.
33///
34/// Carries the operating system's error code, never an address.
35///
36/// # Example
37///
38/// Report the reason, and raise the quota where it is the cause:
39///
40/// ```
41/// use secure_gate::Prekey;
42///
43/// let prekey = Prekey::new();
44/// match prekey.lock_status() {
45/// Ok(()) => {}
46/// // ERROR_WORKING_SET_QUOTA: every `VirtualLock` in a Windows process shares the
47/// // working-set quota (about eleven pre-keys under the default). Raise it at startup
48/// // with `SetProcessWorkingSetSize` if the application holds more.
49/// Err(why) if why.raw_os_error() == Some(1453) => eprintln!("raise the quota: {why}"),
50/// Err(why) => eprintln!("sealed secrets work, but may reach swap: {why}"),
51/// }
52/// ```
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct LockError {
55 os_code: Option<i32>,
56 idle_noaccess: bool,
57 forked: bool,
58}
59
60impl LockError {
61 fn from_guarded(err: guarded_block::LockError) -> Self {
62 Self {
63 os_code: err.raw_os_error(),
64 idle_noaccess: err.idle_noaccess(),
65 forked: err.forked(),
66 }
67 }
68
69 /// The operating system's error code (`GetLastError` on Windows, `errno` on Unix), when
70 /// the lock was attempted and refused.
71 pub fn raw_os_error(&self) -> Option<i32> {
72 self.os_code
73 }
74
75 /// `true` when locking was not attempted because the pre-key was made with
76 /// [`Prekey::new_idle_noaccess`] on Windows: a no-access page leaves the process's
77 /// working set, and its lock with it.
78 pub fn idle_noaccess(&self) -> bool {
79 self.idle_noaccess
80 }
81
82 /// `true` in a `fork()` child of the process that made the pre-key: page locks are not
83 /// inherited, so the child holds the pre-key unlocked whatever the parent was granted.
84 pub fn forked(&self) -> bool {
85 self.forked
86 }
87}
88
89impl core::fmt::Display for LockError {
90 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91 if self.forked {
92 return f.write_str(
93 "the sealed-memory pre-key is not locked in RAM in this process: it was made \
94 before a fork, and page locks are not inherited",
95 );
96 }
97 match (self.idle_noaccess, self.os_code) {
98 (true, _) => f.write_str(
99 "the sealed-memory pre-key is not locked in RAM: it is no-access while idle \
100 (Prekey::new_idle_noaccess), and on Windows a no-access page cannot stay locked",
101 ),
102 (false, Some(code)) => write!(
103 f,
104 "the sealed-memory pre-key could not be locked in RAM (OS error {code})"
105 ),
106 (false, None) => f.write_str("the sealed-memory pre-key could not be locked in RAM"),
107 }
108 }
109}
110
111impl std::error::Error for LockError {}
112
113/// The 16 KiB of random bytes that every sealing key is derived from, in memory of its own
114/// between no-access guard pages, read-only, and locked in RAM where the operating system
115/// allows it.
116///
117/// Each [`SealedFixed`](crate::SealedFixed) and [`SealedDynamic`](crate::SealedDynamic)
118/// keeps an `Arc` to the `Prekey` it was sealed under and derives its key afresh on every
119/// access as `SHA-256(salt ‖ pre-key)`. The key itself is never stored, and the stack the
120/// cipher used is overwritten after each access, since the cipher leaves a copy there. A
121/// lossy readout of memory (Spectre-class side channels, Rowhammer, cold boot) that gets any
122/// one of the pre-key's 131,072 bits wrong yields a useless key, with nothing to say which
123/// bit was wrong. See the [`sealed`](crate::sealed) module for the whole threat model.
124///
125/// # Memory
126///
127/// The pre-key is not on the heap. It has a mapping of its own (from the
128/// [`guarded-block`](https://crates.io/crates/guarded-block) crate, which holds the `unsafe`
129/// this crate forbids), laid out as a no-access guard page, the pre-key's pages, and a
130/// no-access guard page:
131///
132/// - A linear over-read from a buffer below it, such as a C library's in the same process
133/// (Heartbleed-style), faults on the guard page instead of reading the pre-key.
134/// - It is read-only from the moment it is filled until it drops.
135/// - Its pages are locked (`VirtualLock` on Windows, `mlock` on Unix), and on Linux
136/// marked `MADV_DONTDUMP`, which the kernel documents as leaving it out of core dumps.
137/// - With [`new_idle_noaccess`](Self::new_idle_noaccess) it is also no-access except while a
138/// sealing key is being derived from it (unless the system refuses to make it no-access
139/// again), which on Windows costs the lock.
140///
141/// # Lifetime
142///
143/// The pre-key is not a `static`. It lives as long as the last sealed object, or the last
144/// other `Arc`, that refers to it. When that one drops, the pre-key is wiped (while its
145/// pages are still locked, if the lock was granted), then unlocked, then unmapped. An application typically holds one
146/// `Prekey` for a session and drops it, along with everything sealed under it, at lock or
147/// logout.
148///
149/// # Locking
150///
151/// [`new`](Self::new) asks the operating system to keep the pre-key's pages out of swap and
152/// never fails because it could not: [`lock_status`](Self::lock_status) reports the outcome
153/// so the caller can decide. Locking covers swap only. Hibernation and a clean read of the
154/// process's memory (a debugger, `/proc/<pid>/mem`) still see the pre-key, and so does a
155/// crash dump on Windows (unless the pre-key is idle no-access) and on macOS.
156///
157/// # Example
158///
159/// One pre-key per session, created at login and dropped at logout with everything sealed
160/// under it:
161///
162/// ```
163/// use std::sync::Arc;
164/// use secure_gate::{Dynamic, Fixed, Prekey, RevealSecret, SealedDynamic, SealedFixed};
165///
166/// struct Session {
167/// prekey: Arc<Prekey>,
168/// db_key: SealedFixed<32>,
169/// password: SealedDynamic<String>,
170/// }
171///
172/// fn login(db_key: Fixed<[u8; 32]>, password: Dynamic<String>) -> Session {
173/// let prekey = Prekey::new();
174/// if let Err(why) = prekey.lock_status() {
175/// eprintln!("{why}"); // not fatal; see `LockError`
176/// }
177/// Session {
178/// db_key: SealedFixed::seal(db_key, &prekey),
179/// password: SealedDynamic::seal(password, &prekey),
180/// prekey,
181/// }
182/// }
183///
184/// let session = login(Fixed::new([7u8; 32]), Dynamic::new(String::from("hunter2")));
185/// session.db_key.with_secret(|k| assert_eq!(k[0], 7));
186/// session.password.with_secret(|p| assert_eq!(p, "hunter2"));
187/// drop(session); // logout: the sealed values, then the pre-key, are wiped
188/// ```
189///
190/// [`new_idle_noaccess`](Self::new_idle_noaccess) is the alternative where crash dumps or
191/// same-user memory reads matter more than the pagefile; its docs compare the two.
192///
193/// # Fork
194///
195/// A child created by `fork()` shares the parent's pre-key bytes (copy-on-write) but not
196/// its page locks, and [`lock_status`](Self::lock_status) says so in the child. A child that
197/// should not hold the parent's sealing material creates its own `Prekey` and re-seals what
198/// it keeps.
199///
200/// A child of a *multithreaded* parent must not use an inherited
201/// [idle no-access](Self::new_idle_noaccess) pre-key at all: a thread that was mid-access at
202/// the fork leaves the child its open window (the pre-key stays readable) or its held mutex
203/// (the child's first access deadlocks). POSIX allows such a child only async-signal-safe
204/// calls until it execs in any case.
205pub struct Prekey {
206 block: GuardedBlock<PREKEY_LEN>,
207}
208
209impl Prekey {
210 /// Generates a pre-key from the system RNG into guarded, read-only memory, and tries
211 /// to lock its pages in RAM.
212 ///
213 /// Returns an `Arc`, because every sealed object keeps one.
214 ///
215 /// # Panics
216 ///
217 /// Panics if the system RNG fails, as [`Fixed::from_random`] does, or if the operating
218 /// system refuses to map the pre-key or protect its pages (memory or address-space
219 /// exhaustion). A failure to lock is not a panic; see [`lock_status`](Self::lock_status).
220 pub fn new() -> Arc<Self> {
221 Arc::new(Self::create(false))
222 }
223
224 /// As [`new`](Self::new), and the pre-key is also no-access except while a sealing key
225 /// is being derived from it (about 6 µs per access). If the system refuses to make it
226 /// no-access again after an access, which happens only in exhaustion cases (out of
227 /// memory mappings), it stays readable until a later access closes it; nothing reports
228 /// that, and [`lock_status`](Self::lock_status) is about the lock only.
229 ///
230 /// What that adds: while idle, the pre-key cannot be read at its address at all. On
231 /// Windows that keeps it out of other processes' `ReadProcessMemory` and out of a
232 /// full-memory crash dump taken while it is idle, which a locked pre-key does not
233 /// (measured; the repository's `docs/design/sealed_memory.md` has the numbers).
234 ///
235 /// What it costs:
236 ///
237 /// - **On Windows, the lock.** A no-access page leaves the process's working set, and
238 /// its `VirtualLock` is dropped (measured), so the pre-key may be written to the
239 /// pagefile while idle. This mode therefore does not lock on Windows, and
240 /// [`lock_status`](Self::lock_status) returns an error whose
241 /// [`idle_noaccess`](LockError::idle_noaccess) is `true`. On Linux the lock is kept.
242 /// - **Time.** Measured at about 4 µs on top of an access's 6 µs hash in a quiet
243 /// process, 9 µs with 16 busy threads. On Windows most of it is faulting the pre-key's
244 /// pages back into the working set, which a no-access page leaves; on Linux it is the two
245 /// protection changes, which interrupt every CPU running one of the process's threads.
246 /// - **A rare panic.** Making the pre-key readable for an access can fail where mapping
247 /// it would have (the process is out of memory mappings); the access then panics.
248 ///
249 /// Choose it where crash dumps or same-user memory reads are a larger risk than the
250 /// pagefile, for example a Windows machine with an encrypted system drive that collects
251 /// full crash dumps. [`new`](Self::new) is the default because, without an encrypted
252 /// drive, a pre-key in the pagefile outlives the process.
253 ///
254 /// # Panics
255 ///
256 /// As [`new`](Self::new).
257 pub fn new_idle_noaccess() -> Arc<Self> {
258 Arc::new(Self::create(true))
259 }
260
261 fn create(idle: bool) -> Self {
262 let fill = |block: &mut [u8; PREKEY_LEN]| {
263 SysRng
264 .try_fill_bytes(block)
265 .expect("SysRng failure is a program error");
266 };
267 let block = if idle {
268 GuardedBlock::new_with_idle_noaccess(fill)
269 } else {
270 GuardedBlock::new_with(fill)
271 };
272 Self {
273 block: block.unwrap_or_else(|err| {
274 panic!("the sealed-memory pre-key could not be set up: {err}")
275 }),
276 }
277 }
278
279 /// `Ok(())` if the pre-key's pages are locked in RAM; otherwise why they are not.
280 pub fn lock_status(&self) -> Result<(), LockError> {
281 self.block.lock_status().map_err(LockError::from_guarded)
282 }
283
284 /// `true` if this pre-key was made with [`new_idle_noaccess`](Self::new_idle_noaccess).
285 pub fn is_idle_noaccess(&self) -> bool {
286 self.block.is_idle_noaccess()
287 }
288
289 /// `SHA-256(salt ‖ pre-key)`, written straight into a wrapper that wipes it on drop.
290 /// The pre-key is read only inside this call, which in idle no-access mode is its whole
291 /// readable window.
292 #[inline]
293 fn sealing_key(&self, salt: &[u8; SALT_LEN]) -> Fixed<[u8; 32]> {
294 #[cfg(test)]
295 tests::maybe_fail_derivation();
296 Fixed::new_with(|key: &mut [u8; 32]| {
297 self.block.with(|prekey| {
298 let mut hasher = Sha256::new();
299 hasher.update(salt);
300 hasher.update(prekey);
301 hasher.finalize_into(key.into());
302 });
303 })
304 }
305
306 #[inline]
307 fn cipher(&self, salt: &[u8; SALT_LEN]) -> ChaCha20Poly1305 {
308 self.sealing_key(salt)
309 .with_secret(|key| ChaCha20Poly1305::new(key.into()))
310 }
311
312 /// A fresh random salt, and so a fresh sealing key. Drawn before any plaintext is
313 /// copied into a ciphertext buffer, so an RNG failure cannot leave plaintext behind.
314 pub(crate) fn fresh_salt() -> [u8; SALT_LEN] {
315 let mut rng = SysRng;
316 let words: [u64; SALT_LEN / 8] = core::array::from_fn(|_| {
317 rng.try_next_u64()
318 .expect("SysRng failure is a program error")
319 });
320 core::array::from_fn(|i| words[i / 8].to_le_bytes()[i % 8])
321 }
322
323 /// Copies `plain` into `out` and encrypts it there under the key for `salt`; returns
324 /// the detached tag.
325 ///
326 /// The key is derived before `out` is written. Deriving it is the one step that can
327 /// fail (for an idle no-access pre-key, when it cannot be made readable), and a failure
328 /// after the copy would leave plaintext in `out`, a ciphertext buffer nothing wipes.
329 /// Use this, not a copy followed by [`seal_in_place`](Self::seal_in_place), wherever
330 /// `out` is not itself a wiping buffer.
331 pub(crate) fn seal_into(
332 &self,
333 salt: &[u8; SALT_LEN],
334 plain: &[u8],
335 out: &mut [u8],
336 ) -> [u8; TAG_LEN] {
337 let cipher = self.cipher(salt);
338 out.copy_from_slice(plain);
339 cipher
340 .encrypt_inout_detached(&Nonce::default(), &[], out.into())
341 .expect("ChaCha20-Poly1305 rejects only inputs over 256 GiB")
342 .into()
343 }
344
345 /// Encrypts `buf` in place under the key for `salt`; returns the detached tag. Only for
346 /// a `buf` that wipes itself if this panics; see [`seal_into`](Self::seal_into).
347 ///
348 /// The nonce is all-zero. That is sound only because every seal draws a new salt with
349 /// [`fresh_salt`](Self::fresh_salt) and so a new key: no key ever encrypts twice.
350 pub(crate) fn seal_in_place(&self, salt: &[u8; SALT_LEN], buf: &mut [u8]) -> [u8; TAG_LEN] {
351 self.cipher(salt)
352 .encrypt_inout_detached(&Nonce::default(), &[], buf.into())
353 .expect("ChaCha20-Poly1305 rejects only inputs over 256 GiB")
354 .into()
355 }
356
357 /// Decrypts `buf` in place. On a tag mismatch `buf` is wiped and this panics: sealed
358 /// memory that fails to authenticate was modified or corrupted, and nothing can be
359 /// recovered from it. (The tag is checked before any keystream is applied, so `buf`
360 /// still holds ciphertext then; it is wiped regardless.)
361 pub(crate) fn open_in_place(&self, salt: &[u8; SALT_LEN], tag: &[u8; TAG_LEN], buf: &mut [u8]) {
362 let opened = self.cipher(salt).decrypt_inout_detached(
363 &Nonce::default(),
364 &[],
365 buf.into(),
366 tag.into(),
367 );
368 if opened.is_err() {
369 buf.zeroize();
370 panic!("sealed memory was modified or corrupted");
371 }
372 }
373}
374
375/// Size of the stack region [`scrub_stack`] overwrites.
376const SCRUB_LEN: usize = 32 * 1024;
377
378/// Overwrites the stack region below its caller with zeros.
379///
380/// The ChaCha20 backend leaves its state block (constant, then the 32-byte sealing key) on
381/// the stack after a pass, and moves can leave a copy of a temporary plaintext. Neither is
382/// reachable for a wipe. Called at the end of every seal and open, from the frame that made
383/// the calls, this function's frame lies over the region those calls used and zeroes it.
384/// The technique is sequoia's `zero_stack`; `[u64]` keeps the volatile writes word-sized.
385///
386/// Best effort: it covers what the calls left in `SCRUB_LEN` bytes below the caller, not
387/// registers, and not copies a closure parks elsewhere.
388#[inline(never)]
389pub(crate) fn scrub_stack() {
390 let mut area = [0u64; SCRUB_LEN / 8];
391 area.zeroize();
392 core::hint::black_box(&area);
393}
394
395/// Runs `f`, then [`scrub_stack`], on every exit: a panic from `f` (the caller's closure, or
396/// a tag mismatch) is caught, the stack scrubbed, and the panic resumed. Without this an
397/// unwind skips the scrub and leaves the cipher's copy of the sealing key behind. A `Drop`
398/// guard is not a substitute: on Windows, unwinding runs cleanup below the frames that
399/// threw, so the guard's scrub would not lie over them. Under `panic = "abort"` there is
400/// no unwind to survive.
401#[inline(always)]
402pub(crate) fn scrubbed<R>(f: impl FnOnce() -> R) -> R {
403 let result = std::panic::catch_unwind(core::panic::AssertUnwindSafe(f));
404 scrub_stack();
405 match result {
406 Ok(value) => value,
407 Err(payload) => std::panic::resume_unwind(payload),
408 }
409}
410
411/// Always prints `[REDACTED]`.
412impl core::fmt::Debug for Prekey {
413 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
414 f.write_str("[REDACTED]")
415 }
416}
417
418#[cfg(test)]
419pub(crate) mod tests {
420 use super::*;
421
422 /// Every test in this crate's unit tests that creates a pre-key takes this first: they
423 /// share one per-process lock quota (about 11 pre-keys under Windows' default).
424 pub(crate) fn quota() -> std::sync::MutexGuard<'static, ()> {
425 static QUOTA: std::sync::Mutex<()> = std::sync::Mutex::new(());
426 QUOTA
427 .lock()
428 .unwrap_or_else(std::sync::PoisonError::into_inner)
429 }
430
431 std::thread_local! {
432 static FAIL_NEXT_DERIVATION: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
433 }
434
435 /// Makes this thread's next sealing-key derivation panic, as it does in idle no-access
436 /// mode when the pre-key cannot be made readable.
437 pub(crate) fn fail_next_derivation() {
438 FAIL_NEXT_DERIVATION.with(|f| f.set(true));
439 }
440
441 pub(super) fn maybe_fail_derivation() {
442 if FAIL_NEXT_DERIVATION.with(|f| f.replace(false)) {
443 panic!("injected: could not make the guarded block readable");
444 }
445 }
446
447 /// A pre-key holding `bytes`, in the same guarded memory as a real one.
448 fn with_bytes(bytes: &[u8; PREKEY_LEN]) -> Prekey {
449 Prekey {
450 block: GuardedBlock::new_with(|b| b.copy_from_slice(bytes)).unwrap(),
451 }
452 }
453
454 fn protection_at(addr: usize) -> region::Protection {
455 region::query(addr as *const u8).unwrap().protection()
456 }
457
458 #[test]
459 fn the_prekey_lies_between_noaccess_guard_pages_and_is_read_only() {
460 let _q = quota();
461 let prekey = Prekey::new();
462 let [below, block, above] = prekey.block.layout();
463 assert!(block.len() >= PREKEY_LEN);
464 assert_eq!(protection_at(below.start), region::Protection::NONE);
465 assert_eq!(protection_at(block.start), region::Protection::READ);
466 assert_eq!(protection_at(block.end - 1), region::Protection::READ);
467 assert_eq!(protection_at(above.start), region::Protection::NONE);
468 assert!(!prekey.is_idle_noaccess());
469 }
470
471 #[test]
472 fn an_idle_noaccess_prekey_is_noaccess_between_accesses() {
473 let _q = quota();
474 let prekey = Prekey::new_idle_noaccess();
475 assert!(prekey.is_idle_noaccess());
476 let block = prekey.block.layout()[1].clone();
477 assert_eq!(protection_at(block.start), region::Protection::NONE);
478 let salt = Prekey::fresh_salt();
479 let mut buf = *b"idle";
480 let tag = prekey.seal_in_place(&salt, &mut buf);
481 prekey.open_in_place(&salt, &tag, &mut buf);
482 assert_eq!(&buf, b"idle");
483 assert_eq!(protection_at(block.start), region::Protection::NONE);
484 if cfg!(windows) {
485 let err = prekey.lock_status().unwrap_err();
486 assert!(err.idle_noaccess());
487 assert_eq!(err.raw_os_error(), None);
488 }
489 }
490
491 #[test]
492 fn lock_errors_say_why_and_carry_no_address() {
493 let quota = LockError {
494 os_code: Some(1453),
495 idle_noaccess: false,
496 forked: false,
497 };
498 assert_eq!(quota.raw_os_error(), Some(1453));
499 assert!(!quota.idle_noaccess());
500 assert_eq!(
501 format!("{quota:?}"),
502 "LockError { os_code: Some(1453), idle_noaccess: false, forked: false }"
503 );
504 assert_eq!(
505 quota.to_string(),
506 "the sealed-memory pre-key could not be locked in RAM (OS error 1453)"
507 );
508 let bare = LockError {
509 os_code: None,
510 idle_noaccess: false,
511 forked: false,
512 };
513 assert_eq!(
514 bare.to_string(),
515 "the sealed-memory pre-key could not be locked in RAM"
516 );
517 let idle = LockError {
518 os_code: None,
519 idle_noaccess: true,
520 forked: false,
521 };
522 assert_eq!(
523 idle.to_string(),
524 "the sealed-memory pre-key is not locked in RAM: it is no-access while idle \
525 (Prekey::new_idle_noaccess), and on Windows a no-access page cannot stay locked"
526 );
527 let forked = LockError {
528 os_code: None,
529 idle_noaccess: false,
530 forked: true,
531 };
532 assert_eq!(
533 forked.to_string(),
534 "the sealed-memory pre-key is not locked in RAM in this process: it was made \
535 before a fork, and page locks are not inherited"
536 );
537 }
538
539 /// The derivation is the one rc.13 shipped, checked against a vector computed outside
540 /// Rust: Python's `hashlib.sha256(bytes([7]) * 32 + bytes(i % 251 for i in range(16384)))`.
541 #[test]
542 fn the_sealing_key_matches_an_independent_sha256_vector() {
543 let _q = quota();
544 let bytes: [u8; PREKEY_LEN] = core::array::from_fn(|i| (i % 251) as u8);
545 let prekey = with_bytes(&bytes);
546 let key = prekey.sealing_key(&[7u8; SALT_LEN]).with_secret(|k| *k);
547 let hex: alloc::string::String = key.iter().map(|b| alloc::format!("{b:02x}")).collect();
548 assert_eq!(
549 hex,
550 "5fdf15d9989af9c0c55f07f7ba974f74ac47ada2c2b21a6e0539eaebd833cd62"
551 );
552 }
553
554 #[test]
555 fn the_sealing_key_depends_on_every_prekey_bit() {
556 let _q = quota();
557 let bytes: [u8; PREKEY_LEN] = core::array::from_fn(|i| (i % 251) as u8);
558 let mut flipped = bytes;
559 flipped[PREKEY_LEN - 1] ^= 1;
560 let salt = Prekey::fresh_salt();
561 let a = with_bytes(&bytes).sealing_key(&salt).with_secret(|k| *k);
562 let b = with_bytes(&flipped).sealing_key(&salt).with_secret(|k| *k);
563 assert_ne!(a, b);
564 }
565
566 #[test]
567 fn the_sealing_key_is_sha256_of_salt_then_prekey() {
568 let _q = quota();
569 let prekey = Prekey::new();
570 let salt = Prekey::fresh_salt();
571 let expected: [u8; 32] = prekey.block.with(|pk| {
572 let mut hasher = Sha256::new();
573 hasher.update(salt);
574 hasher.update(pk);
575 hasher.finalize().into()
576 });
577 assert_eq!(prekey.sealing_key(&salt).with_secret(|k| *k), expected);
578 }
579
580 #[test]
581 fn every_salt_is_fresh() {
582 assert_ne!(Prekey::fresh_salt(), Prekey::fresh_salt());
583 }
584
585 #[test]
586 #[should_panic(expected = "sealed memory was modified or corrupted")]
587 fn a_wrong_tag_panics() {
588 let _q = quota();
589 let prekey = Prekey::new();
590 let salt = Prekey::fresh_salt();
591 let mut buf = [1u8; 8];
592 let mut tag = prekey.seal_in_place(&salt, &mut buf);
593 tag[0] ^= 1;
594 prekey.open_in_place(&salt, &tag, &mut buf);
595 }
596
597 #[test]
598 fn a_failed_open_wipes_the_buffer_before_panicking() {
599 let _q = quota();
600 let prekey = Prekey::new();
601 let salt = Prekey::fresh_salt();
602 let mut buf = [1u8; 8];
603 let tag = prekey.seal_in_place(&salt, &mut buf);
604 buf[0] ^= 1;
605 let result = std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {
606 prekey.open_in_place(&salt, &tag, &mut buf);
607 }));
608 assert!(result.is_err());
609 assert_eq!(buf, [0u8; 8]);
610 }
611
612 #[test]
613 fn debug_is_redacted() {
614 let _q = quota();
615 assert_eq!(format!("{:?}", Prekey::new()), "[REDACTED]");
616 assert_eq!(format!("{:?}", Prekey::new_idle_noaccess()), "[REDACTED]");
617 }
618}