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/bcryptjsCode 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#
- Use AES-256-GCM for symmetric encryption
- Always use unique IVs/nonces for each encryption
- Store the auth tag with the ciphertext
- Use bcrypt for password hashing (not encryption)
- 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
Related Patterns#
- Secrets Management - Manage encryption keys
- Audit Logging - Track data access
- Input Validation - Validate before encrypting
- Session Management - Secure sessions