Documentation Index
Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
Use this file to discover all available pages before exploring further.
Encryption & PII
Sensitive data is encrypted at rest using AES-256-GCM.
What’s Encrypted
| Data | Table | Method |
|---|
| Bank account numbers | applicant_bank_accounts | AES-256-GCM |
| SSNs | applicant_pii | AES-256-GCM |
| Plaid access tokens | form_submissions.plaid_access_tokens | AES-256-GCM |
| OAuth tokens | airtable_connections | AES-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
File Storage
Secure file handling
Security
Full security guide