Encrypt each record or object with its own data key (DEK) using an AEAD such as AES-256-GCM, wrap the DEK with a key-encryption key (KEK) that stays in a KMS or HSM, and bind the record’s tenant, ID and purpose to both layers. Rotating the KEK rewraps DEKs; it neither re-encrypts data nor repairs a leaked DEK. Keep old KEK versions until every record and backup that needs them is migrated or deliberately expired.
- A DEK encrypts data; a KEK only wraps DEKs and should never leave the key service.
- Rewrap changes the KEK; re-encryption with a new DEK is the only fix for a leaked DEK.
- Managed KEK rotation keeps old key versions for decryption and re-encrypts nothing.
- Pass the record’s context to the key service as encryption context or AAD, and use the same values in the data AEAD.
- Destroying a key erases every ciphertext that depends on it, including backups: disable first, then delete.
- A DEK shared across records with random 96-bit nonces must stay under 2^32 AES-GCM encryptions.
| Setting | Value | Note | Source |
|---|---|---|---|
| Data key (DEK) | AES-256-GCM, a new key per record or object | A shared DEK with random 96-bit nonces must stay under 2^32 encryptions. | NIST SP 800-38D |
| Key-encryption key (KEK) | Symmetric AES-256 key generated and kept in the KMS or HSM | Applications send DEKs to wrap and unwrap; the KEK is never exported. | NIST SP 800-57 Part 1 Rev. 5 |
| Context binding | Tenant, record ID and purpose as encryption context or AAD | Not secret: AWS logs encryption context in CloudTrail. Google Cloud AAD is limited to 64 KiB. | AWS KMS and Cloud KMS documentation |
| KEK rotation | AWS KMS: every 365 days by default, configurable from 90 to 2,560 days | Azure recommends rotating encryption keys at least every two years. | AWS KMS EnableKeyRotation API; Azure Key Vault documentation |
| Deletion safeguard | AWS: 7 to 30 days pending deletion (default 30) | Google Cloud: versions scheduled for destruction for 30 days by default. Azure: soft delete for 7 to 90 days, with purge protection. | AWS, Google Cloud and Azure documentation |
| Cached data keys | Maximum age set; at most 2^32 messages per cached data key | 2^32 messages is the AWS Encryption SDK default and maximum. | AWS Encryption SDK documentation |
- Decrypt a record with another tenant’s or record’s context and confirm both the unwrap and the data decryption fail.
- Rotate the KEK in a test environment and confirm old records still decrypt and new records name the new key version.
- Run the rewrap job, kill it midway, rerun it, and confirm no record lost its only usable wrapped key.
- Disable the old KEK, restore a backup from before the migration, and record which data it can and cannot read.
- Schedule deletion of a test KEK and confirm the alert fires and data becomes unreadable only after the waiting period.
- Applies to
Application-level envelope encryption with a cloud KMS or HSM. Provider limits change; check them when you configure a key.
- Part of
- Learning path: Cryptography without shortcuts (step 6 of 12). Workflows: Fix a finding, Secure a feature
Learning goals and prerequisites
What you will learn
- Distinguish rewrapping a data key from re-encrypting the data.
- Map context binding, rotation and deletion onto AWS KMS, Google Cloud KMS and Azure Key Vault.
- Design an atomic, resumable migration.
- Retire or destroy a key only after backups and retention are reconciled.
Before you start
Developers who encrypt application data with a cloud KMS or HSM, and platform engineers who rotate, disable or delete those keys.
- Understand data keys, wrapping keys, authenticated context and the distinction between rewrap and re-encrypt.
Refresh first: AEAD records: AES-GCM nonces, associated data and key limits
Go to the guided practiceKeep key wrapping separate from record encryption
- Encrypt with a new data keyThe application asks the key service for a data key under KEK v2, passing the record’s tenant, ID and purpose as context, receives it in plaintext and wrapped, encrypts the record with AES-256-GCM bound to the same context, and discards the plaintext key.
- Store the envelopeThe record store keeps the ciphertext, the wrapped data key, the KEK version and the algorithm.
- Unwrap and decryptTo read, the application sends the wrapped key and the same context to the key service, which checks the caller and key policy and returns the data key; the application then decrypts the record.
- Rotate the KEKThe key service rewraps the data key under KEK v3, for example with AWS KMS
ReEncrypt, and the application stores it with a conditional update. The ciphertext is unchanged, so a leaked data key needs re-encryption instead.
Envelope encryption encrypts data with a data-encryption key (DEK), then protects that DEK with a key-encryption key (KEK), usually through a managed key service. A stored record contains the encrypted data, nonce/tag and context information, wrapped DEK, and identifiers needed to select the approved wrapping key.
On read, the application authorizes the request, asks the key service to unwrap the DEK under the expected context, and uses the DEK to authenticate and decrypt the data. The key service can enforce who may unwrap, but once plaintext or a DEK reaches the authorized process, a compromise of that process can expose it.
Rewrapping changes protection of the DEK without necessarily changing the data ciphertext or DEK. Re-encryption creates new data ciphertext, usually with a new DEK and nonce. If the DEK itself was compromised, merely rewrapping it under a new KEK does not remove the attacker’s ability to decrypt a captured old ciphertext.
In layers, choosing each by who must not be able to read the data. Storage encryption with provider-managed keys protects against lost disks and discarded hardware and is usually on by default (Amazon S3 has encrypted every new object since January 2023). Encryption with a customer-managed key in your KMS adds access control, an audit trail of every decryption and the option to crypto-shred. Application-level envelope encryption of sensitive fields or records, as on this page, is the only layer that also keeps the data unreadable to database administrators, database dumps, backups and services that can query the database but cannot use the key.
Every layer decrypts transparently for anyone who reaches the data through the path that holds the key, so none of them stops SQL injection or a compromised application server with decryption rights. Start with storage encryption everywhere, use customer-managed keys for regulated or sensitive stores, and add application-level encryption for the fields whose exposure would hurt most, such as identity documents, health data, payment tokens and secrets.
| Layer | Protects against | Does not protect against | Examples |
|---|---|---|---|
| Disk, volume or storage-service encryption with provider-managed keys | Stolen or discarded disks, physical access to storage | Anyone who can query the database or read the bucket through normal access | Default encryption in cloud storage services, LUKS, BitLocker |
| Database or bucket encryption with a customer-managed KMS key | The above, plus storage-level access without key permission; gives a decryption audit trail and crypto-shredding | Database users, SQL injection and the application, which all see plaintext | Transparent data encryption, S3 SSE-KMS, Cloud SQL with a customer-managed key |
| Application-level envelope encryption of fields or records | The above, plus administrators, dumps, backups and other services without KMS decrypt rights | A compromised process that holds decrypt rights, and plaintext copies in logs or caches | The AES-GCM envelope pattern below, the AWS Encryption SDK, Tink |
Bind tenant, record identity, purpose, and format to data authentication and, where supported, to the wrapping operation’s context. Store key versions explicitly enough to locate the correct old KEK during migration. An alias named current is useful for new writes but insufficient to explain how an old wrapped DEK can be opened.
Keep encryption permission separate from decryption and administration where the service supports it. A worker that writes new records may not need to unwrap all historical data. A key administrator should not automatically become an application data reader merely because both operations live in one console.
Per-record DEKs limit compromise and deletion scope but add metadata and key-service calls. Reusing one DEK across many records reduces those calls but enlarges what one leaked key exposes and creates a nonce budget: with random 96-bit nonces, AES-GCM allows at most 2^32 encryptions per key. The AWS Encryption SDK caps a cached data key at 2^32 messages for the same reason. Caching unwrapped DEKs reduces latency and dependence on the key service but keeps them in application memory longer and delays the effect of revoking unwrap permission.
The design must state behavior during a key-service outage. New writes should not silently downgrade to plaintext or a hard-coded fallback key. Reads may use previously authorized cached keys only within a documented policy; availability decisions should not bypass tenant or key-context checks.
The three services implement the same pattern with different names and limits. Pass record context to the key service where it accepts it, and always bind the same context in the data-layer AEAD, so a wrapped key or a ciphertext copied to another record fails to open.
None of the three re-encrypts data when it rotates a key. Rotation adds a new key version for new wraps; old versions stay usable for decryption until you disable or destroy them. Re-encrypting records is your migration job, and deleting a key version is irreversible once its waiting period ends.
| Operation | AWS KMS | Google Cloud KMS | Azure Key Vault |
|---|---|---|---|
| Bind record context | Encryption context: non-secret key-value pairs, required again to decrypt, usable in policy conditions | Additional authenticated data up to 64 KiB, required again to decrypt | Bind context in the data-layer AEAD around the wrapped key |
| Separate duties | Key policies and IAM, plus grants constrained by encryption context | roles/cloudkms.cryptoKeyEncrypter, cryptoKeyDecrypter; admin cannot decrypt | Key Vault Crypto Service Encryption User for wrap and unwrap; Crypto Officer for management |
| Rotate the KEK | New key material on schedule or on demand; key ID unchanged; old material kept for decryption | New primary version; previous versions stay enabled until destroyed | New key version; store the versioned key URI with each wrapped DEK |
| Delete the KEK | 7 to 30 day waiting period, then data is unrecoverable | Scheduled for destruction, 30 days by default, then data is unrecoverable | Soft delete for 7 to 90 days; purge protection blocks early purge |
For planned KEK rotation, make K2 available for wrapping new DEKs and keep K1 enabled for existing reads. Migrate old wrapped DEKs to K2 with a supported rewrap call (such as AWS KMS ReEncrypt) or an unwrap and rewrap inside your service. Authenticate the existing metadata, write the replacement envelope atomically, and record enough version state to retry safely. With managed automatic rotation the service does the key-version part for you, but old versions stay in use until your own records stop referencing them.
Consider a crash after creating a new wrapped DEK but before committing the record. A retry must either recognize the committed new version or repeat the safe operation from the old version. It must not delete the only usable wrapped key first. A conditional update on the record version prevents a migration worker from overwriting a concurrent application update.
Before disabling K1, measure remaining live references, queued jobs, replicas, exports, and retained backups. A live-table count of zero does not mean every recoverable snapshot uses K2. Restore an old backup in a controlled environment and follow its documented key-access path.
For compromise, distinguish which keys and ciphertexts the attacker may have copied. Rewrapping can reduce future access through a stolen service credential after revocation, but cannot make previously disclosed DEKs or plaintext secret again. Choose re-encryption and incident containment based on that exposure, not simply the desire to finish a rotation dashboard.
This example uses the cryptography package and a dictionary standing in for the KMS, so the logic can run locally. Each record gets its own 256-bit DEK, the same versioned context is authenticated at both layers, and rewrapping changes only the wrapped key. It was tested with cryptography 50 on Python 3.14: a record moved to another tenant or ID fails, a tampered wrapped key fails, and a backup copy that was never rewrapped cannot be opened once the old KEK is retired. In production, replace wrap and the KEK lookup with your KMS client’s encrypt and decrypt calls, passing the same context. The AEAD record guide at /domains/identity-crypto/encryption/authenticated-record-design/ and its project practice the record-binding half with real Web Crypto AES-GCM.
import json
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# Stand-in for a KMS: key-encryption keys (KEKs) by ID. In production the KEK never leaves the KMS
# and wrap/unwrap are KMS calls that take the same context as encryption context or AAD.
KEKS = {"kek-2": AESGCM.generate_key(bit_length=256)}
CURRENT_KEK = "kek-2"
def context(tenant: str, record_id: str, purpose: str) -> bytes:
return json.dumps([1, tenant, record_id, purpose]).encode() # unambiguous, versioned
def wrap(kek_id: str, dek: bytes, aad: bytes) -> dict:
nonce = os.urandom(12)
return {"kek": kek_id, "nonce": nonce, "wrapped": AESGCM(KEKS[kek_id]).encrypt(nonce, dek, aad)}
def encrypt_record(tenant: str, record_id: str, plaintext: bytes) -> dict:
aad = context(tenant, record_id, "invoice")
dek = AESGCM.generate_key(bit_length=256) # one data key per record: no shared nonce budget
nonce = os.urandom(12)
return {"dek": wrap(CURRENT_KEK, dek, aad), "nonce": nonce,
"ciphertext": AESGCM(dek).encrypt(nonce, plaintext, aad)}
def decrypt_record(tenant: str, record_id: str, record: dict) -> bytes:
aad = context(tenant, record_id, "invoice") # from the authorized lookup, not the record
w = record["dek"]
dek = AESGCM(KEKS[w["kek"]]).decrypt(w["nonce"], w["wrapped"], aad)
return AESGCM(dek).decrypt(record["nonce"], record["ciphertext"], aad)
def rewrap(tenant: str, record_id: str, record: dict) -> dict:
"""KEK rotation: same DEK and data ciphertext, new wrapping. Does not help if the DEK leaked."""
aad = context(tenant, record_id, "invoice")
w = record["dek"]
dek = AESGCM(KEKS[w["kek"]]).decrypt(w["nonce"], w["wrapped"], aad)
return {**record, "dek": wrap(CURRENT_KEK, dek, aad)}Crypto-shredding deletes data by destroying the only key that can decrypt it. Give each tenant or data subject its own KEK or DEK, and deleting that key makes every copy of their ciphertext unreadable, including copies in backups that cannot be edited. NIST SP 800-88 Rev. 2 (September 2025) treats this as cryptographic erase.
It works only when no other copy of the key or of the plaintext survives. Exported key backups, cached DEKs, plaintext exports, search indexes and logs all defeat it. The deletion safeguards above also delay it: erasure is complete when the waiting or soft-delete period ends and the key is purged, not when someone clicks delete. Record that date as the completion time for the deletion request; the retention guide at /domains/identity-crypto/privacy/data-minimization-retention/ covers the rest of deletion.
Cloud key services already keep KEKs in hardware security modules. You need to choose a specific HSM tier, a dedicated HSM or an external key store when a regulation, contract or threat model requires it, such as a mandated FIPS 140-3 security level or keys held outside the provider. Check the provider’s current CMVP certificate rather than a product page.
NIST moved all FIPS 140-2 validations to its historical list in September 2026. Existing systems may keep using those modules, but agencies decide when to require FIPS 140-3 modules, so record the certificate and standard that each key service relies on.
Verify new writes use K2, old records remain readable during overlap, migrated records authenticate and yield identical plaintext, and K1 retirement produces the intended outcome. A wrong tenant context or modified wrapped-key metadata must fail. Include a crash/retry and a concurrent record update.
Key destruction is irreversible for data that has no other usable key path. Disable the key first where the service supports it, run a restore test while it is disabled, and destroy it only after that test. State which archives remain decryptable, which have expired, and who accepted that retention decision.
Guided practice
Use the Python envelope code on this page to prove the properties the rotation plan relies on. Write the tests yourself, run them, and only then compare with the worked answers. Then use them to decide the K1 retirement below.
Supplied scenario material
Plan: rotate the key-encryption key from kek-2 to kek-3. Live records are rewrapped by a resumable job; a nightly backup from before the job still holds records wrapped by kek-2.
Claims the plan makes:
P1 A record copied to another tenant or record ID cannot be decrypted.
P2 Rewrapping changes only the wrapped data key, never the data ciphertext or its nonce.
P3 After kek-2 is retired, rewrapped live records still decrypt.
P4 After kek-2 is retired, a backup copy that was never rewrapped cannot be decrypted.
Concurrency note: the rewrap worker read record R1 at version 5; the application then wrote version 6 with new data and a new data key.1. Write a test for P1
Save the page's code as
envelope.py, then write a pytest test that encrypts a record for tenant north, document doc-7, and tries to decrypt it as south/doc-7 and north/doc-8.Python# test_envelope.py, next to envelope.py; run: python -m pytest test_envelope.py import copy import pytest from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import AESGCM import envelope def test_record_moved_to_another_tenant_or_id_fails(): record = envelope.encrypt_record("north", "doc-7", b"invoice 42") ... # your assertionsExpected result and reasoning
Both decryptions raise
cryptography.exceptions.InvalidTag: the context (version, tenant, record ID, purpose) is the associated data at both layers, so unwrapping the data key already fails. Assert it withwith pytest.raises(InvalidTag):around eachdecrypt_recordcall.2. Write tests for P2 and P3
Add
kek-3toenvelope.KEKS, make it current, rewrap a record and check what changed; then check it still decrypts.Expected result and reasoning
After
envelope.rewrap(...),ciphertextandnonceare byte-for-byte equal to the original,dek["kek"]is"kek-3", anddecrypt_recordreturns the original plaintext. That is the difference between rewrapping and re-encrypting: if the old data key had leaked, rewrapping would not help.3. Write a test for P4
Keep a deep copy of a record before rewrapping, delete
kek-2fromenvelope.KEKS, and decrypt both copies.Expected result and reasoning
The rewrapped live record decrypts; the backup copy fails, with a
KeyErrorin this stand-in becausekek-2is gone (a real KMS refuses the decrypt call). The test proves that retiringkek-2makes every un-migrated backup unreadable, which is what the retirement gate must account for.4. Resolve the stale rewrap
Decide whether the worker may write its
kek-3wrapping of R1 version 5 over version 6, and write the update statement it should use.Expected result and reasoning
No: version 5's wrapped data key belongs to the old data, and pairing it with version 6's ciphertext would make R1 unreadable. Use a conditional update such as
UPDATE records SET dek = ?, version = version + 1 WHERE id = ? AND version = 5; it matches no row, so the worker rereads version 6 and rewraps that.5. Set the retirement gate
Write the conditions that must hold before
kek-2is destroyed.Expected result and reasoning
Every live record references
kek-3(counted, not assumed); every backup that must stay restorable has been restored in a test environment and rewrapped, or has passed its retention date;kek-2has been disabled, not destroyed, for a test period with no failed decrypt calls; and the decision names which archives become unreadable and who accepted that.
kek-2 retirement checklist.The dictionary stands in for a KMS, so key retirement shows up as a missing key rather than a refused API call; repeat the P3 and P4 tests against your KMS in a test account before retiring a real key.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Run a local project
Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.
Bind ciphertext to the correct record
Use Web Crypto AES-GCM to reject tampered ciphertext and cross-tenant record transplants. Repair authenticated context binding.
In the extracted folder, run:
node --test exercise.test.mjs10 regression tests · The starter intentionally fails some cases.
Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image.
Part of the learning path Cryptography without shortcuts
Real encryption with temporary keys; this exercise does not implement a production key-management service.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- NIST SP 800-57 Part 1 Revision 5: Key Management. Revision 5 (May 2020).
- OWASP Key Management Cheat Sheet
- AWS KMS: rotate keys
- AWS KMS: encryption context
- Google Cloud KMS: key rotation
- Google Cloud KMS: additional authenticated data
- Azure Key Vault: configure key rotation
- AWS Encryption SDK: data key caching thresholds
- NIST SP 800-88 Rev. 2: Guidelines for Media Sanitization. Revision 2 (September 2025).
- AWS: Amazon S3 now automatically encrypts all new objects (January 2023)
Reference information
- SDLC phase
- DesignMaintenance
- ASVS 5.0.0 requirements
- v5.0.0-11.1.1 · v5.0.0-11.1.2 · v5.0.0-11.3.3 · v5.0.0-11.3.4 · v5.0.0-13.3.1 · v5.0.0-13.3.3