import { describe, expect, test } from 'bun:test'; import { randomBytes } from 'node:crypto'; import { type EncryptedSecret, decryptSecret, encryptSecret, isValidEncryptedSecret, } from './encryption'; describe('Secret Encryption', () => { const masterKey = randomBytes(32); describe('encryptSecret', () => { test('should encrypt a secret', () => { const plaintext = 'my-secret-password'; const encrypted = encryptSecret(plaintext, masterKey); expect(encrypted).toHaveProperty('encryptedValue'); expect(encrypted).toHaveProperty('iv'); expect(encrypted).toHaveProperty('authTag'); expect(encrypted.encryptedValue).not.toBe(plaintext); }); test('should produce different ciphertext for same plaintext', () => { const plaintext = 'same-secret'; const encrypted1 = encryptSecret(plaintext, masterKey); const encrypted2 = encryptSecret(plaintext, masterKey); // Different IVs mean different ciphertext expect(encrypted1.iv).not.toBe(encrypted2.iv); expect(encrypted1.encryptedValue).not.toBe(encrypted2.encryptedValue); }); test('should reject invalid master key length', () => { const shortKey = randomBytes(16); expect(() => encryptSecret('secret', shortKey)).toThrow('Master key must be 32 bytes'); }); test('should reject non-buffer master key', () => { expect(() => encryptSecret('secret', 'not-a-buffer' as unknown as Buffer)).toThrow( 'Master key must be 32 bytes', ); }); test('should reject non-string plaintext', () => { expect(() => encryptSecret(123 as unknown as string, masterKey)).toThrow( 'Plaintext must be a string', ); }); test('should encrypt empty string', () => { const encrypted = encryptSecret('', masterKey); expect(encrypted.encryptedValue).toBeDefined(); expect(encrypted.iv).toBeDefined(); expect(encrypted.authTag).toBeDefined(); }); test('should encrypt long text', () => { const longText = 'a'.repeat(10000); const encrypted = encryptSecret(longText, masterKey); expect(encrypted.encryptedValue.length).toBeGreaterThan(0); }); test('should encrypt unicode characters', () => { const unicode = '🔐 こんにちは 你好 مرحبا'; const encrypted = encryptSecret(unicode, masterKey); expect(encrypted.encryptedValue).toBeDefined(); }); }); describe('decryptSecret', () => { test('should decrypt encrypted secret', () => { const plaintext = 'my-secret-password'; const encrypted = encryptSecret(plaintext, masterKey); const decrypted = decryptSecret(encrypted, masterKey); expect(decrypted).toBe(plaintext); }); test('should decrypt empty string', () => { const encrypted = encryptSecret('', masterKey); const decrypted = decryptSecret(encrypted, masterKey); expect(decrypted).toBe(''); }); test('should decrypt long text', () => { const longText = 'a'.repeat(10000); const encrypted = encryptSecret(longText, masterKey); const decrypted = decryptSecret(encrypted, masterKey); expect(decrypted).toBe(longText); }); test('should decrypt unicode characters', () => { const unicode = '🔐 こんにちは 你好 مرحبا'; const encrypted = encryptSecret(unicode, masterKey); const decrypted = decryptSecret(encrypted, masterKey); expect(decrypted).toBe(unicode); }); test('should fail with wrong master key', () => { const encrypted = encryptSecret('secret', masterKey); const wrongKey = randomBytes(32); expect(() => decryptSecret(encrypted, wrongKey)).toThrow('Failed to decrypt'); }); test('should fail with corrupted ciphertext', () => { const encrypted = encryptSecret('secret', masterKey); const corrupted: EncryptedSecret = { ...encrypted, encryptedValue: 'corrupted', }; expect(() => decryptSecret(corrupted, masterKey)).toThrow('Failed to decrypt'); }); test('should fail with corrupted IV', () => { const encrypted = encryptSecret('secret', masterKey); const corrupted: EncryptedSecret = { ...encrypted, iv: '0'.repeat(32), // Valid length but wrong value }; expect(() => decryptSecret(corrupted, masterKey)).toThrow('Failed to decrypt'); }); test('should fail with corrupted auth tag', () => { const encrypted = encryptSecret('secret', masterKey); const corrupted: EncryptedSecret = { ...encrypted, authTag: '0'.repeat(32), // Valid length but wrong value }; expect(() => decryptSecret(corrupted, masterKey)).toThrow('Failed to decrypt'); }); test('should fail with null encrypted value', () => { const encrypted = { encryptedValue: null, iv: '0'.repeat(32), authTag: '0'.repeat(32), }; expect(() => decryptSecret(encrypted as unknown as EncryptedSecret, masterKey)).toThrow( 'missing required fields', ); }); test('should fail with invalid IV length', () => { const encrypted = encryptSecret('secret', masterKey); const invalid: EncryptedSecret = { ...encrypted, iv: 'too_short', }; expect(() => decryptSecret(invalid, masterKey)).toThrow('Invalid IV length'); }); test('should fail with invalid auth tag length', () => { const encrypted = encryptSecret('secret', masterKey); const invalid: EncryptedSecret = { ...encrypted, authTag: 'too_short', }; expect(() => decryptSecret(invalid, masterKey)).toThrow('Invalid auth tag length'); }); test('should reject invalid master key', () => { const encrypted = encryptSecret('secret', masterKey); const shortKey = randomBytes(16); expect(() => decryptSecret(encrypted, shortKey)).toThrow('Master key must be 32 bytes'); }); }); describe('isValidEncryptedSecret', () => { test('should validate correct encrypted secret', () => { const encrypted = encryptSecret('secret', masterKey); expect(isValidEncryptedSecret(encrypted)).toBe(true); }); test('should reject non-object', () => { expect(isValidEncryptedSecret('not an object')).toBe(false); expect(isValidEncryptedSecret(123)).toBe(false); expect(isValidEncryptedSecret(null)).toBe(false); expect(isValidEncryptedSecret(undefined)).toBe(false); }); test('should reject missing fields', () => { expect(isValidEncryptedSecret({})).toBe(false); expect(isValidEncryptedSecret({ encryptedValue: 'abc' })).toBe(false); expect(isValidEncryptedSecret({ encryptedValue: 'abc', iv: 'def' })).toBe(false); }); test('should reject non-string fields', () => { expect( isValidEncryptedSecret({ encryptedValue: 123, iv: 'abc', authTag: 'def', }), ).toBe(false); }); test('should reject non-hex strings', () => { expect( isValidEncryptedSecret({ encryptedValue: 'not-hex!', iv: '0'.repeat(32), authTag: '0'.repeat(32), }), ).toBe(false); }); test('should reject wrong IV length', () => { expect( isValidEncryptedSecret({ encryptedValue: 'abc123', iv: 'tooshort', authTag: '0'.repeat(32), }), ).toBe(false); }); test('should reject wrong auth tag length', () => { expect( isValidEncryptedSecret({ encryptedValue: 'abc123', iv: '0'.repeat(32), authTag: 'tooshort', }), ).toBe(false); }); }); describe('end-to-end encryption', () => { test('should handle multiple secrets with same key', () => { const secrets = ['secret1', 'secret2', 'secret3']; const encrypted = secrets.map((s) => encryptSecret(s, masterKey)); const decrypted = encrypted.map((e) => decryptSecret(e, masterKey)); expect(decrypted).toEqual(secrets); }); test('should handle special characters', () => { const specialChars = '!@#$%^&*()_+-=[]{}|;:\'",.<>?/`~\n\t\r'; const encrypted = encryptSecret(specialChars, masterKey); const decrypted = decryptSecret(encrypted, masterKey); expect(decrypted).toBe(specialChars); }); test('should be deterministic with same key', () => { const plaintext = 'consistent-secret'; const encrypted1 = encryptSecret(plaintext, masterKey); const encrypted2 = encryptSecret(plaintext, masterKey); // Different ciphertext due to random IV expect(encrypted1.encryptedValue).not.toBe(encrypted2.encryptedValue); // But both decrypt to same plaintext expect(decryptSecret(encrypted1, masterKey)).toBe(plaintext); expect(decryptSecret(encrypted2, masterKey)).toBe(plaintext); }); }); });