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
add_key("key-v2", &new_key)— Register the new key (both keys can decrypt)rotate_to("key-v2")— New publishes encrypt withkey-v2- Old events encrypted with
key-v1remain readable (the key is still registered) - 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 eventsSchema 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).