Skip to main content

Encryption at Rest

Protect stored data with envelope encryption

TL;DR

Encryption at Rest protects data stored in databases, disks, backups. Envelope Encryption separates two keys: DEK (Data Encryption Key) encrypts data, KEK (Key Encryption Key) encrypts DEK. DEK stored alongside encrypted data; KEK stored in vault/KMS. If database stolen, encrypted data useless without KEK. Enables rotation: new KEK decrypts old DEK, encrypts with new DEK.

Learning Objectives

  • Understand envelope encryption architecture
  • Encrypt database columns and tables
  • Design key rotation without re-encrypting data
  • Implement transparent encryption (TDE)
  • Balance security with performance and searchability

Motivating Scenario

Problem: Database backup stolen (accidentally exposed on AWS S3). Contains plaintext customer credit cards, SSNs. Attackers sell data. Compliance violation (PCI, GDPR).

Solution: Credit card column encrypted. Data in backup is ciphertext. Without KEK (stored in vault), ciphertext useless. Even if attacker has all data, can't decrypt. KEK rotated monthly; old backup data auto-decryptable with old key.

Core Concepts

Envelope Encryption Architecture

Application
↓ wants to encrypt data

Generate DEK (Data Encryption Key) = random 256-bit key

Encrypt data: ciphertext = AES-256(data, DEK)

Encrypt DEK: encrypted_dek = RSA(DEK, KEK_public)

Store in database:
{
encrypted_data: "p9k2x...",
encrypted_dek: "jk3l2...", // KEK encrypted this DEK
metadata: { alg: "AES-256-GCM", key_version: "2025-02" }
}

Later, to decrypt:

Fetch encrypted record

Request KEK from KMS (supply encrypted_dek)

KMS decrypts DEK: DEK = RSA_decrypt(encrypted_dek, KEK_private)

Decrypt data: data = AES-256-decrypt(encrypted_data, DEK)

Return plaintext to app (only in memory, never persisted)

Why this matters:

  • DEK varies per record; KEK shared
  • If DEK compromised, only that record affected
  • If KEK compromised, all data at risk (but KEK in vault, hard to compromise)
  • Rotate KEK every 90 days; no need to re-encrypt all data

Transparent Data Encryption (TDE)

Database-level encryption. Transparent to application.

Application writes: data = "secret"

Database encrypts: encrypted_data = AES(data, DEK)

Database stores: encrypted_data (on disk)

Application reads: Database decrypts, returns data = "secret"

App doesn't know encryption happened.

Supported by:

  • Oracle Database TDE
  • Microsoft SQL Server TDE
  • MySQL (InnoDB Transparent Encryption)
  • PostgreSQL (pgcrypto, or encryption extensions)

Field-Level Encryption

Encrypt specific columns. App controls which data encrypted.

CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(100), -- plaintext
ssn VARBINARY(256), -- encrypted
email VARCHAR(100) -- plaintext
);

-- Encrypt SSN
INSERT INTO customers VALUES (1, 'Alice', AES_ENCRYPT('123-45-6789', @key), 'alice@example.com');

-- Decrypt SSN
SELECT AES_DECRYPT(ssn, @key) FROM customers WHERE id = 1;

DEK vs KEK Rotation

DEK Rotation (data-level)
  1. Regenerate new DEK for each record
  2. Re-encrypt data with new DEK
  3. Requires reading/writing all data (slow)
  4. When: Compromise suspected, compliance
KEK Rotation (key-level)
  1. Generate new KEK in vault
  2. Old encrypted DEKs still decryptable with old KEK
  3. New records use new KEK
  4. No data re-encryption needed (fast)
  5. When: Scheduled (90 days), best practice

Practical Examples

// Using AWS KMS + local encryption
const AWS = require('aws-sdk');
const crypto = require('crypto');
const kms = new AWS.KMS();

async function encryptRecord(data, kmsKeyId) {
// Generate DEK
const dek = crypto.randomBytes(32); // 256-bit key

// Encrypt DEK with KEK (in KMS)
const encrypted_dek = await kms.encrypt({
KeyId: kmsKeyId,
Plaintext: dek
}).promise();

// Encrypt data with DEK
const cipher = crypto.createCipheriv('aes-256-gcm', dek, crypto.randomBytes(12));
let encrypted_data = cipher.update(data, 'utf8', 'hex');
encrypted_data += cipher.final('hex');
const authTag = cipher.getAuthTag();

return {
encrypted_data: encrypted_data,
encrypted_dek: encrypted_dek.CiphertextBlob.toString('base64'),
iv: cipher.iv.toString('hex'),
authTag: authTag.toString('hex'),
key_version: '2025-02'
};
}

async function decryptRecord(record, kmsKeyId) {
// Decrypt DEK with KEK (from KMS)
const decrypted_dek = await kms.decrypt({
CiphertextBlob: Buffer.from(record.encrypted_dek, 'base64')
}).promise();

const dek = decrypted_dek.Plaintext;

// Decrypt data with DEK
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
dek,
Buffer.from(record.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(record.authTag, 'hex'));

let data = decipher.update(record.encrypted_data, 'hex', 'utf8');
data += decipher.final('utf8');

return data;
}

// Usage
const encrypted = await encryptRecord('credit_card_4111111111111111', 'kms-key-id');
const decrypted = await decryptRecord(encrypted, 'kms-key-id');

When to Use / When Not to Use

Use Envelope Encryption When
  1. Storing sensitive data (PII, health, financial)
  2. Compliance required (HIPAA, PCI, GDPR)
  3. Data breach impact is high
  4. Key rotation needed without data re-encryption
  5. Scalable encryption of many records
Use TDE When
  1. Whole database encryption needed
  2. Zero application changes desired
  3. Compliance requires encrypted storage
  4. Performance overhead acceptable
  5. Database supports TDE natively

Patterns and Pitfalls

Pitfall: Storing DEK alongside encrypted data in plaintext. Attacker decrypts everything.

Pattern: Always encrypt DEK with KEK. KEK in vault, never in code.

Pitfall: One key for all data. If key compromised, all data at risk.

Pattern: Separate keys per data classification. Financial data, health data, logs all different keys.

Pitfall: Forgetting to encrypt backups. Backup stolen = plaintext data.

Pattern: Backup encryption same as production (envelope encryption works across backups).

Pitfall: No key versioning. Can't rotate keys without breaking old encrypted data.

Pattern: Tag encrypted data with key_version (2025-02). System knows which key to use for decryption.

Design Review Checklist

  • Sensitive data classified (PII, health, financial)
  • Envelope encryption designed (DEK + KEK)
  • KEK stored in vault/KMS (not in code)
  • DEK rotated periodically (or regenerated per record)
  • KEK rotation automated (90-day cycle minimum)
  • Encryption covers backups and archives
  • Key versioning implemented
  • Database encryption enabled (TDE or field-level)
  • Performance impact assessed
  • Audit logs track key usage
  • Recovery procedure tested (can decrypt old data?)

Self-Check

  • What's the difference between DEK and KEK?
  • Why store DEK-encrypted-with-KEK alongside data?
  • How would you rotate KEK without re-encrypting all data?
One Takeaway

Envelope encryption + KEK in vault = data stays protected even if storage medium is stolen.

Next Steps

  • Read Key Management for KEK lifecycle and HSM
  • Study Encryption in Transit for end-to-end encryption
  • Explore Compliance Frameworks for regulatory data handling

References

  • NIST SP 800-175B: Guideline for Use of Cryptographic Standards
  • AWS KMS Envelope Encryption
  • OWASP Data Protection Cheat Sheet
  • Oracle TDE Documentation