Expand description
Envelope encryption, and the erasure it makes provable.
§Why deleting is not erasing
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 version — 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.
Re-exports§
pub use coordinator::ErasureCoordinator;pub use coordinator::Lease;pub use coordinator::LocalCoordinator;pub use coordinator::UnderLock;pub use coordinator::under_lock;
Modules§
- coordinator
- The lifecycle lock cryptographic memory erasure needs, and who can hold it.
Structs§
- DataKey
- A key that seals payload bytes.
- Encrypted
Blobs - A
BlobStorethat seals payloads under a scope’s data key. - Encrypted
Memory Store - A memory store whose content is unreadable once its items’ keys are destroyed.
- Erasure
- What a cryptographic erasure did: the key, then the live ciphertext.
- Sealed
Cases - A
CaseStorethat seals case state under a key ring. - Sealed
Events - An
EventStorethat seals buffered payloads under a key ring. - Sealed
Journal - A
JournalStorethat seals payloads under a key ring. - Sealed
Push - A
PushStorethat seals webhook credentials under a key ring. - Sealed
Tasks - A
TaskStorethat seals task proposals under a key ring. - Vault
Transit - Vault’s transit engine, reached over HTTP.
- Wrapped
Key - A data key as it may safely be stored: sealed by a key this crate never has.
Enums§
- KeyError
- Why a key operation did not happen.
Constants§
- ENVELOPE_
FORMAT_ VERSION - The sealed-envelope construction this build writes, and the only one it reads.
Traits§
- KeyRing
- Where data keys are made, wrapped, and destroyed.
Functions§
- event_
scope - The erasure scope of one buffered event, named by its
(source, id)pair. - probe_
sealed_ case_ state - Prove a case’s sealed state still opens, without keeping the plaintext.
- scope
- The erasure scope for a unit within a tenant.