LCOV - code coverage report
Current view: top level - lib/encryption - olm_manager.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 83.4 % 362 302
Test Date: 2025-01-14 13:39:53 Functions: - 0 0

            Line data    Source code
       1              : /*
       2              :  *   Famedly Matrix SDK
       3              :  *   Copyright (C) 2019, 2020, 2021 Famedly GmbH
       4              :  *
       5              :  *   This program is free software: you can redistribute it and/or modify
       6              :  *   it under the terms of the GNU Affero General Public License as
       7              :  *   published by the Free Software Foundation, either version 3 of the
       8              :  *   License, or (at your option) any later version.
       9              :  *
      10              :  *   This program is distributed in the hope that it will be useful,
      11              :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12              :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13              :  *   GNU Affero General Public License for more details.
      14              :  *
      15              :  *   You should have received a copy of the GNU Affero General Public License
      16              :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17              :  */
      18              : 
      19              : import 'dart:convert';
      20              : 
      21              : import 'package:async/async.dart';
      22              : import 'package:canonical_json/canonical_json.dart';
      23              : import 'package:collection/collection.dart';
      24              : import 'package:olm/olm.dart' as olm;
      25              : 
      26              : import 'package:matrix/encryption/encryption.dart';
      27              : import 'package:matrix/encryption/utils/json_signature_check_extension.dart';
      28              : import 'package:matrix/encryption/utils/olm_session.dart';
      29              : import 'package:matrix/matrix.dart';
      30              : import 'package:matrix/msc_extensions/msc_3814_dehydrated_devices/api.dart';
      31              : import 'package:matrix/src/utils/run_benchmarked.dart';
      32              : import 'package:matrix/src/utils/run_in_root.dart';
      33              : 
      34              : class OlmManager {
      35              :   final Encryption encryption;
      36           72 :   Client get client => encryption.client;
      37              :   olm.Account? _olmAccount;
      38              :   String? ourDeviceId;
      39              : 
      40              :   /// Returns the base64 encoded keys to store them in a store.
      41              :   /// This String should **never** leave the device!
      42           23 :   String? get pickledOlmAccount =>
      43          115 :       enabled ? _olmAccount!.pickle(client.userID!) : null;
      44           23 :   String? get fingerprintKey =>
      45          115 :       enabled ? json.decode(_olmAccount!.identity_keys())['ed25519'] : null;
      46           24 :   String? get identityKey =>
      47          120 :       enabled ? json.decode(_olmAccount!.identity_keys())['curve25519'] : null;
      48              : 
      49            0 :   String? pickleOlmAccountWithKey(String key) =>
      50            0 :       enabled ? _olmAccount!.pickle(key) : null;
      51              : 
      52           48 :   bool get enabled => _olmAccount != null;
      53              : 
      54           24 :   OlmManager(this.encryption);
      55              : 
      56              :   /// A map from Curve25519 identity keys to existing olm sessions.
      57           48 :   Map<String, List<OlmSession>> get olmSessions => _olmSessions;
      58              :   final Map<String, List<OlmSession>> _olmSessions = {};
      59              : 
      60              :   // NOTE(Nico): On initial login we pass null to create a new account
      61           24 :   Future<void> init({
      62              :     String? olmAccount,
      63              :     required String? deviceId,
      64              :     String? pickleKey,
      65              :     String? dehydratedDeviceAlgorithm,
      66              :   }) async {
      67           24 :     ourDeviceId = deviceId;
      68              :     if (olmAccount == null) {
      69              :       try {
      70            4 :         await olm.init();
      71            8 :         _olmAccount = olm.Account();
      72            8 :         _olmAccount!.create();
      73            4 :         if (!await uploadKeys(
      74              :           uploadDeviceKeys: true,
      75              :           updateDatabase: false,
      76              :           dehydratedDeviceAlgorithm: dehydratedDeviceAlgorithm,
      77              :           dehydratedDevicePickleKey:
      78              :               dehydratedDeviceAlgorithm != null ? pickleKey : null,
      79              :         )) {
      80              :           throw ('Upload key failed');
      81              :         }
      82              :       } catch (_) {
      83            0 :         _olmAccount?.free();
      84            0 :         _olmAccount = null;
      85              :         rethrow;
      86              :       }
      87              :     } else {
      88              :       try {
      89           23 :         await olm.init();
      90           46 :         _olmAccount = olm.Account();
      91           92 :         _olmAccount!.unpickle(pickleKey ?? client.userID!, olmAccount);
      92              :       } catch (_) {
      93            2 :         _olmAccount?.free();
      94            1 :         _olmAccount = null;
      95              :         rethrow;
      96              :       }
      97              :     }
      98              :   }
      99              : 
     100              :   /// Adds a signature to this json from this olm account and returns the signed
     101              :   /// json.
     102            5 :   Map<String, dynamic> signJson(Map<String, dynamic> payload) {
     103            5 :     if (!enabled) throw ('Encryption is disabled');
     104            5 :     final Map<String, dynamic>? unsigned = payload['unsigned'];
     105            5 :     final Map<String, dynamic>? signatures = payload['signatures'];
     106            5 :     payload.remove('unsigned');
     107            5 :     payload.remove('signatures');
     108            5 :     final canonical = canonicalJson.encode(payload);
     109           15 :     final signature = _olmAccount!.sign(String.fromCharCodes(canonical));
     110              :     if (signatures != null) {
     111            0 :       payload['signatures'] = signatures;
     112              :     } else {
     113           10 :       payload['signatures'] = <String, dynamic>{};
     114              :     }
     115           20 :     if (!payload['signatures'].containsKey(client.userID)) {
     116           25 :       payload['signatures'][client.userID] = <String, dynamic>{};
     117              :     }
     118           35 :     payload['signatures'][client.userID]['ed25519:$ourDeviceId'] = signature;
     119              :     if (unsigned != null) {
     120            0 :       payload['unsigned'] = unsigned;
     121              :     }
     122              :     return payload;
     123              :   }
     124              : 
     125            4 :   String signString(String s) {
     126            8 :     return _olmAccount!.sign(s);
     127              :   }
     128              : 
     129              :   bool _uploadKeysLock = false;
     130              :   CancelableOperation<Map<String, int>>? currentUpload;
     131              : 
     132           42 :   int? get maxNumberOfOneTimeKeys => _olmAccount?.max_number_of_one_time_keys();
     133              : 
     134              :   /// Generates new one time keys, signs everything and upload it to the server.
     135              :   /// If `retry` is > 0, the request will be retried with new OTKs on upload failure.
     136            5 :   Future<bool> uploadKeys({
     137              :     bool uploadDeviceKeys = false,
     138              :     int? oldKeyCount = 0,
     139              :     bool updateDatabase = true,
     140              :     bool? unusedFallbackKey = false,
     141              :     String? dehydratedDeviceAlgorithm,
     142              :     String? dehydratedDevicePickleKey,
     143              :     int retry = 1,
     144              :   }) async {
     145            5 :     final olmAccount = _olmAccount;
     146              :     if (olmAccount == null) {
     147              :       return true;
     148              :     }
     149              : 
     150            5 :     if (_uploadKeysLock) {
     151              :       return false;
     152              :     }
     153            5 :     _uploadKeysLock = true;
     154              : 
     155            5 :     final signedOneTimeKeys = <String, Map<String, Object?>>{};
     156              :     try {
     157              :       int? uploadedOneTimeKeysCount;
     158              :       if (oldKeyCount != null) {
     159              :         // check if we have OTKs that still need uploading. If we do, we don't try to generate new ones,
     160              :         // instead we try to upload the old ones first
     161              :         final oldOTKsNeedingUpload = json
     162           15 :             .decode(olmAccount.one_time_keys())['curve25519']
     163            5 :             .entries
     164            5 :             .length as int;
     165              :         // generate one-time keys
     166              :         // we generate 2/3rds of max, so that other keys people may still have can
     167              :         // still be used
     168              :         final oneTimeKeysCount =
     169           25 :             (olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() -
     170            5 :                 oldKeyCount -
     171              :                 oldOTKsNeedingUpload;
     172            5 :         if (oneTimeKeysCount > 0) {
     173            5 :           olmAccount.generate_one_time_keys(oneTimeKeysCount);
     174              :         }
     175            5 :         uploadedOneTimeKeysCount = oneTimeKeysCount + oldOTKsNeedingUpload;
     176              :       }
     177              : 
     178           15 :       if (encryption.isMinOlmVersion(3, 2, 7) && unusedFallbackKey == false) {
     179              :         // we don't have an unused fallback key uploaded....so let's change that!
     180            5 :         olmAccount.generate_fallback_key();
     181              :       }
     182              : 
     183              :       // we save the generated OTKs into the database.
     184              :       // in case the app gets killed during upload or the upload fails due to bad network
     185              :       // we can still re-try later
     186              :       if (updateDatabase) {
     187            4 :         await encryption.olmDatabase?.updateClientKeys(pickledOlmAccount!);
     188              :       }
     189              : 
     190              :       // and now generate the payload to upload
     191            5 :       var deviceKeys = <String, dynamic>{
     192           10 :         'user_id': client.userID,
     193            5 :         'device_id': ourDeviceId,
     194            5 :         'algorithms': [
     195              :           AlgorithmTypes.olmV1Curve25519AesSha2,
     196              :           AlgorithmTypes.megolmV1AesSha2,
     197              :         ],
     198            5 :         'keys': <String, dynamic>{},
     199              :       };
     200              : 
     201              :       if (uploadDeviceKeys) {
     202              :         final Map<String, dynamic> keys =
     203           10 :             json.decode(olmAccount.identity_keys());
     204           10 :         for (final entry in keys.entries) {
     205            5 :           final algorithm = entry.key;
     206            5 :           final value = entry.value;
     207           20 :           deviceKeys['keys']['$algorithm:$ourDeviceId'] = value;
     208              :         }
     209            5 :         deviceKeys = signJson(deviceKeys);
     210              :       }
     211              : 
     212              :       // now sign all the one-time keys
     213              :       for (final entry
     214           25 :           in json.decode(olmAccount.one_time_keys())['curve25519'].entries) {
     215            5 :         final key = entry.key;
     216            5 :         final value = entry.value;
     217           20 :         signedOneTimeKeys['signed_curve25519:$key'] = signJson({
     218              :           'key': value,
     219              :         });
     220              :       }
     221              : 
     222            5 :       final signedFallbackKeys = <String, dynamic>{};
     223           10 :       if (encryption.isMinOlmVersion(3, 2, 7)) {
     224           10 :         final fallbackKey = json.decode(olmAccount.unpublished_fallback_key());
     225              :         // now sign all the fallback keys
     226           15 :         for (final entry in fallbackKey['curve25519'].entries) {
     227            5 :           final key = entry.key;
     228            5 :           final value = entry.value;
     229           20 :           signedFallbackKeys['signed_curve25519:$key'] = signJson({
     230              :             'key': value,
     231              :             'fallback': true,
     232              :           });
     233              :         }
     234              :       }
     235              : 
     236            5 :       if (signedFallbackKeys.isEmpty &&
     237            1 :           signedOneTimeKeys.isEmpty &&
     238              :           !uploadDeviceKeys) {
     239            0 :         _uploadKeysLock = false;
     240              :         return true;
     241              :       }
     242              : 
     243              :       // Workaround: Make sure we stop if we got logged out in the meantime.
     244           10 :       if (!client.isLogged()) return true;
     245              : 
     246           20 :       if (ourDeviceId != client.deviceID) {
     247              :         if (dehydratedDeviceAlgorithm == null ||
     248              :             dehydratedDevicePickleKey == null) {
     249            0 :           throw Exception(
     250              :             'You need to provide both the pickle key and the algorithm to use dehydrated devices!',
     251              :           );
     252              :         }
     253              : 
     254            0 :         await client.uploadDehydratedDevice(
     255            0 :           deviceId: ourDeviceId!,
     256              :           initialDeviceDisplayName: 'Dehydrated Device',
     257              :           deviceKeys:
     258            0 :               uploadDeviceKeys ? MatrixDeviceKeys.fromJson(deviceKeys) : null,
     259              :           oneTimeKeys: signedOneTimeKeys,
     260              :           fallbackKeys: signedFallbackKeys,
     261            0 :           deviceData: {
     262              :             'algorithm': dehydratedDeviceAlgorithm,
     263            0 :             'device': encryption.olmManager
     264            0 :                 .pickleOlmAccountWithKey(dehydratedDevicePickleKey),
     265              :           },
     266              :         );
     267              :         return true;
     268              :       }
     269           10 :       final currentUpload = this.currentUpload = CancelableOperation.fromFuture(
     270           10 :         client.uploadKeys(
     271              :           deviceKeys:
     272            5 :               uploadDeviceKeys ? MatrixDeviceKeys.fromJson(deviceKeys) : null,
     273              :           oneTimeKeys: signedOneTimeKeys,
     274              :           fallbackKeys: signedFallbackKeys,
     275              :         ),
     276              :       );
     277            5 :       final response = await currentUpload.valueOrCancellation();
     278              :       if (response == null) {
     279            0 :         _uploadKeysLock = false;
     280              :         return false;
     281              :       }
     282              : 
     283              :       // mark the OTKs as published and save that to datbase
     284            5 :       olmAccount.mark_keys_as_published();
     285              :       if (updateDatabase) {
     286            4 :         await encryption.olmDatabase?.updateClientKeys(pickledOlmAccount!);
     287              :       }
     288              :       return (uploadedOneTimeKeysCount != null &&
     289           10 :               response['signed_curve25519'] == uploadedOneTimeKeysCount) ||
     290              :           uploadedOneTimeKeysCount == null;
     291            0 :     } on MatrixException catch (exception) {
     292            0 :       _uploadKeysLock = false;
     293              : 
     294              :       // we failed to upload the keys. If we only tried to upload one time keys, try to recover by removing them and generating new ones.
     295              :       if (!uploadDeviceKeys &&
     296            0 :           unusedFallbackKey != false &&
     297            0 :           retry > 0 &&
     298              :           dehydratedDeviceAlgorithm != null &&
     299            0 :           signedOneTimeKeys.isNotEmpty &&
     300            0 :           exception.error == MatrixError.M_UNKNOWN) {
     301            0 :         Logs().w('Rotating otks because upload failed', exception);
     302            0 :         for (final otk in signedOneTimeKeys.values) {
     303              :           // Keys can only be removed by creating a session...
     304            0 :           final session = olm.Session();
     305              :           try {
     306              :             final String identity =
     307            0 :                 json.decode(olmAccount.identity_keys())['curve25519'];
     308            0 :             final key = otk.tryGet<String>('key');
     309              :             if (key != null) {
     310            0 :               session.create_outbound(_olmAccount!, identity, key);
     311            0 :               olmAccount.remove_one_time_keys(session);
     312              :             }
     313              :           } finally {
     314            0 :             session.free();
     315              :           }
     316              :         }
     317              : 
     318            0 :         await uploadKeys(
     319              :           uploadDeviceKeys: uploadDeviceKeys,
     320              :           oldKeyCount: oldKeyCount,
     321              :           updateDatabase: updateDatabase,
     322              :           unusedFallbackKey: unusedFallbackKey,
     323            0 :           retry: retry - 1,
     324              :         );
     325              :       }
     326              :     } finally {
     327            5 :       _uploadKeysLock = false;
     328              :     }
     329              : 
     330              :     return false;
     331              :   }
     332              : 
     333              :   final _otkUpdateDedup = AsyncCache<void>.ephemeral();
     334              : 
     335           24 :   Future<void> handleDeviceOneTimeKeysCount(
     336              :     Map<String, int>? countJson,
     337              :     List<String>? unusedFallbackKeyTypes,
     338              :   ) async {
     339           24 :     if (!enabled) {
     340              :       return;
     341              :     }
     342              : 
     343           48 :     await _otkUpdateDedup.fetch(
     344           72 :       () => runBenchmarked('handleOtkUpdate', () async {
     345           48 :         final haveFallbackKeys = encryption.isMinOlmVersion(3, 2, 0);
     346              :         // Check if there are at least half of max_number_of_one_time_keys left on the server
     347              :         // and generate and upload more if not.
     348              : 
     349              :         // If the server did not send us a count, assume it is 0
     350           24 :         final keyCount = countJson?.tryGet<int>('signed_curve25519') ?? 0;
     351              : 
     352              :         // If the server does not support fallback keys, it will not tell us about them.
     353              :         // If the server supports them but has no key, upload a new one.
     354              :         var unusedFallbackKey = true;
     355           26 :         if (unusedFallbackKeyTypes?.contains('signed_curve25519') == false) {
     356              :           unusedFallbackKey = false;
     357              :         }
     358              : 
     359              :         // fixup accidental too many uploads. We delete only one of them so that the server has time to update the counts and because we will get rate limited anyway.
     360           72 :         if (keyCount > _olmAccount!.max_number_of_one_time_keys()) {
     361            0 :           final requestingKeysFrom = {
     362            0 :             client.userID!: {ourDeviceId!: 'signed_curve25519'},
     363              :           };
     364            0 :           await client.claimKeys(requestingKeysFrom, timeout: 10000);
     365              :         }
     366              : 
     367              :         // Only upload keys if they are less than half of the max or we have no unused fallback key
     368           96 :         if (keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) ||
     369              :             !unusedFallbackKey) {
     370            1 :           await uploadKeys(
     371              :             oldKeyCount:
     372            4 :                 keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2)
     373              :                     ? keyCount
     374              :                     : null,
     375              :             unusedFallbackKey: haveFallbackKeys ? unusedFallbackKey : null,
     376              :           );
     377              :         }
     378              :       }),
     379              :     );
     380              :   }
     381              : 
     382           23 :   Future<void> storeOlmSession(OlmSession session) async {
     383           46 :     if (session.sessionId == null || session.pickledSession == null) {
     384              :       return;
     385              :     }
     386              : 
     387           92 :     _olmSessions[session.identityKey] ??= <OlmSession>[];
     388           69 :     final ix = _olmSessions[session.identityKey]!
     389           55 :         .indexWhere((s) => s.sessionId == session.sessionId);
     390           46 :     if (ix == -1) {
     391              :       // add a new session
     392           92 :       _olmSessions[session.identityKey]!.add(session);
     393              :     } else {
     394              :       // update an existing session
     395           28 :       _olmSessions[session.identityKey]![ix] = session;
     396              :     }
     397           69 :     await encryption.olmDatabase?.storeOlmSession(
     398           23 :       session.identityKey,
     399           23 :       session.sessionId!,
     400           23 :       session.pickledSession!,
     401           46 :       session.lastReceived?.millisecondsSinceEpoch ??
     402            0 :           DateTime.now().millisecondsSinceEpoch,
     403              :     );
     404              :   }
     405              : 
     406           24 :   Future<ToDeviceEvent> _decryptToDeviceEvent(ToDeviceEvent event) async {
     407           48 :     if (event.type != EventTypes.Encrypted) {
     408              :       return event;
     409              :     }
     410           24 :     final content = event.parsedRoomEncryptedContent;
     411           48 :     if (content.algorithm != AlgorithmTypes.olmV1Curve25519AesSha2) {
     412            0 :       throw DecryptException(DecryptException.unknownAlgorithm);
     413              :     }
     414           24 :     if (content.ciphertextOlm == null ||
     415           72 :         !content.ciphertextOlm!.containsKey(identityKey)) {
     416            6 :       throw DecryptException(DecryptException.isntSentForThisDevice);
     417              :     }
     418              :     String? plaintext;
     419           23 :     final senderKey = content.senderKey;
     420           92 :     final body = content.ciphertextOlm![identityKey]!.body;
     421           92 :     final type = content.ciphertextOlm![identityKey]!.type;
     422           23 :     if (type != 0 && type != 1) {
     423            0 :       throw DecryptException(DecryptException.unknownMessageType);
     424              :     }
     425           96 :     final device = client.userDeviceKeys[event.sender]?.deviceKeys.values
     426            8 :         .firstWhereOrNull((d) => d.curve25519Key == senderKey);
     427           46 :     final existingSessions = olmSessions[senderKey];
     428           23 :     Future<void> updateSessionUsage([OlmSession? session]) async {
     429              :       try {
     430              :         if (session != null) {
     431            2 :           session.lastReceived = DateTime.now();
     432            1 :           await storeOlmSession(session);
     433              :         }
     434              :         if (device != null) {
     435            2 :           device.lastActive = DateTime.now();
     436            3 :           await encryption.olmDatabase?.setLastActiveUserDeviceKey(
     437            2 :             device.lastActive.millisecondsSinceEpoch,
     438            1 :             device.userId,
     439            1 :             device.deviceId!,
     440              :           );
     441              :         }
     442              :       } catch (e, s) {
     443            0 :         Logs().e('Error while updating olm session timestamp', e, s);
     444              :       }
     445              :     }
     446              : 
     447              :     if (existingSessions != null) {
     448            4 :       for (final session in existingSessions) {
     449            2 :         if (session.session == null) {
     450              :           continue;
     451              :         }
     452            6 :         if (type == 0 && session.session!.matches_inbound(body)) {
     453              :           try {
     454            4 :             plaintext = session.session!.decrypt(type, body);
     455              :           } catch (e) {
     456              :             // The message was encrypted during this session, but is unable to decrypt
     457            1 :             throw DecryptException(
     458              :               DecryptException.decryptionFailed,
     459            1 :               e.toString(),
     460              :             );
     461              :           }
     462            1 :           await updateSessionUsage(session);
     463              :           break;
     464            1 :         } else if (type == 1) {
     465              :           try {
     466            0 :             plaintext = session.session!.decrypt(type, body);
     467            0 :             await updateSessionUsage(session);
     468              :             break;
     469              :           } catch (_) {
     470              :             plaintext = null;
     471              :           }
     472              :         }
     473              :       }
     474              :     }
     475           23 :     if (plaintext == null && type != 0) {
     476            0 :       throw DecryptException(DecryptException.unableToDecryptWithAnyOlmSession);
     477              :     }
     478              : 
     479              :     if (plaintext == null) {
     480           23 :       final newSession = olm.Session();
     481              :       try {
     482           46 :         newSession.create_inbound_from(_olmAccount!, senderKey, body);
     483           46 :         _olmAccount!.remove_one_time_keys(newSession);
     484           92 :         await encryption.olmDatabase?.updateClientKeys(pickledOlmAccount!);
     485              : 
     486           23 :         plaintext = newSession.decrypt(type, body);
     487              : 
     488           23 :         await storeOlmSession(
     489           23 :           OlmSession(
     490           46 :             key: client.userID!,
     491              :             identityKey: senderKey,
     492           23 :             sessionId: newSession.session_id(),
     493              :             session: newSession,
     494           23 :             lastReceived: DateTime.now(),
     495              :           ),
     496              :         );
     497           23 :         await updateSessionUsage();
     498              :       } catch (e) {
     499            0 :         newSession.free();
     500            0 :         throw DecryptException(DecryptException.decryptionFailed, e.toString());
     501              :       }
     502              :     }
     503           23 :     final Map<String, dynamic> plainContent = json.decode(plaintext);
     504           69 :     if (plainContent['sender'] != event.sender) {
     505            0 :       throw DecryptException(DecryptException.senderDoesntMatch);
     506              :     }
     507           92 :     if (plainContent['recipient'] != client.userID) {
     508            0 :       throw DecryptException(DecryptException.recipientDoesntMatch);
     509              :     }
     510           46 :     if (plainContent['recipient_keys'] is Map &&
     511           69 :         plainContent['recipient_keys']['ed25519'] is String &&
     512           92 :         plainContent['recipient_keys']['ed25519'] != fingerprintKey) {
     513            0 :       throw DecryptException(DecryptException.ownFingerprintDoesntMatch);
     514              :     }
     515           23 :     return ToDeviceEvent(
     516           23 :       content: plainContent['content'],
     517           23 :       encryptedContent: event.content,
     518           23 :       type: plainContent['type'],
     519           23 :       sender: event.sender,
     520              :     );
     521              :   }
     522              : 
     523           24 :   Future<List<OlmSession>> getOlmSessionsFromDatabase(String senderKey) async {
     524              :     final olmSessions =
     525          117 :         await encryption.olmDatabase?.getOlmSessions(senderKey, client.userID!);
     526           52 :     return olmSessions?.where((sess) => sess.isValid).toList() ?? [];
     527              :   }
     528              : 
     529           10 :   Future<void> getOlmSessionsForDevicesFromDatabase(
     530              :     List<String> senderKeys,
     531              :   ) async {
     532           30 :     final rows = await encryption.olmDatabase?.getOlmSessionsForDevices(
     533              :       senderKeys,
     534           20 :       client.userID!,
     535              :     );
     536           10 :     final res = <String, List<OlmSession>>{};
     537           14 :     for (final sess in rows ?? []) {
     538           12 :       res[sess.identityKey] ??= <OlmSession>[];
     539            4 :       if (sess.isValid) {
     540           12 :         res[sess.identityKey]!.add(sess);
     541              :       }
     542              :     }
     543           14 :     for (final entry in res.entries) {
     544           16 :       _olmSessions[entry.key] = entry.value;
     545              :     }
     546              :   }
     547              : 
     548           24 :   Future<List<OlmSession>> getOlmSessions(
     549              :     String senderKey, {
     550              :     bool getFromDb = true,
     551              :   }) async {
     552           48 :     var sess = olmSessions[senderKey];
     553            0 :     if ((getFromDb) && (sess == null || sess.isEmpty)) {
     554           24 :       final sessions = await getOlmSessionsFromDatabase(senderKey);
     555           24 :       if (sessions.isEmpty) {
     556           24 :         return [];
     557              :       }
     558            4 :       sess = _olmSessions[senderKey] = sessions;
     559              :     }
     560              :     if (sess == null) {
     561            7 :       return [];
     562              :     }
     563            7 :     sess.sort(
     564            8 :       (a, b) => a.lastReceived == b.lastReceived
     565            0 :           ? (a.sessionId ?? '').compareTo(b.sessionId ?? '')
     566            2 :           : (b.lastReceived ?? DateTime(0))
     567            4 :               .compareTo(a.lastReceived ?? DateTime(0)),
     568              :     );
     569              :     return sess;
     570              :   }
     571              : 
     572              :   final Map<String, DateTime> _restoredOlmSessionsTime = {};
     573              : 
     574            7 :   Future<void> restoreOlmSession(String userId, String senderKey) async {
     575           21 :     if (!client.userDeviceKeys.containsKey(userId)) {
     576              :       return;
     577              :     }
     578           10 :     final device = client.userDeviceKeys[userId]!.deviceKeys.values
     579            8 :         .firstWhereOrNull((d) => d.curve25519Key == senderKey);
     580              :     if (device == null) {
     581              :       return;
     582              :     }
     583              :     // per device only one olm session per hour should be restored
     584            2 :     final mapKey = '$userId;$senderKey';
     585            4 :     if (_restoredOlmSessionsTime.containsKey(mapKey) &&
     586            0 :         DateTime.now()
     587            0 :             .subtract(Duration(hours: 1))
     588            0 :             .isBefore(_restoredOlmSessionsTime[mapKey]!)) {
     589            0 :       Logs().w(
     590              :         '[OlmManager] Skipping restore session, one was restored in the past hour',
     591              :       );
     592              :       return;
     593              :     }
     594            6 :     _restoredOlmSessionsTime[mapKey] = DateTime.now();
     595            4 :     await startOutgoingOlmSessions([device]);
     596            8 :     await client.sendToDeviceEncrypted([device], EventTypes.Dummy, {});
     597              :   }
     598              : 
     599           24 :   Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
     600           48 :     if (event.type != EventTypes.Encrypted) {
     601              :       return event;
     602              :     }
     603           48 :     final senderKey = event.parsedRoomEncryptedContent.senderKey;
     604           24 :     Future<bool> loadFromDb() async {
     605           24 :       final sessions = await getOlmSessions(senderKey);
     606           24 :       return sessions.isNotEmpty;
     607              :     }
     608              : 
     609           48 :     if (!_olmSessions.containsKey(senderKey)) {
     610           24 :       await loadFromDb();
     611              :     }
     612              :     try {
     613           24 :       event = await _decryptToDeviceEvent(event);
     614           46 :       if (event.type != EventTypes.Encrypted || !(await loadFromDb())) {
     615              :         return event;
     616              :       }
     617              :       // retry to decrypt!
     618            0 :       return _decryptToDeviceEvent(event);
     619              :     } catch (_) {
     620              :       // okay, the thing errored while decrypting. It is safe to assume that the olm session is corrupt and we should generate a new one
     621           24 :       runInRoot(() => restoreOlmSession(event.senderId, senderKey));
     622              : 
     623              :       rethrow;
     624              :     }
     625              :   }
     626              : 
     627           10 :   Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys) async {
     628           20 :     Logs().v(
     629           20 :       '[OlmManager] Starting session with ${deviceKeys.length} devices...',
     630              :     );
     631           10 :     final requestingKeysFrom = <String, Map<String, String>>{};
     632           20 :     for (final device in deviceKeys) {
     633           20 :       if (requestingKeysFrom[device.userId] == null) {
     634           30 :         requestingKeysFrom[device.userId] = {};
     635              :       }
     636           40 :       requestingKeysFrom[device.userId]![device.deviceId!] =
     637              :           'signed_curve25519';
     638              :     }
     639              : 
     640           20 :     final response = await client.claimKeys(requestingKeysFrom, timeout: 10000);
     641              : 
     642           30 :     for (final userKeysEntry in response.oneTimeKeys.entries) {
     643           10 :       final userId = userKeysEntry.key;
     644           30 :       for (final deviceKeysEntry in userKeysEntry.value.entries) {
     645           10 :         final deviceId = deviceKeysEntry.key;
     646              :         final fingerprintKey =
     647           60 :             client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.ed25519Key;
     648              :         final identityKey =
     649           60 :             client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.curve25519Key;
     650           30 :         for (final deviceKey in deviceKeysEntry.value.values) {
     651              :           if (fingerprintKey == null ||
     652              :               identityKey == null ||
     653           10 :               deviceKey is! Map<String, Object?> ||
     654           10 :               !deviceKey.checkJsonSignature(fingerprintKey, userId, deviceId) ||
     655           20 :               deviceKey['key'] is! String) {
     656            0 :             Logs().w(
     657            0 :               'Skipping invalid device key from $userId:$deviceId',
     658              :               deviceKey,
     659              :             );
     660              :             continue;
     661              :           }
     662           30 :           Logs().v('[OlmManager] Starting session with $userId:$deviceId');
     663           10 :           final session = olm.Session();
     664              :           try {
     665           10 :             session.create_outbound(
     666           10 :               _olmAccount!,
     667              :               identityKey,
     668           10 :               deviceKey.tryGet<String>('key')!,
     669              :             );
     670           10 :             await storeOlmSession(
     671           10 :               OlmSession(
     672           20 :                 key: client.userID!,
     673              :                 identityKey: identityKey,
     674           10 :                 sessionId: session.session_id(),
     675              :                 session: session,
     676              :                 lastReceived:
     677           10 :                     DateTime.now(), // we want to use a newly created session
     678              :               ),
     679              :             );
     680              :           } catch (e, s) {
     681            0 :             session.free();
     682            0 :             Logs()
     683            0 :                 .e('[LibOlm] Could not create new outbound olm session', e, s);
     684              :           }
     685              :         }
     686              :       }
     687              :     }
     688              :   }
     689              : 
     690              :   /// Encryptes a ToDeviceMessage for the given device with an existing
     691              :   /// olm session.
     692              :   /// Throws `NoOlmSessionFoundException` if there is no olm session with this
     693              :   /// device and none could be created.
     694           10 :   Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
     695              :     DeviceKeys device,
     696              :     String type,
     697              :     Map<String, dynamic> payload, {
     698              :     bool getFromDb = true,
     699              :   }) async {
     700              :     final sess =
     701           20 :         await getOlmSessions(device.curve25519Key!, getFromDb: getFromDb);
     702           10 :     if (sess.isEmpty) {
     703            7 :       throw NoOlmSessionFoundException(device);
     704              :     }
     705            7 :     final fullPayload = {
     706              :       'type': type,
     707              :       'content': payload,
     708           14 :       'sender': client.userID,
     709           14 :       'keys': {'ed25519': fingerprintKey},
     710            7 :       'recipient': device.userId,
     711           14 :       'recipient_keys': {'ed25519': device.ed25519Key},
     712              :     };
     713           28 :     final encryptResult = sess.first.session!.encrypt(json.encode(fullPayload));
     714           14 :     await storeOlmSession(sess.first);
     715           14 :     if (encryption.olmDatabase != null) {
     716              :       try {
     717           21 :         await encryption.olmDatabase?.setLastSentMessageUserDeviceKey(
     718           14 :           json.encode({
     719              :             'type': type,
     720              :             'content': payload,
     721              :           }),
     722            7 :           device.userId,
     723            7 :           device.deviceId!,
     724              :         );
     725              :       } catch (e, s) {
     726              :         // we can ignore this error, since it would just make us use a different olm session possibly
     727            0 :         Logs().w('Error while updating olm usage timestamp', e, s);
     728              :       }
     729              :     }
     730            7 :     final encryptedBody = <String, dynamic>{
     731              :       'algorithm': AlgorithmTypes.olmV1Curve25519AesSha2,
     732            7 :       'sender_key': identityKey,
     733            7 :       'ciphertext': <String, dynamic>{},
     734              :     };
     735           28 :     encryptedBody['ciphertext'][device.curve25519Key] = {
     736            7 :       'type': encryptResult.type,
     737            7 :       'body': encryptResult.body,
     738              :     };
     739              :     return encryptedBody;
     740              :   }
     741              : 
     742           10 :   Future<Map<String, Map<String, Map<String, dynamic>>>> encryptToDeviceMessage(
     743              :     List<DeviceKeys> deviceKeys,
     744              :     String type,
     745              :     Map<String, dynamic> payload,
     746              :   ) async {
     747           10 :     final data = <String, Map<String, Map<String, dynamic>>>{};
     748              :     // first check if any of our sessions we want to encrypt for are in the database
     749           20 :     if (encryption.olmDatabase != null) {
     750           10 :       await getOlmSessionsForDevicesFromDatabase(
     751           40 :         deviceKeys.map((d) => d.curve25519Key!).toList(),
     752              :       );
     753              :     }
     754           10 :     final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
     755           10 :     deviceKeysWithoutSession.removeWhere(
     756           10 :       (DeviceKeys deviceKeys) =>
     757           34 :           olmSessions[deviceKeys.curve25519Key]?.isNotEmpty ?? false,
     758              :     );
     759           10 :     if (deviceKeysWithoutSession.isNotEmpty) {
     760           10 :       await startOutgoingOlmSessions(deviceKeysWithoutSession);
     761              :     }
     762           20 :     for (final device in deviceKeys) {
     763           30 :       final userData = data[device.userId] ??= {};
     764              :       try {
     765           27 :         userData[device.deviceId!] = await encryptToDeviceMessagePayload(
     766              :           device,
     767              :           type,
     768              :           payload,
     769              :           getFromDb: false,
     770              :         );
     771            7 :       } on NoOlmSessionFoundException catch (e) {
     772           14 :         Logs().d('[LibOlm] Error encrypting to-device event', e);
     773              :         continue;
     774              :       } catch (e, s) {
     775            0 :         Logs().wtf('[LibOlm] Error encrypting to-device event', e, s);
     776              :         continue;
     777              :       }
     778              :     }
     779              :     return data;
     780              :   }
     781              : 
     782            1 :   Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
     783            2 :     if (event.type == EventTypes.Dummy) {
     784              :       // We received an encrypted m.dummy. This means that the other end was not able to
     785              :       // decrypt our last message. So, we re-send it.
     786            1 :       final encryptedContent = event.encryptedContent;
     787            2 :       if (encryptedContent == null || encryption.olmDatabase == null) {
     788              :         return;
     789              :       }
     790            2 :       final device = client.getUserDeviceKeysByCurve25519Key(
     791            1 :         encryptedContent.tryGet<String>('sender_key') ?? '',
     792              :       );
     793              :       if (device == null) {
     794              :         return; // device not found
     795              :       }
     796            2 :       Logs().v(
     797            3 :         '[OlmManager] Device ${device.userId}:${device.deviceId} generated a new olm session, replaying last sent message...',
     798              :       );
     799            2 :       final lastSentMessageRes = await encryption.olmDatabase
     800            3 :           ?.getLastSentMessageUserDeviceKey(device.userId, device.deviceId!);
     801              :       if (lastSentMessageRes == null ||
     802            1 :           lastSentMessageRes.isEmpty ||
     803            2 :           lastSentMessageRes.first.isEmpty) {
     804              :         return;
     805              :       }
     806            2 :       final lastSentMessage = json.decode(lastSentMessageRes.first);
     807              :       // We do *not* want to re-play m.dummy events, as they hold no value except of saying
     808              :       // what olm session is the most recent one. In fact, if we *do* replay them, then
     809              :       // we can easily land in an infinite ping-pong trap!
     810            2 :       if (lastSentMessage['type'] != EventTypes.Dummy) {
     811              :         // okay, time to send the message!
     812            2 :         await client.sendToDeviceEncrypted(
     813            1 :           [device],
     814            1 :           lastSentMessage['type'],
     815            1 :           lastSentMessage['content'],
     816              :         );
     817              :       }
     818              :     }
     819              :   }
     820              : 
     821           21 :   Future<void> dispose() async {
     822           26 :     await currentUpload?.cancel();
     823           62 :     for (final sessions in olmSessions.values) {
     824           40 :       for (final sess in sessions) {
     825           20 :         sess.dispose();
     826              :       }
     827              :     }
     828           42 :     _olmAccount?.free();
     829           21 :     _olmAccount = null;
     830              :   }
     831              : }
     832              : 
     833              : class NoOlmSessionFoundException implements Exception {
     834              :   final DeviceKeys device;
     835              : 
     836            7 :   NoOlmSessionFoundException(this.device);
     837              : 
     838            7 :   @override
     839              :   String toString() =>
     840           35 :       'No olm session found for ${device.userId}:${device.deviceId}';
     841              : }
        

Generated by: LCOV version 2.0-1