APS-103Recommended
Wrap data keys with a key-encryption key
Encrypt data with data keys, and store each data key only in wrapped form. A key-encryption key that never leaves the key service or HSM wraps it. Rotate the key-encryption key without decrypting the data.
How to do it
This is envelope encryption, and it gives you a key hierarchy. A root key sits in a hardware security module (HSM) or a cloud key service. It wraps key-encryption keys, one per purpose or tenant, and these wrap the data keys. Ask the key service for a new data key, encrypt locally with it, then keep only the wrapped copy next to the ciphertext. Rotating a key-encryption key means unwrapping and rewrapping the small data keys, never re-encrypting the data. Give each data key one purpose, such as one table or one file, so a leak stays small. Record the key version with every wrapped key. Key storage and rotation periods are in Keep encryption keys in a key service. Keys per tenant are in Give each tenant its own encryption key.
Examples
Python example
import boto3, os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
kms = boto3.client("kms")
def seal(plaintext: bytes, kek_id: str) -> dict:
key = kms.generate_data_key(KeyId=kek_id, KeySpec="AES_256")
nonce = os.urandom(12)
return {"wrapped_key": key["CiphertextBlob"], "nonce": nonce,
"data": AESGCM(key["Plaintext"]).encrypt(nonce, plaintext, None)}
def rewrap(sealed: dict, new_kek_id: str) -> dict:
out = kms.re_encrypt(CiphertextBlob=sealed["wrapped_key"], DestinationKeyId=new_kek_id)
return {**sealed, "wrapped_key": out["CiphertextBlob"]}
JavaScript example
import { KMSClient, GenerateDataKeyCommand, ReEncryptCommand } from '@aws-sdk/client-kms';
import { createCipheriv, randomBytes } from 'node:crypto';
const kms = new KMSClient({});
export async function seal(plaintext, kekId) {
const key = await kms.send(new GenerateDataKeyCommand({ KeyId: kekId, KeySpec: 'AES_256' }));
const nonce = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key.Plaintext, nonce);
const data = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
return { wrappedKey: key.CiphertextBlob, nonce, data };
}
export async function rewrap(sealed, newKekId) {
const out = await kms.send(new ReEncryptCommand({ CiphertextBlob: sealed.wrappedKey, DestinationKeyId: newKekId }));
return { ...sealed, wrappedKey: out.CiphertextBlob };
}
How to check
A search of the database and storage finds no unwrapped data key. A test encrypts a record, rotates the key-encryption key in a sandbox and rewraps the data keys. The record still decrypts, its ciphertext bytes are unchanged, and every wrapped key names the new version.
Sources
- NIST SP 800-57 Part 1 Rev. 5 Recommendation for Key Management
- AWS KMS concepts, envelope encryption
Related rules
- APS-57 Keep encryption keys in a key service in Application security
- DAT-74 Give each tenant its own encryption key when customers ask to hold it in Data and databases
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/application-security/cryptography-and-keys/envelope-encryption">Agent Wranglers application security playbook</a>. In code you write, add a comment next to it with the same link.