DekStore¶
The DekStore manages Data Encryption Keys (DEKs) per event stream.
Each aggregate instance (identified by StreamId) gets its own DEK,
providing fine-grained encryption isolation.
DEKs are versioned – each stream can have multiple DEK versions (e.g. after algorithm migration). The version is embedded as a 4-byte prefix in the encrypted payload, allowing the read path to select the correct cipher for decryption.
class IDekStore(metaclass=ABCMeta):
async def get_or_create(self, session, stream_id: StreamId) -> ICipher
async def get(self, session, stream_id: StreamId, key_version: int) -> ICipher
async def get_all(self, session, stream_id: StreamId) -> ICipher
async def delete(self, session, stream_id: StreamId) -> None
async def rewrap(self, session, tenant_id) -> int
get_or_create– used on the write path. ReturnsICipherfor the latest DEK version, creating a new DEK (version 1) if none exists. The cipher prepends the version prefix onencrypt().get– returnsICipherfor a specific DEK version. Used when the version is already known (e.g. extracted from an event payload). RaisesDekNotFoundif no DEK exists for the given version.get_all– returns a compositeICipherthat handles all DEK versions for a stream.encrypt()uses the latest version,decrypt()dispatches by the version prefix in the ciphertext. Used on the read path when loading multiple events that may span different DEK versions.delete– removes all DEK versions for a stream.rewrap– re-encrypts all DEKs for a tenant with the current KEK version (afterrotate_kek). Does not change the DEK algorithm. Returns the number of re-wrapped DEKs.
DEKs are stored encrypted in the stream_deks table. Decryption
requires the tenant’s KEK from the KMS module.
See ADR-0009: Envelope Encryption for the architectural decision.
tenant_id is typed as typing.Any – the actual type is determined
by the user’s DDL schema (varchar, integer, etc.). The DDL
can include REFERENCES to enforce referential integrity.
DEK versioning¶
Each DEK version is stored as a separate row in stream_deks, with
its own algorithm. The encrypted event payload starts with a 4-byte
version prefix identifying which DEK was used:
[4 bytes version][12 bytes nonce][ciphertext][16 bytes tag]
This enables safe algorithm migration: new events are encrypted with the latest DEK version (and its algorithm), while old events remain decryptable with their original DEK version.
Codec¶
The ICodec interface provides composable encode/decode transformations
for event payloads using the Decorator pattern:
cipher = await dek_store.get_or_create(session, stream_id)
EncryptionCodec(cipher, ZlibCodec(JsonCodec()))
JsonCodec– serializesdictto JSON bytes and backZlibCodec– compresses/decompresses bytesEncryptionCodec– encrypts/decrypts using anyICipherimplementation. Wraps the cipher as a codec decorator.DekStoreconstructs the cipher internally (Aes256GcmCipherwith AAD derived fromstream_id), dispatching by thealgorithmcolumn.