Expand description
Field-level envelope encryption + crypto-shredding.
§Why this is load-bearing
The durability machinery (outbox rows, idempotency replay cache, DLQ, hash-chained audit) copies payloads into stores that are append-only on purpose. Masking protects what leaves over HTTP; this module protects what stays. Sensitive fields are sealed with a per-tenant or per-subject data key (DEK) before they reach any sink, and GDPR Art. 17 erasure is satisfied by destroying the DEK (crypto-shredding) — every ciphertext copy, including ones inside the sealed audit chain, becomes permanently unreadable without breaking the chain’s integrity.
§Key hierarchy
- KEK (key-encryption-key) lives in the
KekSource— Vault / cloud KMS in production, an env-derived source in dev. The framework never sees it; the source hands back unwrapped DEKs. - DEK (data-encryption-key, AES-256) is scoped by
KeyId:tenant:acmefor tenant-wide data,subject:user-42for per-person shredding. Rotation adds a new DEK version; old versions stay readable until re-encryption, because every ciphertext records the version that sealed it.
§Zero-lock mechanics
The unwrapped key ring lives behind one ArcSwap snapshot — the proven
pattern from secrets / tenants / masking. encrypt/decrypt cost one
atomic pointer load, one hash probe, and one AES-256-GCM operation
(AES-NI). All I/O — provisioning, rotation, shredding — happens on the
control plane, which serializes through a tokio mutex that no request
path ever touches.
§Usage
// boot (plugin on_init):
let vault = CryptoVault::bootstrap(Arc::new(VaultKekSource::new(...))).await?;
ctx.provide(vault);
// declare what to seal on the DTO:
#[EncryptFields(key = "tenant:acme", fields("ssn", "card.number"))]
#[derive(serde::Serialize, serde::Deserialize)]
struct PatientRecord { ssn: String, card: Card, name: String }
// write path — seal before any sink:
let sealed: serde_json::Value = record.seal(vault)?;
// read path — unseal after load:
let record = PatientRecord::unseal(sealed, vault)?;
// GDPR erasure — every copy of this subject's data dies at once:
vault.shred(&KeyId::subject("user-42")).await?;Structs§
- Crypto
Vault - Process-wide encryption service. Provide once via
ctx.provide(CryptoVault::bootstrap(source).await?), resolve anywhere withInject<CryptoVault>/ctx.inject::<CryptoVault>(). - DataKey
- One unwrapped AES-256 DEK version. Material is zeroized on drop
(
secrecy) and never serialized. - Encrypted
Field - Self-describing ciphertext for one field:
enc:v1:<key_id>:<key_version>:<b64(nonce ‖ ciphertext ‖ tag)>. Records the key version so rotation never forces immediate re-encryption. - KeyId
- Identifies one DEK lineage. Conventional scopes:
tenant:<id>(tenant-wide) andsubject:<id>(per-person, shreddable).
Enums§
Traits§
- Encrypt
Record - Implemented by
#[EncryptFields(...)]on a DTO. The default methods are the whole write/read contract: - KekSource
Type Aliases§
- Loaded
Keyring - Bridges to the key-management system. Production implementations wrap Vault Transit / AWS KMS / GCP KMS; dev uses an env-derived source. Every method runs on the control plane — never on a request path. A full key ring as loaded from the KMS: every key with all live versions.