/** * Tests for encryption service */ import { encrypt, decrypt, isEncrypted, clearEncryptionKey } from '../encryption'; import * as SecureStore from 'expo-secure-store'; import * as Crypto from 'expo-crypto'; // expo-crypto and expo-secure-store are already mocked in jest.setup.js describe('Encryption Service', () => { beforeEach(() => { jest.clearAllMocks(); // Reset SecureStore mock to return null (no existing key) (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null); }); afterEach(async () => { // Clear encryption key after each test await clearEncryptionKey(); }); describe('encrypt', () => { it('should encrypt a string successfully', async () => { const plaintext = 'MySecurePassword123'; const encrypted = await encrypt(plaintext); // Should return base64 string expect(typeof encrypted).toBe('string'); expect(encrypted.length).toBeGreaterThan(0); // Should not be the same as plaintext expect(encrypted).not.toBe(plaintext); // Should be base64 expect(() => Buffer.from(encrypted, 'base64')).not.toThrow(); }); it('should generate a master key and store it', async () => { const plaintext = 'test password'; await encrypt(plaintext); // Should have stored the master key expect(SecureStore.setItemAsync).toHaveBeenCalledWith( 'WELLNUO_ENCRYPTION_KEY', expect.any(String) ); }); it('should encrypt empty string', async () => { const plaintext = ''; const encrypted = await encrypt(plaintext); expect(typeof encrypted).toBe('string'); expect(encrypted.length).toBeGreaterThan(0); }); it('should encrypt unicode characters', async () => { const plaintext = '密码 🔐 пароль'; const encrypted = await encrypt(plaintext); expect(typeof encrypted).toBe('string'); expect(encrypted.length).toBeGreaterThan(0); }); it('should produce different ciphertext for same input (due to random IV)', async () => { const plaintext = 'same password'; // Mock to return different random bytes each time let callCount = 0; (Crypto.getRandomBytesAsync as jest.Mock).mockImplementation((size: number) => { callCount++; const bytes = new Uint8Array(size); for (let i = 0; i < size; i++) { bytes[i] = (i * callCount + 13) % 256; } return Promise.resolve(bytes); }); const encrypted1 = await encrypt(plaintext); const encrypted2 = await encrypt(plaintext); expect(encrypted1).not.toBe(encrypted2); }); }); describe('decrypt', () => { it('should decrypt encrypted data correctly', async () => { const plaintext = 'MySecurePassword123'; const encrypted = await encrypt(plaintext); const decrypted = await decrypt(encrypted); expect(decrypted).toBe(plaintext); }); it('should decrypt empty string', async () => { const plaintext = ''; const encrypted = await encrypt(plaintext); const decrypted = await decrypt(encrypted); expect(decrypted).toBe(plaintext); }); it('should decrypt unicode characters', async () => { const plaintext = '密码 🔐 пароль'; const encrypted = await encrypt(plaintext); const decrypted = await decrypt(encrypted); expect(decrypted).toBe(plaintext); }); it('should handle long strings', async () => { const plaintext = 'A'.repeat(10000); const encrypted = await encrypt(plaintext); const decrypted = await decrypt(encrypted); expect(decrypted).toBe(plaintext); }); it('should throw error for invalid ciphertext', async () => { const invalidCiphertext = 'not-valid-base64!@#$'; await expect(decrypt(invalidCiphertext)).rejects.toThrow(); }); it('should throw error for corrupted data', async () => { const plaintext = 'test'; const encrypted = await encrypt(plaintext); // Corrupt the ciphertext const buffer = Buffer.from(encrypted, 'base64'); buffer[buffer.length - 1] ^= 0xFF; // Flip bits in last byte const corrupted = buffer.toString('base64'); await expect(decrypt(corrupted)).rejects.toThrow(); }); it('should throw error if ciphertext is too short', async () => { // Create invalid ciphertext (too short) const tooShort = Buffer.from('short').toString('base64'); await expect(decrypt(tooShort)).rejects.toThrow(); }); }); describe('isEncrypted', () => { it('should return true for encrypted data', async () => { const plaintext = 'test password'; const encrypted = await encrypt(plaintext); expect(isEncrypted(encrypted)).toBe(true); }); it('should return false for plaintext', () => { const plaintext = 'just a regular password'; expect(isEncrypted(plaintext)).toBe(false); }); it('should return false for short base64 strings', () => { const shortBase64 = Buffer.from('short').toString('base64'); expect(isEncrypted(shortBase64)).toBe(false); }); it('should return false for invalid base64', () => { const invalid = 'not-base64!@#$%'; expect(isEncrypted(invalid)).toBe(false); }); it('should return false for empty string', () => { expect(isEncrypted('')).toBe(false); }); }); describe('clearEncryptionKey', () => { it('should clear the master encryption key', async () => { // First encrypt something to generate a key await encrypt('test'); // Clear the key await clearEncryptionKey(); // Should have called deleteItemAsync expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('WELLNUO_ENCRYPTION_KEY'); }); }); describe('key persistence', () => { it('should reuse existing master key', async () => { const existingKey = Buffer.from('existing-master-key-32-bytes!!').toString('base64'); (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(existingKey); await encrypt('test'); // Should have retrieved existing key expect(SecureStore.getItemAsync).toHaveBeenCalledWith('WELLNUO_ENCRYPTION_KEY'); // Should NOT have set a new key expect(SecureStore.setItemAsync).not.toHaveBeenCalledWith( 'WELLNUO_ENCRYPTION_KEY', expect.any(String) ); }); }); describe('round-trip encryption', () => { const testCases = [ { name: 'simple password', value: 'password123' }, { name: 'complex password', value: 'P@ssw0rd!#$%^&*()' }, { name: 'unicode', value: '密码 🔐 пароль ñoño' }, { name: 'empty string', value: '' }, { name: 'spaces', value: ' spaces ' }, { name: 'newlines', value: 'line1\nline2\nline3' }, { name: 'long string', value: 'x'.repeat(1000) }, { name: 'WiFi password', value: 'MyHomeWiFi2024!' }, ]; testCases.forEach(({ name, value }) => { it(`should correctly encrypt and decrypt: ${name}`, async () => { const encrypted = await encrypt(value); const decrypted = await decrypt(encrypted); expect(decrypted).toBe(value); }); }); }); });