Post 03

Deep Dive: Envelope Encryption (MEK/DEK)

Enterprise-grade encryption with AES-256-GCM, per-user keys, and crypto-shredding capability

Nov 26, 2025 15 min read Security, Encryption, Prisma

Why Envelope Encryption?

Most applications encrypt data with a single key. If that key is compromised, everything is exposed. TimOS needed something stronger - the ability to say with confidence: "We cannot read your data."

Envelope encryption solves this with a two-tier key hierarchy:

┌─────────────────────────────────────────────────────────┐
│                    ENVELOPE ENCRYPTION                   │
├─────────────────────────────────────────────────────────┤
│                                                          │
│   MASTER ENCRYPTION KEY (MEK)                           │
│   ├── Stored in environment variable                    │
│   ├── Never stored in database                          │
│   └── Used to encrypt DEKs                              │
│                                                          │
│         ↓ encrypts                                       │
│                                                          │
│   DATA ENCRYPTION KEY (DEK) - One per user              │
│   ├── Generated on user signup                          │
│   ├── Stored encrypted in platform DB                   │
│   └── Used to encrypt user's actual data                │
│                                                          │
│         ↓ encrypts                                       │
│                                                          │
│   USER CONTENT - In vault database                      │
│   ├── Journal entries                                    │
│   ├── Pillar data                                        │
│   ├── Feels entries                                      │
│   └── Weekly recaps                                      │
│                                                          │
└─────────────────────────────────────────────────────────┘
The Key Insight

Crypto-shredding: When a user deletes their account, we don't need to find and delete all their encrypted content across multiple tables. We simply delete their DEK. Without the key, their data becomes cryptographically unrecoverable - even if the encrypted content remains in the database.

The Implementation

Key Generation

When a user signs up, we generate a unique 256-bit DEK using cryptographically secure random bytes:

// api/src/services/encryption.ts
import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
const KEY_LENGTH = 32; // 256 bits

export function generateDEK(): string {
  return crypto.randomBytes(KEY_LENGTH).toString('hex');
}

export function encryptDEK(dek: string, mek: string): {
  encryptedKey: string;
  iv: string;
} {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv(
    ALGORITHM,
    Buffer.from(mek, 'hex'),
    iv
  );

  let encrypted = cipher.update(dek, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const authTag = cipher.getAuthTag();

  return {
    encryptedKey: encrypted + authTag.toString('hex'),
    iv: iv.toString('hex'),
  };
}

Encrypting User Content

Every piece of user content goes through the same encryption process. We use AES-256-GCM because it provides both confidentiality and authenticity (the auth tag ensures the data hasn't been tampered with):

export function encrypt(
  plaintext: string,
  dekHex: string
): { ciphertext: string; iv: string } {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv(
    ALGORITHM,
    Buffer.from(dekHex, 'hex'),
    iv
  );

  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const authTag = cipher.getAuthTag();

  return {
    ciphertext: encrypted + authTag.toString('hex'),
    iv: iv.toString('hex'),
  };
}

export function decrypt(
  ciphertext: string,
  iv: string,
  dekHex: string
): string {
  const authTag = Buffer.from(
    ciphertext.slice(-AUTH_TAG_LENGTH * 2),
    'hex'
  );
  const encryptedData = ciphertext.slice(0, -AUTH_TAG_LENGTH * 2);

  const decipher = crypto.createDecipheriv(
    ALGORITHM,
    Buffer.from(dekHex, 'hex'),
    Buffer.from(iv, 'hex')
  );
  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

What We Can Honestly Say

With this implementation, we can make strong privacy claims:

  • Database breach: Attackers get ciphertext and encrypted DEKs - useless without MEK
  • Admin access: We see encrypted blobs, not user content
  • Subpoena: We can only provide encrypted data without the ability to decrypt
  • Account deletion: DEK deletion = cryptographic data destruction

Key Takeaways

  • Envelope encryption provides defense in depth
  • Per-user keys enable true data isolation
  • Crypto-shredding simplifies account deletion
  • AES-256-GCM provides both confidentiality and integrity
  • Dual databases keep keys separate from content