In short

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.

Verify it

  1. Decrypt a record with another tenant’s or record’s context and confirm both the unwrap and the data decryption fail.
  2. Rotate the KEK in a test environment and confirm old records still decrypt and new records name the new key version.
  3. Run the rewrap job, kill it midway, rerun it, and confirm no record lost its only usable wrapped key.
  4. Disable the old KEK, restore a backup from before the migration, and record which data it can and cannot read.
  5. 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 prerequisitesAbout 32 minutes: 10 to read and 22 for the exercise

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 practice

Keep key wrapping separate from record encryption

ApplicationKey managementStorageApplicationKey serviceRecord storeData key under KEK v2, withrecord context1Plaintext DEK and wrapped DEK1Encrypt record with DEK andrecord context1Ciphertext, wrapped DEK, key ID, algorithm2Read ciphertext and wrapped DEK3Unwrap with the same context3Plaintext DEK, if policy allows3Rewrap the DEK under KEK v34New wrapped DEK4Conditional update; data unchanged4Trust boundary1Step, described below
  1. 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.
  2. Store the envelopeThe record store keeps the ciphertext, the wrapped data key, the KEK version and the algorithm.
  3. 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.
  4. 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.
Keep the old KEK only for the migration window and tested recovery needs: a successful write under the new key does not prove old records and backups remain recoverable.

Two key roles create two different rotation operations

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.

How should you encrypt data at rest?

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.

How should you encrypt data at rest?
LayerProtects againstDoes not protect againstExamples
Disk, volume or storage-service encryption with provider-managed keysStolen or discarded disks, physical access to storageAnyone who can query the database or read the bucket through normal accessDefault encryption in cloud storage services, LUKS, BitLocker
Database or bucket encryption with a customer-managed KMS keyThe above, plus storage-level access without key permission; gives a decryption audit trail and crypto-shreddingDatabase users, SQL injection and the application, which all see plaintextTransparent data encryption, S3 SSE-KMS, Cloud SQL with a customer-managed key
Application-level envelope encryption of fields or recordsThe above, plus administrators, dumps, backups and other services without KMS decrypt rightsA compromised process that holds decrypt rights, and plaintext copies in logs or cachesThe AES-GCM envelope pattern below, the AWS Encryption SDK, Tink

The record and the key service need matching context

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.

How do AWS KMS, Google Cloud KMS and Azure Key Vault handle these operations?

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.

How do AWS KMS, Google Cloud KMS and Azure Key Vault handle these operations?
OperationAWS KMSGoogle Cloud KMSAzure Key Vault
Bind record contextEncryption context: non-secret key-value pairs, required again to decrypt, usable in policy conditionsAdditional authenticated data up to 64 KiB, required again to decryptBind context in the data-layer AEAD around the wrapped key
Separate dutiesKey policies and IAM, plus grants constrained by encryption contextroles/cloudkms.cryptoKeyEncrypter, cryptoKeyDecrypter; admin cannot decryptKey Vault Crypto Service Encryption User for wrap and unwrap; Crypto Officer for management
Rotate the KEKNew key material on schedule or on demand; key ID unchanged; old material kept for decryptionNew primary version; previous versions stay enabled until destroyedNew key version; store the versioned key URI with each wrapped DEK
Delete the KEK7 to 30 day waiting period, then data is unrecoverableScheduled for destruction, 30 days by default, then data is unrecoverableSoft delete for 7 to 90 days; purge protection blocks early purge

Use a resumable state machine rather than a one-shot script

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.

Wrap, rewrap and decrypt a record in Python

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.

Fixedenvelope.pyPython
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)}

Delete data by destroying its key

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.

When do you need an HSM or FIPS 140-3 validation?

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.

A retired key must be unnecessary or deliberately retained

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
Plain text
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. 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 assertions
    Expected 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 with with pytest.raises(InvalidTag): around each decrypt_record call.

  2. 2. Write tests for P2 and P3

    Add kek-3 to envelope.KEKS, make it current, rewrap a record and check what changed; then check it still decrypts.

    Expected result and reasoning

    After envelope.rewrap(...), ciphertext and nonce are byte-for-byte equal to the original, dek["kek"] is "kek-3", and decrypt_record returns the original plaintext. That is the difference between rewrapping and re-encrypting: if the old data key had leaked, rewrapping would not help.

  3. 3. Write a test for P4

    Keep a deep copy of a record before rewrapping, delete kek-2 from envelope.KEKS, and decrypt both copies.

    Expected result and reasoning

    The rewrapped live record decrypts; the backup copy fails, with a KeyError in this stand-in because kek-2 is gone (a real KMS refuses the decrypt call). The test proves that retiring kek-2 makes every un-migrated backup unreadable, which is what the retirement gate must account for.

  4. 4. Resolve the stale rewrap

    Decide whether the worker may write its kek-3 wrapping 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. 5. Set the retirement gate

    Write the conditions that must hold before kek-2 is 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-2 has 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.

Your deliverable: A test file with four passing tests for P1 to P4, the conditional update statement, and a 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.

Node.js 22+ · no dependencies · 30–45 min

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:

Shell
node --test exercise.test.mjs

10 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.

  1. NIST SP 800-57 Part 1 Revision 5: Key Management. Revision 5 (May 2020).
  2. OWASP Key Management Cheat Sheet
  3. AWS KMS: rotate keys
  4. AWS KMS: encryption context
  5. Google Cloud KMS: key rotation
  6. Google Cloud KMS: additional authenticated data
  7. Azure Key Vault: configure key rotation
  8. AWS Encryption SDK: data key caching thresholds
  9. NIST SP 800-88 Rev. 2: Guidelines for Media Sanitization. Revision 2 (September 2025).
  10. AWS: Amazon S3 now automatically encrypts all new objects (January 2023)

Reference information

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

Download this guide as Markdown