/ tests / unit / crypto / encryption.test.ts
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  });