/** * Unit tests for crypto primitives (AES-256-GCM, RSA-OAEP) */ import { AESEncryption } from '../../src/core/crypto/encryption'; import { RSAOperations } from '../../src/core/crypto/rsa'; import { arrayBufferToBase64, base64ToArrayBuffer, stringToArrayBuffer, arrayBufferToString } from '../../src/core/crypto/utils'; describe('AESEncryption', () => { let aes: AESEncryption; beforeEach(() => { aes = new AESEncryption(); }); test('generateKey produces 256-bit key', async () => { const key = await aes.generateKey(); expect(key.type).toBe('secret'); expect((key.algorithm as AesKeyAlgorithm).length).toBe(256); }); test('encrypt/decrypt roundtrip', async () => { const key = await aes.generateKey(); const plaintext = stringToArrayBuffer('hello secure world'); const { ciphertext, nonce } = await aes.encrypt(plaintext, key); expect(ciphertext.byteLength).toBeGreaterThan(0); expect(nonce.byteLength).toBe(12); // Web Crypto appends 16-byte tag β€” decrypt should succeed const decrypted = await aes.decrypt(ciphertext, nonce, key); expect(arrayBufferToString(decrypted)).toBe('hello secure world'); }); test('decrypt fails with wrong key', async () => { const key1 = await aes.generateKey(); const key2 = await aes.generateKey(); const { ciphertext, nonce } = await aes.encrypt(stringToArrayBuffer('secret'), key1); await expect(aes.decrypt(ciphertext, nonce, key2)).rejects.toThrow(); }); test('exportKey / importKey roundtrip', async () => { const key = await aes.generateKey(); const exported = await aes.exportKey(key); expect(exported.byteLength).toBe(32); // 256-bit const imported = await aes.importKey(exported); const plaintext = stringToArrayBuffer('roundtrip test'); const { ciphertext, nonce } = await aes.encrypt(plaintext, imported); const decrypted = await aes.decrypt(ciphertext, nonce, imported); expect(arrayBufferToString(decrypted)).toBe('roundtrip test'); }); test('GCM tag split/join compatibility with Python format', async () => { // Simulate what SecureCompletionClient.performEncryption does: // split the 16-byte tag from Web Crypto output, then re-join for decrypt const key = await aes.generateKey(); const plaintext = stringToArrayBuffer('tag split test'); const { ciphertext, nonce } = await aes.encrypt(plaintext, key); const TAG_LENGTH = 16; const ciphertextBytes = new Uint8Array(ciphertext); const ciphertextOnly = ciphertextBytes.slice(0, ciphertextBytes.length - TAG_LENGTH); const tag = ciphertextBytes.slice(ciphertextBytes.length - TAG_LENGTH); // Re-join before decrypt (as in decryptResponse) const combined = new Uint8Array(ciphertextOnly.length + tag.length); combined.set(ciphertextOnly, 0); combined.set(tag, ciphertextOnly.length); const decrypted = await aes.decrypt(combined.buffer, nonce, key); expect(arrayBufferToString(decrypted)).toBe('tag split test'); }); }); describe('RSAOperations', () => { let rsa: RSAOperations; beforeEach(() => { rsa = new RSAOperations(); }); test('generateKeyPair produces usable 2048-bit keys', async () => { const kp = await rsa.generateKeyPair(2048); expect(kp.publicKey.type).toBe('public'); expect(kp.privateKey.type).toBe('private'); expect((kp.publicKey.algorithm as RsaHashedKeyAlgorithm).modulusLength).toBe(2048); }, 30000); test('encrypt/decrypt AES key roundtrip', async () => { const kp = await rsa.generateKeyPair(2048); const aes = new AESEncryption(); const aesKey = await aes.generateKey(); const aesKeyBytes = await aes.exportKey(aesKey); const encrypted = await rsa.encryptKey(aesKeyBytes, kp.publicKey); const decrypted = await rsa.decryptKey(encrypted, kp.privateKey); expect(arrayBufferToBase64(decrypted)).toBe(arrayBufferToBase64(aesKeyBytes)); }, 30000); test('exportPublicKey / importPublicKey roundtrip', async () => { const kp = await rsa.generateKeyPair(2048); const pem = await rsa.exportPublicKey(kp.publicKey); expect(pem).toContain('-----BEGIN PUBLIC KEY-----'); const imported = await rsa.importPublicKey(pem); expect(imported.type).toBe('public'); }, 30000); test('exportPrivateKey / importPrivateKey roundtrip (no password)', async () => { const kp = await rsa.generateKeyPair(2048); const pem = await rsa.exportPrivateKey(kp.privateKey); expect(pem).toContain('-----BEGIN PRIVATE KEY-----'); const imported = await rsa.importPrivateKey(pem); expect(imported.type).toBe('private'); }, 30000); test('importPrivateKey fails with wrong password', async () => { const kp = await rsa.generateKeyPair(2048); const pem = await rsa.exportPrivateKey(kp.privateKey, 'correct-password'); await expect(rsa.importPrivateKey(pem, 'wrong-password')).rejects.toThrow(); }, 30000); }); describe('Base64 utilities', () => { test('arrayBufferToBase64 / base64ToArrayBuffer roundtrip', () => { const original = new Uint8Array([0, 1, 127, 128, 255]); const b64 = arrayBufferToBase64(original.buffer); const restored = new Uint8Array(base64ToArrayBuffer(b64)); expect(Array.from(restored)).toEqual([0, 1, 127, 128, 255]); }); test('stringToArrayBuffer / arrayBufferToString roundtrip', () => { const text = 'Hello, δΈ–η•Œ! πŸ”'; const buf = stringToArrayBuffer(text); expect(arrayBufferToString(buf)).toBe(text); }); });