diff --git a/server/src/engine/crypto.test.ts b/server/src/engine/crypto.test.ts new file mode 100644 index 0000000..f2da45a --- /dev/null +++ b/server/src/engine/crypto.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest' +import { encrypt, decrypt } from './crypto.js' + +describe('crypto — AES-256-GCM', () => { + it('encrypt then decrypt returns original plaintext', () => { + const plaintext = 'nostr+walletconnect://relay.example.com?secret=abc123' + const ciphertext = encrypt(plaintext) + const decrypted = decrypt(ciphertext) + expect(decrypted).toBe(plaintext) + }) + + it('encrypting same plaintext twice produces different ciphertexts (random IV)', () => { + const plaintext = 'same-text-every-time' + const c1 = encrypt(plaintext) + const c2 = encrypt(plaintext) + expect(c1).not.toBe(c2) + // Both should still decrypt to the same value + expect(decrypt(c1)).toBe(plaintext) + expect(decrypt(c2)).toBe(plaintext) + }) + + it('ciphertext format is iv:authTag:encrypted (3 hex parts)', () => { + const ciphertext = encrypt('test data') + const parts = ciphertext.split(':') + expect(parts.length).toBe(3) + // IV should be 16 bytes = 32 hex chars + expect(parts[0].length).toBe(32) + expect(parts[0]).toMatch(/^[0-9a-f]+$/) + // Auth tag should be 16 bytes = 32 hex chars + expect(parts[1].length).toBe(32) + expect(parts[1]).toMatch(/^[0-9a-f]+$/) + // Encrypted data is hex + expect(parts[2]).toMatch(/^[0-9a-f]+$/) + }) + + it('decrypt throws on tampered ciphertext (auth tag verification)', () => { + const ciphertext = encrypt('sensitive wallet data') + const parts = ciphertext.split(':') + // Tamper with the encrypted data + const tampered = parts[0] + ':' + parts[1] + ':' + 'ff'.repeat(parts[2].length / 2) + expect(() => decrypt(tampered)).toThrow() + }) + + it('decrypt throws on tampered auth tag', () => { + const ciphertext = encrypt('sensitive wallet data') + const parts = ciphertext.split(':') + // Tamper with the auth tag + const tampered = parts[0] + ':' + '00'.repeat(16) + ':' + parts[2] + expect(() => decrypt(tampered)).toThrow() + }) + + it('handles empty string encryption', () => { + const ciphertext = encrypt('') + const decrypted = decrypt(ciphertext) + expect(decrypted).toBe('') + }) + + it('handles unicode content', () => { + const plaintext = '₿⚡ Bitcoin Lightning 🟠' + const ciphertext = encrypt(plaintext) + const decrypted = decrypt(ciphertext) + expect(decrypted).toBe(plaintext) + }) +})