Envelope Encryption at Rest

SecurityEncryptionKMSCloud
Share on LinkedIn

Storing AES-256 keys next to the data they encrypt is security theater with better labels. Envelope encryption separates the problem: a random data encryption key (DEK) encrypts your payload; a key encryption key (KEK) managed in KMS or an HSM wraps the DEK. Compromise of one database backup exposes only wrapped blobs — useless without KMS Decrypt permission and audit-logged access. Rotation becomes wrapping a new DEK instead of decrypting a terabyte warehouse and re-encrypting overnight.

The envelope pattern

                    ┌─────────────┐
 Plaintext ──DEK──► │ Ciphertext  │
                    └─────────────┘
                           │
 DEK ──KEK (KMS)──► Wrapped DEK stored alongside ciphertext

Operations:

  1. GenerateDataKey — KMS returns plaintext DEK (memory only) + wrapped DEK blob
  2. Encrypt data locally with plaintext DEK (AES-GCM)
  3. Discard plaintext DEK from memory
  4. Store {ciphertext, wrapped_dek, iv, aad} — never store plaintext DEK at rest

Decrypt reverses: KMS unwraps DEK, local AES-GCM decrypts payload.

AWS KMS example

import boto3
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

kms = boto3.client("kms")
KEK_ID = "arn:aws:kms:us-east-1:123456789012:key/abcd-..."

def encrypt_field(plaintext: bytes, aad: bytes) -> dict:
    resp = kms.generate_data_key(KeyId=KEK_ID, KeySpec="AES_256")
    dek = resp["Plaintext"]
    wrapped = resp["CiphertextBlob"]

    iv = os.urandom(12)
    aesgcm = AESGCM(dek)
    ct = aesgcm.encrypt(iv, plaintext, aad)

    dek = b"\x00" * len(dek)  # zeroize best-effort
    return {"ct": ct, "wrapped_dek": wrapped, "iv": iv}

def decrypt_field(blob: dict, aad: bytes) -> bytes:
    dek = kms.decrypt(CiphertextBlob=blob["wrapped_dek"])["Plaintext"]
    aesgcm = AESGCM(dek)
    return aesgcm.decrypt(blob["iv"], blob["ct"], aad)

Use AAD (additional authenticated data) for tenant ID or record type — tampering with metadata fails GCM verification.

Per-tenant and per-object DEKs

Shared DEK across all rows simplifies implementation but enables bulk decrypt on one key leak. Per-tenant DEKs:

def tenant_kek_alias(tenant_id: str) -> str:
    return f"alias/tenant-{tenant_id}"

KMS supports per-tenant CMKs or envelope with tenant ID in AAD. Crypto-shredding a tenant: delete their CMK or wrapped DEKs — data becomes unrecoverable even if ciphertext leaks.

Object storage (S3) client-side envelope encryption before upload ensures ciphertext at rest even if bucket policy misconfigured — SSE-KMS server-side is simpler but less portable across clouds.

Key rotation without downtime

KMS CMK rotation — AWS rotates backing material annually; re-wrap DEKs on next read:

def rewrap_if_needed(wrapped_dek: bytes) -> bytes:
    # Decrypt still works with old material; ReEncrypt wraps under new
    return kms.re_encrypt(
        CiphertextBlob=wrapped_dek,
        DestinationKeyId=KEK_ID,
    )["CiphertextBlob"]

Lazy DEK rotation — on write path, generate new DEK; old records decrypt with stored wrapped blob until touched.

Never log plaintext DEKs or KMS responses containing them.

Database field encryption

For PostgreSQL application-level encryption:

Column Contents
ssn_ciphertext bytea
ssn_wrapped_dek bytea
ssn_iv bytea
dek_version int

Index on hash of searchable fields (HMAC with separate key) if equality search needed — encrypted columns are not indexable for range queries without deterministic encryption (weaker — use sparingly).

Threat model clarity

Envelope encryption protects data at rest on stolen disks/backups. It does not protect against:

Combine with TLS in transit, least-privilege IAM, and field-level access controls in application code.

KMS key hierarchy and rotation

Organize keys in a hierarchy for blast radius control:

AWS Account
└── CMK: master-key (annual rotation)
    ├── DEK: tenant-a-data-key (per-tenant isolation)
    ├── DEK: tenant-b-data-key
    └── DEK: application-secrets-key

CMK (Customer Master Key) never encrypts data directly — only wraps DEKs. Rotate CMK annually; re-wrap DEKs with new CMK version. DEK rotation on write path — old records decrypt with stored wrapped blob until touched.

def encrypt_field(plaintext: bytes, kms_client, cmk_id: str) -> EncryptedField:
    # Generate fresh DEK per record
    dek = os.urandom(32)
    ciphertext = aes_gcm_encrypt(plaintext, dek)
    wrapped_dek = kms_client.encrypt(KeyId=cmk_id, Plaintext=dek)
    return EncryptedField(ciphertext=ciphertext, wrapped_dek=wrapped_dek['CiphertextBlob'])

Envelope encryption performance

KMS calls add latency — batch and cache:

Operation Latency Mitigation
KMS GenerateDataKey 10–50ms Generate DEK locally, wrap async
KMS Decrypt 10–50ms Cache unwrapped DEK in memory (TTL 5min)
AES-GCM encrypt <1ms Always local

Cache unwrapped DEKs in application memory with short TTL — never cache in Redis or shared storage. KMS call only on cache miss.

Compliance and key custody

Requirement Implementation
PCI DSS CMK in HSM; no plaintext key export
HIPAA Encryption at rest + access audit log
GDPR Key deletion = crypto-shredding
SOC 2 Key rotation documented and tested

Crypto-shredding: delete wrapped DEK → data permanently unrecoverable without brute force. Faster and more complete than overwriting ciphertext.

Failure modes

Production checklist

Test key rotation in staging quarterly — a rotation procedure that hasn't been exercised is a compliance checkbox, not a recovery capability.

Resources

Frequently asked questions

What is the difference between envelope encryption and full-disk encryption?

Full-disk or volume encryption (LUKS, BitLocker, EBS encryption) protects storage media from physical theft. Envelope encryption protects individual objects or records with unique data keys wrapped by a KMS master key — enabling per-tenant keys, granular rotation, and crypto-shredding without re-encrypting entire disks.

How often should I rotate DEKs versus KMS keys?

Rotate KMS master keys annually or on compromise per policy — wrapped DEKs re-wrap without decrypting data. Rotate or version DEKs when you need crypto-shredding (delete customer data irreversibly) or on a schedule for high-sensitivity fields. Re-encrypting all data with new DEKs is expensive — design for lazy rotation on read/write.

Can I implement envelope encryption without a cloud KMS?

Yes — use HashiCorp Vault transit, SoftHSM with PKCS#11, or on-prem HSMs. The pattern is the same: master key never leaves HSM; application requests GenerateDataKey and Decrypt(wrapped_dek). Cloud KMS reduces HSM ops burden but adds vendor dependency.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →