Barazo AppView backend barazo.forum
at main 173 lines 6.0 kB view raw
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})