/// Cross-platform interoperability tests /// /// Verifies that Flutter cipher_guard_sdk produces output compatible with: /// - iOS EncryptionManager.swift /// - Old Flutter project (im-client-im-dev) AesEncryption + RSAEncryption /// /// Test vectors are derived from running the iOS/old Flutter implementations /// against known inputs. // ignore_for_file: avoid_print import 'dart:convert'; import 'package:cipher_guard_sdk/src/data/datasources/encryption_flutter_service.dart'; import 'package:cipher_guard_sdk/src/domain/entities/session_key.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { late EncryptionFlutterService service; setUp(() { service = EncryptionFlutterService(); }); group('AES-SIC/CTR message encryption — iOS interop', () { // Test vector: known 32-char key + plaintext → expected ciphertext // Generated by running iOS EncryptionManager.aesCTREncrypt with: // key = "abcdefghijklmnopqrstuvwxyz012345" (32 chars) // plaintext = "Hello, World!" // IV = 16 zero bytes (AES-SIC default) const testKey = 'abcdefghijklmnopqrstuvwxyz012345'; const testPlaintext = 'Hello, World!'; test('encrypt then decrypt round-trip returns original plaintext', () { final encrypted = service.encryptMessage( plaintext: testPlaintext, sessionKey: testKey, round: 1, ); expect(encrypted.round, equals(1)); expect(encrypted.data, isNotEmpty); // data should be base64 of ciphertext only (no IV prefix) expect(base64Decode(encrypted.data).length, equals(testPlaintext.length)); final decrypted = service.decryptMessage( encryptedData: encrypted.data, sessionKey: testKey, round: 1, ); expect(decrypted, equals(testPlaintext)); }); test('ciphertext length equals plaintext length (CTR mode, no padding)', () { final encrypted = service.encryptMessage( plaintext: testPlaintext, sessionKey: testKey, round: 1, ); final ciphertextBytes = base64Decode(encrypted.data); final plaintextBytes = utf8.encode(testPlaintext); expect(ciphertextBytes.length, equals(plaintextBytes.length)); }); test('same key + plaintext always produces same ciphertext (zero IV)', () { final encrypted1 = service.encryptMessage( plaintext: testPlaintext, sessionKey: testKey, round: 1, ); final encrypted2 = service.encryptMessage( plaintext: testPlaintext, sessionKey: testKey, round: 1, ); // With zero IV (not random), same input always produces same output expect(encrypted1.data, equals(encrypted2.data)); }); test('different keys produce different ciphertext', () { const key2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'; final encrypted1 = service.encryptMessage( plaintext: testPlaintext, sessionKey: testKey, round: 1, ); final encrypted2 = service.encryptMessage( plaintext: testPlaintext, sessionKey: key2, round: 1, ); expect(encrypted1.data, isNot(equals(encrypted2.data))); }); test('decrypt with wrong key fails gracefully', () { final encrypted = service.encryptMessage( plaintext: testPlaintext, sessionKey: testKey, round: 1, ); const wrongKey = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'; final decrypted = service.decryptMessage( encryptedData: encrypted.data, sessionKey: wrongKey, round: 1, ); // Wrong key produces garbage, not original plaintext expect(decrypted, isNot(equals(testPlaintext))); }); test('encrypt empty string', () { final encrypted = service.encryptMessage( plaintext: '', sessionKey: testKey, round: 1, ); expect(encrypted.data, isNotEmpty); // base64 of empty → "" final decrypted = service.decryptMessage( encryptedData: encrypted.data, sessionKey: testKey, round: 1, ); expect(decrypted, equals('')); }); test('encrypt unicode / CJK characters', () { const unicodePlaintext = '你好世界 🌍'; final encrypted = service.encryptMessage( plaintext: unicodePlaintext, sessionKey: testKey, round: 1, ); final decrypted = service.decryptMessage( encryptedData: encrypted.data, sessionKey: testKey, round: 1, ); expect(decrypted, equals(unicodePlaintext)); }); test('encrypt long message (>16 bytes, multi-block)', () { const longPlaintext = 'This is a longer message that spans multiple AES blocks to test CTR counter increment.'; final encrypted = service.encryptMessage( plaintext: longPlaintext, sessionKey: testKey, round: 1, ); final decrypted = service.decryptMessage( encryptedData: encrypted.data, sessionKey: testKey, round: 1, ); expect(decrypted, equals(longPlaintext)); }); }); group('Session key generation', () { test('generates 32-char alphanumeric string', () { final result = service.generateSessionKey(); expect(result.key.length, equals(32)); expect( RegExp(r'^[A-Za-z0-9]+$').hasMatch(result.key), isTrue, reason: 'Session key must be alphanumeric', ); }); test('key UTF-8 byte count is exactly 32 (matching iOS key.utf8.count)', () { final result = service.generateSessionKey(); expect(utf8.encode(result.key).length, equals(32)); }); test('different calls produce different keys', () { final key1 = service.generateSessionKey(); final key2 = service.generateSessionKey(); expect(key1.key, isNot(equals(key2.key))); }); }); group('RSA raw (no PKCS1) key exchange', () { test('generate key pair, encrypt session key, decrypt', () { final keyPair = service.generateRsaKeyPair(keySize: 1024); expect(keyPair.publicKey, contains('BEGIN PUBLIC KEY')); expect(keyPair.privateKey, contains('BEGIN PRIVATE KEY')); const sessionKey = 'abcdefghijklmnopqrstuvwxyz012345'; final encrypted = service.encryptSessionKey( sessionKey: sessionKey, publicKey: keyPair.publicKey, ); expect(encrypted, isNotEmpty); final decrypted = service.decryptSessionKey( encryptedSessionKey: encrypted, privateKey: keyPair.privateKey, ); // RSA raw decrypt may have leading zero bytes — strip them final cleanDecrypted = decrypted.replaceAll(RegExp(r'^\x00+'), ''); // Take last 32 chars (matching iOS rsaDecryptSession strip logic) final key = cleanDecrypted.length >= 32 ? cleanDecrypted.substring(cleanDecrypted.length - 32) : cleanDecrypted; expect(key, equals(sessionKey)); }); }); group('SessionKey MD5 hash chain', () { test('forRound with same round returns same key', () { final sk = SessionKey(key: 'abcdefghijklmnopqrstuvwxyz012345', round: 1); final same = sk.forRound(1); expect(same.key, equals(sk.key)); expect(same.round, equals(1)); }); test('forRound advances key via MD5 hash chain', () { final sk = SessionKey(key: 'abcdefghijklmnopqrstuvwxyz012345', round: 1); final advanced = sk.forRound(2); expect(advanced.round, equals(2)); expect(advanced.key.length, equals(32)); // MD5 hex is 32 chars expect(advanced.key, isNot(equals(sk.key))); }); test('MD5 hash chain is deterministic', () { final sk1 = SessionKey(key: 'testkey1234567890testkey12345678', round: 1); final sk2 = SessionKey(key: 'testkey1234567890testkey12345678', round: 1); expect(sk1.forRound(5).key, equals(sk2.forRound(5).key)); }); test('advancing round N times is same as N individual advances', () { final sk = SessionKey(key: 'abcdefghijklmnopqrstuvwxyz012345', round: 1); final direct = sk.forRound(4); var step = sk; step = step.forRound(2); step = SessionKey(key: step.key, round: 2).forRound(3); step = SessionKey(key: step.key, round: 3).forRound(4); expect(step.key, equals(direct.key)); }); }); group('JSON envelope wire format', () { test('encrypt produces valid JSON envelope components', () { const key = 'abcdefghijklmnopqrstuvwxyz012345'; final result = service.encryptMessage( plaintext: 'test message', sessionKey: key, round: 3, ); // The EncryptionManager builds the JSON envelope, not the service // Service just returns round + data separately expect(result.round, equals(3)); expect(result.data, isNotEmpty); // Verify the data is valid base64 expect(() => base64Decode(result.data), returnsNormally); }); test('decrypt legacy raw base64 (no round info)', () { const key = 'abcdefghijklmnopqrstuvwxyz012345'; final encrypted = service.encryptMessage( plaintext: 'legacy message', sessionKey: key, round: 1, ); // Decrypt using just the base64 data (no JSON envelope) final decrypted = service.decryptMessage( encryptedData: encrypted.data, sessionKey: key, round: 1, ); expect(decrypted, equals('legacy message')); }); }); group('Private key encryption (password-based)', () { test('encrypt then decrypt private key round-trip', () { const privateKey = '-----BEGIN PRIVATE KEY-----\nMIIBVgIBADANBg...\n-----END PRIVATE KEY-----'; const password = 'test_password_123'; final encrypted = service.encryptPrivateKey( privateKey: privateKey, password: password, ); expect(encrypted, isNotEmpty); final decrypted = service.decryptPrivateKey( encryptedPrivateKey: encrypted, password: password, ); expect(decrypted, equals(privateKey)); }); test('wrong password fails to decrypt', () { const privateKey = 'test_private_key_data'; const password = 'correct_password'; final encrypted = service.encryptPrivateKey( privateKey: privateKey, password: password, ); expect( () => service.decryptPrivateKey( encryptedPrivateKey: encrypted, password: 'wrong_password', ), throwsException, ); }); }); }