LiteBus
CatalogDurable core

Payload Encryption at Rest

ID: durable-core.payload-encryption Maturity: GA

Summary

Inbox and outbox writers can encrypt serialized payload bodies before persistence. Dispatchers decrypt after load and before deserialization. Contract names, versions, routing fields, and operator metadata remain readable.

Public Surface

SurfaceRole
IPayloadEncryptorApplication implementation of EncryptAsync and DecryptAsync
IContextualPayloadEncryptorOptional authenticated encryption contract that receives message and contract metadata as associated data
PayloadProtectionContextImmutable message, contract, tenant, and axis metadata passed to contextual encryptors
IInboxPayloadProtectorInbox-specific dependency key adapted from the configured encryptor
IOutboxPayloadProtectorOutbox-specific dependency key adapted from the configured encryptor
InboxModuleBuilder.UsePayloadEncryptionApplies protection to inbox acceptance and dispatch
OutboxModuleBuilder.UsePayloadEncryptionApplies protection to outbox enqueue and dispatch

Register protection on each durable axis that shares the encrypted store:

var encryptor = new ApplicationPayloadEncryptor(keyProvider);

registry.AddInbox(inbox =>
{
    inbox.UsePayloadEncryption(encryptor);
});

registry.AddOutbox(outbox =>
{
    outbox.UsePayloadEncryption(encryptor);
});

Inbox and outbox can use separate implementations when they use different keys or providers.

Runtime Boundaries

ComponentProtection Operation
InboxEnvelopeFactoryEncrypts serialized commands before store insertion
OutboxEnvelopeFactoryEncrypts serialized events before store insertion
Transactional EF Core inbox and outbox writersEncrypt within the caller's unit of work
In-process and transport dispatchersDecrypt stored payloads before validation or mediation

Storage adapters treat the protected payload as opaque text. EF Core and PostgreSQL adapters map payload columns to text so ciphertext does not need to be valid JSON. Contract name and version stay plaintext so the registry can select a CLR type before deserialization. Topic, correlation, tenant, visibility, and status fields also remain plaintext for leasing and operations.

Implement IContextualPayloadEncryptor when ciphertext must be bound to its row metadata. LiteBus passes MessageId, ContractName, ContractVersion, TenantId, and an axis value of inbox or outbox. Use these fields as authenticated associated data in an AEAD implementation. Existing IPayloadEncryptor implementations remain supported but do not receive associated data.

Key Rotation

LiteBus does not own keys or rotation schedules. The application encryptor can implement online rotation with a key identifier in its ciphertext format:

  1. Keep historical read keys available.
  2. Change the active write key.
  3. Write new payloads with the new key identifier.
  4. Select the matching read key from each stored ciphertext.
  5. Retire a historical key only after no retained row depends on it.

For example, rows prefixed with key-v1: and key-v2: can coexist while DecryptAsync selects from an application key ring. New writes use key-v2; old rows remain dispatchable through key-v1.

The test key ring in PayloadEncryptionTests models identifier selection and mixed historical reads. It is not a cryptographic algorithm reference. Applications should use an authenticated encryption scheme and managed key material suitable for their security requirements.

Failure Behavior

  • An encryption failure aborts accept or enqueue before the store write completes.
  • A decryption failure is a dispatch failure and follows processor retry and dead-letter policy.
  • Removing a historical read key while retained rows still reference it makes those rows undispatchable.
  • Changing the ciphertext format requires a reader that recognizes every retained format version.

Repeated decrypt failures after a deployment usually identify missing keys, mismatched configuration, or an incompatible ciphertext format. The processor failure and dead-letter counters expose the durable impact; the encryptor should log a non-secret key identifier and failure category.

Packages

PackageRole
LiteBus.DurableMessaging.AbstractionsShared IPayloadEncryptor contract
LiteBus.Inbox.AbstractionsInbox protector contract
LiteBus.Outbox.AbstractionsOutbox protector contract
LiteBus.InboxInbox builder surface and encryption/decryption integration
LiteBus.OutboxOutbox builder surface and encryption/decryption integration
ApplicationAlgorithm, key provider, key identifiers, rotation, and audit policy

Invariants

  • Protection is optional; the default stores serialized JSON as plaintext.
  • Encryption runs before the payload reaches the store.
  • Decryption runs before payload validation or deserialization.
  • All readers of one store must retain keys for every non-expired ciphertext.
  • Contract and operational metadata are not encrypted.
  • Contextual encryptors must reject decryption when supplied metadata differs from the values authenticated during encryption.
  • Transactional writers use the same axis protector as non-transactional writers.

Non-Goals

  • Built-in KMS, HSM, or key-vault integration.
  • Automatic key generation, rotation, re-encryption, or retirement.
  • Field-level payload encryption.
  • Encryption of contract, routing, lease, or status columns.

Observability

LiteBus does not emit key identifiers as metric tags because key and tenant sets can create high cardinality. Use these signals:

SignalMeaning
Writer exceptionEncryption failed before durable insertion
litebus.inbox.processor.failedInbox decrypt, deserialize, hook, or handler failure scheduled a retry
litebus.outbox.processor.failedOutbox decrypt, validate, or dispatch failure scheduled a retry
Dead-letter countersRetry policy exhausted, including persistent decrypt failures
Dispatch duration histogramIncludes decrypt time inside the dispatch operation

Test Coverage

  • PayloadEncryptionTests.Inbox_RoundTripsEncryptedPayloadThroughDispatch verifies ciphertext at rest and plaintext delivery.
  • PayloadEncryptionTests.CommandInboxDispatcher_DecryptsProtectedPayload and EventOutboxDispatcher_DecryptsProtectedPayload verify both in-process dispatch paths.
  • PayloadEncryptionTests.Inbox_WithRotatedKeyRing_ShouldDispatchHistoricalAndCurrentCiphertext writes one row with key-v1, rotates the active key to key-v2, writes another row, and dispatches both through one retained key ring.
  • InboxEnvelopeFactoryTests.CreateAsync_should_encrypt_payload_when_protector_configured verifies the inbox writer boundary.
  • TransactionalInboxAcceptTests and TransactionalOutboxEnqueueTests verify encryption inside EF Core transactional staging.

PostgreSQL stores use the same envelope factories and opaque payload contract. Store-specific encryption does not exist.

On this page