Data Encryption

Patterns for encrypting sensitive data at rest and in transit.

Overview#

Data encryption protects sensitive information from unauthorized access. This pattern covers:

  • Field-level encryption
  • AES-256-GCM encryption
  • Password hashing
  • Key derivation
  • Database encryption

Prerequisites#

npm install bcryptjs # Types for TypeScript npm install -D @types/bcryptjs

Code Example#

AES-256-GCM Encryption#

1// lib/encryption.ts 2import { 3 createCipheriv, 4 createDecipheriv, 5 randomBytes, 6 scrypt 7} from 'crypto' 8import { promisify } from 'util' 9 10const scryptAsync = promisify(scrypt) 11const ALGORITHM = 'aes-256-gcm' 12const ENCODING = 'base64' 13 14export async function encrypt( 15 plaintext: string, 16 password: string 17): Promise<string> { 18 // Generate salt and derive key 19 const salt = randomBytes(16) 20 const key = (await scryptAsync(password, salt, 32)) as Buffer 21 22 // Generate IV 23 const iv = randomBytes(12) 24 25 // Encrypt 26 const cipher = createCipheriv(ALGORITHM, key, iv) 27 const encrypted = Buffer.concat([ 28 cipher.update(plaintext, 'utf8'), 29 cipher.final() 30 ]) 31 const authTag = cipher.getAuthTag() 32 33 // Combine: salt (16) + iv (12) + authTag (16) + encrypted 34 const result = Buffer.concat([salt, iv, authTag, encrypted]) 35 return result.toString(ENCODING) 36} 37 38export async function decrypt( 39 ciphertext: string, 40 password: string 41): Promise<string> { 42 const data = Buffer.from(ciphertext, ENCODING) 43 44 // Extract components 45 const salt = data.subarray(0, 16) 46 const iv = data.subarray(16, 28) 47 const authTag = data.subarray(28, 44) 48 const encrypted = data.subarray(44) 49 50 // Derive key 51 const key = (await scryptAsync(password, salt, 32)) as Buffer 52 53 // Decrypt 54 const decipher = createDecipheriv(ALGORITHM, key, iv) 55 decipher.setAuthTag(authTag) 56 57 const decrypted = Buffer.concat([ 58 decipher.update(encrypted), 59 decipher.final() 60 ]) 61 62 return decrypted.toString('utf8') 63}

Password Hashing#

1// lib/password.ts 2import bcrypt from 'bcryptjs' 3 4const SALT_ROUNDS = 12 5 6export async function hashPassword(password: string): Promise<string> { 7 return bcrypt.hash(password, SALT_ROUNDS) 8} 9 10export async function verifyPassword( 11 password: string, 12 hash: string 13): Promise<boolean> { 14 return bcrypt.compare(password, hash) 15} 16 17// Usage in authentication 18export async function authenticateUser(email: string, password: string) { 19 const user = await prisma.user.findUnique({ 20 where: { email } 21 }) 22 23 if (!user || !user.passwordHash) { 24 return null 25 } 26 27 const isValid = await verifyPassword(password, user.passwordHash) 28 29 if (!isValid) { 30 return null 31 } 32 33 return user 34}

Field-Level Encryption#

1// lib/encrypted-fields.ts 2import { encrypt, decrypt } from './encryption' 3 4const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY! 5 6// Encrypt sensitive fields before saving 7export async function encryptUserData(data: { 8 ssn?: string 9 bankAccount?: string 10}) { 11 const encrypted: Record<string, string> = {} 12 13 if (data.ssn) { 14 encrypted.ssnEncrypted = await encrypt(data.ssn, ENCRYPTION_KEY) 15 } 16 17 if (data.bankAccount) { 18 encrypted.bankAccountEncrypted = await encrypt( 19 data.bankAccount, 20 ENCRYPTION_KEY 21 ) 22 } 23 24 return encrypted 25} 26 27// Decrypt when reading 28export async function decryptUserData(user: { 29 ssnEncrypted?: string | null 30 bankAccountEncrypted?: string | null 31}) { 32 const decrypted: Record<string, string | null> = { 33 ssn: null, 34 bankAccount: null 35 } 36 37 if (user.ssnEncrypted) { 38 decrypted.ssn = await decrypt(user.ssnEncrypted, ENCRYPTION_KEY) 39 } 40 41 if (user.bankAccountEncrypted) { 42 decrypted.bankAccount = await decrypt( 43 user.bankAccountEncrypted, 44 ENCRYPTION_KEY 45 ) 46 } 47 48 return decrypted 49}

Prisma Middleware for Automatic Encryption#

1// lib/prisma-encryption.ts 2import { Prisma } from '@prisma/client' 3import { encrypt, decrypt } from './encryption' 4 5const ENCRYPTED_FIELDS = { 6 User: ['ssn', 'bankAccount'], 7 Payment: ['cardNumber', 'cvv'] 8} 9 10const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY! 11 12export const encryptionMiddleware: Prisma.Middleware = async (params, next) => { 13 const { model, action, args } = params 14 15 if (!model || !ENCRYPTED_FIELDS[model as keyof typeof ENCRYPTED_FIELDS]) { 16 return next(params) 17 } 18 19 const fields = ENCRYPTED_FIELDS[model as keyof typeof ENCRYPTED_FIELDS] 20 21 // Encrypt on create/update 22 if (action === 'create' || action === 'update') { 23 for (const field of fields) { 24 if (args.data?.[field]) { 25 args.data[`${field}Encrypted`] = await encrypt( 26 args.data[field], 27 ENCRYPTION_KEY 28 ) 29 delete args.data[field] 30 } 31 } 32 } 33 34 const result = await next(params) 35 36 // Decrypt on read 37 if (result && (action === 'findUnique' || action === 'findFirst')) { 38 for (const field of fields) { 39 const encryptedField = `${field}Encrypted` 40 if (result[encryptedField]) { 41 result[field] = await decrypt(result[encryptedField], ENCRYPTION_KEY) 42 } 43 } 44 } 45 46 return result 47} 48 49// Register: prisma.$use(encryptionMiddleware)

Envelope Encryption#

1// lib/envelope-encryption.ts 2import { randomBytes, createCipheriv, createDecipheriv } from 'crypto' 3 4// Generate a data encryption key (DEK) 5export function generateDek(): Buffer { 6 return randomBytes(32) 7} 8 9// Encrypt DEK with master key (KEK) 10export function encryptDek(dek: Buffer, kek: Buffer): string { 11 const iv = randomBytes(12) 12 const cipher = createCipheriv('aes-256-gcm', kek, iv) 13 14 const encrypted = Buffer.concat([ 15 cipher.update(dek), 16 cipher.final() 17 ]) 18 const authTag = cipher.getAuthTag() 19 20 return Buffer.concat([iv, authTag, encrypted]).toString('base64') 21} 22 23// Decrypt DEK with master key 24export function decryptDek(encryptedDek: string, kek: Buffer): Buffer { 25 const data = Buffer.from(encryptedDek, 'base64') 26 27 const iv = data.subarray(0, 12) 28 const authTag = data.subarray(12, 28) 29 const encrypted = data.subarray(28) 30 31 const decipher = createDecipheriv('aes-256-gcm', kek, iv) 32 decipher.setAuthTag(authTag) 33 34 return Buffer.concat([ 35 decipher.update(encrypted), 36 decipher.final() 37 ]) 38} 39 40// Usage: Each record gets its own DEK, encrypted by KEK 41interface EncryptedRecord { 42 encryptedDek: string 43 encryptedData: string 44} 45 46export async function encryptWithEnvelope( 47 data: string, 48 kek: Buffer 49): Promise<EncryptedRecord> { 50 const dek = generateDek() 51 const encryptedDek = encryptDek(dek, kek) 52 const encryptedData = await encrypt(data, dek.toString('hex')) 53 54 return { encryptedDek, encryptedData } 55}

Client-Side Encryption#

1// lib/client-encryption.ts (browser) 2export async function encryptInBrowser( 3 data: string, 4 password: string 5): Promise<string> { 6 const encoder = new TextEncoder() 7 const salt = crypto.getRandomValues(new Uint8Array(16)) 8 9 // Derive key from password 10 const keyMaterial = await crypto.subtle.importKey( 11 'raw', 12 encoder.encode(password), 13 'PBKDF2', 14 false, 15 ['deriveKey'] 16 ) 17 18 const key = await crypto.subtle.deriveKey( 19 { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, 20 keyMaterial, 21 { name: 'AES-GCM', length: 256 }, 22 false, 23 ['encrypt'] 24 ) 25 26 // Encrypt 27 const iv = crypto.getRandomValues(new Uint8Array(12)) 28 const encrypted = await crypto.subtle.encrypt( 29 { name: 'AES-GCM', iv }, 30 key, 31 encoder.encode(data) 32 ) 33 34 // Combine salt + iv + encrypted 35 const result = new Uint8Array(salt.length + iv.length + encrypted.byteLength) 36 result.set(salt, 0) 37 result.set(iv, salt.length) 38 result.set(new Uint8Array(encrypted), salt.length + iv.length) 39 40 return btoa(String.fromCharCode(...result)) 41}

Usage Instructions#

  1. Use AES-256-GCM for symmetric encryption
  2. Always use unique IVs/nonces for each encryption
  3. Store the auth tag with the ciphertext
  4. Use bcrypt for password hashing (not encryption)
  5. Consider envelope encryption for large datasets

Best Practices#

  • Never roll your own crypto - Use established libraries
  • Use authenticated encryption - GCM mode provides integrity
  • Rotate keys regularly - Implement key rotation procedures
  • Separate encryption keys - Different keys for different data types
  • Hash passwords - Use bcrypt, not encryption for passwords
  • Protect keys - Store encryption keys in secure vaults
  • Audit key access - Log when encryption keys are used