Expand description
cqrs-es-crypto — transparent PII encryption and GDPR crypto-shredding for [cqrs-es].
§Overview
This crate wraps any cqrs_es::persist::PersistedEventRepository with a
crypto-shredding layer that:
- Encrypts PII fields in designated event types on the write path using AES-256-GCM, keyed by a per-subject Data Encryption Key (DEK).
- Decrypts PII fields transparently on the read path when the DEK is present.
- Redacts PII fields when the DEK has been deleted (crypto-shredded), making the data permanently irrecoverable without touching individual events.
Which event types carry PII, and how their payloads are structured, is defined by
the caller through the PiiEventCodec trait. The crate itself has no knowledge
of any particular domain or event schema.
§Known limitations
stream_all_eventsis not supported:CryptoShreddingEventRepositoryreturns an error if called, because thecqrs_es::persist::ReplayStreamAPI does not expose rawcqrs_es::persist::SerializedEventitems for interception. Use [CryptoShreddingEventRepository::get_events] or [CryptoShreddingEventRepository::stream_events] per aggregate instead.- Snapshots are not encrypted. If your aggregate state contains PII, snapshots will store it in plaintext and crypto-shredding will not redact it.
§Key management
Each subject gets a unique DEK wrapped by a Key Encryption Key (KEK) via
StaticKekProvider (env-var backed) or any KekProvider implementation.
[PostgresKeyStore] persists wrapped DEKs in subject_encryption_keys.
Deleting a row is the shredding operation. The kek_id column records which
KEK version wrapped each DEK, enabling zero-downtime KEK rotation.
The required DDL is:
CREATE TABLE subject_encryption_keys (
key_id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
subject_id UUID NOT NULL UNIQUE,
wrapped_key BYTEA NOT NULL,
kek_id TEXT NOT NULL,
rewrapped_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX ON subject_encryption_keys (kek_id);§Usage
use std::sync::Arc;
use cqrs_es_crypto::{
CryptoShreddingEventRepository, EncryptedPiiSentinel, FieldCipher,
PiiEventCodec, PiiFields, PostgresKeyStore, StaticKekProvider,
};
// 1. Implement PiiEventCodec for your domain.
struct MyCodec;
impl PiiEventCodec for MyCodec { /* ... */ }
// 2. Build the crypto repository around your inner repository.
let provider = Arc::new(StaticKekProvider::single("v1", kek_bytes)?);
let key_store = Arc::new(PostgresKeyStore::new(pool.clone(), Arc::clone(&provider)));
let codec = Arc::new(MyCodec);
let repo = CryptoShreddingEventRepository::new(
inner_repo, key_store, FieldCipher::new(), codec,
);§Cargo features
derive: enables#[derive(PiiCodec)]fromcqrs-es-crypto-derive.chrono: impliesderive; teaches the derive macro to redactchrono::NaiveDatesecret fields to"0000-01-01"by default. Per-field overrides are available via#[pii(secret, redact = "...")]. See the derive crate’s docs.testing: exposes [InMemoryEventRepository] for downstream tests.
Re-exports§
pub use cipher::CryptoError;pub use cipher::EncryptedPayload;pub use cipher::FieldCipher;pub use cipher::KeyMaterial;pub use cipher::PiiCipher;Deprecated pub use kek::KekError;pub use kek::KekHandle;pub use kek::KekProvider;pub use kek::StaticKekProvider;pub use kek::WrappedDek;pub use key_store::InMemoryKeyStore;pub use key_store::KeyStore;pub use key_store::KeyStoreError;pub use rewrap::RewrapStats;pub use rewrap::RewrapWorker;pub use rewrap::RewrapWorkerOptions;pub use repository::CryptoShreddingEventRepository;pub use repository::EncryptedPiiExtract;pub use repository::EncryptedPiiSentinel;pub use repository::PiiEventCodec;pub use repository::PiiFields;
Modules§
- cipher
- AES-256-GCM field encryption and AES-256-KWP key wrapping for GDPR crypto-shredding.
- kek
- Key Encryption Key (KEK) provider abstraction for DEK wrap/unwrap.
- key_
store - Per-subject Data Encryption Key (DEK) management for GDPR crypto-shredding.
- repository
- Generic transparent PII encryption and GDPR crypto-shredding for
cqrs-es. - rewrap
- Background worker for re-wrapping DEKs still encrypted under a retired KEK version.