A3S Docs
A3S Event

Encryption

AES-256-GCM payload encryption with key rotation

Encryption

Requires the encryption feature (enabled by default).

A3S Event provides transparent AES-256-GCM payload encryption. Events are encrypted before publishing and decrypted when reading history. Key rotation is supported with multiple active decryption keys.

Enable Encryption

use a3s_event::{EventBus, MemoryProvider, Aes256GcmEncryptor};
use std::sync::Arc;

let mut bus = EventBus::new(MemoryProvider::default());

// 32-byte key for AES-256
let key = b"0123456789abcdef0123456789abcdef";
let encryptor = Aes256GcmEncryptor::new("key-v1", key);

bus.set_encryptor(Arc::new(encryptor));

Once set, all publish() and publish_event() calls automatically encrypt the payload. list_events() automatically decrypts.

Encrypted Payload Format

Encrypted events use a sealed envelope in the payload field:

{
  "encrypted": true,
  "keyId": "key-v1",
  "nonce": "base64-encoded-12-byte-nonce",
  "ciphertext": "base64-encoded-ciphertext"
}

Each message gets a unique random 12-byte nonce, ensuring identical payloads produce different ciphertext.

Key Rotation

Add new keys and rotate without downtime:

let mut encryptor = Aes256GcmEncryptor::new("key-v1", &old_key);

// Add a new key
encryptor.add_key("key-v2", &new_key);

// Rotate: new publishes use key-v2, old messages still decrypt with key-v1
encryptor.rotate_to("key-v2")?;

Rotation Workflow

  1. add_key("key-v2", &new_key) — Register the new key (both keys can decrypt)
  2. rotate_to("key-v2") — New publishes encrypt with key-v2
  3. Old events encrypted with key-v1 remain readable (the key is still registered)
  4. After all old events expire, optionally remove key-v1

EventEncryptor Trait

Implement custom encryption backends:

use a3s_event::EventEncryptor;

pub trait EventEncryptor: Send + Sync {
    /// Encrypt a JSON payload, returning an encrypted envelope as JSON
    fn encrypt(&self, payload: &serde_json::Value) -> Result<serde_json::Value>;

    /// Decrypt an encrypted envelope back to the original JSON payload
    fn decrypt(&self, encrypted: &serde_json::Value) -> Result<serde_json::Value>;

    /// The current active key ID used for encryption
    fn active_key_id(&self) -> &str;
}

Detecting Encrypted Payloads

use a3s_event::EncryptedPayload;

if EncryptedPayload::is_encrypted(&event.payload) {
    println!("This payload is encrypted with key: {}",
        serde_json::from_value::<EncryptedPayload>(event.payload.clone())
            .unwrap().key_id);
}

Processing Order

When both schema validation and encryption are enabled:

Publish:   payload → schema validate → encrypt → provider.publish()
History:   provider.history() → decrypt → return plaintext events

Schema validation always runs on plaintext, never on encrypted data.

Security Notes

  • Algorithm: AES-256-GCM (authenticated encryption with associated data)
  • Nonce: Random 12-byte nonce per message (never reused)
  • Encoding: Base64 for JSON serialization of binary data
  • Key storage: Keys are held in memory. Use a secrets manager for production key material.
  • Scope: Application-layer encryption. For transport encryption, configure TLS on the provider (e.g., NATS TLS).

On this page