Barazo AppView backend
barazo.forum
1import { describe, it, expect } from 'vitest'
2import { encrypt, decrypt } from '../../../src/lib/encryption.js'
3
4// ---------------------------------------------------------------------------
5// Fixtures
6// ---------------------------------------------------------------------------
7
8const TEST_KEK = 'a'.repeat(32) // Minimum 32 characters
9const TEST_PLAINTEXT = 'deadbeef'.repeat(8) // 64-char hex string (like a signing key)
10
11/** Split encrypted string into [iv, ciphertext, tag] with type safety. */
12function splitEncrypted(encrypted: string): [string, string, string] {
13 const [iv, ciphertext, tag] = encrypted.split(':')
14 if (iv === undefined || ciphertext === undefined || tag === undefined) {
15 throw new Error('Expected 3 colon-separated parts')
16 }
17 return [iv, ciphertext, tag]
18}
19
20// ---------------------------------------------------------------------------
21// encrypt / decrypt roundtrip
22// ---------------------------------------------------------------------------
23
24describe('encrypt', () => {
25 it('returns a base64-encoded string with three colon-separated parts (iv:ciphertext:tag)', () => {
26 const encrypted = encrypt(TEST_PLAINTEXT, TEST_KEK)
27
28 const parts = encrypted.split(':')
29 expect(parts).toHaveLength(3)
30
31 // Each part should be valid base64
32 for (const part of parts) {
33 expect(() => Buffer.from(part, 'base64')).not.toThrow()
34 expect(part.length).toBeGreaterThan(0)
35 }
36 })
37
38 it('produces different ciphertext on each call (unique IV)', () => {
39 const encrypted1 = encrypt(TEST_PLAINTEXT, TEST_KEK)
40 const encrypted2 = encrypt(TEST_PLAINTEXT, TEST_KEK)
41
42 expect(encrypted1).not.toBe(encrypted2)
43
44 // IVs should differ
45 const [iv1] = splitEncrypted(encrypted1)
46 const [iv2] = splitEncrypted(encrypted2)
47 expect(iv1).not.toBe(iv2)
48 })
49
50 it('uses a 12-byte IV', () => {
51 const encrypted = encrypt(TEST_PLAINTEXT, TEST_KEK)
52 const [ivBase64] = splitEncrypted(encrypted)
53 const ivBytes = Buffer.from(ivBase64, 'base64')
54 expect(ivBytes.length).toBe(12)
55 })
56
57 it('produces a 16-byte auth tag', () => {
58 const encrypted = encrypt(TEST_PLAINTEXT, TEST_KEK)
59 const [, , tagBase64] = splitEncrypted(encrypted)
60 const tagBytes = Buffer.from(tagBase64, 'base64')
61 expect(tagBytes.length).toBe(16)
62 })
63})
64
65describe('decrypt', () => {
66 it('recovers the original plaintext', () => {
67 const encrypted = encrypt(TEST_PLAINTEXT, TEST_KEK)
68 const decrypted = decrypt(encrypted, TEST_KEK)
69
70 expect(decrypted).toBe(TEST_PLAINTEXT)
71 })
72
73 it('handles empty string plaintext', () => {
74 const encrypted = encrypt('', TEST_KEK)
75 const decrypted = decrypt(encrypted, TEST_KEK)
76
77 expect(decrypted).toBe('')
78 })
79
80 it('handles unicode plaintext', () => {
81 const unicode = 'hello world \u{1F600} \u00E9\u00E8\u00EA'
82 const encrypted = encrypt(unicode, TEST_KEK)
83 const decrypted = decrypt(encrypted, TEST_KEK)
84
85 expect(decrypted).toBe(unicode)
86 })
87})
88
89// ---------------------------------------------------------------------------
90// Wrong key
91// ---------------------------------------------------------------------------
92
93describe('decrypt with wrong key', () => {
94 it('throws when decrypting with a different KEK', () => {
95 const encrypted = encrypt(TEST_PLAINTEXT, TEST_KEK)
96 const wrongKek = 'b'.repeat(32)
97
98 expect(() => decrypt(encrypted, wrongKek)).toThrow()
99 })
100})
101
102// ---------------------------------------------------------------------------
103// Corrupted data
104// ---------------------------------------------------------------------------
105
106describe('decrypt with corrupted data', () => {
107 it('throws when ciphertext is corrupted', () => {
108 const encrypted = encrypt(TEST_PLAINTEXT, TEST_KEK)
109 const [iv, ciphertextB64, tag] = splitEncrypted(encrypted)
110
111 // Corrupt the ciphertext by flipping bits
112 const ciphertextBytes = Buffer.from(ciphertextB64, 'base64')
113 ciphertextBytes[0] = (ciphertextBytes[0] ?? 0) ^ 0xff
114 const corrupted = `${iv}:${ciphertextBytes.toString('base64')}:${tag}`
115
116 expect(() => decrypt(corrupted, TEST_KEK)).toThrow()
117 })
118
119 it('throws when auth tag is corrupted', () => {
120 const encrypted = encrypt(TEST_PLAINTEXT, TEST_KEK)
121 const [iv, ciphertext, tagB64] = splitEncrypted(encrypted)
122
123 // Corrupt the auth tag
124 const tagBytes = Buffer.from(tagB64, 'base64')
125 tagBytes[0] = (tagBytes[0] ?? 0) ^ 0xff
126 const corrupted = `${iv}:${ciphertext}:${tagBytes.toString('base64')}`
127
128 expect(() => decrypt(corrupted, TEST_KEK)).toThrow()
129 })
130
131 it('throws when IV is corrupted', () => {
132 const encrypted = encrypt(TEST_PLAINTEXT, TEST_KEK)
133 const [ivB64, ciphertext, tag] = splitEncrypted(encrypted)
134
135 // Corrupt the IV
136 const ivBytes = Buffer.from(ivB64, 'base64')
137 ivBytes[0] = (ivBytes[0] ?? 0) ^ 0xff
138 const corrupted = `${ivBytes.toString('base64')}:${ciphertext}:${tag}`
139
140 expect(() => decrypt(corrupted, TEST_KEK)).toThrow()
141 })
142
143 it('throws when encrypted string has wrong format (missing parts)', () => {
144 expect(() => decrypt('onlyonepart', TEST_KEK)).toThrow()
145 expect(() => decrypt('two:parts', TEST_KEK)).toThrow()
146 })
147
148 it('throws when encrypted string has empty parts', () => {
149 expect(() => decrypt('::', TEST_KEK)).toThrow()
150 })
151})
152
153// ---------------------------------------------------------------------------
154// HKDF key derivation
155// ---------------------------------------------------------------------------
156
157describe('key derivation', () => {
158 it('derives different encryption keys from different KEKs', () => {
159 const kek1 = 'a'.repeat(32)
160 const kek2 = 'b'.repeat(32)
161
162 const encrypted1 = encrypt(TEST_PLAINTEXT, kek1)
163 const encrypted2 = encrypt(TEST_PLAINTEXT, kek2)
164
165 // Can decrypt with matching key
166 expect(decrypt(encrypted1, kek1)).toBe(TEST_PLAINTEXT)
167 expect(decrypt(encrypted2, kek2)).toBe(TEST_PLAINTEXT)
168
169 // Cannot cross-decrypt
170 expect(() => decrypt(encrypted1, kek2)).toThrow()
171 expect(() => decrypt(encrypted2, kek1)).toThrow()
172 })
173})