Agent Wranglers

APS-58Recommended

Encrypt the most sensitive fields

Encrypt fields whose leak would cause serious harm, such as ID numbers or health data, in the app before they reach the database.

How to do it

Encrypt with authenticated encryption from a well-known library, such as AES-256-GCM or libsodium's secretbox. Use a fresh random nonce for every value and a key held in a secret store, not in the database. To search by the value, store an HMAC-SHA-256 of it under a second key. Disk encryption does not protect against someone who can query the database, as Encrypt database storage with a key you control explains. Store the key's version in front of each value. After a rotation, new values use the new key, and a background job re-encrypts old rows until no value names the old version. The rotation period lives in Keep encryption keys in a key service. Where a backup cannot be edited, encrypt each person's fields with a data key of their own. Destroying that key erases those fields in every copy, a method called crypto-shredding.

Examples

JavaScript example

import { createCipheriv, createHmac, randomBytes } from 'node:crypto';

function encryptField(plaintext, key, keyVersion) {
  const nonce = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, nonce);
  const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  return `v${keyVersion}:` + Buffer.concat([nonce, cipher.getAuthTag(), ciphertext]).toString('base64');
}
const lookupHash = (value, hashKey) => createHmac('sha256', hashKey).update(value).digest('hex');

Python example

import base64, hashlib, hmac, os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def encrypt_field(plaintext: str, key: bytes, key_version: int) -> str:
    nonce = os.urandom(12)
    sealed = nonce + AESGCM(key).encrypt(nonce, plaintext.encode(), None)
    return f"v{key_version}:" + base64.b64encode(sealed).decode()

def lookup_hash(value: str, hash_key: bytes) -> str:
    return hmac.new(hash_key, value.encode(), hashlib.sha256).hexdigest()

How to check

Query a sensitive column directly in the database. It shows unreadable ciphertext, and two rows with the same value hold different ciphertext. Thirty days after a key rotation, a count of values that name the old key version returns 0.

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/field-encryption">Agent Wranglers application security playbook</a>. In code you write, add a comment next to it with the same link.