A Rust implementation of envelope encryption with per-user key encryption keys and PostgreSQL-backed storage.
Envelope encryption is a data protection strategy where:
- Data Encryption Key (DEK) - One-time key that encrypts the actual data
- Key Encryption Key (KEK) - Per-user master key that wraps/encrypts DEKs
- Database Encryption - Database-level encryption protects KEKs at rest
This creates layers of protection and enables key rotation without re-encrypting all data.
┌─────────────────────────────────────────────────────────────┐
│ Database Encryption (at rest) │
└─────────────────────────┬───────────────────────────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ KEK-1 (User 1) │ │ KEK-2 (User 2) │
│ 32-byte plaintext │ │ 32-byte plaintext │
│ Stored in DB │ │ Stored in DB │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
┌──────┴──────┐ ┌───────┴───────┐
│ │ │ │
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ DEK-1 │ │ DEK-2 │ │ DEK-3 │ │ DEK-4 │
│(EDEK) │ │(EDEK) │ │(EDEK) │ │(EDEK) │
└───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘
│ │ │ │
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ Data │ │ Data │ │ Data │ │ Data │
│ A │ │ B │ │ C │ │ D │
└───────┘ └───────┘ └───────┘ └───────┘
- AES-256-GCM: Industry-standard authenticated encryption with AEAD
- Per-User KEKs: Each user has their own Key Encryption Key
- KEK Rotation: Rotate KEKs individually or in bulk
- Version Tracking: KEKs maintain version numbers for backward compatibility
- One-Time DEKs: DEKs are generated per encryption operation (no rotation needed)
- PostgreSQL Storage: Production-ready persistent storage
- Zero-copy Security: Keys are zeroized from memory when dropped
- User Isolation: Each user's data is protected by their unique KEK
# Run schema (this will drop and recreate the database)
psql -U postgres -f schema.sqlCreate a .env file:
DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@localhost:5432/envelope_encryptioncargo runuse envelope_encryption::{PostgresStorage, PostgresEnvelopeService};
use sqlx::PgPool;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load environment
dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL")?;
// Connect to PostgreSQL
let pool = PgPool::connect(&database_url).await?;
// Initialize service
let storage = PostgresStorage::new(pool);
let service = PostgresEnvelopeService::new(storage).await?;
Ok(())
}use uuid::Uuid;
// Generate a DEK for a user (automatically creates KEK if needed)
let user_id = Uuid::new_v4();
let dek = service.generate_dek(&user_id).await?;
println!("DEK ID: {}", dek.dek_id);
println!("KEK Version: {}", dek.kek_version);
println!("EDEK blob size: {} bytes", dek.edek_blob.len());use envelope_encryption::AesGcmCipher;
let plaintext = b"Sensitive user data";
let content_id = Uuid::new_v4();
// Encrypt using the DEK
let encrypted = AesGcmCipher::encrypt(
&dek.dek,
plaintext,
Some(content_id.as_bytes())
)?;
println!("Ciphertext: {} bytes", encrypted.ciphertext.len());// Decrypt the EDEK to recover the DEK
let recovered_dek = service.decrypt_edek(
&dek.dek_id,
&dek.edek_blob,
&user_id,
dek.kek_version
).await?;
// Decrypt the data
let decrypted = AesGcmCipher::decrypt(
&recovered_dek,
&encrypted,
Some(content_id.as_bytes())
)?;
assert_eq!(plaintext, &decrypted[..]);Rotate a specific user's KEK on demand:
use envelope_encryption::UserKekRotationResult;
let user_id = Uuid::new_v4();
// First ensure the user has a KEK
let dek = service.generate_dek(&user_id).await?;
// Rotate this user's KEK
let result = service.rotate_user_kek(&user_id).await?;
println!("User: {}", result.user_id);
println!("Old version: {}", result.old_version);
println!("New version: {}", result.new_version);Rotate all users' KEKs in batches of 50:
use envelope_encryption::BulkRotationResult;
let result = service.bulk_rotate_all_keks().await?;
println!("KEKs marked as RETIRED: {}", result.keks_marked_retired);
println!("KEKs rotated: {}", result.keks_rotated);KEKs are automatically rotated when a RETIRED KEK is accessed:
// If a RETIRED KEK is accessed during generate_dek or decrypt_edek,
// it will be automatically rotated to a new ACTIVE version
let dek = service.generate_dek(&user_id).await?;
// ↑ If user's KEK is RETIRED, it's automatically rotated hereMark a RETIRED KEK as DISABLED (safe to delete):
let disabled = service.disable_kek(&user_id, old_version).await?;
if disabled {
println!("KEK disabled successfully");
}Delete a DISABLED KEK from the database:
let deleted = service.delete_kek(&user_id, old_version).await?;
if deleted {
println!("KEK deleted successfully");
}Monitor KEK lifecycle across all users:
let stats = service.get_kek_stats().await?;
for (status, count) in stats {
println!("{}: {}", status, count);
}
// Output example:
// ACTIVE: 125
// RETIRED: 50
// DISABLED: 10┌─────────┐ generate_dek() ┌────────────┐
│ NEW │ ─────────────────> │ ACTIVE │
│ (user) │ │ (current) │
└─────────┘ └──────┬─────┘
│
rotate_user_kek() or
bulk_rotate_all_keks()
│
▼
┌─────────────┐
│ RETIRED │
│ (decrypt- │
│ only) │
└──────┬──────┘
│
disable_kek()
│
▼
┌─────────────┐
│ DISABLED │
│ (safe to │
│ delete) │
└──────┬──────┘
│
delete_kek()
│
▼
[DELETED]
This library follows a Hardware Security Module (HSM) style architecture with clear separation of concerns:
- Library Responsibility: KEK lifecycle management (create, rotate, disable, delete)
- Application Responsibility: DEK and EDEK management (caching, storage, retrieval)
This design ensures that the library acts as a secure key management service, while applications maintain full control over their data encryption workflow.
| Module | Description |
|---|---|
crypto |
AES-256-GCM encryption primitives |
postgres_storage |
PostgreSQL storage backend for KEKs |
postgres_envelope |
High-level envelope encryption service |
error |
Error types and Result aliases |
- KEKs: Stored as plaintext (32 bytes) in PostgreSQL, encrypted at rest by database encryption
- DEKs: Ephemeral, generated on-demand by library, managed by application
- EDEKs: Application-managed, stored with encrypted data by application
| Key Type | Size | Lifetime | Rotation | Storage |
|---|---|---|---|---|
| KEK | 32 bytes | Per-user, versioned | Manual via API | PostgreSQL user_keks table (library) |
| DEK | 32 bytes | One-time use | Not needed | Application-managed (ephemeral/cached) |
| EDEK | 60 bytes | Tied to data | Via KEK rotation | Application-managed (with encrypted data) |
┌─────────────┬──────────────┬─────────────┐
│ Nonce │ Ciphertext │ Tag │
│ 12 bytes │ 32 bytes │ 16 bytes │
└─────────────┴──────────────┴─────────────┘
Total: 60 bytes
- Database Encryption at Rest: Enable PostgreSQL encryption at rest for production
- Key Zeroization: All key material is automatically zeroized when dropped
- AAD Binding:
- Data ciphertexts are bound to
content_idusing Additional Authenticated Data - EDEKs are bound to
dek_id
- Data ciphertexts are bound to
- Unique Nonces: Each encryption generates a fresh random nonce (12 bytes)
- User Isolation: Each user's data can only be decrypted with their specific KEK
- Version Tracking: Old KEKs remain accessible for decrypting old data
// Service
pub struct PostgresEnvelopeService { /* ... */ }
// Storage
pub struct PostgresStorage { /* ... */ }
// Result types
pub struct GeneratedDek {
pub dek_id: Uuid,
pub dek: SecureKey,
pub edek_blob: Vec<u8>,
pub kek_version: i64,
}
pub struct BulkRotationResult {
pub keks_marked_retired: i64,
pub keks_rotated: i64,
}
pub struct UserKekRotationResult {
pub user_id: Uuid,
pub old_version: i64,
pub new_version: i64,
}
pub struct StoredKek {
pub user_id: Uuid,
pub version: i64,
pub kek_plaintext: Vec<u8>,
pub status: KekStatus,
pub created_at: DateTime<Utc>,
pub last_accessed_at: Option<DateTime<Utc>>,
pub last_rotated_at: Option<DateTime<Utc>>,
}
pub enum KekStatus {
Active,
Retired,
Disabled,
}
// Crypto primitives
pub struct AesGcmCipher;
pub struct SecureKey { /* ... */ }
pub struct EncryptedData {
pub ciphertext: Vec<u8>,
pub nonce: Vec<u8>,
pub tag: Vec<u8>,
}// Service initialization
impl PostgresEnvelopeService {
pub async fn new(storage: PostgresStorage) -> Result<Self>;
// DEK operations
pub async fn generate_dek(&self, user_id: &Uuid) -> Result<GeneratedDek>;
pub async fn decrypt_edek(
&self,
dek_id: &Uuid,
edek_blob: &[u8],
user_id: &Uuid,
kek_version: i64
) -> Result<SecureKey>;
// KEK rotation
pub async fn rotate_user_kek(&self, user_id: &Uuid) -> Result<UserKekRotationResult>;
pub async fn bulk_rotate_all_keks(&self) -> Result<BulkRotationResult>;
// KEK lifecycle
pub async fn disable_kek(&self, user_id: &Uuid, version: i64) -> Result<bool>;
pub async fn delete_kek(&self, user_id: &Uuid, version: i64) -> Result<bool>;
// Statistics
pub async fn get_kek_stats(&self) -> Result<Vec<(String, i64)>>;
pub fn get_cached_dek_count(&self) -> usize;
}
// Crypto operations
impl AesGcmCipher {
pub fn encrypt(
key: &SecureKey,
plaintext: &[u8],
aad: Option<&[u8]>
) -> Result<EncryptedData>;
pub fn decrypt(
key: &SecureKey,
encrypted: &EncryptedData,
aad: Option<&[u8]>
) -> Result<Vec<u8>>;
}Run all tests:
cargo testRun PostgreSQL integration tests:
# Ensure PostgreSQL is running with schema loaded
psql -U postgres -f schema.sql
# Run tests
cargo test --features postgres- Bulk Rotation: Processes KEKs in batches of 50 with
SKIP LOCKEDfor concurrent workers - Lazy Rotation: Automatically rotates on-demand when RETIRED KEKs are accessed
- Indexed Queries: Optimized PostgreSQL indexes for hot paths
idx_user_keks_active: Fast lookup of ACTIVE KEKsidx_user_keks_retired_for_rotation: Efficient batch rotation
use envelope_encryption::{EnvelopeError, Result};
match service.generate_dek(&user_id).await {
Ok(dek) => println!("Success: {}", dek.dek_id),
Err(EnvelopeError::KeyNotFound(msg)) => eprintln!("Key not found: {}", msg),
Err(EnvelopeError::Crypto(msg)) => eprintln!("Crypto error: {}", msg),
Err(EnvelopeError::Storage(msg)) => eprintln!("Storage error: {}", msg),
Err(e) => eprintln!("Other error: {}", e),
}- Enable PostgreSQL encryption at rest: Configure your database for encryption
- Connection pooling: Already included via
sqlx::PgPool - Monitoring: Use
get_kek_stats()to track KEK lifecycle - Backup strategy: Regular database backups include all KEKs
- Key rotation policy: Rotate KEKs periodically using bulk or individual rotation
MIT