Skip to main content

Encryption & PII

Sensitive data is encrypted at rest using AES-256-GCM.

What’s Encrypted

DataTableMethod
Bank account numbersapplicant_bank_accountsAES-256-GCM
SSNsapplicant_piiAES-256-GCM
Plaid access tokensform_submissions.plaid_access_tokensAES-256-GCM
OAuth tokensairtable_connectionsAES-256-GCM

Encryption Implementation

// src/lib/encryption.ts

import { createCipheriv, createDecipheriv, randomBytes } from "crypto";

const ALGORITHM = "aes-256-gcm";
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, "hex");

export function encrypt(plaintext: string): string {
  const iv = randomBytes(16);
  const cipher = createCipheriv(ALGORITHM, KEY, iv);

  let encrypted = cipher.update(plaintext, "utf8", "hex");
  encrypted += cipher.final("hex");

  const authTag = cipher.getAuthTag();

  // Format: iv:authTag:ciphertext
  return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
}

export function decrypt(encrypted: string): string {
  const [ivHex, authTagHex, ciphertext] = encrypted.split(":");

  const iv = Buffer.from(ivHex, "hex");
  const authTag = Buffer.from(authTagHex, "hex");

  const decipher = createDecipheriv(ALGORITHM, KEY, iv);
  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(ciphertext, "hex", "utf8");
  decrypted += decipher.final("utf8");

  return decrypted;
}

Masking for Display

When showing encrypted data in the UI, we display masks:
// Show last 4 digits only
function maskAccountNumber(encrypted: string): string {
  const decrypted = decrypt(encrypted);
  return `****${decrypted.slice(-4)}`;
}

Key Management

  • Encryption key stored in Doppler as ENCRYPTION_KEY
  • 256-bit key (64 hex characters)
  • Key rotation requires re-encrypting existing data