LCOV - code coverage report
Current view: top level - lib/encryption - key_manager.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 486 563 86.3 %
Date: 2024-09-28 12:47:43 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(roomId, 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(() async => request(
     256             :             room,
     257             :             sessionId,
     258             :             senderKey,
     259             :             tryOnlineBackup: tryOnlineBackup,
     260             :             onlineKeyBackupOnly: onlineKeyBackupOnly,
     261             :           ));
     262             :     }
     263             :   }
     264             : 
     265             :   /// Loads an inbound group session
     266           8 :   Future<SessionKey?> loadInboundGroupSession(
     267             :       String roomId, String sessionId) async {
     268          21 :     final sess = _inboundGroupSessions[roomId]?[sessionId];
     269             :     if (sess != null) {
     270          10 :       if (sess.sessionId != sessionId && sess.sessionId.isNotEmpty) {
     271             :         return null; // session_id does not match....better not do anything
     272             :       }
     273             :       return sess; // nothing to do
     274             :     }
     275             :     final session =
     276          15 :         await client.database?.getInboundGroupSession(roomId, sessionId);
     277             :     if (session == null) return null;
     278           4 :     final userID = client.userID;
     279             :     if (userID == null) return null;
     280           2 :     final dbSess = SessionKey.fromDb(session, userID);
     281             :     final roomInboundGroupSessions =
     282           6 :         _inboundGroupSessions[roomId] ??= <String, SessionKey>{};
     283           2 :     if (!dbSess.isValid ||
     284           4 :         dbSess.sessionId.isEmpty ||
     285           4 :         dbSess.sessionId != sessionId) {
     286             :       return null;
     287             :     }
     288           2 :     roomInboundGroupSessions[sessionId] = dbSess;
     289             :     return sess;
     290             :   }
     291             : 
     292           5 :   Map<String, Map<String, bool>> _getDeviceKeyIdMap(
     293             :       List<DeviceKeys> deviceKeys) {
     294           5 :     final deviceKeyIds = <String, Map<String, bool>>{};
     295           8 :     for (final device in deviceKeys) {
     296           3 :       final deviceId = device.deviceId;
     297             :       if (deviceId == null) {
     298           0 :         Logs().w('[KeyManager] ignoring device without deviceid');
     299             :         continue;
     300             :       }
     301           9 :       final userDeviceKeyIds = deviceKeyIds[device.userId] ??= <String, bool>{};
     302           6 :       userDeviceKeyIds[deviceId] = !device.encryptToDevice;
     303             :     }
     304             :     return deviceKeyIds;
     305             :   }
     306             : 
     307             :   /// clear all cached inbound group sessions. useful for testing
     308           3 :   void clearOutboundGroupSessions() {
     309           6 :     _outboundGroupSessions.clear();
     310             :   }
     311             : 
     312             :   /// Clears the existing outboundGroupSession but first checks if the participating
     313             :   /// devices have been changed. Returns false if the session has not been cleared because
     314             :   /// it wasn't necessary. Otherwise returns true.
     315           5 :   Future<bool> clearOrUseOutboundGroupSession(String roomId,
     316             :       {bool wipe = false, bool use = true}) async {
     317          10 :     final room = client.getRoomById(roomId);
     318           5 :     final sess = getOutboundGroupSession(roomId);
     319           4 :     if (room == null || sess == null || sess.outboundGroupSession == null) {
     320             :       return true;
     321             :     }
     322             : 
     323             :     if (!wipe) {
     324             :       // first check if it needs to be rotated
     325             :       final encryptionContent =
     326           6 :           room.getState(EventTypes.Encryption)?.parsedRoomEncryptionContent;
     327           3 :       final maxMessages = encryptionContent?.rotationPeriodMsgs ?? 100;
     328           3 :       final maxAge = encryptionContent?.rotationPeriodMs ??
     329             :           604800000; // default of one week
     330           6 :       if ((sess.sentMessages ?? maxMessages) >= maxMessages ||
     331           3 :           sess.creationTime
     332           6 :               .add(Duration(milliseconds: maxAge))
     333           6 :               .isBefore(DateTime.now())) {
     334             :         wipe = true;
     335             :       }
     336             :     }
     337             : 
     338           4 :     final inboundSess = await loadInboundGroupSession(
     339          12 :         room.id, sess.outboundGroupSession!.session_id());
     340             :     if (inboundSess == null) {
     341             :       wipe = true;
     342             :     }
     343             : 
     344             :     if (!wipe) {
     345             :       // next check if the devices in the room changed
     346           3 :       final devicesToReceive = <DeviceKeys>[];
     347           3 :       final newDeviceKeys = await room.getUserDeviceKeys();
     348           3 :       final newDeviceKeyIds = _getDeviceKeyIdMap(newDeviceKeys);
     349             :       // first check for user differences
     350           9 :       final oldUserIds = Set.from(sess.devices.keys);
     351           6 :       final newUserIds = Set.from(newDeviceKeyIds.keys);
     352           6 :       if (oldUserIds.difference(newUserIds).isNotEmpty) {
     353             :         // a user left the room, we must wipe the session
     354             :         wipe = true;
     355             :       } else {
     356           3 :         final newUsers = newUserIds.difference(oldUserIds);
     357           3 :         if (newUsers.isNotEmpty) {
     358             :           // new user! Gotta send the megolm session to them
     359             :           devicesToReceive
     360           5 :               .addAll(newDeviceKeys.where((d) => newUsers.contains(d.userId)));
     361             :         }
     362             :         // okay, now we must test all the individual user devices, if anything new got blocked
     363             :         // or if we need to send to any new devices.
     364             :         // 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.
     365             :         // we also know that all the old user IDs appear in the old one, else we have already wiped the session
     366           5 :         for (final userId in oldUserIds) {
     367           4 :           final oldBlockedDevices = sess.devices.containsKey(userId)
     368           8 :               ? Set.from(sess.devices[userId]!.entries
     369           6 :                   .where((e) => e.value)
     370           2 :                   .map((e) => e.key))
     371             :               : <String>{};
     372           2 :           final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
     373           4 :               ? Set.from(newDeviceKeyIds[userId]!
     374           2 :                   .entries
     375           6 :                   .where((e) => e.value)
     376           4 :                   .map((e) => e.key))
     377             :               : <String>{};
     378             :           // 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
     379             :           // check if new devices got blocked
     380           4 :           if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) {
     381             :             wipe = true;
     382             :             break;
     383             :           }
     384             :           // and now add all the new devices!
     385           4 :           final oldDeviceIds = sess.devices.containsKey(userId)
     386           8 :               ? Set.from(sess.devices[userId]!.entries
     387           6 :                   .where((e) => !e.value)
     388           6 :                   .map((e) => e.key))
     389             :               : <String>{};
     390           2 :           final newDeviceIds = newDeviceKeyIds.containsKey(userId)
     391           4 :               ? Set.from(newDeviceKeyIds[userId]!
     392           2 :                   .entries
     393           6 :                   .where((e) => !e.value)
     394           6 :                   .map((e) => e.key))
     395             :               : <String>{};
     396             : 
     397             :           // check if a device got removed
     398           4 :           if (oldDeviceIds.difference(newDeviceIds).isNotEmpty) {
     399             :             wipe = true;
     400             :             break;
     401             :           }
     402             : 
     403             :           // check if any new devices need keys
     404           2 :           final newDevices = newDeviceIds.difference(oldDeviceIds);
     405           2 :           if (newDeviceIds.isNotEmpty) {
     406           4 :             devicesToReceive.addAll(newDeviceKeys.where(
     407          10 :                 (d) => d.userId == userId && newDevices.contains(d.deviceId)));
     408             :           }
     409             :         }
     410             :       }
     411             : 
     412             :       if (!wipe) {
     413             :         if (!use) {
     414             :           return false;
     415             :         }
     416             :         // okay, we use the outbound group session!
     417           3 :         sess.devices = newDeviceKeyIds;
     418           3 :         final rawSession = <String, dynamic>{
     419             :           'algorithm': AlgorithmTypes.megolmV1AesSha2,
     420           3 :           'room_id': room.id,
     421           6 :           'session_id': sess.outboundGroupSession!.session_id(),
     422           6 :           'session_key': sess.outboundGroupSession!.session_key(),
     423             :         };
     424             :         try {
     425           5 :           devicesToReceive.removeWhere((k) => !k.encryptToDevice);
     426           3 :           if (devicesToReceive.isNotEmpty) {
     427             :             // update allowedAtIndex
     428           2 :             for (final device in devicesToReceive) {
     429           4 :               inboundSess!.allowedAtIndex[device.userId] ??= <String, int>{};
     430           3 :               if (!inboundSess.allowedAtIndex[device.userId]!
     431           2 :                       .containsKey(device.curve25519Key) ||
     432           0 :                   inboundSess.allowedAtIndex[device.userId]![
     433           0 :                           device.curve25519Key]! >
     434           0 :                       sess.outboundGroupSession!.message_index()) {
     435             :                 inboundSess
     436           5 :                         .allowedAtIndex[device.userId]![device.curve25519Key!] =
     437           2 :                     sess.outboundGroupSession!.message_index();
     438             :               }
     439             :             }
     440           3 :             await client.database?.updateInboundGroupSessionAllowedAtIndex(
     441           2 :                 json.encode(inboundSess!.allowedAtIndex),
     442           1 :                 room.id,
     443           2 :                 sess.outboundGroupSession!.session_id());
     444             :             // send out the key
     445           2 :             await client.sendToDeviceEncryptedChunked(
     446             :                 devicesToReceive, EventTypes.RoomKey, rawSession);
     447             :           }
     448             :         } catch (e, s) {
     449           0 :           Logs().e(
     450             :               '[LibOlm] Unable to re-send the session key at later index to new devices',
     451             :               e,
     452             :               s);
     453             :         }
     454             :         return false;
     455             :       }
     456             :     }
     457           2 :     sess.dispose();
     458           4 :     _outboundGroupSessions.remove(roomId);
     459           6 :     await client.database?.removeOutboundGroupSession(roomId);
     460             :     return true;
     461             :   }
     462             : 
     463             :   /// Store an outbound group session in the database
     464           5 :   Future<void> storeOutboundGroupSession(
     465             :       String roomId, OutboundGroupSession sess) async {
     466          10 :     final userID = client.userID;
     467             :     if (userID == null) return;
     468          15 :     await client.database?.storeOutboundGroupSession(
     469             :         roomId,
     470          10 :         sess.outboundGroupSession!.pickle(userID),
     471          10 :         json.encode(sess.devices),
     472          10 :         sess.creationTime.millisecondsSinceEpoch);
     473             :   }
     474             : 
     475             :   final Map<String, Future<OutboundGroupSession>>
     476             :       _pendingNewOutboundGroupSessions = {};
     477             : 
     478             :   /// Creates an outbound group session for a given room id
     479           5 :   Future<OutboundGroupSession> createOutboundGroupSession(String roomId) async {
     480          10 :     final sess = _pendingNewOutboundGroupSessions[roomId];
     481             :     if (sess != null) {
     482             :       return sess;
     483             :     }
     484          10 :     final newSess = _pendingNewOutboundGroupSessions[roomId] =
     485           5 :         _createOutboundGroupSession(roomId);
     486             : 
     487             :     try {
     488             :       await newSess;
     489             :     } finally {
     490           5 :       _pendingNewOutboundGroupSessions
     491          15 :           .removeWhere((_, value) => value == newSess);
     492             :     }
     493             : 
     494             :     return newSess;
     495             :   }
     496             : 
     497             :   /// Prepares an outbound group session for a given room ID. That is, load it from
     498             :   /// the database, cycle it if needed and create it if absent.
     499           1 :   Future<void> prepareOutboundGroupSession(String roomId) async {
     500           1 :     if (getOutboundGroupSession(roomId) == null) {
     501           0 :       await loadOutboundGroupSession(roomId);
     502             :     }
     503           1 :     await clearOrUseOutboundGroupSession(roomId, use: false);
     504           1 :     if (getOutboundGroupSession(roomId) == null) {
     505           1 :       await createOutboundGroupSession(roomId);
     506             :     }
     507             :   }
     508             : 
     509           5 :   Future<OutboundGroupSession> _createOutboundGroupSession(
     510             :       String roomId) async {
     511           5 :     await clearOrUseOutboundGroupSession(roomId, wipe: true);
     512          10 :     await client.firstSyncReceived;
     513          10 :     final room = client.getRoomById(roomId);
     514             :     if (room == null) {
     515           0 :       throw Exception(
     516           0 :           'Tried to create a megolm session in a non-existing room ($roomId)!');
     517             :     }
     518          10 :     final userID = client.userID;
     519             :     if (userID == null) {
     520           0 :       throw Exception(
     521             :           'Tried to create a megolm session without being logged in!');
     522             :     }
     523             : 
     524           5 :     final deviceKeys = await room.getUserDeviceKeys();
     525           5 :     final deviceKeyIds = _getDeviceKeyIdMap(deviceKeys);
     526          11 :     deviceKeys.removeWhere((k) => !k.encryptToDevice);
     527           5 :     final outboundGroupSession = olm.OutboundGroupSession();
     528             :     try {
     529           5 :       outboundGroupSession.create();
     530             :     } catch (e, s) {
     531           0 :       outboundGroupSession.free();
     532           0 :       Logs().e('[LibOlm] Unable to create new outboundGroupSession', e, s);
     533             :       rethrow;
     534             :     }
     535           5 :     final rawSession = <String, dynamic>{
     536             :       'algorithm': AlgorithmTypes.megolmV1AesSha2,
     537           5 :       'room_id': room.id,
     538           5 :       'session_id': outboundGroupSession.session_id(),
     539           5 :       'session_key': outboundGroupSession.session_key(),
     540             :     };
     541           5 :     final allowedAtIndex = <String, Map<String, int>>{};
     542           8 :     for (final device in deviceKeys) {
     543           3 :       if (!device.isValid) {
     544           0 :         Logs().e('Skipping invalid device');
     545             :         continue;
     546             :       }
     547           9 :       allowedAtIndex[device.userId] ??= <String, int>{};
     548          12 :       allowedAtIndex[device.userId]![device.curve25519Key!] =
     549           3 :           outboundGroupSession.message_index();
     550             :     }
     551           5 :     await setInboundGroupSession(
     552          15 :         roomId, rawSession['session_id'], encryption.identityKey!, rawSession,
     553             :         allowedAtIndex: allowedAtIndex);
     554           5 :     final sess = OutboundGroupSession(
     555             :       devices: deviceKeyIds,
     556           5 :       creationTime: DateTime.now(),
     557             :       outboundGroupSession: outboundGroupSession,
     558             :       key: userID,
     559             :     );
     560             :     try {
     561          10 :       await client.sendToDeviceEncryptedChunked(
     562             :           deviceKeys, EventTypes.RoomKey, rawSession);
     563           5 :       await storeOutboundGroupSession(roomId, sess);
     564          10 :       _outboundGroupSessions[roomId] = sess;
     565             :     } catch (e, s) {
     566           0 :       Logs().e(
     567             :           '[LibOlm] Unable to send the session key to the participating devices',
     568             :           e,
     569             :           s);
     570           0 :       sess.dispose();
     571             :       rethrow;
     572             :     }
     573             :     return sess;
     574             :   }
     575             : 
     576             :   /// Get an outbound group session for a room id
     577           5 :   OutboundGroupSession? getOutboundGroupSession(String roomId) {
     578          10 :     return _outboundGroupSessions[roomId];
     579             :   }
     580             : 
     581             :   /// Load an outbound group session from database
     582           3 :   Future<void> loadOutboundGroupSession(String roomId) async {
     583           6 :     final database = client.database;
     584           6 :     final userID = client.userID;
     585           6 :     if (_loadedOutboundGroupSessions.contains(roomId) ||
     586           6 :         _outboundGroupSessions.containsKey(roomId) ||
     587             :         database == null ||
     588             :         userID == null) {
     589             :       return; // nothing to do
     590             :     }
     591           6 :     _loadedOutboundGroupSessions.add(roomId);
     592           3 :     final sess = await database.getOutboundGroupSession(
     593             :       roomId,
     594             :       userID,
     595             :     );
     596           1 :     if (sess == null || !sess.isValid) {
     597             :       return;
     598             :     }
     599           2 :     _outboundGroupSessions[roomId] = sess;
     600             :   }
     601             : 
     602          23 :   Future<bool> isCached() async {
     603          46 :     await client.accountDataLoading;
     604          23 :     if (!enabled) {
     605             :       return false;
     606             :     }
     607          46 :     await client.userDeviceKeysLoading;
     608          69 :     return (await encryption.ssss.getCached(megolmKey)) != null;
     609             :   }
     610             : 
     611             :   GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
     612             :   DateTime? _roomKeysVersionCacheDate;
     613             : 
     614           5 :   Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo(
     615             :       [bool useCache = true]) async {
     616           5 :     if (_roomKeysVersionCache != null &&
     617           3 :         _roomKeysVersionCacheDate != null &&
     618             :         useCache &&
     619           1 :         DateTime.now()
     620           2 :             .subtract(Duration(minutes: 5))
     621           2 :             .isBefore(_roomKeysVersionCacheDate!)) {
     622           1 :       return _roomKeysVersionCache!;
     623             :     }
     624          15 :     _roomKeysVersionCache = await client.getRoomKeysVersionCurrent();
     625          10 :     _roomKeysVersionCacheDate = DateTime.now();
     626           5 :     return _roomKeysVersionCache!;
     627             :   }
     628             : 
     629           1 :   Future<void> loadFromResponse(RoomKeys keys) async {
     630           1 :     if (!(await isCached())) {
     631             :       return;
     632             :     }
     633             :     final privateKey =
     634           4 :         base64decodeUnpadded((await encryption.ssss.getCached(megolmKey))!);
     635           1 :     final decryption = olm.PkDecryption();
     636           1 :     final info = await getRoomKeysBackupInfo();
     637             :     String backupPubKey;
     638             :     try {
     639           1 :       backupPubKey = decryption.init_with_private_key(privateKey);
     640             : 
     641           2 :       if (info.algorithm != BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 ||
     642           3 :           info.authData['public_key'] != backupPubKey) {
     643             :         return;
     644             :       }
     645           3 :       for (final roomEntry in keys.rooms.entries) {
     646           1 :         final roomId = roomEntry.key;
     647           4 :         for (final sessionEntry in roomEntry.value.sessions.entries) {
     648           1 :           final sessionId = sessionEntry.key;
     649           1 :           final session = sessionEntry.value;
     650           1 :           final sessionData = session.sessionData;
     651             :           Map<String, Object?>? decrypted;
     652             :           try {
     653           2 :             decrypted = json.decode(decryption.decrypt(
     654           1 :                 sessionData['ephemeral'] as String,
     655           1 :                 sessionData['mac'] as String,
     656           1 :                 sessionData['ciphertext'] as String));
     657             :           } catch (e, s) {
     658           0 :             Logs().e('[LibOlm] Error decrypting room key', e, s);
     659             :           }
     660           1 :           final senderKey = decrypted?.tryGet<String>('sender_key');
     661             :           if (decrypted != null && senderKey != null) {
     662           1 :             decrypted['session_id'] = sessionId;
     663           1 :             decrypted['room_id'] = roomId;
     664           1 :             await setInboundGroupSession(
     665             :                 roomId, sessionId, senderKey, decrypted,
     666             :                 forwarded: true,
     667             :                 senderClaimedKeys: decrypted
     668           1 :                         .tryGetMap<String, String>('sender_claimed_keys') ??
     669           0 :                     <String, String>{},
     670             :                 uploaded: true);
     671             :           }
     672             :         }
     673             :       }
     674             :     } finally {
     675           1 :       decryption.free();
     676             :     }
     677             :   }
     678             : 
     679             :   /// Loads and stores all keys from the online key backup. This may take a
     680             :   /// while for older and big accounts.
     681           1 :   Future<void> loadAllKeys() async {
     682           1 :     final info = await getRoomKeysBackupInfo();
     683           3 :     final ret = await client.getRoomKeys(info.version);
     684           1 :     await loadFromResponse(ret);
     685             :   }
     686             : 
     687             :   /// Loads all room keys for a single room and stores them. This may take a
     688             :   /// while for older and big rooms.
     689           1 :   Future<void> loadAllKeysFromRoom(String roomId) async {
     690           1 :     final info = await getRoomKeysBackupInfo();
     691           3 :     final ret = await client.getRoomKeysByRoomId(roomId, info.version);
     692           2 :     final keys = RoomKeys.fromJson({
     693           1 :       'rooms': {
     694           1 :         roomId: {
     695           5 :           'sessions': ret.sessions.map((k, s) => MapEntry(k, s.toJson())),
     696             :         },
     697             :       },
     698             :     });
     699           1 :     await loadFromResponse(keys);
     700             :   }
     701             : 
     702             :   /// Loads a single key for the specified room from the online key backup
     703             :   /// and stores it.
     704           1 :   Future<void> loadSingleKey(String roomId, String sessionId) async {
     705           1 :     final info = await getRoomKeysBackupInfo();
     706             :     final ret =
     707           3 :         await client.getRoomKeyBySessionId(roomId, sessionId, info.version);
     708           2 :     final keys = RoomKeys.fromJson({
     709           1 :       'rooms': {
     710           1 :         roomId: {
     711           1 :           'sessions': {
     712           1 :             sessionId: ret.toJson(),
     713             :           },
     714             :         },
     715             :       },
     716             :     });
     717           1 :     await loadFromResponse(keys);
     718             :   }
     719             : 
     720             :   /// Request a certain key from another device
     721           3 :   Future<void> request(
     722             :     Room room,
     723             :     String sessionId,
     724             :     String? senderKey, {
     725             :     bool tryOnlineBackup = true,
     726             :     bool onlineKeyBackupOnly = false,
     727             :   }) async {
     728           2 :     if (tryOnlineBackup && await isCached()) {
     729             :       // let's first check our online key backup store thingy...
     730           2 :       final hadPreviously = getInboundGroupSession(room.id, sessionId) != null;
     731             :       try {
     732           2 :         await loadSingleKey(room.id, sessionId);
     733             :       } catch (err, stacktrace) {
     734           0 :         if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
     735           0 :           Logs().i(
     736             :               '[KeyManager] Key not in online key backup, requesting it from other devices...');
     737             :         } else {
     738           0 :           Logs().e('[KeyManager] Failed to access online key backup', err,
     739             :               stacktrace);
     740             :         }
     741             :       }
     742             :       // TODO: also don't request from others if we have an index of 0 now
     743             :       if (!hadPreviously &&
     744           2 :           getInboundGroupSession(room.id, sessionId) != null) {
     745             :         return; // we managed to load the session from online backup, no need to care about it now
     746             :       }
     747             :     }
     748             :     if (onlineKeyBackupOnly) {
     749             :       return; // we only want to do the online key backup
     750             :     }
     751             :     try {
     752             :       // while we just send the to-device event to '*', we still need to save the
     753             :       // devices themself to know where to send the cancel to after receiving a reply
     754           2 :       final devices = await room.getUserDeviceKeys();
     755           4 :       final requestId = client.generateUniqueTransactionId();
     756           2 :       final request = KeyManagerKeyShareRequest(
     757             :         requestId: requestId,
     758             :         devices: devices,
     759             :         room: room,
     760             :         sessionId: sessionId,
     761             :       );
     762           2 :       final userList = await room.requestParticipants();
     763           4 :       await client.sendToDevicesOfUserIds(
     764           6 :         userList.map<String>((u) => u.id).toSet(),
     765             :         EventTypes.RoomKeyRequest,
     766           2 :         {
     767             :           'action': 'request',
     768           2 :           'body': {
     769           2 :             'algorithm': AlgorithmTypes.megolmV1AesSha2,
     770           4 :             'room_id': room.id,
     771           2 :             'session_id': sessionId,
     772           2 :             if (senderKey != null) 'sender_key': senderKey,
     773             :           },
     774             :           'request_id': requestId,
     775           4 :           'requesting_device_id': client.deviceID,
     776             :         },
     777             :       );
     778           6 :       outgoingShareRequests[request.requestId] = request;
     779             :     } catch (e, s) {
     780           0 :       Logs().e('[Key Manager] Sending key verification request failed', e, s);
     781             :     }
     782             :   }
     783             : 
     784             :   Future<void>? _uploadingFuture;
     785             : 
     786          24 :   void startAutoUploadKeys() {
     787         144 :     _uploadKeysOnSync = encryption.client.onSync.stream.listen(
     788          48 :         (_) async => uploadInboundGroupSessions(skipIfInProgress: true));
     789             :   }
     790             : 
     791             :   /// This task should be performed after sync processing but should not block
     792             :   /// the sync. To make sure that it never gets executed multiple times, it is
     793             :   /// skipped when an upload task is already in progress. Set `skipIfInProgress`
     794             :   /// to `false` to await the pending upload task instead.
     795          24 :   Future<void> uploadInboundGroupSessions(
     796             :       {bool skipIfInProgress = false}) async {
     797          48 :     final database = client.database;
     798          48 :     final userID = client.userID;
     799             :     if (database == null || userID == null) {
     800             :       return;
     801             :     }
     802             : 
     803             :     // Make sure to not run in parallel
     804          23 :     if (_uploadingFuture != null) {
     805             :       if (skipIfInProgress) return;
     806             :       try {
     807           0 :         await _uploadingFuture;
     808             :       } finally {
     809             :         // 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
     810           0 :         _uploadingFuture = null;
     811             :       }
     812             :     }
     813             : 
     814          23 :     Future<void> uploadInternal() async {
     815             :       try {
     816          46 :         await client.userDeviceKeysLoading;
     817             : 
     818          23 :         if (!(await isCached())) {
     819             :           return; // we can't backup anyways
     820             :         }
     821           5 :         final dbSessions = await database.getInboundGroupSessionsToUpload();
     822           5 :         if (dbSessions.isEmpty) {
     823             :           return; // nothing to do
     824             :         }
     825             :         final privateKey =
     826          20 :             base64decodeUnpadded((await encryption.ssss.getCached(megolmKey))!);
     827             :         // decryption is needed to calculate the public key and thus see if the claimed information is in fact valid
     828           5 :         final decryption = olm.PkDecryption();
     829           5 :         final info = await getRoomKeysBackupInfo(false);
     830             :         String backupPubKey;
     831             :         try {
     832           5 :           backupPubKey = decryption.init_with_private_key(privateKey);
     833             : 
     834          10 :           if (info.algorithm !=
     835             :                   BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 ||
     836          15 :               info.authData['public_key'] != backupPubKey) {
     837           1 :             decryption.free();
     838             :             return;
     839             :           }
     840           4 :           final args = GenerateUploadKeysArgs(
     841             :             pubkey: backupPubKey,
     842           4 :             dbSessions: <DbInboundGroupSessionBundle>[],
     843             :             userId: userID,
     844             :           );
     845             :           // we need to calculate verified beforehand, as else we pass a closure to an isolate
     846             :           // with 500 keys they do, however, noticably block the UI, which is why we give brief async suspentions in here
     847             :           // so that the event loop can progress
     848             :           var i = 0;
     849           8 :           for (final dbSession in dbSessions) {
     850             :             final device =
     851          12 :                 client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
     852          12 :             args.dbSessions.add(DbInboundGroupSessionBundle(
     853             :               dbSession: dbSession,
     854           4 :               verified: device?.verified ?? false,
     855             :             ));
     856           4 :             i++;
     857           4 :             if (i > 10) {
     858           0 :               await Future.delayed(Duration(milliseconds: 1));
     859             :               i = 0;
     860             :             }
     861             :           }
     862             :           final roomKeys =
     863          12 :               await client.nativeImplementations.generateUploadKeys(args);
     864          16 :           Logs().i('[Key Manager] Uploading ${dbSessions.length} room keys...');
     865             :           // upload the payload...
     866          12 :           await client.putRoomKeys(info.version, roomKeys);
     867             :           // and now finally mark all the keys as uploaded
     868             :           // no need to optimze this, as we only run it so seldomly and almost never with many keys at once
     869           8 :           for (final dbSession in dbSessions) {
     870           4 :             await database.markInboundGroupSessionAsUploaded(
     871           8 :                 dbSession.roomId, dbSession.sessionId);
     872             :           }
     873             :         } finally {
     874           5 :           decryption.free();
     875             :         }
     876             :       } catch (e, s) {
     877           2 :         Logs().e('[Key Manager] Error uploading room keys', e, s);
     878             :       }
     879             :     }
     880             : 
     881          46 :     _uploadingFuture = uploadInternal();
     882             :     try {
     883          23 :       await _uploadingFuture;
     884             :     } finally {
     885          23 :       _uploadingFuture = null;
     886             :     }
     887             :   }
     888             : 
     889             :   /// Handle an incoming to_device event that is related to key sharing
     890          23 :   Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
     891          46 :     if (event.type == EventTypes.RoomKeyRequest) {
     892           3 :       if (event.content['request_id'] is! String) {
     893             :         return; // invalid event
     894             :       }
     895           3 :       if (event.content['action'] == 'request') {
     896             :         // we are *receiving* a request
     897           2 :         Logs().i(
     898           4 :             '[KeyManager] Received key sharing request from ${event.sender}:${event.content['requesting_device_id']}...');
     899           2 :         if (!event.content.containsKey('body')) {
     900           2 :           Logs().w('[KeyManager] No body, doing nothing');
     901             :           return; // no body
     902             :         }
     903           2 :         final body = event.content.tryGetMap<String, Object?>('body');
     904             :         if (body == null) {
     905           0 :           Logs().w('[KeyManager] Wrong type for body, doing nothing');
     906             :           return; // wrong type for body
     907             :         }
     908           1 :         final roomId = body.tryGet<String>('room_id');
     909             :         if (roomId == null) {
     910           0 :           Logs().w(
     911             :               '[KeyManager] Wrong type for room_id or no room_id, doing nothing');
     912             :           return; // wrong type for roomId or no roomId found
     913             :         }
     914           4 :         final device = client.userDeviceKeys[event.sender]
     915           4 :             ?.deviceKeys[event.content['requesting_device_id']];
     916             :         if (device == null) {
     917           2 :           Logs().w('[KeyManager] Device not found, doing nothing');
     918             :           return; // device not found
     919             :         }
     920           4 :         if (device.userId == client.userID &&
     921           4 :             device.deviceId == client.deviceID) {
     922           0 :           Logs().i('[KeyManager] Request is by ourself, ignoring');
     923             :           return; // ignore requests by ourself
     924             :         }
     925           2 :         final room = client.getRoomById(roomId);
     926             :         if (room == null) {
     927           2 :           Logs().i('[KeyManager] Unknown room, ignoring');
     928             :           return; // unknown room
     929             :         }
     930           1 :         final sessionId = body.tryGet<String>('session_id');
     931             :         if (sessionId == null) {
     932           0 :           Logs().w(
     933             :               '[KeyManager] Wrong type for session_id or no session_id, doing nothing');
     934             :           return; // wrong type for session_id
     935             :         }
     936             :         // okay, let's see if we have this session at all
     937           2 :         final session = await loadInboundGroupSession(room.id, sessionId);
     938             :         if (session == null) {
     939           2 :           Logs().i('[KeyManager] Unknown session, ignoring');
     940             :           return; // we don't have this session anyways
     941             :         }
     942           3 :         if (event.content['request_id'] is! String) {
     943           0 :           Logs().w(
     944             :               '[KeyManager] Wrong type for request_id or no request_id, doing nothing');
     945             :           return; // wrong type for request_id
     946             :         }
     947           1 :         final request = KeyManagerKeyShareRequest(
     948           2 :           requestId: event.content.tryGet<String>('request_id')!,
     949           1 :           devices: [device],
     950             :           room: room,
     951             :           sessionId: sessionId,
     952             :         );
     953           3 :         if (incomingShareRequests.containsKey(request.requestId)) {
     954           0 :           Logs().i('[KeyManager] Already processed this request, ignoring');
     955             :           return; // we don't want to process one and the same request multiple times
     956             :         }
     957           3 :         incomingShareRequests[request.requestId] = request;
     958             :         final roomKeyRequest =
     959           1 :             RoomKeyRequest.fromToDeviceEvent(event, this, request);
     960           4 :         if (device.userId == client.userID &&
     961           1 :             device.verified &&
     962           1 :             !device.blocked) {
     963           2 :           Logs().i('[KeyManager] All checks out, forwarding key...');
     964             :           // alright, we can forward the key
     965           1 :           await roomKeyRequest.forwardKey();
     966           1 :         } else if (device.encryptToDevice &&
     967           1 :             session.allowedAtIndex
     968           2 :                     .tryGet<Map<String, Object?>>(device.userId)
     969           2 :                     ?.tryGet(device.curve25519Key!) !=
     970             :                 null) {
     971             :           // if we know the user may see the message, then we can just forward the key.
     972             :           // we do not need to check if the device is verified, just if it is not blocked,
     973             :           // as that is the logic we already initially try to send out the room keys.
     974             :           final index =
     975           5 :               session.allowedAtIndex[device.userId]![device.curve25519Key]!;
     976           2 :           Logs().i(
     977           1 :               '[KeyManager] Valid foreign request, forwarding key at index $index...');
     978           1 :           await roomKeyRequest.forwardKey(index);
     979             :         } else {
     980           1 :           Logs()
     981           1 :               .i('[KeyManager] Asking client, if the key should be forwarded');
     982           2 :           client.onRoomKeyRequest
     983           1 :               .add(roomKeyRequest); // let the client handle this
     984             :         }
     985           0 :       } else if (event.content['action'] == 'request_cancellation') {
     986             :         // we got told to cancel an incoming request
     987           0 :         if (!incomingShareRequests.containsKey(event.content['request_id'])) {
     988             :           return; // we don't know this request anyways
     989             :         }
     990             :         // alright, let's just cancel this request
     991           0 :         final request = incomingShareRequests[event.content['request_id']]!;
     992           0 :         request.canceled = true;
     993           0 :         incomingShareRequests.remove(request.requestId);
     994             :       }
     995          46 :     } else if (event.type == EventTypes.ForwardedRoomKey) {
     996             :       // we *received* an incoming key request
     997           1 :       final encryptedContent = event.encryptedContent;
     998             :       if (encryptedContent == null) {
     999           2 :         Logs().w(
    1000             :           'Ignoring an unencrypted forwarded key from a to device message',
    1001           1 :           event.toJson(),
    1002             :         );
    1003             :         return;
    1004             :       }
    1005           4 :       final request = outgoingShareRequests.values.firstWhereOrNull((r) =>
    1006           5 :           r.room.id == event.content['room_id'] &&
    1007           4 :           r.sessionId == event.content['session_id']);
    1008           1 :       if (request == null || request.canceled) {
    1009             :         return; // no associated request found or it got canceled
    1010             :       }
    1011           3 :       final device = request.devices.firstWhereOrNull((d) =>
    1012           3 :           d.userId == event.sender &&
    1013           3 :           d.curve25519Key == encryptedContent['sender_key']);
    1014             :       if (device == null) {
    1015             :         return; // someone we didn't send our request to replied....better ignore this
    1016             :       }
    1017             :       // we add the sender key to the forwarded key chain
    1018           3 :       if (event.content['forwarding_curve25519_key_chain'] is! List) {
    1019           0 :         event.content['forwarding_curve25519_key_chain'] = <String>[];
    1020             :       }
    1021           2 :       (event.content['forwarding_curve25519_key_chain'] as List)
    1022           2 :           .add(encryptedContent['sender_key']);
    1023           3 :       if (event.content['sender_claimed_ed25519_key'] is! String) {
    1024           0 :         Logs().w('sender_claimed_ed255519_key has wrong type');
    1025             :         return; // wrong type
    1026             :       }
    1027             :       // TODO: verify that the keys work to decrypt a message
    1028             :       // alright, all checks out, let's go ahead and store this session
    1029           4 :       await setInboundGroupSession(request.room.id, request.sessionId,
    1030           2 :           device.curve25519Key!, event.content,
    1031             :           forwarded: true,
    1032           1 :           senderClaimedKeys: {
    1033           2 :             'ed25519': event.content['sender_claimed_ed25519_key'] as String,
    1034             :           });
    1035           2 :       request.devices.removeWhere(
    1036           7 :           (k) => k.userId == device.userId && k.deviceId == device.deviceId);
    1037           3 :       outgoingShareRequests.remove(request.requestId);
    1038             :       // send cancel to all other devices
    1039           2 :       if (request.devices.isEmpty) {
    1040             :         return; // no need to send any cancellation
    1041             :       }
    1042             :       // Send with send-to-device messaging
    1043           1 :       final sendToDeviceMessage = {
    1044             :         'action': 'request_cancellation',
    1045           1 :         'request_id': request.requestId,
    1046           2 :         'requesting_device_id': client.deviceID,
    1047             :       };
    1048           1 :       final data = <String, Map<String, Map<String, dynamic>>>{};
    1049           2 :       for (final device in request.devices) {
    1050           3 :         final userData = data[device.userId] ??= {};
    1051           2 :         userData[device.deviceId!] = sendToDeviceMessage;
    1052             :       }
    1053           2 :       await client.sendToDevice(
    1054             :         EventTypes.RoomKeyRequest,
    1055           2 :         client.generateUniqueTransactionId(),
    1056             :         data,
    1057             :       );
    1058          46 :     } else if (event.type == EventTypes.RoomKey) {
    1059          46 :       Logs().v(
    1060          69 :           '[KeyManager] Received room key with session ${event.content['session_id']}');
    1061          23 :       final encryptedContent = event.encryptedContent;
    1062             :       if (encryptedContent == null) {
    1063           2 :         Logs().v('[KeyManager] not encrypted, ignoring...');
    1064             :         return; // the event wasn't encrypted, this is a security risk;
    1065             :       }
    1066          46 :       final roomId = event.content.tryGet<String>('room_id');
    1067          46 :       final sessionId = event.content.tryGet<String>('session_id');
    1068             :       if (roomId == null || sessionId == null) {
    1069           0 :         Logs().w(
    1070             :             'Either room_id or session_id are not the expected type or missing');
    1071             :         return;
    1072             :       }
    1073          92 :       final sender_ed25519 = client.userDeviceKeys[event.sender]
    1074           4 :           ?.deviceKeys[event.content['requesting_device_id']]?.ed25519Key;
    1075             :       if (sender_ed25519 != null) {
    1076           0 :         event.content['sender_claimed_ed25519_key'] = sender_ed25519;
    1077             :       }
    1078          46 :       Logs().v('[KeyManager] Keeping room key');
    1079          23 :       await setInboundGroupSession(
    1080          46 :           roomId, sessionId, encryptedContent['sender_key'], event.content,
    1081             :           forwarded: false);
    1082             :     }
    1083             :   }
    1084             : 
    1085             :   StreamSubscription<SyncUpdate>? _uploadKeysOnSync;
    1086             : 
    1087          21 :   void dispose() {
    1088             :     // ignore: discarded_futures
    1089          42 :     _uploadKeysOnSync?.cancel();
    1090          46 :     for (final sess in _outboundGroupSessions.values) {
    1091           4 :       sess.dispose();
    1092             :     }
    1093          62 :     for (final entries in _inboundGroupSessions.values) {
    1094          40 :       for (final sess in entries.values) {
    1095          20 :         sess.dispose();
    1096             :       }
    1097             :     }
    1098             :   }
    1099             : }
    1100             : 
    1101             : class KeyManagerKeyShareRequest {
    1102             :   final String requestId;
    1103             :   final List<DeviceKeys> devices;
    1104             :   final Room room;
    1105             :   final String sessionId;
    1106             :   bool canceled;
    1107             : 
    1108           2 :   KeyManagerKeyShareRequest(
    1109             :       {required this.requestId,
    1110             :       List<DeviceKeys>? devices,
    1111             :       required this.room,
    1112             :       required this.sessionId,
    1113             :       this.canceled = false})
    1114           0 :       : devices = devices ?? [];
    1115             : }
    1116             : 
    1117             : class RoomKeyRequest extends ToDeviceEvent {
    1118             :   KeyManager keyManager;
    1119             :   KeyManagerKeyShareRequest request;
    1120             : 
    1121           1 :   RoomKeyRequest.fromToDeviceEvent(
    1122             :       ToDeviceEvent toDeviceEvent, this.keyManager, this.request)
    1123           1 :       : super(
    1124           1 :             sender: toDeviceEvent.sender,
    1125           1 :             content: toDeviceEvent.content,
    1126           1 :             type: toDeviceEvent.type);
    1127             : 
    1128           3 :   Room get room => request.room;
    1129             : 
    1130           4 :   DeviceKeys get requestingDevice => request.devices.first;
    1131             : 
    1132           1 :   Future<void> forwardKey([int? index]) async {
    1133           2 :     if (request.canceled) {
    1134           0 :       keyManager.incomingShareRequests.remove(request.requestId);
    1135             :       return; // request is canceled, don't send anything
    1136             :     }
    1137           1 :     final room = this.room;
    1138             :     final session =
    1139           5 :         await keyManager.loadInboundGroupSession(room.id, request.sessionId);
    1140           1 :     if (session?.inboundGroupSession == null) {
    1141           0 :       Logs().v("[KeyManager] Not forwarding key we don't have");
    1142             :       return;
    1143             :     }
    1144             : 
    1145           2 :     final message = session!.content.copy();
    1146           1 :     message['forwarding_curve25519_key_chain'] =
    1147           2 :         List<String>.from(session.forwardingCurve25519KeyChain);
    1148             : 
    1149           2 :     if (session.senderKey.isNotEmpty) {
    1150           2 :       message['sender_key'] = session.senderKey;
    1151             :     }
    1152           1 :     message['sender_claimed_ed25519_key'] =
    1153           2 :         session.senderClaimedKeys['ed25519'] ??
    1154           2 :             (session.forwardingCurve25519KeyChain.isEmpty
    1155           3 :                 ? keyManager.encryption.fingerprintKey
    1156             :                 : null);
    1157           3 :     message['session_key'] = session.inboundGroupSession!.export_session(
    1158           2 :         index ?? session.inboundGroupSession!.first_known_index());
    1159             :     // send the actual reply of the key back to the requester
    1160           3 :     await keyManager.client.sendToDeviceEncrypted(
    1161           2 :       [requestingDevice],
    1162             :       EventTypes.ForwardedRoomKey,
    1163             :       message,
    1164             :     );
    1165           5 :     keyManager.incomingShareRequests.remove(request.requestId);
    1166             :   }
    1167             : }
    1168             : 
    1169             : /// you would likely want to use [NativeImplementations] and
    1170             : /// [Client.nativeImplementations] instead
    1171           4 : RoomKeys generateUploadKeysImplementation(GenerateUploadKeysArgs args) {
    1172           4 :   final enc = olm.PkEncryption();
    1173             :   try {
    1174           8 :     enc.set_recipient_key(args.pubkey);
    1175             :     // first we generate the payload to upload all the session keys in this chunk
    1176           8 :     final roomKeys = RoomKeys(rooms: {});
    1177           8 :     for (final dbSession in args.dbSessions) {
    1178          12 :       final sess = SessionKey.fromDb(dbSession.dbSession, args.userId);
    1179           4 :       if (!sess.isValid) {
    1180             :         continue;
    1181             :       }
    1182             :       // create the room if it doesn't exist
    1183             :       final roomKeyBackup =
    1184          20 :           roomKeys.rooms[sess.roomId] ??= RoomKeyBackup(sessions: {});
    1185             :       // generate the encrypted content
    1186           4 :       final payload = <String, dynamic>{
    1187             :         'algorithm': AlgorithmTypes.megolmV1AesSha2,
    1188           4 :         'forwarding_curve25519_key_chain': sess.forwardingCurve25519KeyChain,
    1189           4 :         'sender_key': sess.senderKey,
    1190           4 :         'sender_claimed_keys': sess.senderClaimedKeys,
    1191           4 :         'session_key': sess.inboundGroupSession!
    1192          12 :             .export_session(sess.inboundGroupSession!.first_known_index()),
    1193             :       };
    1194             :       // encrypt the content
    1195           8 :       final encrypted = enc.encrypt(json.encode(payload));
    1196             :       // fetch the device, if available...
    1197             :       //final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey);
    1198             :       // aaaand finally add the session key to our payload
    1199          16 :       roomKeyBackup.sessions[sess.sessionId] = KeyBackupData(
    1200           8 :         firstMessageIndex: sess.inboundGroupSession!.first_known_index(),
    1201           8 :         forwardedCount: sess.forwardingCurve25519KeyChain.length,
    1202           4 :         isVerified: dbSession.verified, //device?.verified ?? false,
    1203           4 :         sessionData: {
    1204           4 :           'ephemeral': encrypted.ephemeral,
    1205           4 :           'ciphertext': encrypted.ciphertext,
    1206           4 :           'mac': encrypted.mac,
    1207             :         },
    1208             :       );
    1209             :     }
    1210           4 :     enc.free();
    1211             :     return roomKeys;
    1212             :   } catch (e, s) {
    1213           0 :     Logs().e('[Key Manager] Error generating payload', e, s);
    1214           0 :     enc.free();
    1215             :     rethrow;
    1216             :   }
    1217             : }
    1218             : 
    1219             : class DbInboundGroupSessionBundle {
    1220           4 :   DbInboundGroupSessionBundle(
    1221             :       {required this.dbSession, required this.verified});
    1222             : 
    1223           0 :   factory DbInboundGroupSessionBundle.fromJson(Map<dynamic, dynamic> json) =>
    1224           0 :       DbInboundGroupSessionBundle(
    1225             :         dbSession:
    1226           0 :             StoredInboundGroupSession.fromJson(Map.from(json['dbSession'])),
    1227           0 :         verified: json['verified'],
    1228             :       );
    1229             : 
    1230           0 :   Map<String, Object> toJson() => {
    1231           0 :         'dbSession': dbSession.toJson(),
    1232           0 :         'verified': verified,
    1233             :       };
    1234             :   StoredInboundGroupSession dbSession;
    1235             :   bool verified;
    1236             : }
    1237             : 
    1238             : class GenerateUploadKeysArgs {
    1239           4 :   GenerateUploadKeysArgs(
    1240             :       {required this.pubkey, required this.dbSessions, required this.userId});
    1241             : 
    1242           0 :   factory GenerateUploadKeysArgs.fromJson(Map<dynamic, dynamic> json) =>
    1243           0 :       GenerateUploadKeysArgs(
    1244           0 :         pubkey: json['pubkey'],
    1245           0 :         dbSessions: (json['dbSessions'] as Iterable)
    1246           0 :             .map((e) => DbInboundGroupSessionBundle.fromJson(e))
    1247           0 :             .toList(),
    1248           0 :         userId: json['userId'],
    1249             :       );
    1250             : 
    1251           0 :   Map<String, Object> toJson() => {
    1252           0 :         'pubkey': pubkey,
    1253           0 :         'dbSessions': dbSessions.map((e) => e.toJson()).toList(),
    1254           0 :         'userId': userId,
    1255             :       };
    1256             : 
    1257             :   String pubkey;
    1258             :   List<DbInboundGroupSessionBundle> dbSessions;
    1259             :   String userId;
    1260             : }

Generated by: LCOV version 1.14