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

Generated by: LCOV version 2.0-1