Agent Wranglers

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

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.