cqrs_es_crypto/lib.rs
1//! `cqrs-es-crypto` — transparent PII encryption and GDPR crypto-shredding for [`cqrs-es`].
2//!
3//! # Overview
4//!
5//! This crate wraps any [`cqrs_es::persist::PersistedEventRepository`] with a
6//! crypto-shredding layer that:
7//!
8//! - **Encrypts** PII fields in designated event types on the write path using
9//! AES-256-GCM, keyed by a per-subject Data Encryption Key (DEK).
10//! - **Decrypts** PII fields transparently on the read path when the DEK is present.
11//! - **Redacts** PII fields when the DEK has been deleted (crypto-shredded), making
12//! the data permanently irrecoverable without touching individual events.
13//!
14//! Which event types carry PII, and how their payloads are structured, is defined by
15//! the caller through the [`PiiEventCodec`] trait. The crate itself has no knowledge
16//! of any particular domain or event schema.
17//!
18//! # Known limitations
19//!
20//! - **`stream_all_events`** is not supported: [`CryptoShreddingEventRepository`]
21//! returns an error if called, because the [`cqrs_es::persist::ReplayStream`] API
22//! does not expose raw [`cqrs_es::persist::SerializedEvent`] items for interception.
23//! Use [`CryptoShreddingEventRepository::get_events`] or
24//! [`CryptoShreddingEventRepository::stream_events`] per aggregate instead.
25//! - **Snapshots** are not encrypted. If your aggregate state contains PII, snapshots
26//! will store it in plaintext and crypto-shredding will not redact it.
27//!
28//! # Key management
29//!
30//! Each subject gets a unique DEK wrapped by a Key Encryption Key (KEK) via
31//! [`StaticKekProvider`] (env-var backed) or any [`KekProvider`] implementation.
32//! [`PostgresKeyStore`] persists wrapped DEKs in `subject_encryption_keys`.
33//! Deleting a row is the shredding operation. The `kek_id` column records which
34//! KEK version wrapped each DEK, enabling zero-downtime KEK rotation.
35//!
36//! The required DDL is:
37//!
38//! ```sql
39//! CREATE TABLE subject_encryption_keys (
40//! key_id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
41//! subject_id UUID NOT NULL UNIQUE,
42//! wrapped_key BYTEA NOT NULL,
43//! kek_id TEXT NOT NULL,
44//! rewrapped_at TIMESTAMP,
45//! created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
46//! );
47//! CREATE INDEX ON subject_encryption_keys (kek_id);
48//! ```
49//!
50//! # Usage
51//!
52//! ```rust,ignore
53//! use std::sync::Arc;
54//! use cqrs_es_crypto::{
55//! CryptoShreddingEventRepository, EncryptedPiiSentinel, FieldCipher,
56//! PiiEventCodec, PiiFields, PostgresKeyStore, StaticKekProvider,
57//! };
58//!
59//! // 1. Implement PiiEventCodec for your domain.
60//! struct MyCodec;
61//! impl PiiEventCodec for MyCodec { /* ... */ }
62//!
63//! // 2. Build the crypto repository around your inner repository.
64//! let provider = Arc::new(StaticKekProvider::single("v1", kek_bytes)?);
65//! let key_store = Arc::new(PostgresKeyStore::new(pool.clone(), Arc::clone(&provider)));
66//! let codec = Arc::new(MyCodec);
67//! let repo = CryptoShreddingEventRepository::new(
68//! inner_repo, key_store, FieldCipher::new(), codec,
69//! );
70//! ```
71//!
72//! # Cargo features
73//!
74//! - `derive`: enables `#[derive(PiiCodec)]` from `cqrs-es-crypto-derive`.
75//! - `chrono`: implies `derive`; teaches the derive macro to redact
76//! `chrono::NaiveDate` secret fields to `"0000-01-01"` by default.
77//! Per-field overrides are available via
78//! `#[pii(secret, redact = "...")]`. See the derive crate's docs.
79//! - `testing`: exposes [`InMemoryEventRepository`] for downstream tests.
80
81pub mod cipher;
82pub mod kek;
83pub mod key_store;
84pub mod repository;
85pub mod rewrap;
86
87// ── Cipher ──────────────────────────────────────────────────────────────────────────────
88
89#[allow(deprecated)] // PiiCipher is re-exported for backwards compatibility.
90pub use cipher::{CryptoError, EncryptedPayload, FieldCipher, KeyMaterial, PiiCipher};
91
92// ── KEK provider ───────────────────────────────────────────────────────────────────────────
93
94pub use kek::{KekError, KekHandle, KekProvider, StaticKekProvider, WrappedDek};
95
96// ── Key store ─────────────────────────────────────────────────────────────────
97
98pub use key_store::{InMemoryKeyStore, KeyStore, KeyStoreError};
99
100#[cfg(feature = "postgres")]
101pub use key_store::{PostgresKeyStore, PostgresKeyStoreOptions};
102
103// ── Re-wrap worker (requires postgres feature for real use, but the worker itself is generic) ────
104
105pub use rewrap::{RewrapStats, RewrapWorker, RewrapWorkerOptions};
106
107// ── Repository ────────────────────────────────────────────────────────────────
108
109pub use repository::{
110 CryptoShreddingEventRepository, EncryptedPiiExtract, EncryptedPiiSentinel, PiiEventCodec,
111 PiiFields,
112};
113
114#[cfg(feature = "postgres")]
115pub use repository::PersistHook;
116
117// ── Testing helpers (opt-in via the `testing` feature) ───────────────────────
118
119#[cfg(any(test, feature = "testing"))]
120pub use repository::InMemoryEventRepository;
121
122// ── Derive macro (opt-in via the `derive` feature) ───────────────────────────
123
124#[cfg(feature = "derive")]
125pub use cqrs_es_crypto_derive::PiiCodec;