Barazo AppView backend
barazo.forum
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}