encryption.test.ts
1 /** 2 * Unit tests for encryption utilities 3 */ 4 5 import { describe, it, expect } from 'vitest'; 6 import { 7 deriveKey, 8 encrypt, 9 decrypt, 10 exportKey, 11 importKey, 12 generateSalt, 13 generateIV, 14 toBase64, 15 fromBase64, 16 verifyKey, 17 DEFAULT_ITERATIONS, 18 } from '../../../src/lib/crypto/encryption'; 19 20 describe('encryption utilities', () => { 21 describe('toBase64 / fromBase64', () => { 22 it('should round-trip encode and decode', () => { 23 const original = new Uint8Array([0, 1, 2, 255, 128, 64]); 24 const encoded = toBase64(original); 25 const decoded = fromBase64(encoded); 26 expect(decoded).toEqual(original); 27 }); 28 29 it('should encode empty array', () => { 30 const empty = new Uint8Array([]); 31 const encoded = toBase64(empty); 32 const decoded = fromBase64(encoded); 33 expect(decoded).toEqual(empty); 34 }); 35 }); 36 37 describe('generateSalt', () => { 38 it('should generate 16-byte salt', () => { 39 const salt = generateSalt(); 40 expect(salt.length).toBe(16); 41 }); 42 43 it('should generate unique salts', () => { 44 const salt1 = generateSalt(); 45 const salt2 = generateSalt(); 46 expect(toBase64(salt1)).not.toBe(toBase64(salt2)); 47 }); 48 }); 49 50 describe('generateIV', () => { 51 it('should generate 12-byte IV', () => { 52 const iv = generateIV(); 53 expect(iv.length).toBe(12); 54 }); 55 56 it('should generate unique IVs', () => { 57 const iv1 = generateIV(); 58 const iv2 = generateIV(); 59 expect(toBase64(iv1)).not.toBe(toBase64(iv2)); 60 }); 61 }); 62 63 describe('deriveKey', () => { 64 it('should derive a CryptoKey from password', async () => { 65 const password = 'test-password-123'; 66 const salt = generateSalt(); 67 const key = await deriveKey(password, salt); 68 69 expect(key).toBeDefined(); 70 expect(key.type).toBe('secret'); 71 expect(key.algorithm.name).toBe('AES-GCM'); 72 }); 73 74 it('should derive same key for same password and salt', async () => { 75 const password = 'test-password-123'; 76 const salt = generateSalt(); 77 78 const key1 = await deriveKey(password, salt); 79 const key2 = await deriveKey(password, salt); 80 81 // Export and compare 82 const exported1 = await exportKey(key1); 83 const exported2 = await exportKey(key2); 84 85 expect(exported1).toBe(exported2); 86 }); 87 88 it('should derive different keys for different passwords', async () => { 89 const salt = generateSalt(); 90 91 const key1 = await deriveKey('password1', salt); 92 const key2 = await deriveKey('password2', salt); 93 94 const exported1 = await exportKey(key1); 95 const exported2 = await exportKey(key2); 96 97 expect(exported1).not.toBe(exported2); 98 }); 99 100 it('should derive different keys for different salts', async () => { 101 const password = 'same-password'; 102 103 const key1 = await deriveKey(password, generateSalt()); 104 const key2 = await deriveKey(password, generateSalt()); 105 106 const exported1 = await exportKey(key1); 107 const exported2 = await exportKey(key2); 108 109 expect(exported1).not.toBe(exported2); 110 }); 111 }); 112 113 describe('encrypt / decrypt', () => { 114 it('should encrypt and decrypt plaintext', async () => { 115 const password = 'test-password'; 116 const salt = generateSalt(); 117 const key = await deriveKey(password, salt); 118 119 const plaintext = 'Hello, World!'; 120 const { ciphertext, iv } = await encrypt(plaintext, key); 121 122 expect(ciphertext).toBeDefined(); 123 expect(iv).toBeDefined(); 124 expect(ciphertext).not.toBe(plaintext); 125 126 const decrypted = await decrypt(ciphertext, iv, key); 127 expect(decrypted).toBe(plaintext); 128 }); 129 130 it('should handle empty string', async () => { 131 const key = await deriveKey('password', generateSalt()); 132 133 const plaintext = ''; 134 const { ciphertext, iv } = await encrypt(plaintext, key); 135 const decrypted = await decrypt(ciphertext, iv, key); 136 137 expect(decrypted).toBe(''); 138 }); 139 140 it('should handle unicode characters', async () => { 141 const key = await deriveKey('password', generateSalt()); 142 143 const plaintext = 'Hello 世界 🌍 Привет'; 144 const { ciphertext, iv } = await encrypt(plaintext, key); 145 const decrypted = await decrypt(ciphertext, iv, key); 146 147 expect(decrypted).toBe(plaintext); 148 }); 149 150 it('should handle large plaintext', async () => { 151 const key = await deriveKey('password', generateSalt()); 152 153 const plaintext = 'A'.repeat(100000); 154 const { ciphertext, iv } = await encrypt(plaintext, key); 155 const decrypted = await decrypt(ciphertext, iv, key); 156 157 expect(decrypted).toBe(plaintext); 158 }); 159 160 it('should fail decryption with wrong key', async () => { 161 const salt = generateSalt(); 162 const key1 = await deriveKey('password1', salt); 163 const key2 = await deriveKey('password2', salt); 164 165 const { ciphertext, iv } = await encrypt('secret', key1); 166 167 await expect(decrypt(ciphertext, iv, key2)).rejects.toThrow(); 168 }); 169 170 it('should generate unique ciphertext for same plaintext', async () => { 171 const key = await deriveKey('password', generateSalt()); 172 const plaintext = 'same text'; 173 174 const result1 = await encrypt(plaintext, key); 175 const result2 = await encrypt(plaintext, key); 176 177 // Different IVs mean different ciphertexts 178 expect(result1.ciphertext).not.toBe(result2.ciphertext); 179 expect(result1.iv).not.toBe(result2.iv); 180 }); 181 }); 182 183 describe('exportKey / importKey', () => { 184 it('should round-trip export and import', async () => { 185 const password = 'test-password'; 186 const salt = generateSalt(); 187 const originalKey = await deriveKey(password, salt); 188 189 const exported = await exportKey(originalKey); 190 const importedKey = await importKey(exported); 191 192 // Verify by encrypting/decrypting 193 const plaintext = 'test message'; 194 const { ciphertext, iv } = await encrypt(plaintext, originalKey); 195 const decrypted = await decrypt(ciphertext, iv, importedKey); 196 197 expect(decrypted).toBe(plaintext); 198 }); 199 }); 200 201 describe('verifyKey', () => { 202 it('should return true for correct key', async () => { 203 const password = 'correct-password'; 204 const salt = generateSalt(); 205 const key = await deriveKey(password, salt); 206 207 const testPlaintext = 'verification-text'; 208 const { ciphertext, iv } = await encrypt(testPlaintext, key); 209 210 const isValid = await verifyKey(key, ciphertext, iv, testPlaintext); 211 expect(isValid).toBe(true); 212 }); 213 214 it('should return false for wrong key', async () => { 215 const salt = generateSalt(); 216 const correctKey = await deriveKey('correct-password', salt); 217 const wrongKey = await deriveKey('wrong-password', salt); 218 219 const testPlaintext = 'verification-text'; 220 const { ciphertext, iv } = await encrypt(testPlaintext, correctKey); 221 222 const isValid = await verifyKey(wrongKey, ciphertext, iv, testPlaintext); 223 expect(isValid).toBe(false); 224 }); 225 226 it('should return false for wrong expected plaintext', async () => { 227 const key = await deriveKey('password', generateSalt()); 228 229 const { ciphertext, iv } = await encrypt('actual-text', key); 230 231 const isValid = await verifyKey(key, ciphertext, iv, 'different-text'); 232 expect(isValid).toBe(false); 233 }); 234 }); 235 236 describe('DEFAULT_ITERATIONS', () => { 237 it('should be 100000', () => { 238 expect(DEFAULT_ITERATIONS).toBe(100000); 239 }); 240 }); 241 });