Skip to main content

Module crypto

Module crypto 

Source
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:acme for tenant-wide data, subject:user-42 for 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§

CryptoVault
Process-wide encryption service. Provide once via ctx.provide(CryptoVault::bootstrap(source).await?), resolve anywhere with Inject<CryptoVault> / ctx.inject::<CryptoVault>().
DataKey
One unwrapped AES-256 DEK version. Material is zeroized on drop (secrecy) and never serialized.
EncryptedField
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) and subject:<id> (per-person, shreddable).

Enums§

CryptoError

Traits§

EncryptRecord
Implemented by #[EncryptFields(...)] on a DTO. The default methods are the whole write/read contract:
KekSource

Type Aliases§

LoadedKeyring
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.