1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
//! Secrets held **encrypted in RAM while idle**, opened for one closure call at a time.
//! Requires the `sealed` feature.
//!
//! [`Fixed`](crate::Fixed) and [`Dynamic`](crate::Dynamic) hold their secret in plaintext
//! for their whole lifetime and wipe it on drop. [`SealedFixed`] and [`SealedDynamic`] hold
//! it as ChaCha20-Poly1305 ciphertext instead, and decrypt only into a temporary that
//! exists for the length of a `with_secret` / `with_secret_mut` call. The design follows
//! `sequoia-openpgp`'s `crypto::mem::Encrypted`, itself following OpenSSH.
//!
//! ```rust
//! use secure_gate::{Fixed, Prekey, SealedFixed};
//!
//! // One per session; drop it, and everything sealed under it, at lock or logout.
//! let prekey = Prekey::new();
//! if let Err(why) = prekey.lock_status() {
//! eprintln!("note: {why}"); // not fatal
//! }
//!
//! let mut key = SealedFixed::seal(Fixed::new([7u8; 32]), &prekey);
//! key.with_secret(|k| assert_eq!(k[0], 7));
//! key.with_secret_mut(|k| k[0] = 8); // re-sealed under a fresh salt
//! let plain: Fixed<[u8; 32]> = key.unseal();
//! ```
//!
//! # How it works
//!
//! - A [`Prekey`] is 16 KiB of random bytes in a memory mapping of its own, between
//! no-access guard pages, read-only, and locked in RAM where the operating system allows
//! it. Each sealed object keeps an `Arc` to the one it was sealed under.
//! - Every seal draws a random 32-byte salt. The sealing key is `SHA-256(salt โ pre-key)`,
//! recomputed on every access and wiped as soon as the cipher is keyed. It is never
//! stored in the object. The cipher leaves a copy of it on the stack, so every seal and
//! open ends by overwriting the 32 KiB of stack below the call (sequoia's `zero_stack`
//! technique), on a panic as well as a normal return, which also removes plaintext a move
//! left there. Measured on x86_64 Windows:
//! `docs/design/receipts/sealed-stack-residue/` in the repository.
//! - Because every seal has its own key, the AEAD nonce is all-zero. Every re-seal after
//! [`with_secret_mut`](SealedFixed::with_secret_mut) draws a new salt, so no key ever
//! encrypts twice.
//!
//! # What it protects against
//!
//! **Lossy readout** of process memory: Spectre-class side channels, Rowhammer, cold boot.
//! These recover memory with bit errors. A 32-byte key read with a few flipped bits is
//! still recoverable by search; a sealing key that depends on all 131,072 bits of the
//! pre-key is not, because one wrong bit anywhere avalanches through SHA-256.
//!
//! With the pre-key locked, a sealed secret that reaches the **swap file** cannot be
//! opened from it, because the pre-key never gets there.
//!
//! A **linear over-read** from a buffer elsewhere in the process (a C library's,
//! Heartbleed-style) does not reach the pre-key. It is not on the heap: measured, a 64 KiB
//! read from a C buffer captured the whole pre-key in every run while it lived there
//! (rc.14), and none of it in its own mapping. And a read that runs into it faults on its
//! guard page: measured with readable memory placed directly against the mapping, where the
//! same read of an unguarded mapping takes all 16 KiB.
//!
//! # What it does not protect against
//!
//! - A **clean** read of the address space: a debugger, `/proc/<pid>/mem`, an attacker who
//! can run code in the process. That read gets pre-key, salt and ciphertext together.
//! - **Hibernation** files, which capture locked pages too, and **crash dumps** on Windows
//! and macOS. On Linux the pre-key is marked `MADV_DONTDUMP`, which the kernel documents as leaving it out of core dumps. On
//! Windows a [`Prekey::new_idle_noaccess`] pre-key is absent from a full-memory dump taken
//! while it is idle, at the price of its lock.
//! - Over-reads of the sealed objects themselves: their ciphertext and salt are ordinary
//! memory, but useless without the pre-key.
//! - A failed lock ([`Prekey::lock_status`] says so): the pre-key can then be swapped out
//! like any other memory.
//! - The plaintext temporary while a closure runs. It lives on the stack
//! ([`SealedFixed`]) or the heap ([`SealedDynamic`]) and is not locked; it is wiped, and
//! the stack below the call overwritten, when the call returns. Copies the closure makes
//! are the caller's to wipe.
//! - Registers, and stack residue beyond the overwritten 32 KiB. The overwrite was measured
//! on one target; another target's cipher backend or frame layout may differ.
//! - What [`unseal`](SealedFixed::unseal) returns: an ordinary `Fixed`, whose by-value
//! return can leave a copy in the caller's frame (`SECURITY.md` ยง1). Read through
//! `with_secret` instead where you can.
//!
//! It narrows one threat. It is not a substitute for encrypted swap, disabled core dumps,
//! or anything else in `SECURITY.md`.
//!
//! # Cost
//!
//! Each access needs about 32 KiB of free stack for the scrub, on top of the cipher's own
//! frames: mind small custom thread stacks. Measured on x86_64 Windows: an access on a
//! 64 KiB thread stack completes in a release build and overflows in a debug build; 96 KiB
//! completes in both.
//!
//! Not zero-overhead. Each access hashes 16 KiB and runs one AEAD pass; each mutation adds
//! an RNG call and a second pass. The repository's `sealed_vs_fixed` benchmark measures it.
//!
//! # Not implemented, on purpose
//!
//! - `expose_secret` / [`RevealSecret`](crate::RevealSecret): plaintext must not outlive
//! the call. Use [`RevealScoped`](crate::scoped::RevealScoped) for generic scoped access.
//! - `Hash`: it would feed the plaintext into a hasher whose state is never wiped.
//! - `Debug` beyond `[REDACTED]`: no salt, no tag, no length.
pub use ;
pub use SealedFixed;
pub use ;
/// What the sealed types must not offer, checked by compiling each misuse.
///
/// No `expose_secret`, so no reference outlives the call:
///
/// ```compile_fail,E0599
/// use secure_gate::{Fixed, Prekey, SealedFixed};
/// let s = SealedFixed::seal(Fixed::new([0u8; 32]), &Prekey::new());
/// let _ = s.expose_secret();
/// ```
///
/// No `RevealSecret` impl to reach it through either:
///
/// ```compile_fail,E0277
/// use secure_gate::{Fixed, Prekey, RevealSecret, SealedFixed};
/// fn needs<S: RevealSecret>(_: &S) {}
/// needs(&SealedFixed::seal(Fixed::new([0u8; 32]), &Prekey::new()));
/// ```
///
/// No `Hash`, which would feed the plaintext to a hasher that is never wiped:
///
/// ```compile_fail,E0277
/// use secure_gate::{Fixed, Prekey, SealedFixed};
/// fn needs<H: core::hash::Hash>(_: &H) {}
/// needs(&SealedFixed::seal(Fixed::new([0u8; 32]), &Prekey::new()));
/// ```
///
/// `with_secret_mut` on a sealed `Vec` hands out a fixed-length slice, so it cannot grow:
///
/// ```compile_fail,E0599
/// use secure_gate::{Dynamic, Prekey, SealedDynamic};
/// let mut s = SealedDynamic::seal(Dynamic::<Vec<u8>>::new(vec![1]), &Prekey::new());
/// s.with_secret_mut(|v| v.push(2));
/// ```
///
/// Nor can a sealed `String`:
///
/// ```compile_fail,E0599
/// use secure_gate::{Dynamic, Prekey, SealedDynamic};
/// let mut s = SealedDynamic::seal(Dynamic::<String>::new(String::from("a")), &Prekey::new());
/// s.with_secret_mut(|v| v.push_str("b"));
/// ```
///
/// A borrow from `with_secret` cannot escape the closure:
///
/// ```compile_fail
/// use secure_gate::{Fixed, Prekey, SealedFixed};
/// let s = SealedFixed::seal(Fixed::new([0u8; 32]), &Prekey::new());
/// let leaked: &[u8; 32] = s.with_secret(|b| b);
/// ```
///
/// Not `Clone`, even with the `cloneable` feature: like `Fixed<[u8; N]>`, a sealed byte
/// secret is not copied implicitly.
///
/// ```compile_fail,E0277
/// use secure_gate::{Fixed, Prekey, SealedFixed};
/// fn needs<C: Clone>(_: &C) {}
/// needs(&SealedFixed::seal(Fixed::new([0u8; 32]), &Prekey::new()));
/// ```
///
/// Only `Vec<u8>` and `String` can be sealed as variable-length secrets:
///
/// ```compile_fail,E0277
/// use secure_gate::{Dynamic, Prekey, SealedDynamic};
/// let _ = SealedDynamic::seal(Dynamic::<Vec<u16>>::new(vec![1]), &Prekey::new());
/// ```
;