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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
//! Envelope encryption, and the erasure it makes provable.
//!
//! # Why deleting is not erasing
//!
//! [`BlobStore::expire`](crate::blob::BlobStore::expire) drops a payload's bytes
//! and leaves a tombstone, and the hash chain still verifies because it only
//! ever committed to a digest. That is the right shape, and it is not enough:
//! it erases the bytes *in the live store*. Every backup taken before the
//! request, every replica, every snapshot an operator forgot about still holds
//! them, and an erasure obligation is not discharged by deleting one copy of
//! something that exists in six places.
//!
//! Chasing the copies does not work either. Backups are the point of backups —
//! they are offline, offsite, and frequently immutable by design, because that
//! is what makes them survive the incident they exist for. A retention story
//! that requires rewriting immutable backups has two guarantees in direct
//! conflict, and whichever one loses, loses silently.
//!
//! # What this does instead
//!
//! Payload bytes are sealed under a **data key**, and the data key is wrapped by
//! a key this crate never holds — a KMS, an HSM, whatever the deployment trusts.
//! Erasure destroys the data key. Every copy of the ciphertext becomes
//! unreadable at the same instant, including the ones nobody can reach, because
//! the thing that was destroyed was never in them.
//!
//! This is the standard answer to the immutable-log-versus-erasure problem, and
//! it is the only one that survives contact with a backup regime.
//!
//! Two operations fall out of the same structure, which is the argument for it:
//!
//! * **Erasure** — destroy a scope's wrapping key. Every payload ever sealed
//! under that scope is unreadable, everywhere, including the copies nobody
//! can reach.
//! * **Revocation** — the same act for a different reason. The blast radius a
//! compromised key should have is exactly the scope it wrapped.
//!
//! # Sealed bytes are rotation-immutable
//!
//! A third operation is conspicuously absent, and its absence is a decision
//! rather than unfinished work. Envelope encryption's usual selling point is
//! that a wrapping key rotates by *re-wrapping* data keys, leaving bulk data
//! alone. That operation cannot exist here, and offering it would be a control
//! that quietly does nothing:
//!
//! An envelope carries its wrapped data key **inline**, and the journal's hash
//! chain commits to the envelope bytes — which is what lets an auditor holding
//! no keys verify a run whose payloads have been erased. Re-wrapping a journal
//! payload therefore rewrites a record the chain covers, so it breaks the chain
//! it sits inside. Nor could re-wrapping the *other* stores buy the operational
//! thing rotation is wanted for: an erasure scope's journal payloads and its
//! case state share one wrapping key, so a scope stays pinned to the oldest
//! version any of its journal envelopes names, and no amount of re-wrapping
//! case rows moves that floor.
//!
//! So the rule is the other half of the choice, stated rather than discovered:
//! **sealed payload bytes never change, and the erasure scope is the rotation
//! unit.** A scope is already narrow — one case, one run, one memory subject —
//! so a compromised wrapping key exposes that unit and nothing else, which is
//! the blast radius rotation is bought for. Adding a key version is safe and
//! needs nothing from this crate: envelopes sealed before a rotation keep
//! opening, which is what AWS KMS does by construction — it retains every prior
//! version of a key's material in perpetuity, resolves the right one from the
//! ciphertext, and lets you delete only the whole key, which is erasure.
//!
//! *Retiring* a version is the exposure, and only some services offer it.
//! Vault's transit engine does: `min_decryption_version` refuses ciphertext
//! below a floor, so raising that floor past a live envelope makes un-erased
//! history unreadable — an erasure nobody requested and no retention record
//! explains. That is why [`KeyError::Retired`] exists as its own answer; see
//! its documentation for why it must not be reported as loss.
//!
//! # What is deliberately absent
//!
//! No key material is generated by, or stored in, this crate beyond the process
//! that is using it. [`KeyRing`] is a seam: a deployment points it at the thing
//! that already holds its keys. There is a `MemoryKeyRing` for tests, and it
//! lives behind the `testkit` feature rather than here: a key ring in the same
//! process as the data it protects protects it from nobody, so the gate is the
//! guarantee instead of a warning in a doc comment.
use Debug;
use async_trait;
use ;
use ;
use crate;
/// A key that seals payload bytes.
///
/// Zeroized on drop. Not `Clone`, not `Debug`-printable, and it does not
/// serialize: a data key that reaches a log line or a journal record is a data
/// key whose destruction no longer erases anything.
;
// Written by hand so the key cannot reach a log through a derived `Debug`.
/// A data key as it may safely be stored: sealed by a key this crate never has.
///
/// Safe to journal, back up, and replicate — which is the point, because it
/// travels *with* the payload it sealed. A backup therefore contains everything
/// needed to restore and nothing needed to read: the wrapping key stayed in the
/// service, and destroying it is what makes the backup unreadable.
/// Why a key operation did not happen.
/// Where data keys are made, wrapped, and destroyed.
///
/// The wrapping key never leaves the implementation. That is the whole seam: a
/// deployment can put it in a KMS and this crate is still only ever holding a
/// data key it was handed, for as long as it takes to seal or open one payload.
/// The erasure scope for a unit within a tenant.
///
/// Derived here and nowhere else. The write path and the erasure path must
/// agree byte-for-byte about which key seals a payload; deriving the string
/// separately in each is how writes come to seal under `tenant/case` while
/// erasure destroys `case`, so every erasure reports success and destroys
/// nothing. Two places deriving one fact is the defect, so there is one place.
///
/// [`TenantId`](crate::core::TenantId) refuses `/`, which is what stops a tenant
/// named `acme/prod` from colliding with tenant `acme`, unit `prod`.
///
/// The implementation is [`crate::core::erasure_scope`], because the blob
/// layer consumes the same string as the unit that leads a storage address —
/// and the key a scope destroys and the addresses it expires must name one
/// unit, not two spellings of one.
/// Refuse a sealing scope that is not the scope the wrapped store writes under.
///
/// Every wrapper here takes the store it seals and the tenant to seal for. Those
/// are two spellings of one fact, and when they differ nothing fails: both
/// scopes are real, the rows are written, the state is sealed, and the tenant's
/// erasure destroys a key that does not reach them. The failure surfaces only as
/// data that survived a deletion request, long after anyone could connect it to
/// the wiring.
///
/// So the pair is checked where both halves are in hand. The plane's own
/// `try_build` asks the same question of every store it is given; this covers
/// the wrapper an embedder builds and hands over already sealed, which `build`
/// sees only from the outside.
///
/// # Panics
///
/// If the two disagree. A startup wiring mistake, not a runtime condition.
pub
/// The sealed-envelope construction this build writes, and the only one it
/// reads.
///
/// Public for the reason [`canon::VERSION`](crate::core::canon::VERSION) and
/// [`export::FORMAT_VERSION`](crate::export::FORMAT_VERSION) are: an operator
/// planning a restore, or diagnosing a
/// [`KeyError::UnknownFormat`], needs to name what this build speaks without
/// reading the source.
///
/// It stays `1` until the durable-format freeze. A number counting the
/// pre-release cuts would advertise that older envelopes are readable, and the
/// whole point of a hard cut is that they are not — here more sharply than
/// elsewhere, because sealed bytes cannot be rewritten into the new shape.
pub const ENVELOPE_FORMAT_VERSION: u8 = FORMAT_VERSION;
pub use ;
pub use SealedEvents;
pub use SealedTasks;
pub use SealedJournal;
pub use SealedPush;
pub use EncryptedBlobs;
pub use ;
pub use EncryptedMemoryStore;
pub use VaultTransit;