LCOV - code coverage report
Current view: top level - lib/encryption - key_manager.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 86.8 % 605 525
Test Date: 2025-02-25 10:35:24 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:async';
      20              : import 'dart:convert';
      21              : 
      22              : import 'package:collection/collection.dart';
      23              : import 'package:olm/olm.dart' as olm;
      24              : 
      25              : import 'package:matrix/encryption/encryption.dart';
      26              : import 'package:matrix/encryption/utils/base64_unpadded.dart';
      27              : import 'package:matrix/encryption/utils/outbound_group_session.dart';
      28              : import 'package:matrix/encryption/utils/session_key.dart';
      29              : import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
      30              : import 'package:matrix/matrix.dart';
      31              : import 'package:matrix/src/utils/run_in_root.dart';
      32              : 
      33              : const megolmKey = EventTypes.MegolmBackup;
      34              : 
      35              : class KeyManager {
      36              :   final Encryption encryption;
      37              : 
      38           72 :   Client get client => encryption.client;
      39              :   final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
      40              :   final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
      41              :   final _inboundGroupSessions = <String, Map<String, SessionKey>>{};
      42              :   final _outboundGroupSessions = <String, OutboundGroupSession>{};
      43              :   final Set<String> _loadedOutboundGroupSessions = <String>{};
      44              :   final Set<String> _requestedSessionIds = <String>{};
      45              : 
      46           24 :   KeyManager(this.encryption) {
      47           73 :     encryption.ssss.setValidator(megolmKey, (String secret) async {
      48            1 :       final keyObj = olm.PkDecryption();
      49              :       try {
      50            1 :         final info = await getRoomKeysBackupInfo(false);
      51            2 :         if (info.algorithm !=
      52              :             BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2) {
      53              :           return false;
      54              :         }
      55            3 :         return keyObj.init_with_private_key(base64decodeUnpadded(secret)) ==
      56            2 :             info.authData['public_key'];
      57              :       } catch (_) {
      58              :         return false;
      59              :       } finally {
      60            1 :         keyObj.free();
      61              :       }
      62              :     });
      63           73 :     encryption.ssss.setCacheCallback(megolmKey, (String secret) {
      64              :       // we got a megolm key cached, clear our requested keys and try to re-decrypt
      65              :       // last events
      66            2 :       _requestedSessionIds.clear();
      67            3 :       for (final room in client.rooms) {
      68            1 :         final lastEvent = room.lastEvent;
      69              :         if (lastEvent != null &&
      70            2 :             lastEvent.type == EventTypes.Encrypted &&
      71            0 :             lastEvent.content['can_request_session'] == true) {
      72            0 :           final sessionId = lastEvent.content.tryGet<String>('session_id');
      73            0 :           final senderKey = lastEvent.content.tryGet<String>('sender_key');
      74              :           if (sessionId != null && senderKey != null) {
      75            0 :             maybeAutoRequest(
      76            0 :               room.id,
      77              :               sessionId,
      78              :               senderKey,
      79              :             );
      80              :           }
      81              :         }
      82              :       }
      83              :     });
      84              :   }
      85              : 
      86           92 :   bool get enabled => encryption.ssss.isSecret(megolmKey);
      87              : 
      88              :   /// clear all cached inbound group sessions. useful for testing
      89            4 :   void clearInboundGroupSessions() {
      90            8 :     _inboundGroupSessions.clear();
      91              :   }
      92              : 
      93           23 :   Future<void> setInboundGroupSession(
      94              :     String roomId,
      95              :     String sessionId,
      96              :     String senderKey,
      97              :     Map<String, dynamic> content, {
      98              :     bool forwarded = false,
      99              :     Map<String, String>? senderClaimedKeys,
     100              :     bool uploaded = false,
     101              :     Map<String, Map<String, int>>? allowedAtIndex,
     102              :   }) async {
     103           23 :     final senderClaimedKeys_ = senderClaimedKeys ?? <String, String>{};
     104           23 :     final allowedAtIndex_ = allowedAtIndex ?? <String, Map<String, int>>{};
     105           46 :     final userId = client.userID;
     106            0 :     if (userId == null) return Future.value();
     107              : 
     108           23 :     if (!senderClaimedKeys_.containsKey('ed25519')) {
     109           46 :       final device = client.getUserDeviceKeysByCurve25519Key(senderKey);
     110            6 :       if (device != null && device.ed25519Key != null) {
     111           12 :         senderClaimedKeys_['ed25519'] = device.ed25519Key!;
     112              :       }
     113              :     }
     114           23 :     final oldSession = getInboundGroupSession(
     115              :       roomId,
     116              :       sessionId,
     117              :     );
     118           46 :     if (content['algorithm'] != AlgorithmTypes.megolmV1AesSha2) {
     119              :       return;
     120              :     }
     121              :     late olm.InboundGroupSession inboundGroupSession;
     122              :     try {
     123           23 :       inboundGroupSession = olm.InboundGroupSession();
     124              :       if (forwarded) {
     125            6 :         inboundGroupSession.import_session(content['session_key']);
     126              :       } else {
     127           46 :         inboundGroupSession.create(content['session_key']);
     128              :       }
     129              :     } catch (e, s) {
     130            0 :       inboundGroupSession.free();
     131            0 :       Logs().e('[LibOlm] Could not create new InboundGroupSession', e, s);
     132            0 :       return Future.value();
     133              :     }
     134           23 :     final newSession = SessionKey(
     135              :       content: content,
     136              :       inboundGroupSession: inboundGroupSession,
     137           23 :       indexes: {},
     138              :       roomId: roomId,
     139              :       sessionId: sessionId,
     140              :       key: userId,
     141              :       senderKey: senderKey,
     142              :       senderClaimedKeys: senderClaimedKeys_,
     143              :       allowedAtIndex: allowedAtIndex_,
     144              :     );
     145              :     final oldFirstIndex =
     146            2 :         oldSession?.inboundGroupSession?.first_known_index() ?? 0;
     147           46 :     final newFirstIndex = newSession.inboundGroupSession!.first_known_index();
     148              :     if (oldSession == null ||
     149            1 :         newFirstIndex < oldFirstIndex ||
     150            1 :         (oldFirstIndex == newFirstIndex &&
     151            3 :             newSession.forwardingCurve25519KeyChain.length <
     152            2 :                 oldSession.forwardingCurve25519KeyChain.length)) {
     153              :       // use new session
     154            1 :       oldSession?.dispose();
     155              :     } else {
     156              :       // we are gonna keep our old session
     157            1 :       newSession.dispose();
     158              :       return;
     159              :     }
     160              : 
     161              :     final roomInboundGroupSessions =
     162           69 :         _inboundGroupSessions[roomId] ??= <String, SessionKey>{};
     163           23 :     roomInboundGroupSessions[sessionId] = newSession;
     164           92 :     if (!client.isLogged() || client.encryption == null) {
     165              :       return;
     166              :     }
     167              : 
     168           46 :     final storeFuture = client.database
     169           23 :         ?.storeInboundGroupSession(
     170              :       roomId,
     171              :       sessionId,
     172           23 :       inboundGroupSession.pickle(userId),
     173           23 :       json.encode(content),
     174           46 :       json.encode({}),
     175           23 :       json.encode(allowedAtIndex_),
     176              :       senderKey,
     177           23 :       json.encode(senderClaimedKeys_),
     178              :     )
     179           46 :         .then((_) async {
     180           92 :       if (!client.isLogged() || client.encryption == null) {
     181              :         return;
     182              :       }
     183              :       if (uploaded) {
     184            2 :         await client.database
     185            1 :             ?.markInboundGroupSessionAsUploaded(roomId, sessionId);
     186              :       }
     187              :     });
     188           46 :     final room = client.getRoomById(roomId);
     189              :     if (room != null) {
     190              :       // attempt to decrypt the last event
     191            7 :       final event = room.lastEvent;
     192              :       if (event != null &&
     193           14 :           event.type == EventTypes.Encrypted &&
     194            6 :           event.content['session_id'] == sessionId) {
     195            4 :         final decrypted = encryption.decryptRoomEventSync(event);
     196            4 :         if (decrypted.type != EventTypes.Encrypted) {
     197              :           // Update the last event in memory first
     198            2 :           room.lastEvent = decrypted;
     199              : 
     200              :           // To persist it in database and trigger UI updates:
     201            8 :           await client.database?.transaction(() async {
     202            4 :             await client.handleSync(
     203            2 :               SyncUpdate(
     204              :                 nextBatch: '',
     205            2 :                 rooms: switch (room.membership) {
     206            2 :                   Membership.join =>
     207            4 :                     RoomsUpdate(join: {room.id: JoinedRoomUpdate()}),
     208            1 :                   Membership.ban ||
     209            1 :                   Membership.leave =>
     210            4 :                     RoomsUpdate(leave: {room.id: LeftRoomUpdate()}),
     211            0 :                   Membership.invite =>
     212            0 :                     RoomsUpdate(invite: {room.id: InvitedRoomUpdate()}),
     213            0 :                   Membership.knock =>
     214            0 :                     RoomsUpdate(knock: {room.id: KnockRoomUpdate()}),
     215              :                 },
     216              :               ),
     217              :             );
     218              :           });
     219              :         }
     220              :       }
     221              :       // and finally broadcast the new session
     222           14 :       room.onSessionKeyReceived.add(sessionId);
     223              :     }
     224              : 
     225            0 :     return storeFuture ?? Future.value();
     226              :   }
     227              : 
     228           23 :   SessionKey? getInboundGroupSession(String roomId, String sessionId) {
     229           51 :     final sess = _inboundGroupSessions[roomId]?[sessionId];
     230              :     if (sess != null) {
     231           10 :       if (sess.sessionId != sessionId && sess.sessionId.isNotEmpty) {
     232              :         return null;
     233              :       }
     234              :       return sess;
     235              :     }
     236              :     return null;
     237              :   }
     238              : 
     239              :   /// Attempt auto-request for a key
     240            3 :   void maybeAutoRequest(
     241              :     String roomId,
     242              :     String sessionId,
     243              :     String? senderKey, {
     244              :     bool tryOnlineBackup = true,
     245              :     bool onlineKeyBackupOnly = true,
     246              :   }) {
     247            6 :     final room = client.getRoomById(roomId);
     248            3 :     final requestIdent = '$roomId|$sessionId';
     249              :     if (room != null &&
     250            4 :         !_requestedSessionIds.contains(requestIdent) &&
     251            4 :         !client.isUnknownSession) {
     252              :       // do e2ee recovery
     253            0 :       _requestedSessionIds.add(requestIdent);
     254              : 
     255            0 :       runInRoot(
     256            0 :         () async => request(
     257              :           room,
     258              :           sessionId,
     259              :           senderKey,
     260              :           tryOnlineBackup: tryOnlineBackup,
     261              :           onlineKeyBackupOnly: onlineKeyBackupOnly,
     262              :         ),
     263              :       );
     264              :     }
     265              :   }
     266              : 
     267              :   /// Loads an inbound group session
     268            8 :   Future<SessionKey?> loadInboundGroupSession(
     269              :     String roomId,
     270              :     String sessionId,
     271              :   ) async {
     272           21 :     final sess = _inboundGroupSessions[roomId]?[sessionId];
     273              :     if (sess != null) {
     274           10 :       if (sess.sessionId != sessionId && sess.sessionId.isNotEmpty) {
     275              :         return null; // session_id does not match....better not do anything
     276              :       }
     277              :       return sess; // nothing to do
     278              :     }
     279              :     final session =
     280           15 :         await client.database?.getInboundGroupSession(roomId, sessionId);
     281              :     if (session == null) return null;
     282            4 :     final userID = client.userID;
     283              :     if (userID == null) return null;
     284            2 :     final dbSess = SessionKey.fromDb(session, userID);
     285              :     final roomInboundGroupSessions =
     286            6 :         _inboundGroupSessions[roomId] ??= <String, SessionKey>{};
     287            2 :     if (!dbSess.isValid ||
     288            4 :         dbSess.sessionId.isEmpty ||
     289            4 :         dbSess.sessionId != sessionId) {
     290              :       return null;
     291              :     }
     292            2 :     return roomInboundGroupSessions[sessionId] = dbSess;
     293              :   }
     294              : 
     295            1 :   void _sendEncryptionInfoEvent({
     296              :     required String roomId,
     297              :     required List<String> userIds,
     298              :     List<String>? deviceIds,
     299              :   }) async {
     300            4 :     await client.database?.transaction(() async {
     301            2 :       await client.handleSync(
     302            1 :         SyncUpdate(
     303              :           nextBatch: '',
     304            1 :           rooms: RoomsUpdate(
     305            1 :             join: {
     306            1 :               roomId: JoinedRoomUpdate(
     307            1 :                 timeline: TimelineUpdate(
     308            1 :                   events: [
     309            1 :                     MatrixEvent(
     310              :                       eventId:
     311            3 :                           'fake_event_${client.generateUniqueTransactionId()}',
     312            1 :                       content: {
     313            1 :                         'body':
     314            4 :                             '${userIds.join(', ')} can now read along${deviceIds != null ? ' on ${deviceIds.length} new device(s)' : ''}',
     315            1 :                         if (deviceIds != null) 'devices': deviceIds,
     316            1 :                         'users': userIds,
     317              :                       },
     318              :                       type: EventTypes.encryptionInfo,
     319            2 :                       senderId: client.userID!,
     320            1 :                       originServerTs: DateTime.now(),
     321              :                     ),
     322              :                   ],
     323              :                 ),
     324              :               ),
     325              :             },
     326              :           ),
     327              :         ),
     328              :       );
     329              :     });
     330              :   }
     331              : 
     332            5 :   Map<String, Map<String, bool>> _getDeviceKeyIdMap(
     333              :     List<DeviceKeys> deviceKeys,
     334              :   ) {
     335            5 :     final deviceKeyIds = <String, Map<String, bool>>{};
     336            8 :     for (final device in deviceKeys) {
     337            3 :       final deviceId = device.deviceId;
     338              :       if (deviceId == null) {
     339            0 :         Logs().w('[KeyManager] ignoring device without deviceid');
     340              :         continue;
     341              :       }
     342            9 :       final userDeviceKeyIds = deviceKeyIds[device.userId] ??= <String, bool>{};
     343            6 :       userDeviceKeyIds[deviceId] = !device.encryptToDevice;
     344              :     }
     345              :     return deviceKeyIds;
     346              :   }
     347              : 
     348              :   /// clear all cached inbound group sessions. useful for testing
     349            3 :   void clearOutboundGroupSessions() {
     350            6 :     _outboundGroupSessions.clear();
     351              :   }
     352              : 
     353              :   /// Clears the existing outboundGroupSession but first checks if the participating
     354              :   /// devices have been changed. Returns false if the session has not been cleared because
     355              :   /// it wasn't necessary. Otherwise returns true.
     356            5 :   Future<bool> clearOrUseOutboundGroupSession(
     357              :     String roomId, {
     358              :     bool wipe = false,
     359              :     bool use = true,
     360              :   }) async {
     361           10 :     final room = client.getRoomById(roomId);
     362            5 :     final sess = getOutboundGroupSession(roomId);
     363            4 :     if (room == null || sess == null || sess.outboundGroupSession == null) {
     364              :       return true;
     365              :     }
     366              : 
     367            4 :     final inboundSess = await loadInboundGroupSession(
     368            4 :       room.id,
     369            8 :       sess.outboundGroupSession!.session_id(),
     370              :     );
     371              :     if (inboundSess == null) {
     372            0 :       Logs().w('No inbound megolm session found for outbound session!');
     373            0 :       assert(inboundSess != null);
     374              :       wipe = true;
     375              :     }
     376              : 
     377              :     // next check if the devices in the room changed
     378            4 :     final devicesToReceive = <DeviceKeys>[];
     379            4 :     final newDeviceKeys = await room.getUserDeviceKeys();
     380            4 :     final newDeviceKeyIds = _getDeviceKeyIdMap(newDeviceKeys);
     381              :     // first check for user differences
     382           12 :     final oldUserIds = sess.devices.keys.toSet();
     383            8 :     final newUserIds = newDeviceKeyIds.keys.toSet();
     384            8 :     if (oldUserIds.difference(newUserIds).isNotEmpty) {
     385              :       // a user left the room, we must wipe the session
     386              :       wipe = true;
     387              :     } else {
     388            4 :       final newUsers = newUserIds.difference(oldUserIds);
     389            4 :       if (newUsers.isNotEmpty) {
     390              :         // new user! Gotta send the megolm session to them
     391              :         devicesToReceive
     392            5 :             .addAll(newDeviceKeys.where((d) => newUsers.contains(d.userId)));
     393            2 :         _sendEncryptionInfoEvent(roomId: roomId, userIds: newUsers.toList());
     394              :       }
     395              :       // okay, now we must test all the individual user devices, if anything new got blocked
     396              :       // or if we need to send to any new devices.
     397              :       // for this it is enough if we iterate over the old user Ids, as the new ones already have the needed keys in the list.
     398              :       // we also know that all the old user IDs appear in the old one, else we have already wiped the session
     399            6 :       for (final userId in oldUserIds) {
     400            4 :         final oldBlockedDevices = sess.devices.containsKey(userId)
     401            6 :             ? sess.devices[userId]!.entries
     402            6 :                 .where((e) => e.value)
     403            4 :                 .map((e) => e.key)
     404            2 :                 .toSet()
     405              :             : <String>{};
     406            2 :         final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
     407            2 :             ? newDeviceKeyIds[userId]!
     408            2 :                 .entries
     409            6 :                 .where((e) => e.value)
     410            4 :                 .map((e) => e.key)
     411            2 :                 .toSet()
     412              :             : <String>{};
     413              :         // we don't really care about old devices that got dropped (deleted), we only care if new ones got added and if new ones got blocked
     414              :         // check if new devices got blocked
     415            4 :         if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) {
     416              :           wipe = true;
     417              :         }
     418              :         // and now add all the new devices!
     419            4 :         final oldDeviceIds = sess.devices.containsKey(userId)
     420            6 :             ? sess.devices[userId]!.entries
     421            6 :                 .where((e) => !e.value)
     422            6 :                 .map((e) => e.key)
     423            2 :                 .toSet()
     424              :             : <String>{};
     425            2 :         final newDeviceIds = newDeviceKeyIds.containsKey(userId)
     426            2 :             ? newDeviceKeyIds[userId]!
     427            2 :                 .entries
     428            6 :                 .where((e) => !e.value)
     429            6 :                 .map((e) => e.key)
     430            2 :                 .toSet()
     431              :             : <String>{};
     432              : 
     433              :         // check if a device got removed
     434            4 :         if (oldDeviceIds.difference(newDeviceIds).isNotEmpty) {
     435              :           wipe = true;
     436              :         }
     437              : 
     438              :         // check if any new devices need keys
     439            2 :         final newDevices = newDeviceIds.difference(oldDeviceIds);
     440            2 :         if (newDeviceIds.isNotEmpty) {
     441            2 :           devicesToReceive.addAll(
     442            2 :             newDeviceKeys.where(
     443           10 :               (d) => d.userId == userId && newDevices.contains(d.deviceId),
     444              :             ),
     445              :           );
     446            7 :           if (userId != client.userID && newDevices.isNotEmpty) {
     447            1 :             _sendEncryptionInfoEvent(
     448              :               roomId: roomId,
     449            1 :               userIds: [userId],
     450            1 :               deviceIds: newDevices.toList(),
     451              :             );
     452              :           }
     453              :         }
     454              :       }
     455              : 
     456              :       if (!wipe) {
     457              :         // first check if it needs to be rotated
     458              :         final encryptionContent =
     459            6 :             room.getState(EventTypes.Encryption)?.parsedRoomEncryptionContent;
     460            3 :         final maxMessages = encryptionContent?.rotationPeriodMsgs ?? 100;
     461            3 :         final maxAge = encryptionContent?.rotationPeriodMs ??
     462              :             604800000; // default of one week
     463            6 :         if ((sess.sentMessages ?? maxMessages) >= maxMessages ||
     464            3 :             sess.creationTime
     465            6 :                 .add(Duration(milliseconds: maxAge))
     466            6 :                 .isBefore(DateTime.now())) {
     467              :           wipe = true;
     468              :         }
     469              :       }
     470              : 
     471              :       if (!wipe) {
     472              :         if (!use) {
     473              :           return false;
     474              :         }
     475              :         // okay, we use the outbound group session!
     476            3 :         sess.devices = newDeviceKeyIds;
     477            3 :         final rawSession = <String, dynamic>{
     478              :           'algorithm': AlgorithmTypes.megolmV1AesSha2,
     479            3 :           'room_id': room.id,
     480            6 :           'session_id': sess.outboundGroupSession!.session_id(),
     481            6 :           'session_key': sess.outboundGroupSession!.session_key(),
     482              :         };
     483              :         try {
     484            5 :           devicesToReceive.removeWhere((k) => !k.encryptToDevice);
     485            3 :           if (devicesToReceive.isNotEmpty) {
     486              :             // update allowedAtIndex
     487            2 :             for (final device in devicesToReceive) {
     488            4 :               inboundSess!.allowedAtIndex[device.userId] ??= <String, int>{};
     489            3 :               if (!inboundSess.allowedAtIndex[device.userId]!
     490            2 :                       .containsKey(device.curve25519Key) ||
     491            0 :                   inboundSess.allowedAtIndex[device.userId]![
     492            0 :                           device.curve25519Key]! >
     493            0 :                       sess.outboundGroupSession!.message_index()) {
     494              :                 inboundSess
     495            5 :                         .allowedAtIndex[device.userId]![device.curve25519Key!] =
     496            2 :                     sess.outboundGroupSession!.message_index();
     497              :               }
     498              :             }
     499            3 :             await client.database?.updateInboundGroupSessionAllowedAtIndex(
     500            2 :               json.encode(inboundSess!.allowedAtIndex),
     501            1 :               room.id,
     502            2 :               sess.outboundGroupSession!.session_id(),
     503              :             );
     504              :             // send out the key
     505            2 :             await client.sendToDeviceEncryptedChunked(
     506              :               devicesToReceive,
     507              :               EventTypes.RoomKey,
     508              :               rawSession,
     509              :             );
     510              :           }
     511              :         } catch (e, s) {
     512            0 :           Logs().e(
     513              :             '[LibOlm] Unable to re-send the session key at later index to new devices',
     514              :             e,
     515              :             s,
     516              :           );
     517              :         }
     518              :         return false;
     519              :       }
     520              :     }
     521            2 :     sess.dispose();
     522            4 :     _outboundGroupSessions.remove(roomId);
     523            6 :     await client.database?.removeOutboundGroupSession(roomId);
     524              :     return true;
     525              :   }
     526              : 
     527              :   /// Store an outbound group session in the database
     528            5 :   Future<void> storeOutboundGroupSession(
     529              :     String roomId,
     530              :     OutboundGroupSession sess,
     531              :   ) async {
     532           10 :     final userID = client.userID;
     533              :     if (userID == null) return;
     534           15 :     await client.database?.storeOutboundGroupSession(
     535              :       roomId,
     536           10 :       sess.outboundGroupSession!.pickle(userID),
     537           10 :       json.encode(sess.devices),
     538           10 :       sess.creationTime.millisecondsSinceEpoch,
     539              :     );
     540              :   }
     541              : 
     542              :   final Map<String, Future<OutboundGroupSession>>
     543              :       _pendingNewOutboundGroupSessions = {};
     544              : 
     545              :   /// Creates an outbound group session for a given room id
     546            5 :   Future<OutboundGroupSession> createOutboundGroupSession(String roomId) async {
     547           10 :     final sess = _pendingNewOutboundGroupSessions[roomId];
     548              :     if (sess != null) {
     549              :       return sess;
     550              :     }
     551           10 :     final newSess = _pendingNewOutboundGroupSessions[roomId] =
     552            5 :         _createOutboundGroupSession(roomId);
     553              : 
     554              :     try {
     555              :       await newSess;
     556              :     } finally {
     557            5 :       _pendingNewOutboundGroupSessions
     558           15 :           .removeWhere((_, value) => value == newSess);
     559              :     }
     560              : 
     561              :     return newSess;
     562              :   }
     563              : 
     564              :   /// Prepares an outbound group session for a given room ID. That is, load it from
     565              :   /// the database, cycle it if needed and create it if absent.
     566            1 :   Future<void> prepareOutboundGroupSession(String roomId) async {
     567            1 :     if (getOutboundGroupSession(roomId) == null) {
     568            0 :       await loadOutboundGroupSession(roomId);
     569              :     }
     570            1 :     await clearOrUseOutboundGroupSession(roomId, use: false);
     571            1 :     if (getOutboundGroupSession(roomId) == null) {
     572            1 :       await createOutboundGroupSession(roomId);
     573              :     }
     574              :   }
     575              : 
     576            5 :   Future<OutboundGroupSession> _createOutboundGroupSession(
     577              :     String roomId,
     578              :   ) async {
     579            5 :     await clearOrUseOutboundGroupSession(roomId, wipe: true);
     580           10 :     await client.firstSyncReceived;
     581           10 :     final room = client.getRoomById(roomId);
     582              :     if (room == null) {
     583            0 :       throw Exception(
     584            0 :         'Tried to create a megolm session in a non-existing room ($roomId)!',
     585              :       );
     586              :     }
     587           10 :     final userID = client.userID;
     588              :     if (userID == null) {
     589            0 :       throw Exception(
     590              :         'Tried to create a megolm session without being logged in!',
     591              :       );
     592              :     }
     593              : 
     594            5 :     final deviceKeys = await room.getUserDeviceKeys();
     595            5 :     final deviceKeyIds = _getDeviceKeyIdMap(deviceKeys);
     596           11 :     deviceKeys.removeWhere((k) => !k.encryptToDevice);
     597            5 :     final outboundGroupSession = olm.OutboundGroupSession();
     598              :     try {
     599            5 :       outboundGroupSession.create();
     600              :     } catch (e, s) {
     601            0 :       outboundGroupSession.free();
     602            0 :       Logs().e('[LibOlm] Unable to create new outboundGroupSession', e, s);
     603              :       rethrow;
     604              :     }
     605            5 :     final rawSession = <String, dynamic>{
     606              :       'algorithm': AlgorithmTypes.megolmV1AesSha2,
     607            5 :       'room_id': room.id,
     608            5 :       'session_id': outboundGroupSession.session_id(),
     609            5 :       'session_key': outboundGroupSession.session_key(),
     610              :     };
     611            5 :     final allowedAtIndex = <String, Map<String, int>>{};
     612            8 :     for (final device in deviceKeys) {
     613            3 :       if (!device.isValid) {
     614            0 :         Logs().e('Skipping invalid device');
     615              :         continue;
     616              :       }
     617            9 :       allowedAtIndex[device.userId] ??= <String, int>{};
     618           12 :       allowedAtIndex[device.userId]![device.curve25519Key!] =
     619            3 :           outboundGroupSession.message_index();
     620              :     }
     621            5 :     await setInboundGroupSession(
     622              :       roomId,
     623            5 :       rawSession['session_id'],
     624           10 :       encryption.identityKey!,
     625              :       rawSession,
     626              :       allowedAtIndex: allowedAtIndex,
     627              :     );
     628            5 :     final sess = OutboundGroupSession(
     629              :       devices: deviceKeyIds,
     630            5 :       creationTime: DateTime.now(),
     631              :       outboundGroupSession: outboundGroupSession,
     632              :       key: userID,
     633              :     );
     634              :     try {
     635           10 :       await client.sendToDeviceEncryptedChunked(
     636              :         deviceKeys,
     637              :         EventTypes.RoomKey,
     638              :         rawSession,
     639              :       );
     640            5 :       await storeOutboundGroupSession(roomId, sess);
     641           10 :       _outboundGroupSessions[roomId] = sess;
     642              :     } catch (e, s) {
     643            0 :       Logs().e(
     644              :         '[LibOlm] Unable to send the session key to the participating devices',
     645              :         e,
     646              :         s,
     647              :       );
     648            0 :       sess.dispose();
     649              :       rethrow;
     650              :     }
     651              :     return sess;
     652              :   }
     653              : 
     654              :   /// Get an outbound group session for a room id
     655            5 :   OutboundGroupSession? getOutboundGroupSession(String roomId) {
     656           10 :     return _outboundGroupSessions[roomId];
     657              :   }
     658              : 
     659              :   /// Load an outbound group session from database
     660            3 :   Future<void> loadOutboundGroupSession(String roomId) async {
     661            6 :     final database = client.database;
     662            6 :     final userID = client.userID;
     663            6 :     if (_loadedOutboundGroupSessions.contains(roomId) ||
     664            6 :         _outboundGroupSessions.containsKey(roomId) ||
     665              :         database == null ||
     666              :         userID == null) {
     667              :       return; // nothing to do
     668              :     }
     669            6 :     _loadedOutboundGroupSessions.add(roomId);
     670            3 :     final sess = await database.getOutboundGroupSession(
     671              :       roomId,
     672              :       userID,
     673              :     );
     674            1 :     if (sess == null || !sess.isValid) {
     675              :       return;
     676              :     }
     677            2 :     _outboundGroupSessions[roomId] = sess;
     678              :   }
     679              : 
     680           23 :   Future<bool> isCached() async {
     681           46 :     await client.accountDataLoading;
     682           23 :     if (!enabled) {
     683              :       return false;
     684              :     }
     685           46 :     await client.userDeviceKeysLoading;
     686           69 :     return (await encryption.ssss.getCached(megolmKey)) != null;
     687              :   }
     688              : 
     689              :   GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
     690              :   DateTime? _roomKeysVersionCacheDate;
     691              : 
     692            5 :   Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo([
     693              :     bool useCache = true,
     694              :   ]) async {
     695            5 :     if (_roomKeysVersionCache != null &&
     696            3 :         _roomKeysVersionCacheDate != null &&
     697              :         useCache &&
     698            1 :         DateTime.now()
     699            2 :             .subtract(Duration(minutes: 5))
     700            2 :             .isBefore(_roomKeysVersionCacheDate!)) {
     701            1 :       return _roomKeysVersionCache!;
     702              :     }
     703           15 :     _roomKeysVersionCache = await client.getRoomKeysVersionCurrent();
     704           10 :     _roomKeysVersionCacheDate = DateTime.now();
     705            5 :     return _roomKeysVersionCache!;
     706              :   }
     707              : 
     708            1 :   Future<void> loadFromResponse(RoomKeys keys) async {
     709            1 :     if (!(await isCached())) {
     710              :       return;
     711              :     }
     712              :     final privateKey =
     713            4 :         base64decodeUnpadded((await encryption.ssss.getCached(megolmKey))!);
     714            1 :     final decryption = olm.PkDecryption();
     715            1 :     final info = await getRoomKeysBackupInfo();
     716              :     String backupPubKey;
     717              :     try {
     718            1 :       backupPubKey = decryption.init_with_private_key(privateKey);
     719              : 
     720            2 :       if (info.algorithm != BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 ||
     721            3 :           info.authData['public_key'] != backupPubKey) {
     722              :         return;
     723              :       }
     724            3 :       for (final roomEntry in keys.rooms.entries) {
     725            1 :         final roomId = roomEntry.key;
     726            4 :         for (final sessionEntry in roomEntry.value.sessions.entries) {
     727            1 :           final sessionId = sessionEntry.key;
     728            1 :           final session = sessionEntry.value;
     729            1 :           final sessionData = session.sessionData;
     730              :           Map<String, Object?>? decrypted;
     731              :           try {
     732            1 :             decrypted = json.decode(
     733            1 :               decryption.decrypt(
     734            1 :                 sessionData['ephemeral'] as String,
     735            1 :                 sessionData['mac'] as String,
     736            1 :                 sessionData['ciphertext'] as String,
     737              :               ),
     738              :             );
     739              :           } catch (e, s) {
     740            0 :             Logs().e('[LibOlm] Error decrypting room key', e, s);
     741              :           }
     742            1 :           final senderKey = decrypted?.tryGet<String>('sender_key');
     743              :           if (decrypted != null && senderKey != null) {
     744            1 :             decrypted['session_id'] = sessionId;
     745            1 :             decrypted['room_id'] = roomId;
     746            1 :             await setInboundGroupSession(
     747              :               roomId,
     748              :               sessionId,
     749              :               senderKey,
     750              :               decrypted,
     751              :               forwarded: true,
     752              :               senderClaimedKeys:
     753            1 :                   decrypted.tryGetMap<String, String>('sender_claimed_keys') ??
     754            0 :                       <String, String>{},
     755              :               uploaded: true,
     756              :             );
     757              :           }
     758              :         }
     759              :       }
     760              :     } finally {
     761            1 :       decryption.free();
     762              :     }
     763              :   }
     764              : 
     765              :   /// Loads and stores all keys from the online key backup. This may take a
     766              :   /// while for older and big accounts.
     767            1 :   Future<void> loadAllKeys() async {
     768            1 :     final info = await getRoomKeysBackupInfo();
     769            3 :     final ret = await client.getRoomKeys(info.version);
     770            1 :     await loadFromResponse(ret);
     771              :   }
     772              : 
     773              :   /// Loads all room keys for a single room and stores them. This may take a
     774              :   /// while for older and big rooms.
     775            1 :   Future<void> loadAllKeysFromRoom(String roomId) async {
     776            1 :     final info = await getRoomKeysBackupInfo();
     777            3 :     final ret = await client.getRoomKeysByRoomId(roomId, info.version);
     778            2 :     final keys = RoomKeys.fromJson({
     779            1 :       'rooms': {
     780            1 :         roomId: {
     781            5 :           'sessions': ret.sessions.map((k, s) => MapEntry(k, s.toJson())),
     782              :         },
     783              :       },
     784              :     });
     785            1 :     await loadFromResponse(keys);
     786              :   }
     787              : 
     788              :   /// Loads a single key for the specified room from the online key backup
     789              :   /// and stores it.
     790            1 :   Future<void> loadSingleKey(String roomId, String sessionId) async {
     791            1 :     final info = await getRoomKeysBackupInfo();
     792              :     final ret =
     793            3 :         await client.getRoomKeyBySessionId(roomId, sessionId, info.version);
     794            2 :     final keys = RoomKeys.fromJson({
     795            1 :       'rooms': {
     796            1 :         roomId: {
     797            1 :           'sessions': {
     798            1 :             sessionId: ret.toJson(),
     799              :           },
     800              :         },
     801              :       },
     802              :     });
     803            1 :     await loadFromResponse(keys);
     804              :   }
     805              : 
     806              :   /// Request a certain key from another device
     807            3 :   Future<void> request(
     808              :     Room room,
     809              :     String sessionId,
     810              :     String? senderKey, {
     811              :     bool tryOnlineBackup = true,
     812              :     bool onlineKeyBackupOnly = false,
     813              :   }) async {
     814            2 :     if (tryOnlineBackup && await isCached()) {
     815              :       // let's first check our online key backup store thingy...
     816            2 :       final hadPreviously = getInboundGroupSession(room.id, sessionId) != null;
     817              :       try {
     818            2 :         await loadSingleKey(room.id, sessionId);
     819              :       } catch (err, stacktrace) {
     820            0 :         if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
     821            0 :           Logs().i(
     822              :             '[KeyManager] Key not in online key backup, requesting it from other devices...',
     823              :           );
     824              :         } else {
     825            0 :           Logs().e(
     826              :             '[KeyManager] Failed to access online key backup',
     827              :             err,
     828              :             stacktrace,
     829              :           );
     830              :         }
     831              :       }
     832              :       // TODO: also don't request from others if we have an index of 0 now
     833              :       if (!hadPreviously &&
     834            2 :           getInboundGroupSession(room.id, sessionId) != null) {
     835              :         return; // we managed to load the session from online backup, no need to care about it now
     836              :       }
     837              :     }
     838              :     if (onlineKeyBackupOnly) {
     839              :       return; // we only want to do the online key backup
     840              :     }
     841              :     try {
     842              :       // while we just send the to-device event to '*', we still need to save the
     843              :       // devices themself to know where to send the cancel to after receiving a reply
     844            2 :       final devices = await room.getUserDeviceKeys();
     845            4 :       final requestId = client.generateUniqueTransactionId();
     846            2 :       final request = KeyManagerKeyShareRequest(
     847              :         requestId: requestId,
     848              :         devices: devices,
     849              :         room: room,
     850              :         sessionId: sessionId,
     851              :       );
     852            2 :       final userList = await room.requestParticipants();
     853            4 :       await client.sendToDevicesOfUserIds(
     854            6 :         userList.map<String>((u) => u.id).toSet(),
     855              :         EventTypes.RoomKeyRequest,
     856            2 :         {
     857              :           'action': 'request',
     858            2 :           'body': {
     859            2 :             'algorithm': AlgorithmTypes.megolmV1AesSha2,
     860            4 :             'room_id': room.id,
     861            2 :             'session_id': sessionId,
     862            2 :             if (senderKey != null) 'sender_key': senderKey,
     863              :           },
     864              :           'request_id': requestId,
     865            4 :           'requesting_device_id': client.deviceID,
     866              :         },
     867              :       );
     868            6 :       outgoingShareRequests[request.requestId] = request;
     869              :     } catch (e, s) {
     870            0 :       Logs().e('[Key Manager] Sending key verification request failed', e, s);
     871              :     }
     872              :   }
     873              : 
     874              :   Future<void>? _uploadingFuture;
     875              : 
     876           24 :   void startAutoUploadKeys() {
     877          144 :     _uploadKeysOnSync = encryption.client.onSync.stream.listen(
     878           48 :       (_) async => uploadInboundGroupSessions(skipIfInProgress: true),
     879              :     );
     880              :   }
     881              : 
     882              :   /// This task should be performed after sync processing but should not block
     883              :   /// the sync. To make sure that it never gets executed multiple times, it is
     884              :   /// skipped when an upload task is already in progress. Set `skipIfInProgress`
     885              :   /// to `false` to await the pending upload task instead.
     886           24 :   Future<void> uploadInboundGroupSessions({
     887              :     bool skipIfInProgress = false,
     888              :   }) async {
     889           48 :     final database = client.database;
     890           48 :     final userID = client.userID;
     891              :     if (database == null || userID == null) {
     892              :       return;
     893              :     }
     894              : 
     895              :     // Make sure to not run in parallel
     896           23 :     if (_uploadingFuture != null) {
     897              :       if (skipIfInProgress) return;
     898              :       try {
     899            0 :         await _uploadingFuture;
     900              :       } finally {
     901              :         // shouldn't be necessary, since it will be unset already by the other process that started it, but just to be safe, also unset the future here
     902            0 :         _uploadingFuture = null;
     903              :       }
     904              :     }
     905              : 
     906           23 :     Future<void> uploadInternal() async {
     907              :       try {
     908           46 :         await client.userDeviceKeysLoading;
     909              : 
     910           23 :         if (!(await isCached())) {
     911              :           return; // we can't backup anyways
     912              :         }
     913            5 :         final dbSessions = await database.getInboundGroupSessionsToUpload();
     914            5 :         if (dbSessions.isEmpty) {
     915              :           return; // nothing to do
     916              :         }
     917              :         final privateKey =
     918           20 :             base64decodeUnpadded((await encryption.ssss.getCached(megolmKey))!);
     919              :         // decryption is needed to calculate the public key and thus see if the claimed information is in fact valid
     920            5 :         final decryption = olm.PkDecryption();
     921            5 :         final info = await getRoomKeysBackupInfo(false);
     922              :         String backupPubKey;
     923              :         try {
     924            5 :           backupPubKey = decryption.init_with_private_key(privateKey);
     925              : 
     926           10 :           if (info.algorithm !=
     927              :                   BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 ||
     928           15 :               info.authData['public_key'] != backupPubKey) {
     929            1 :             decryption.free();
     930              :             return;
     931              :           }
     932            4 :           final args = GenerateUploadKeysArgs(
     933              :             pubkey: backupPubKey,
     934            4 :             dbSessions: <DbInboundGroupSessionBundle>[],
     935              :             userId: userID,
     936              :           );
     937              :           // we need to calculate verified beforehand, as else we pass a closure to an isolate
     938              :           // with 500 keys they do, however, noticably block the UI, which is why we give brief async suspentions in here
     939              :           // so that the event loop can progress
     940              :           var i = 0;
     941            8 :           for (final dbSession in dbSessions) {
     942              :             final device =
     943           12 :                 client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
     944            8 :             args.dbSessions.add(
     945            4 :               DbInboundGroupSessionBundle(
     946              :                 dbSession: dbSession,
     947            4 :                 verified: device?.verified ?? false,
     948              :               ),
     949              :             );
     950            4 :             i++;
     951            4 :             if (i > 10) {
     952            0 :               await Future.delayed(Duration(milliseconds: 1));
     953              :               i = 0;
     954              :             }
     955              :           }
     956              :           final roomKeys =
     957           12 :               await client.nativeImplementations.generateUploadKeys(args);
     958           16 :           Logs().i('[Key Manager] Uploading ${dbSessions.length} room keys...');
     959              :           // upload the payload...
     960           12 :           await client.putRoomKeys(info.version, roomKeys);
     961              :           // and now finally mark all the keys as uploaded
     962              :           // no need to optimze this, as we only run it so seldomly and almost never with many keys at once
     963            8 :           for (final dbSession in dbSessions) {
     964            4 :             await database.markInboundGroupSessionAsUploaded(
     965            4 :               dbSession.roomId,
     966            4 :               dbSession.sessionId,
     967              :             );
     968              :           }
     969              :         } finally {
     970            5 :           decryption.free();
     971              :         }
     972              :       } catch (e, s) {
     973            4 :         Logs().e('[Key Manager] Error uploading room keys', e, s);
     974              :       }
     975              :     }
     976              : 
     977           46 :     _uploadingFuture = uploadInternal();
     978              :     try {
     979           23 :       await _uploadingFuture;
     980              :     } finally {
     981           23 :       _uploadingFuture = null;
     982              :     }
     983              :   }
     984              : 
     985              :   /// Handle an incoming to_device event that is related to key sharing
     986           23 :   Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
     987           46 :     if (event.type == EventTypes.RoomKeyRequest) {
     988            3 :       if (event.content['request_id'] is! String) {
     989              :         return; // invalid event
     990              :       }
     991            3 :       if (event.content['action'] == 'request') {
     992              :         // we are *receiving* a request
     993            2 :         Logs().i(
     994            4 :           '[KeyManager] Received key sharing request from ${event.sender}:${event.content['requesting_device_id']}...',
     995              :         );
     996            2 :         if (!event.content.containsKey('body')) {
     997            2 :           Logs().w('[KeyManager] No body, doing nothing');
     998              :           return; // no body
     999              :         }
    1000            2 :         final body = event.content.tryGetMap<String, Object?>('body');
    1001              :         if (body == null) {
    1002            0 :           Logs().w('[KeyManager] Wrong type for body, doing nothing');
    1003              :           return; // wrong type for body
    1004              :         }
    1005            1 :         final roomId = body.tryGet<String>('room_id');
    1006              :         if (roomId == null) {
    1007            0 :           Logs().w(
    1008              :             '[KeyManager] Wrong type for room_id or no room_id, doing nothing',
    1009              :           );
    1010              :           return; // wrong type for roomId or no roomId found
    1011              :         }
    1012            4 :         final device = client.userDeviceKeys[event.sender]
    1013            4 :             ?.deviceKeys[event.content['requesting_device_id']];
    1014              :         if (device == null) {
    1015            2 :           Logs().w('[KeyManager] Device not found, doing nothing');
    1016              :           return; // device not found
    1017              :         }
    1018            4 :         if (device.userId == client.userID &&
    1019            4 :             device.deviceId == client.deviceID) {
    1020            0 :           Logs().i('[KeyManager] Request is by ourself, ignoring');
    1021              :           return; // ignore requests by ourself
    1022              :         }
    1023            2 :         final room = client.getRoomById(roomId);
    1024              :         if (room == null) {
    1025            2 :           Logs().i('[KeyManager] Unknown room, ignoring');
    1026              :           return; // unknown room
    1027              :         }
    1028            1 :         final sessionId = body.tryGet<String>('session_id');
    1029              :         if (sessionId == null) {
    1030            0 :           Logs().w(
    1031              :             '[KeyManager] Wrong type for session_id or no session_id, doing nothing',
    1032              :           );
    1033              :           return; // wrong type for session_id
    1034              :         }
    1035              :         // okay, let's see if we have this session at all
    1036            2 :         final session = await loadInboundGroupSession(room.id, sessionId);
    1037              :         if (session == null) {
    1038            2 :           Logs().i('[KeyManager] Unknown session, ignoring');
    1039              :           return; // we don't have this session anyways
    1040              :         }
    1041            3 :         if (event.content['request_id'] is! String) {
    1042            0 :           Logs().w(
    1043              :             '[KeyManager] Wrong type for request_id or no request_id, doing nothing',
    1044              :           );
    1045              :           return; // wrong type for request_id
    1046              :         }
    1047            1 :         final request = KeyManagerKeyShareRequest(
    1048            2 :           requestId: event.content.tryGet<String>('request_id')!,
    1049            1 :           devices: [device],
    1050              :           room: room,
    1051              :           sessionId: sessionId,
    1052              :         );
    1053            3 :         if (incomingShareRequests.containsKey(request.requestId)) {
    1054            0 :           Logs().i('[KeyManager] Already processed this request, ignoring');
    1055              :           return; // we don't want to process one and the same request multiple times
    1056              :         }
    1057            3 :         incomingShareRequests[request.requestId] = request;
    1058              :         final roomKeyRequest =
    1059            1 :             RoomKeyRequest.fromToDeviceEvent(event, this, request);
    1060            4 :         if (device.userId == client.userID &&
    1061            1 :             device.verified &&
    1062            1 :             !device.blocked) {
    1063            2 :           Logs().i('[KeyManager] All checks out, forwarding key...');
    1064              :           // alright, we can forward the key
    1065            1 :           await roomKeyRequest.forwardKey();
    1066            1 :         } else if (device.encryptToDevice &&
    1067            1 :             session.allowedAtIndex
    1068            2 :                     .tryGet<Map<String, Object?>>(device.userId)
    1069            2 :                     ?.tryGet(device.curve25519Key!) !=
    1070              :                 null) {
    1071              :           // if we know the user may see the message, then we can just forward the key.
    1072              :           // we do not need to check if the device is verified, just if it is not blocked,
    1073              :           // as that is the logic we already initially try to send out the room keys.
    1074              :           final index =
    1075            5 :               session.allowedAtIndex[device.userId]![device.curve25519Key]!;
    1076            2 :           Logs().i(
    1077            1 :             '[KeyManager] Valid foreign request, forwarding key at index $index...',
    1078              :           );
    1079            1 :           await roomKeyRequest.forwardKey(index);
    1080              :         } else {
    1081            1 :           Logs()
    1082            1 :               .i('[KeyManager] Asking client, if the key should be forwarded');
    1083            2 :           client.onRoomKeyRequest
    1084            1 :               .add(roomKeyRequest); // let the client handle this
    1085              :         }
    1086            0 :       } else if (event.content['action'] == 'request_cancellation') {
    1087              :         // we got told to cancel an incoming request
    1088            0 :         if (!incomingShareRequests.containsKey(event.content['request_id'])) {
    1089              :           return; // we don't know this request anyways
    1090              :         }
    1091              :         // alright, let's just cancel this request
    1092            0 :         final request = incomingShareRequests[event.content['request_id']]!;
    1093            0 :         request.canceled = true;
    1094            0 :         incomingShareRequests.remove(request.requestId);
    1095              :       }
    1096           46 :     } else if (event.type == EventTypes.ForwardedRoomKey) {
    1097              :       // we *received* an incoming key request
    1098            1 :       final encryptedContent = event.encryptedContent;
    1099              :       if (encryptedContent == null) {
    1100            2 :         Logs().w(
    1101              :           'Ignoring an unencrypted forwarded key from a to device message',
    1102            1 :           event.toJson(),
    1103              :         );
    1104              :         return;
    1105              :       }
    1106            3 :       final request = outgoingShareRequests.values.firstWhereOrNull(
    1107            1 :         (r) =>
    1108            5 :             r.room.id == event.content['room_id'] &&
    1109            4 :             r.sessionId == event.content['session_id'],
    1110              :       );
    1111            1 :       if (request == null || request.canceled) {
    1112              :         return; // no associated request found or it got canceled
    1113              :       }
    1114            2 :       final device = request.devices.firstWhereOrNull(
    1115            1 :         (d) =>
    1116            3 :             d.userId == event.sender &&
    1117            3 :             d.curve25519Key == encryptedContent['sender_key'],
    1118              :       );
    1119              :       if (device == null) {
    1120              :         return; // someone we didn't send our request to replied....better ignore this
    1121              :       }
    1122              :       // we add the sender key to the forwarded key chain
    1123            3 :       if (event.content['forwarding_curve25519_key_chain'] is! List) {
    1124            0 :         event.content['forwarding_curve25519_key_chain'] = <String>[];
    1125              :       }
    1126            2 :       (event.content['forwarding_curve25519_key_chain'] as List)
    1127            2 :           .add(encryptedContent['sender_key']);
    1128            3 :       if (event.content['sender_claimed_ed25519_key'] is! String) {
    1129            0 :         Logs().w('sender_claimed_ed255519_key has wrong type');
    1130              :         return; // wrong type
    1131              :       }
    1132              :       // TODO: verify that the keys work to decrypt a message
    1133              :       // alright, all checks out, let's go ahead and store this session
    1134            1 :       await setInboundGroupSession(
    1135            2 :         request.room.id,
    1136            1 :         request.sessionId,
    1137            1 :         device.curve25519Key!,
    1138            1 :         event.content,
    1139              :         forwarded: true,
    1140            1 :         senderClaimedKeys: {
    1141            2 :           'ed25519': event.content['sender_claimed_ed25519_key'] as String,
    1142              :         },
    1143              :       );
    1144            2 :       request.devices.removeWhere(
    1145            7 :         (k) => k.userId == device.userId && k.deviceId == device.deviceId,
    1146              :       );
    1147            3 :       outgoingShareRequests.remove(request.requestId);
    1148              :       // send cancel to all other devices
    1149            2 :       if (request.devices.isEmpty) {
    1150              :         return; // no need to send any cancellation
    1151              :       }
    1152              :       // Send with send-to-device messaging
    1153            1 :       final sendToDeviceMessage = {
    1154              :         'action': 'request_cancellation',
    1155            1 :         'request_id': request.requestId,
    1156            2 :         'requesting_device_id': client.deviceID,
    1157              :       };
    1158            1 :       final data = <String, Map<String, Map<String, dynamic>>>{};
    1159            2 :       for (final device in request.devices) {
    1160            3 :         final userData = data[device.userId] ??= {};
    1161            2 :         userData[device.deviceId!] = sendToDeviceMessage;
    1162              :       }
    1163            2 :       await client.sendToDevice(
    1164              :         EventTypes.RoomKeyRequest,
    1165            2 :         client.generateUniqueTransactionId(),
    1166              :         data,
    1167              :       );
    1168           46 :     } else if (event.type == EventTypes.RoomKey) {
    1169           46 :       Logs().v(
    1170           69 :         '[KeyManager] Received room key with session ${event.content['session_id']}',
    1171              :       );
    1172           23 :       final encryptedContent = event.encryptedContent;
    1173              :       if (encryptedContent == null) {
    1174            2 :         Logs().v('[KeyManager] not encrypted, ignoring...');
    1175              :         return; // the event wasn't encrypted, this is a security risk;
    1176              :       }
    1177           46 :       final roomId = event.content.tryGet<String>('room_id');
    1178           46 :       final sessionId = event.content.tryGet<String>('session_id');
    1179              :       if (roomId == null || sessionId == null) {
    1180            0 :         Logs().w(
    1181              :           'Either room_id or session_id are not the expected type or missing',
    1182              :         );
    1183              :         return;
    1184              :       }
    1185           92 :       final sender_ed25519 = client.userDeviceKeys[event.sender]
    1186            4 :           ?.deviceKeys[event.content['requesting_device_id']]?.ed25519Key;
    1187              :       if (sender_ed25519 != null) {
    1188            0 :         event.content['sender_claimed_ed25519_key'] = sender_ed25519;
    1189              :       }
    1190           46 :       Logs().v('[KeyManager] Keeping room key');
    1191           23 :       await setInboundGroupSession(
    1192              :         roomId,
    1193              :         sessionId,
    1194           23 :         encryptedContent['sender_key'],
    1195           23 :         event.content,
    1196              :         forwarded: false,
    1197              :       );
    1198              :     }
    1199              :   }
    1200              : 
    1201              :   StreamSubscription<SyncUpdate>? _uploadKeysOnSync;
    1202              : 
    1203           21 :   void dispose() {
    1204              :     // ignore: discarded_futures
    1205           42 :     _uploadKeysOnSync?.cancel();
    1206           46 :     for (final sess in _outboundGroupSessions.values) {
    1207            4 :       sess.dispose();
    1208              :     }
    1209           62 :     for (final entries in _inboundGroupSessions.values) {
    1210           40 :       for (final sess in entries.values) {
    1211           20 :         sess.dispose();
    1212              :       }
    1213              :     }
    1214              :   }
    1215              : }
    1216              : 
    1217              : class KeyManagerKeyShareRequest {
    1218              :   final String requestId;
    1219              :   final List<DeviceKeys> devices;
    1220              :   final Room room;
    1221              :   final String sessionId;
    1222              :   bool canceled;
    1223              : 
    1224            2 :   KeyManagerKeyShareRequest({
    1225              :     required this.requestId,
    1226              :     List<DeviceKeys>? devices,
    1227              :     required this.room,
    1228              :     required this.sessionId,
    1229              :     this.canceled = false,
    1230            0 :   }) : devices = devices ?? [];
    1231              : }
    1232              : 
    1233              : class RoomKeyRequest extends ToDeviceEvent {
    1234              :   KeyManager keyManager;
    1235              :   KeyManagerKeyShareRequest request;
    1236              : 
    1237            1 :   RoomKeyRequest.fromToDeviceEvent(
    1238              :     ToDeviceEvent toDeviceEvent,
    1239              :     this.keyManager,
    1240              :     this.request,
    1241            1 :   ) : super(
    1242            1 :           sender: toDeviceEvent.sender,
    1243            1 :           content: toDeviceEvent.content,
    1244            1 :           type: toDeviceEvent.type,
    1245              :         );
    1246              : 
    1247            3 :   Room get room => request.room;
    1248              : 
    1249            4 :   DeviceKeys get requestingDevice => request.devices.first;
    1250              : 
    1251            1 :   Future<void> forwardKey([int? index]) async {
    1252            2 :     if (request.canceled) {
    1253            0 :       keyManager.incomingShareRequests.remove(request.requestId);
    1254              :       return; // request is canceled, don't send anything
    1255              :     }
    1256            1 :     final room = this.room;
    1257              :     final session =
    1258            5 :         await keyManager.loadInboundGroupSession(room.id, request.sessionId);
    1259            1 :     if (session?.inboundGroupSession == null) {
    1260            0 :       Logs().v("[KeyManager] Not forwarding key we don't have");
    1261              :       return;
    1262              :     }
    1263              : 
    1264            2 :     final message = session!.content.copy();
    1265            1 :     message['forwarding_curve25519_key_chain'] =
    1266            2 :         List<String>.from(session.forwardingCurve25519KeyChain);
    1267              : 
    1268            2 :     if (session.senderKey.isNotEmpty) {
    1269            2 :       message['sender_key'] = session.senderKey;
    1270              :     }
    1271            1 :     message['sender_claimed_ed25519_key'] =
    1272            2 :         session.senderClaimedKeys['ed25519'] ??
    1273            2 :             (session.forwardingCurve25519KeyChain.isEmpty
    1274            3 :                 ? keyManager.encryption.fingerprintKey
    1275              :                 : null);
    1276            3 :     message['session_key'] = session.inboundGroupSession!.export_session(
    1277            2 :       index ?? session.inboundGroupSession!.first_known_index(),
    1278              :     );
    1279              :     // send the actual reply of the key back to the requester
    1280            3 :     await keyManager.client.sendToDeviceEncrypted(
    1281            2 :       [requestingDevice],
    1282              :       EventTypes.ForwardedRoomKey,
    1283              :       message,
    1284              :     );
    1285            5 :     keyManager.incomingShareRequests.remove(request.requestId);
    1286              :   }
    1287              : }
    1288              : 
    1289              : /// you would likely want to use [NativeImplementations] and
    1290              : /// [Client.nativeImplementations] instead
    1291            4 : RoomKeys generateUploadKeysImplementation(GenerateUploadKeysArgs args) {
    1292            4 :   final enc = olm.PkEncryption();
    1293              :   try {
    1294            8 :     enc.set_recipient_key(args.pubkey);
    1295              :     // first we generate the payload to upload all the session keys in this chunk
    1296            8 :     final roomKeys = RoomKeys(rooms: {});
    1297            8 :     for (final dbSession in args.dbSessions) {
    1298           12 :       final sess = SessionKey.fromDb(dbSession.dbSession, args.userId);
    1299            4 :       if (!sess.isValid) {
    1300              :         continue;
    1301              :       }
    1302              :       // create the room if it doesn't exist
    1303              :       final roomKeyBackup =
    1304           20 :           roomKeys.rooms[sess.roomId] ??= RoomKeyBackup(sessions: {});
    1305              :       // generate the encrypted content
    1306            4 :       final payload = <String, dynamic>{
    1307              :         'algorithm': AlgorithmTypes.megolmV1AesSha2,
    1308            4 :         'forwarding_curve25519_key_chain': sess.forwardingCurve25519KeyChain,
    1309            4 :         'sender_key': sess.senderKey,
    1310            4 :         'sender_claimed_keys': sess.senderClaimedKeys,
    1311            4 :         'session_key': sess.inboundGroupSession!
    1312           12 :             .export_session(sess.inboundGroupSession!.first_known_index()),
    1313              :       };
    1314              :       // encrypt the content
    1315            8 :       final encrypted = enc.encrypt(json.encode(payload));
    1316              :       // fetch the device, if available...
    1317              :       //final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey);
    1318              :       // aaaand finally add the session key to our payload
    1319           16 :       roomKeyBackup.sessions[sess.sessionId] = KeyBackupData(
    1320            8 :         firstMessageIndex: sess.inboundGroupSession!.first_known_index(),
    1321            8 :         forwardedCount: sess.forwardingCurve25519KeyChain.length,
    1322            4 :         isVerified: dbSession.verified, //device?.verified ?? false,
    1323            4 :         sessionData: {
    1324            4 :           'ephemeral': encrypted.ephemeral,
    1325            4 :           'ciphertext': encrypted.ciphertext,
    1326            4 :           'mac': encrypted.mac,
    1327              :         },
    1328              :       );
    1329              :     }
    1330            4 :     enc.free();
    1331              :     return roomKeys;
    1332              :   } catch (e, s) {
    1333            0 :     Logs().e('[Key Manager] Error generating payload', e, s);
    1334            0 :     enc.free();
    1335              :     rethrow;
    1336              :   }
    1337              : }
    1338              : 
    1339              : class DbInboundGroupSessionBundle {
    1340            4 :   DbInboundGroupSessionBundle({
    1341              :     required this.dbSession,
    1342              :     required this.verified,
    1343              :   });
    1344              : 
    1345            0 :   factory DbInboundGroupSessionBundle.fromJson(Map<dynamic, dynamic> json) =>
    1346            0 :       DbInboundGroupSessionBundle(
    1347              :         dbSession:
    1348            0 :             StoredInboundGroupSession.fromJson(Map.from(json['dbSession'])),
    1349            0 :         verified: json['verified'],
    1350              :       );
    1351              : 
    1352            0 :   Map<String, Object> toJson() => {
    1353            0 :         'dbSession': dbSession.toJson(),
    1354            0 :         'verified': verified,
    1355              :       };
    1356              :   StoredInboundGroupSession dbSession;
    1357              :   bool verified;
    1358              : }
    1359              : 
    1360              : class GenerateUploadKeysArgs {
    1361            4 :   GenerateUploadKeysArgs({
    1362              :     required this.pubkey,
    1363              :     required this.dbSessions,
    1364              :     required this.userId,
    1365              :   });
    1366              : 
    1367            0 :   factory GenerateUploadKeysArgs.fromJson(Map<dynamic, dynamic> json) =>
    1368            0 :       GenerateUploadKeysArgs(
    1369            0 :         pubkey: json['pubkey'],
    1370            0 :         dbSessions: (json['dbSessions'] as Iterable)
    1371            0 :             .map((e) => DbInboundGroupSessionBundle.fromJson(e))
    1372            0 :             .toList(),
    1373            0 :         userId: json['userId'],
    1374              :       );
    1375              : 
    1376            0 :   Map<String, Object> toJson() => {
    1377            0 :         'pubkey': pubkey,
    1378            0 :         'dbSessions': dbSessions.map((e) => e.toJson()).toList(),
    1379            0 :         'userId': userId,
    1380              :       };
    1381              : 
    1382              :   String pubkey;
    1383              :   List<DbInboundGroupSessionBundle> dbSessions;
    1384              :   String userId;
    1385              : }
        

Generated by: LCOV version 2.0-1