Barazo AppView backend barazo.forum
at main 72 lines 2.3 kB view raw
1import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto' 2 3/** 4 * HKDF info string for community key encryption. 5 * Binds derived keys to this specific use case. 6 */ 7const HKDF_INFO = 'barazo:community-keys' 8 9/** 10 * Derive a 256-bit AES key from the KEK using HKDF (SHA-256). 11 */ 12function deriveKey(kek: string): Buffer { 13 return Buffer.from(hkdfSync('sha256', kek, '', HKDF_INFO, 32)) 14} 15 16/** 17 * Encrypt plaintext using AES-256-GCM. 18 * 19 * @param plaintext - The string to encrypt 20 * @param kek - Key Encryption Key (minimum 32 characters, from AI_ENCRYPTION_KEY env var) 21 * @returns Base64-encoded string in format `iv:ciphertext:tag` 22 */ 23export function encrypt(plaintext: string, kek: string): string { 24 const key = deriveKey(kek) 25 const iv = randomBytes(12) 26 27 const cipher = createCipheriv('aes-256-gcm', key, iv) 28 const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]) 29 const tag = cipher.getAuthTag() 30 31 return `${iv.toString('base64')}:${encrypted.toString('base64')}:${tag.toString('base64')}` 32} 33 34/** 35 * Decrypt a string encrypted with {@link encrypt}. 36 * 37 * @param encrypted - Base64-encoded string in format `iv:ciphertext:tag` 38 * @param kek - The same KEK used during encryption 39 * @returns The original plaintext 40 * @throws If the data is corrupted, tampered with, or the wrong key is used 41 */ 42export function decrypt(encrypted: string, kek: string): string { 43 const parts = encrypted.split(':') 44 if (parts.length !== 3) { 45 throw new Error('Invalid encrypted data format: expected iv:ciphertext:tag') 46 } 47 48 const [ivB64, ciphertextB64, tagB64] = parts as [string, string, string] 49 50 if (!ivB64 || !tagB64) { 51 throw new Error('Invalid encrypted data format: empty component') 52 } 53 54 const iv = Buffer.from(ivB64, 'base64') 55 const ciphertext = Buffer.from(ciphertextB64, 'base64') 56 const tag = Buffer.from(tagB64, 'base64') 57 58 if (iv.length !== 12) { 59 throw new Error('Invalid IV length: expected 12 bytes') 60 } 61 62 if (tag.length !== 16) { 63 throw new Error('Invalid auth tag length: expected 16 bytes') 64 } 65 66 const key = deriveKey(kek) 67 68 const decipher = createDecipheriv('aes-256-gcm', key, iv) 69 decipher.setAuthTag(tag) 70 71 return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8') 72}