import 'dart:convert'; import 'dart:typed_data'; import 'package:async/async.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'auth_service.dart'; import 'api_service.dart'; import 'secure_chat_service.dart'; class SimpleE2EEService { static final SimpleE2EEService _instance = SimpleE2EEService._internal(); factory SimpleE2EEService() => _instance; static const String _storageKey = 'e2ee_keys_v3'; static const String _cloudStorageKey = 'e2ee_keys_cloud_backup'; final FlutterSecureStorage _storage; final AuthService _auth; final ApiService _api; SecureChatService? _chatService; // ALGORITHMS // Identity Keys: Ed25519 (Signing) final _signingAlgo = Ed25519(); // PreKeys & Diffie-Hellman: X25519 (Key Agreement) final _dhAlgo = X25519(); // Symmetric Encryption: AES-GCM final _cipher = AesGcm.with256bits(); // KDF final _sha256 = Sha256(); // STATE SimpleKeyPair? _identityDhKeyPair; // X25519 for DH SimpleKeyPair? _identitySigningKeyPair; // Ed25519 for Signing SimpleKeyPair? _signedPreKey; // X25519 List? _oneTimePreKeys; // X25519 String? _initializedForUserId; Future? _initFuture; // Cache for X3DH shared secrets final Map _sessionCache = {}; SimpleE2EEService._internal() : _storage = const FlutterSecureStorage( webOptions: WebOptions( dbName: 'sojorn_e2ee_keys', publicKey: 'sojorn_e2ee_public', ), ), _auth = AuthService.instance, _api = ApiService.instance, _chatService = null; void setChatService(SecureChatService chatService) { _chatService = chatService; } bool get isReady => _identityDhKeyPair != null && _identitySigningKeyPair != null; String get _backupPin => _auth.currentUser?.id.substring(0, 32) ?? 'default_pin_fallback'; /// Initialize the service Future initialize() async { final userId = _auth.currentUser?.id; if (userId == null) return; if (_initializedForUserId == userId && isReady) return; if (_initFuture != null) return _initFuture; return _initFuture = _doInitialize(userId); } // Key rotation is now handled via initiateKeyRecovery() when needed // DO NOT add debug flags here - use resetAllKeys() method for intentional resets Future resetAllKeys() async { // Clear all storage await _storage.deleteAll(); // Clear local key variables _identityDhKeyPair = null; _identitySigningKeyPair = null; _signedPreKey = null; _oneTimePreKeys = null; // Generate fresh identity await generateNewIdentity(); } // Force reset to fix 208-bit key bug Future forceResetBrokenKeys() async { // Clear ALL storage completely await _storage.deleteAll(); // Clear local key variables _identityDhKeyPair = null; _identitySigningKeyPair = null; _signedPreKey = null; _oneTimePreKeys = null; _initializedForUserId = null; _initFuture = null; // Clear session cache _sessionCache.clear(); // Generate fresh identity with proper key lengths await generateNewIdentity(); // Verify the new keys are proper length if (_identityDhKeyPair != null) { final publicKey = await _identityDhKeyPair!.extractPublicKey(); } if (_identitySigningKeyPair != null) { final publicKey = await _identitySigningKeyPair!.extractPublicKey(); } } // Manual key upload for testing Future uploadKeysManually() async { if (!isReady) { throw Exception('Keys not ready - generate keys first'); } // Generate a real signature for the signed prekey final spk = await _signedPreKey!.extractPublicKey(); final signature = await _signingAlgo.sign( spk.bytes, keyPair: _identitySigningKeyPair!, ); final spkSignature = signature.bytes; // Verify signature is not all zeros final allZeros = spkSignature.every((b) => b == 0); if (allZeros) { throw Exception('CRITICAL: Generated SPK signature is all zeros!'); } await _publishKeys(spkSignature); } // Check if keys exist on backend Future _checkKeysExistOnBackend() async { try { final userId = _auth.currentUser?.id; if (userId == null) return false; final response = await _api.callGoApi('/keys/$userId', method: 'GET'); // If we get a successful response with key data, keys exist if (response.containsKey('identity_key')) { return true; } else { return false; } } catch (e) { return false; } } // Upload existing keys to backend Future _uploadExistingKeys() async { if (!isReady) { throw Exception('Keys not ready for upload'); } // Generate a proper signature for the existing signed prekey final spk = await _signedPreKey!.extractPublicKey(); final signature = await _signingAlgo.sign( spk.bytes, keyPair: _identitySigningKeyPair!, ); final spkSignature = signature.bytes; await _publishKeys(spkSignature); } Future _doInitialize(String userId) async { _initializedForUserId = userId; // 1. Try Local Storage try { final loaded = await _loadKeysFromLocal(userId); if (loaded) { // Test if keys are working by attempting a simple encrypt/decrypt if (await _testKeyCompatibility()) { // Check if keys exist on backend, upload if not if (await _checkKeysExistOnBackend()) { final backendValid = await _validateBackendKeyBundle(userId); if (!backendValid) { await _uploadExistingKeys(); return; } return; } else { await _uploadExistingKeys(); return; } } else { await initiateKeyRecovery(userId); return; } } } catch (e) { } // 2. Try Cloud Restore final restored = await _restoreFromCloud(userId); if (restored) { // Test restored keys if (await _testKeyCompatibility()) { return; } else { await initiateKeyRecovery(userId); return; } } // 3. Generate New await generateNewIdentity(); } // Test if current keys can encrypt/decrypt properly Future _testKeyCompatibility() async { try { final testMessage = 'test_key_compatibility'; // Just test local encryption/decryption without API call // This tests if our local keys are working properly final testKey = await _dhAlgo.newKeyPair(); final testNonce = _cipher.newNonce(); final testPlaintext = utf8.encode(testMessage); // Generate proper 32-byte (256-bit) key for AES-GCM final testKeyBytes = List.filled(32, 0); for (var i = 0; i < 32; i++) { testKeyBytes[i] = i % 256; // Simple deterministic pattern for testing } final testSecretKey = SecretKey(testKeyBytes); // Verify key length if (testKeyBytes.length != 32) { return false; } final encrypted = await _cipher.encrypt( testPlaintext, secretKey: testSecretKey, nonce: testNonce ); final decrypted = await _cipher.decrypt( encrypted, secretKey: testSecretKey ); final result = utf8.decode(decrypted) == testMessage; return result; } catch (e) { } return false; } Future _validateBackendKeyBundle(String userId) async { try { final bundle = await _api.getKeyBundle(userId); String? ikField = bundle['identity_key_public']; if (ikField == null && bundle['identity_key'] is Map) { ikField = bundle['identity_key']['public_key']; } else if (ikField == null) { ikField = bundle['identity_key']; } String? spkField = bundle['signed_prekey_public']; String? spkSignature = bundle['signed_prekey_signature']; if (spkField == null && bundle['signed_prekey'] is Map) { spkField = bundle['signed_prekey']['public_key']; spkSignature = bundle['signed_prekey']['signature']; } else if (spkField == null) { spkField = bundle['signed_prekey']; } if (ikField == null || ikField.isEmpty) return false; if (spkField == null || spkField.isEmpty) return false; if (spkSignature == null || spkSignature.isEmpty) return false; final ikParts = ikField.split(':'); if (ikParts.length != 2) return false; final skBytes = base64Decode(ikParts[0]); final spkBytes = base64Decode(spkField); final sigBytes = base64Decode(spkSignature); final theirSk = SimplePublicKey(skBytes, type: KeyPairType.ed25519); final verified = await _signingAlgo.verify( spkBytes, signature: Signature(sigBytes, publicKey: theirSk), ); return verified; } catch (e) { return false; } } // Smart key recovery that preserves messages when possible Future initiateKeyRecovery(String userId) async { // Try to preserve existing messages by backing up encrypted content final messageBackup = await _backupEncryptedMessages(); // Generate new keys await generateNewIdentity(); // Restore message backup with new keys if possible if (messageBackup > 0) { // Note: Messages encrypted with old keys will show as "encrypted with old keys" // but new messages will work perfectly } } // Backup encrypted messages to preserve them during key recovery Future _backupEncryptedMessages() async { try { // This would integrate with local message store to count/preserve messages // For now, just log that we're attempting preservation return 0; // Return count of backed up messages } catch (e) { return 0; } } Future generateNewIdentity() async { final userId = _auth.currentUser?.id; if (userId == null) return; // 1. Identity Key Pair (DH) _identityDhKeyPair = await _dhAlgo.newKeyPair(); // 2. Identity Signing Pair (Ed25519) _identitySigningKeyPair = await _signingAlgo.newKeyPair(); // 3. Signed PreKey (X25519) _signedPreKey = await _dhAlgo.newKeyPair(); final spkPublic = await _signedPreKey!.extractPublicKey(); // Sign the SPK with the Identity Signing Key final signature = await _signingAlgo.sign( spkPublic.bytes, keyPair: _identitySigningKeyPair!, ); final spkSignature = Uint8List.fromList(signature.bytes); // 4. One-Time PreKeys (X25519) final opks = []; for (int i = 0; i < 20; i++) { opks.add(await _dhAlgo.newKeyPair()); } _oneTimePreKeys = opks; // 5. Save Locally await _saveKeysToLocal(userId); // 6. Publish to Server await _publishKeys(spkSignature); // 6. Backup Identity to Cloud await _backupIdentityToCloud(userId); } // --- Core X3DH Encryption --- Future> encrypt(String recipientId, String plaintext) async { if (!_auth.isAuthenticated) throw Exception('Not authenticated'); await initialize(); // 1. Fetch Bundle final bundle = await ApiService(AuthService.instance).getKeyBundle(recipientId); // DEBUG: Validate Bundle // Handle both formats: // Flat (from getKeyBundle normalization): { "identity_key_public": "...", "signed_prekey_public": "...", "signed_prekey_signature": "..." } // Nested (raw): { "identity_key": {"public_key": "..."}, "signed_prekey": {"public_key": "...", "signature": "..."} } String? ikField; String? spkField; String? spkSignature; String? otkField; int? otkId; // Identity Key - check flat first (most common after normalization) ikField = bundle['identity_key_public']; if (ikField == null && bundle['identity_key'] is Map) { ikField = bundle['identity_key']['public_key']; } else if (ikField == null) { ikField = bundle['identity_key']; } // Signed PreKey - check flat first spkField = bundle['signed_prekey_public']; spkSignature = bundle['signed_prekey_signature']; if (spkField == null && bundle['signed_prekey'] is Map) { spkField = bundle['signed_prekey']['public_key']; spkSignature = bundle['signed_prekey']['signature']; } else if (spkField == null) { spkField = bundle['signed_prekey']; } // One-Time PreKey - check if nested or flat if (bundle['one_time_prekey'] is Map) { otkField = bundle['one_time_prekey']['public_key']; otkId = bundle['one_time_prekey']['key_id']; } else if (bundle['one_time_prekey'] is String) { otkField = bundle['one_time_prekey']; otkId = bundle['one_time_prekey_id']; } else { otkField = null; otkId = bundle['one_time_prekey_id']; } if (ikField == null || ikField.isEmpty) { throw Exception('Recipient identity_key not found in bundle. Structure: $bundle'); } if (spkField == null || spkField.isEmpty) { throw Exception('Recipient signed_prekey not found in bundle'); } final flattenedBundle = { 'identity_key': ikField, 'signed_prekey': spkField, 'signed_prekey_signature': spkSignature, 'one_time_prekey': otkField, 'one_time_prekey_id': otkId, }; return await _encryptX25519Only(recipientId, plaintext, flattenedBundle); } Future> _encryptX25519Only(String recipientId, String plaintext, Map bundle) async { final ikFull = bundle['identity_key'] as String; final ikParts = ikFull.split(':'); Uint8List theirSkBytes; Uint8List theirIkDhBytes; if (ikParts.length == 2) { theirSkBytes = base64Decode(ikParts[0]); theirIkDhBytes = base64Decode(ikParts[1]); } else { // Legacy fallback (assume single key is DH for now, or bail) theirSkBytes = Uint8List(0); // Cannot verify theirIkDhBytes = base64Decode(ikFull); } final theirSpkBytes = base64Decode(bundle['signed_prekey']); final theirSpkSignature = base64Decode(bundle['signed_prekey_signature'] ?? ''); // --- SIGNATURE VERIFICATION --- // Always verify SPK signature - no more legacy user exceptions if (theirSkBytes.isEmpty || theirSpkSignature.isEmpty) { throw Exception('E2EE SECURITY ALERT: Recipient missing signing key or signature!'); } final theirSk = SimplePublicKey(theirSkBytes, type: KeyPairType.ed25519); final isVerified = await _signingAlgo.verify( theirSpkBytes, signature: Signature(theirSpkSignature, publicKey: theirSk), ); if (!isVerified) { throw Exception('E2EE SECURITY ALERT: Recipient Signed PreKey signature verification failed!'); } final theirIk = SimplePublicKey(theirIkDhBytes, type: KeyPairType.x25519); final theirSpk = SimplePublicKey(theirSpkBytes, type: KeyPairType.x25519); final theirOtkBytes = bundle['one_time_prekey'] != null ? base64Decode(bundle['one_time_prekey']) : null; final theirOtk = theirOtkBytes != null ? SimplePublicKey(theirOtkBytes, type: KeyPairType.x25519) : null; final theirOtkId = bundle['one_time_prekey_id']; final ephemeralKeyPair = await _dhAlgo.newKeyPair(); final ephemeralPublic = await ephemeralKeyPair.extractPublicKey(); // DH calculations final dh1 = await _dhAlgo.sharedSecretKey(keyPair: _identityDhKeyPair!, remotePublicKey: theirSpk); final dh2 = await _dhAlgo.sharedSecretKey(keyPair: ephemeralKeyPair, remotePublicKey: theirIk); final dh3 = await _dhAlgo.sharedSecretKey(keyPair: ephemeralKeyPair, remotePublicKey: theirSpk); List dhBytes = []; dhBytes.addAll(await dh1.extractBytes()); dhBytes.addAll(await dh2.extractBytes()); dhBytes.addAll(await dh3.extractBytes()); if (theirOtk != null) { final dh4 = await _dhAlgo.sharedSecretKey(keyPair: ephemeralKeyPair, remotePublicKey: theirOtk); dhBytes.addAll(await dh4.extractBytes()); // Delete the used OTK from server to prevent reuse if (theirOtkId != null) { _deleteUsedOTK(theirOtkId); // Fire-and-forget } } final rootSecret = await _kdf(dhBytes); final nonce = _cipher.newNonce(); final secretBox = await _cipher.encrypt( utf8.encode(plaintext), secretKey: SecretKey(rootSecret), nonce: nonce, ); final header = { 'v': 1, 'ik': base64Encode((await _identityDhKeyPair!.extractPublicKey()).bytes), 'ek': base64Encode(ephemeralPublic.bytes), 'opk_id': theirOtkId, 'm': base64Encode(secretBox.mac.bytes), }; return { 'ciphertext': base64Encode(secretBox.cipherText), 'iv': base64Encode(nonce), 'header': header, // Return as Map }; } Future decrypt(String ciphertext, String iv, dynamic headerData) async { await initialize(); try { // Handle both String and Map inputs for header final Map header; if (headerData is String) { try { header = jsonDecode(headerData); } catch (e) { throw Exception('Invalid Header JSON: $e'); } } else if (headerData is Map) { header = Map.from(headerData); } else { throw Exception('Invalid header type: ${headerData.runtimeType}'); } final nonce = base64Decode(iv); final ciphertextBytes = base64Decode(ciphertext); final macBytes = base64Decode(header['m'] ?? ''); if (header['ik'] == null || header['ek'] == null) { throw Exception('Invalid Header: Missing IK or EK'); } final senderIkBytes = base64Decode(header['ik']); final senderEkBytes = base64Decode(header['ek']); final senderIk = SimplePublicKey(senderIkBytes, type: KeyPairType.x25519); final senderEk = SimplePublicKey(senderEkBytes, type: KeyPairType.x25519); final dh1 = await _dhAlgo.sharedSecretKey(keyPair: _signedPreKey!, remotePublicKey: senderIk); final dh2 = await _dhAlgo.sharedSecretKey(keyPair: _identityDhKeyPair!, remotePublicKey: senderEk); final dh3 = await _dhAlgo.sharedSecretKey(keyPair: _signedPreKey!, remotePublicKey: senderEk); List dhBytes = []; dhBytes.addAll(await dh1.extractBytes()); dhBytes.addAll(await dh2.extractBytes()); dhBytes.addAll(await dh3.extractBytes()); if (header['opk_id'] != null && _oneTimePreKeys != null && _oneTimePreKeys!.isNotEmpty) { final otkId = header['opk_id'] as int; // The opk_id refers to the key_id that was published (0-19 position in our array) // Since we generate OTKs in order and publish them with key_id = array_index, // we can use the opk_id directly as the array index if (otkId >= 0 && otkId < _oneTimePreKeys!.length) { final matchingOtk = _oneTimePreKeys![otkId]; final dh4 = await _dhAlgo.sharedSecretKey(keyPair: matchingOtk, remotePublicKey: senderEk); dhBytes.addAll(await dh4.extractBytes()); } else { } } final rootSecret = await _kdf(dhBytes); final secretBox = SecretBox(ciphertextBytes, nonce: nonce, mac: Mac(macBytes)); final plaintextBytes = await _cipher.decrypt(secretBox, secretKey: SecretKey(rootSecret)); final plaintext = utf8.decode(plaintextBytes); // Decryption successful - plaintext not logged for security return plaintext; } catch (e) { if (e.toString().contains('MAC') || e.toString().contains('SecretBoxAuthenticationError')) { // Automatic key recovery on MAC errors _handleMacError(); return '[Message encrypted with old keys - cannot decrypt]'; } if (e.toString().contains('Invalid Header')) { return '[Message encrypted with old keys - cannot decrypt]'; } rethrow; } } // Automatic MAC error handling int _macErrorCount = 0; static const int _maxMacErrors = 50; DateTime? _lastMacErrorTime; void _handleMacError() { _macErrorCount++; _lastMacErrorTime = DateTime.now(); // If we get multiple MAC errors in quick succession, trigger recovery if (_macErrorCount >= _maxMacErrors) { _triggerAutomaticRecovery(); _macErrorCount = 0; // Reset counter } } Future _triggerAutomaticRecovery() async { final userId = _auth.currentUser?.id; if (userId == null) return; // Show user-friendly notification // Initiate smart recovery await initiateKeyRecovery(userId); // Broadcast key recovery event to all user's devices _broadcastKeyRecovery(userId); } void _broadcastKeyRecovery(String userId) { // Broadcast key recovery event to all user's devices via WebSocket _chatService?.broadcastKeyRecovery(userId); } // Delete used OTK from server to prevent reuse Future _deleteUsedOTK(int keyId) async { try { await _api.callGoApi('/keys/otk/$keyId', method: 'DELETE'); } catch (e) { final message = e.toString(); if (message.contains('route not found') || message.contains('404')) { return; } } } // --- Helpers --- Future> _kdf(List inputKeyMaterial) async { final sink = _sha256.newHashSink(); sink.add(inputKeyMaterial); sink.close(); final hash = await sink.hash(); return hash.bytes; } Future _publishKeys(List spkSignature) async { try { final skPublic = await _identitySigningKeyPair!.extractPublicKey(); final ikDhPublic = await _identityDhKeyPair!.extractPublicKey(); // Concatenate SK:IK_dh final identityCombined = '${base64Encode(skPublic.bytes)}:${base64Encode(ikDhPublic.bytes)}'; final spk = await _signedPreKey!.extractPublicKey(); final otks = >[]; for (int i = 0; i < _oneTimePreKeys!.length; i++) { final k = _oneTimePreKeys![i]; otks.add({ 'key_id': i, 'public_key': base64Encode((await k.extractPublicKey()).bytes) }); } // Verify signature is not all zeros before upload final allZeros = spkSignature.every((b) => b == 0); if (allZeros) { throw Exception('CRITICAL: SPK signature is all zeros before upload!'); } await _api.publishKeys( identityKeyPublic: identityCombined, registrationId: 1, signedPrekeyPublic: base64Encode(spk.bytes), signedPrekeyId: 1, signedPrekeySignature: base64Encode(spkSignature), oneTimePrekeys: otks, ); } catch (e) { rethrow; } } Future _saveKeysToLocal(String userId) async { final otksEncoded = []; if (_oneTimePreKeys != null) { for (final otk in _oneTimePreKeys!) { otksEncoded.add(base64Encode(await otk.extractPrivateKeyBytes())); } } final data = jsonEncode({ 'ik_dh': base64Encode(await _identityDhKeyPair!.extractPrivateKeyBytes()), 'ik_sk': base64Encode(await _identitySigningKeyPair!.extractPrivateKeyBytes()), 'spk': base64Encode(await _signedPreKey!.extractPrivateKeyBytes()), 'otks': otksEncoded, }); await _storage.write(key: 'e2ee_keys_$userId', value: data); // Also save to SharedPreferences on web as a fallback if (kIsWeb) { final prefs = await SharedPreferences.getInstance(); await prefs.setString('e2ee_keys_$userId', data); } } Future _loadKeysFromLocal(String userId) async { // Try FlutterSecureStorage first var data = await _storage.read(key: 'e2ee_keys_$userId'); // Fallback to SharedPreferences on web if secure storage fails if (data == null && kIsWeb) { final prefs = await SharedPreferences.getInstance(); data = prefs.getString('e2ee_keys_$userId'); } if (data == null) { return false; } final map = jsonDecode(data); if (map['ik_dh'] != null) { _identityDhKeyPair = await _dhAlgo.newKeyPairFromSeed(base64Decode(map['ik_dh'])); } else if (map['ik'] != null) { // Legacy load _identityDhKeyPair = await _dhAlgo.newKeyPairFromSeed(base64Decode(map['ik'])); } if (map['ik_sk'] != null) { _identitySigningKeyPair = await _signingAlgo.newKeyPairFromSeed(base64Decode(map['ik_sk'])); } _signedPreKey = await _dhAlgo.newKeyPairFromSeed(base64Decode(map['spk'])); // Load OTKs _oneTimePreKeys = []; if (map['otks'] != null && map['otks'] is List) { for (final otkSeed in map['otks']) { _oneTimePreKeys!.add(await _dhAlgo.newKeyPairFromSeed(base64Decode(otkSeed))); } } return isReady; } Future _backupIdentityToCloud(String userId) async { final dhSeed = await _identityDhKeyPair!.extractPrivateKeyBytes(); final skSeed = await _identitySigningKeyPair!.extractPrivateKeyBytes(); final blobData = base64Encode(dhSeed) + ":" + base64Encode(skSeed); final pinKey = await _deriveKeyFromPin(_backupPin); final nonce = _cipher.newNonce(); final box = await _cipher.encrypt(utf8.encode(blobData), secretKey: pinKey, nonce: nonce); final blob = jsonEncode({ 'c': base64Encode(box.cipherText), 'n': base64Encode(nonce), 'm': base64Encode(box.mac.bytes), }); await ApiService(_auth).updateProfile(encryptedPrivateKey: blob); } Future _restoreFromCloud(String userId) async { try { // FIX 1: Correct Profile Access final profileMap = await ApiService(_auth).getProfile(); // ApiService returns map with 'profile' key containing Profile object final profileObj = profileMap['profile']; String? blobJson; // Safety check if it returned Map or Object unexpectedly if (profileObj is Map) { blobJson = profileObj['encrypted_private_key']; } else { // Assume Profile object // DYNAMIC ACCESS OR CAST // Since we can't import Profile here to cast easily without cycle or logic change, // we use dynamic access if supported, or assume getProfile implementation. // Actually, earlier viewed Profile.dart shows it's a class. // We'll trust dynamic dispatch or use `.encryptedPrivateKey` if typed. // However, ApiService.getProfile returns Map. // Whatever is in 'profile' key IS a Profile instance. // Dynamic access .encryptedPrivateKey should work. blobJson = (profileObj as dynamic).encryptedPrivateKey; } if (blobJson == null) return false; final blob = jsonDecode(blobJson); final pinKey = await _deriveKeyFromPin(_backupPin); final box = SecretBox(base64Decode(blob['c']), nonce: base64Decode(blob['n']), mac: Mac(base64Decode(blob['m']))); final decryptedBytes = await _cipher.decrypt(box, secretKey: pinKey); final blobData = utf8.decode(decryptedBytes); final seeds = blobData.split(':'); if (seeds.length == 2) { _identityDhKeyPair = await _dhAlgo.newKeyPairFromSeed(base64Decode(seeds[0])); _identitySigningKeyPair = await _signingAlgo.newKeyPairFromSeed(base64Decode(seeds[1])); } else { // Legacy restore _identityDhKeyPair = await _dhAlgo.newKeyPairFromSeed(base64Decode(seeds[0])); } // After cloud restore, regenerate SPK and OTKs _signedPreKey = await _dhAlgo.newKeyPair(); final spkPublic = await _signedPreKey!.extractPublicKey(); final signature = await _signingAlgo.sign(spkPublic.bytes, keyPair: _identitySigningKeyPair!); final spkSignature = Uint8List.fromList(signature.bytes); // Generate new OTKs final opks = []; for (int i = 0; i < 20; i++) { opks.add(await _dhAlgo.newKeyPair()); } _oneTimePreKeys = opks; // Save locally and publish await _saveKeysToLocal(userId); await _publishKeys(spkSignature); return isReady; } catch (e) { return false; } } Future> exportAllKeys() async { if (!isReady) { throw Exception('Keys not ready for export'); } try { final identityDhPublic = await _identityDhKeyPair!.extractPublicKey(); final identitySigningPublic = await _identitySigningKeyPair!.extractPublicKey(); final spkPublic = await _signedPreKey!.extractPublicKey(); // Generate SPK signature for backup final spkSignature = await _signingAlgo.sign( spkPublic.bytes, keyPair: _identitySigningKeyPair!, ); // Export OTKs final otkData = >[]; for (int i = 0; i < _oneTimePreKeys!.length; i++) { final otk = _oneTimePreKeys![i]; final otkPublic = await otk.extractPublicKey(); otkData.add({ 'key_id': i, 'public_key': base64Encode(otkPublic.bytes), 'private_key': base64Encode(await otk.extractPrivateKeyBytes()), }); } final exportData = { 'version': '1.0', 'exported_at': DateTime.now().toIso8601String(), 'keys': { 'identity_dh_private': base64Encode(await _identityDhKeyPair!.extractPrivateKeyBytes()), 'identity_dh_public': base64Encode(identityDhPublic.bytes), 'identity_signing_private': base64Encode(await _identitySigningKeyPair!.extractPrivateKeyBytes()), 'identity_signing_public': base64Encode(identitySigningPublic.bytes), 'signed_prekey_private': base64Encode(await _signedPreKey!.extractPrivateKeyBytes()), 'signed_prekey_public': base64Encode(spkPublic.bytes), 'signed_prekey_signature': base64Encode(spkSignature.bytes), 'one_time_prekeys': otkData, }, 'metadata': { 'otk_count': _oneTimePreKeys!.length, 'user_id': _initializedForUserId, }, }; return exportData; } catch (e) { rethrow; } } Future importAllKeys(Map backupData) async { try { if (!backupData.containsKey('keys')) { throw ArgumentError('Invalid backup format: missing keys'); } final keys = backupData['keys'] as Map; // 1. Restore Identity Keys if (keys.containsKey('identity_dh_private')) { _identityDhKeyPair = await _dhAlgo.newKeyPairFromSeed(base64Decode(keys['identity_dh_private'])); } if (keys.containsKey('identity_signing_private')) { _identitySigningKeyPair = await _signingAlgo.newKeyPairFromSeed(base64Decode(keys['identity_signing_private'])); } // 2. Restore Signed PreKey if (keys.containsKey('signed_prekey_private')) { _signedPreKey = await _dhAlgo.newKeyPairFromSeed(base64Decode(keys['signed_prekey_private'])); } // 3. Restore One-Time PreKeys if (keys.containsKey('one_time_prekeys') && keys['one_time_prekeys'] is List) { final otkList = keys['one_time_prekeys'] as List; final importedOTKs = []; for (final item in otkList) { if (item is Map && item.containsKey('private_key')) { importedOTKs.add(await _dhAlgo.newKeyPairFromSeed(base64Decode(item['private_key']))); } } _oneTimePreKeys = importedOTKs; } // 4. Set User Context from metadata if (backupData.containsKey('metadata')) { final metadata = backupData['metadata'] as Map; if (metadata.containsKey('user_id')) { _initializedForUserId = metadata['user_id']; } } // Fallback if metadata missing if (_initializedForUserId == null) { _initializedForUserId = _auth.currentUser?.id; } // 5. Persist and Synchronize if (_initializedForUserId != null) { await _saveKeysToLocal(_initializedForUserId!); // Republish to server to ensure backend is synchronized // This is safe even if keys are identical if (_identitySigningKeyPair != null && _signedPreKey != null) { final spkPublic = await _signedPreKey!.extractPublicKey(); final signature = await _signingAlgo.sign( spkPublic.bytes, keyPair: _identitySigningKeyPair! ); await _publishKeys(signature.bytes); } } } catch (e) { rethrow; } } Future _deriveKeyFromPin(String pin) async { final sink = _sha256.newHashSink(); sink.add(utf8.encode(pin)); sink.close(); return SecretKey((await sink.hash()).bytes); } }