LCOV - code coverage report
Current view: top level - lib/encryption - encryption.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 83.7 % 178 149
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:matrix/encryption/cross_signing.dart';
      23              : import 'package:matrix/encryption/key_manager.dart';
      24              : import 'package:matrix/encryption/key_verification_manager.dart';
      25              : import 'package:matrix/encryption/olm_manager.dart';
      26              : import 'package:matrix/encryption/ssss.dart';
      27              : import 'package:matrix/encryption/utils/bootstrap.dart';
      28              : import 'package:matrix/matrix.dart';
      29              : import 'package:matrix/src/utils/copy_map.dart';
      30              : import 'package:matrix/src/utils/run_in_root.dart';
      31              : 
      32              : class Encryption {
      33              :   final Client client;
      34              :   final bool debug;
      35              : 
      36           84 :   bool get enabled => olmManager.enabled;
      37              : 
      38              :   /// Returns the base64 encoded keys to store them in a store.
      39              :   /// This String should **never** leave the device!
      40           84 :   String? get pickledOlmAccount => olmManager.pickledOlmAccount;
      41              : 
      42           84 :   String? get fingerprintKey => olmManager.fingerprintKey;
      43           27 :   String? get identityKey => olmManager.identityKey;
      44              : 
      45              :   /// Returns the database used to store olm sessions and the olm account.
      46              :   /// We don't want to store olm keys for dehydrated devices.
      47           28 :   DatabaseApi? get olmDatabase =>
      48          168 :       ourDeviceId == client.deviceID ? client.database : null;
      49              : 
      50              :   late final KeyManager keyManager;
      51              :   late final OlmManager olmManager;
      52              :   late final KeyVerificationManager keyVerificationManager;
      53              :   late final CrossSigning crossSigning;
      54              :   late SSSS ssss; // some tests mock this, which is why it isn't final
      55              : 
      56              :   late String ourDeviceId;
      57              : 
      58           28 :   Encryption({
      59              :     required this.client,
      60              :     this.debug = false,
      61              :   }) {
      62           56 :     ssss = SSSS(this);
      63           56 :     keyManager = KeyManager(this);
      64           56 :     olmManager = OlmManager(this);
      65           56 :     keyVerificationManager = KeyVerificationManager(this);
      66           56 :     crossSigning = CrossSigning(this);
      67              :   }
      68              : 
      69              :   // initial login passes null to init a new olm account
      70           28 :   Future<void> init(
      71              :     String? olmAccount, {
      72              :     String? deviceId,
      73              :     String? pickleKey,
      74              :     String? dehydratedDeviceAlgorithm,
      75              :   }) async {
      76           84 :     ourDeviceId = deviceId ?? client.deviceID!;
      77              :     final isDehydratedDevice = dehydratedDeviceAlgorithm != null;
      78           56 :     await olmManager.init(
      79              :       olmAccount: olmAccount,
      80           28 :       deviceId: isDehydratedDevice ? deviceId : ourDeviceId,
      81              :       pickleKey: pickleKey,
      82              :       dehydratedDeviceAlgorithm: dehydratedDeviceAlgorithm,
      83              :     );
      84              : 
      85           56 :     if (!isDehydratedDevice) keyManager.startAutoUploadKeys();
      86              :   }
      87              : 
      88            2 :   Bootstrap bootstrap({void Function(Bootstrap)? onUpdate}) => Bootstrap(
      89              :         encryption: this,
      90              :         onUpdate: onUpdate,
      91              :       );
      92              : 
      93           28 :   void handleDeviceOneTimeKeysCount(
      94              :     Map<String, int>? countJson,
      95              :     List<String>? unusedFallbackKeyTypes,
      96              :   ) {
      97           28 :     runInRoot(
      98           84 :       () async => olmManager.handleDeviceOneTimeKeysCount(
      99              :         countJson,
     100              :         unusedFallbackKeyTypes,
     101              :       ),
     102              :     );
     103              :   }
     104              : 
     105           28 :   void onSync() {
     106              :     // ignore: discarded_futures
     107           56 :     keyVerificationManager.cleanup();
     108              :   }
     109              : 
     110           28 :   Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
     111           56 :     if (event.type == EventTypes.RoomKey) {
     112              :       // a new room key. We need to handle this asap, before other
     113              :       // events in /sync are handled
     114           54 :       await keyManager.handleToDeviceEvent(event);
     115              :     }
     116           28 :     if ([EventTypes.RoomKeyRequest, EventTypes.ForwardedRoomKey]
     117           56 :         .contains(event.type)) {
     118              :       // "just" room key request things. We don't need these asap, so we handle
     119              :       // them in the background
     120            0 :       runInRoot(() => keyManager.handleToDeviceEvent(event));
     121              :     }
     122           56 :     if (event.type == EventTypes.Dummy) {
     123              :       // the previous device just had to create a new olm session, due to olm session
     124              :       // corruption. We want to try to send it the last message we just sent it, if possible
     125            0 :       runInRoot(() => olmManager.handleToDeviceEvent(event));
     126              :     }
     127           56 :     if (event.type.startsWith('m.key.verification.')) {
     128              :       // some key verification event. No need to handle it now, we can easily
     129              :       // do this in the background
     130              : 
     131            0 :       runInRoot(() => keyVerificationManager.handleToDeviceEvent(event));
     132              :     }
     133           56 :     if (event.type.startsWith('m.secret.')) {
     134              :       // some ssss thing. We can do this in the background
     135            0 :       runInRoot(() => ssss.handleToDeviceEvent(event));
     136              :     }
     137          112 :     if (event.sender == client.userID) {
     138              :       // maybe we need to re-try SSSS secrets
     139            8 :       runInRoot(() => ssss.periodicallyRequestMissingCache());
     140              :     }
     141              :   }
     142              : 
     143           28 :   Future<void> handleEventUpdate(Event event, EventUpdateType type) async {
     144           28 :     if (type == EventUpdateType.history) {
     145              :       return;
     146              :     }
     147           56 :     if (event.type.startsWith('m.key.verification.') ||
     148           56 :         (event.type == EventTypes.Message &&
     149           28 :             event.content
     150           28 :                     .tryGet<String>('msgtype')
     151           56 :                     ?.startsWith('m.key.verification.') ==
     152              :                 true)) {
     153              :       // "just" key verification, no need to do this in sync
     154            8 :       runInRoot(() => keyVerificationManager.handleEventUpdate(event));
     155              :     }
     156          168 :     if (event.senderId == client.userID && event.status.isSynced) {
     157              :       // maybe we need to re-try SSSS secrets
     158          112 :       runInRoot(() => ssss.periodicallyRequestMissingCache());
     159              :     }
     160              :   }
     161              : 
     162           28 :   Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
     163              :     try {
     164           56 :       return await olmManager.decryptToDeviceEvent(event);
     165              :     } catch (e, s) {
     166           12 :       Logs().w(
     167           18 :         '[Vodozemac] Could not decrypt to device event from ${event.sender} with content: ${event.content}',
     168              :         e,
     169              :         s,
     170              :       );
     171           18 :       client.onEncryptionError.add(
     172            6 :         SdkError(
     173            6 :           exception: e is Exception ? e : Exception(e),
     174              :           stackTrace: s,
     175              :         ),
     176              :       );
     177              :       return event;
     178              :     }
     179              :   }
     180              : 
     181            6 :   Event decryptRoomEventSync(Event event) {
     182           18 :     if (event.type != EventTypes.Encrypted || event.redacted) {
     183              :       return event;
     184              :     }
     185            6 :     final content = event.parsedRoomEncryptedContent;
     186           12 :     if (event.type != EventTypes.Encrypted ||
     187            6 :         content.ciphertextMegolm == null) {
     188              :       return event;
     189              :     }
     190              :     Map<String, dynamic> decryptedPayload;
     191              :     var canRequestSession = false;
     192              :     try {
     193           10 :       if (content.algorithm != AlgorithmTypes.megolmV1AesSha2) {
     194            0 :         throw DecryptException(DecryptException.unknownAlgorithm);
     195              :       }
     196            5 :       final sessionId = content.sessionId;
     197              :       if (sessionId == null) {
     198            0 :         throw DecryptException(DecryptException.unknownSession);
     199              :       }
     200              : 
     201              :       final inboundGroupSession =
     202           20 :           keyManager.getInboundGroupSession(event.room.id, sessionId);
     203            3 :       if (!(inboundGroupSession?.isValid ?? false)) {
     204              :         canRequestSession = true;
     205            3 :         throw DecryptException(DecryptException.unknownSession);
     206              :       }
     207              : 
     208              :       // decrypt errors here may mean we have a bad session key - others might have a better one
     209              :       canRequestSession = true;
     210              : 
     211            3 :       final decryptResult = inboundGroupSession!.inboundGroupSession!
     212            6 :           .decrypt(content.ciphertextMegolm!);
     213              :       canRequestSession = false;
     214              : 
     215              :       // we can't have the key be an int, else json-serializing will fail, thus we need it to be a string
     216            3 :       final messageIndexKey = 'key-${decryptResult.messageIndex}';
     217              :       final messageIndexValue =
     218           12 :           '${event.eventId}|${event.originServerTs.millisecondsSinceEpoch}';
     219              :       final haveIndex =
     220            6 :           inboundGroupSession.indexes.containsKey(messageIndexKey);
     221              :       if (haveIndex &&
     222            3 :           inboundGroupSession.indexes[messageIndexKey] != messageIndexValue) {
     223            0 :         Logs().e('[Decrypt] Could not decrypt due to a corrupted session.');
     224            0 :         throw DecryptException(DecryptException.channelCorrupted);
     225              :       }
     226              : 
     227            6 :       inboundGroupSession.indexes[messageIndexKey] = messageIndexValue;
     228              :       if (!haveIndex) {
     229              :         // now we persist the udpated indexes into the database.
     230              :         // the entry should always exist. In the case it doesn't, the following
     231              :         // line *could* throw an error. As that is a future, though, and we call
     232              :         // it un-awaited here, nothing happens, which is exactly the result we want
     233            6 :         client.database
     234              :             // ignore: discarded_futures
     235            3 :             .updateInboundGroupSessionIndexes(
     236            6 :               json.encode(inboundGroupSession.indexes),
     237            6 :               event.room.id,
     238              :               sessionId,
     239              :             )
     240              :             // ignore: discarded_futures
     241            3 :             .onError((e, _) => Logs().e('Ignoring error for updating indexes'));
     242              :       }
     243            3 :       decryptedPayload = json.decode(decryptResult.plaintext);
     244              :     } catch (exception) {
     245            6 :       Logs().d('Could not decrypt event', exception);
     246              :       // alright, if this was actually by our own outbound group session, we might as well clear it
     247            6 :       if (exception.toString() != DecryptException.unknownSession &&
     248            1 :           (keyManager
     249            3 :                       .getOutboundGroupSession(event.room.id)
     250            0 :                       ?.outboundGroupSession
     251            0 :                       ?.sessionId ??
     252            1 :                   '') ==
     253            1 :               content.sessionId) {
     254            0 :         runInRoot(
     255            0 :           () async => keyManager.clearOrUseOutboundGroupSession(
     256            0 :             event.room.id,
     257              :             wipe: true,
     258              :           ),
     259              :         );
     260              :       }
     261              :       if (canRequestSession) {
     262            3 :         decryptedPayload = {
     263            3 :           'content': event.content,
     264              :           'type': EventTypes.Encrypted,
     265              :         };
     266            9 :         decryptedPayload['content']['body'] = exception.toString();
     267            6 :         decryptedPayload['content']['msgtype'] = MessageTypes.BadEncrypted;
     268            6 :         decryptedPayload['content']['can_request_session'] = true;
     269              :       } else {
     270            0 :         decryptedPayload = {
     271            0 :           'content': <String, dynamic>{
     272              :             'msgtype': MessageTypes.BadEncrypted,
     273            0 :             'body': exception.toString(),
     274              :           },
     275              :           'type': EventTypes.Encrypted,
     276              :         };
     277              :       }
     278              :     }
     279           10 :     if (event.content['m.relates_to'] != null) {
     280            0 :       decryptedPayload['content']['m.relates_to'] =
     281            0 :           event.content['m.relates_to'];
     282              :     }
     283            5 :     return Event(
     284            5 :       content: decryptedPayload['content'],
     285            5 :       type: decryptedPayload['type'],
     286            5 :       senderId: event.senderId,
     287            5 :       eventId: event.eventId,
     288            5 :       room: event.room,
     289            5 :       originServerTs: event.originServerTs,
     290            5 :       unsigned: event.unsigned,
     291            5 :       stateKey: event.stateKey,
     292            5 :       prevContent: event.prevContent,
     293            5 :       status: event.status,
     294              :       originalSource: event,
     295              :     );
     296              :   }
     297              : 
     298            5 :   Future<Event> decryptRoomEvent(
     299              :     Event event, {
     300              :     bool store = false,
     301              :     EventUpdateType updateType = EventUpdateType.timeline,
     302              :   }) async {
     303              :     try {
     304           15 :       if (event.type != EventTypes.Encrypted || event.redacted) {
     305              :         return event;
     306              :       }
     307            5 :       final content = event.parsedRoomEncryptedContent;
     308            5 :       final sessionId = content.sessionId;
     309              :       if (sessionId != null &&
     310            4 :           !(keyManager
     311            4 :                   .getInboundGroupSession(
     312            8 :                     event.room.id,
     313              :                     sessionId,
     314              :                   )
     315            1 :                   ?.isValid ??
     316              :               false)) {
     317            8 :         await keyManager.loadInboundGroupSession(
     318            8 :           event.room.id,
     319              :           sessionId,
     320              :         );
     321              :       }
     322            5 :       event = decryptRoomEventSync(event);
     323           10 :       if (event.type == EventTypes.Encrypted &&
     324           12 :           event.content['can_request_session'] == true &&
     325              :           sessionId != null) {
     326            6 :         keyManager.maybeAutoRequest(
     327            6 :           event.room.id,
     328              :           sessionId,
     329            3 :           content.senderKey,
     330              :         );
     331              :       }
     332           10 :       if (event.type != EventTypes.Encrypted && store) {
     333            1 :         if (updateType != EventUpdateType.history) {
     334            2 :           event.room.setState(event);
     335              :         }
     336            0 :         await client.database.storeEventUpdate(
     337            0 :           event.room.id,
     338              :           event,
     339              :           updateType,
     340            0 :           client,
     341              :         );
     342              :       }
     343              :       return event;
     344              :     } catch (e, s) {
     345            2 :       Logs().e('[Decrypt] Could not decrpyt event', e, s);
     346              :       return event;
     347              :     }
     348              :   }
     349              : 
     350              :   /// Encrypts the given json payload and creates a send-ready m.room.encrypted
     351              :   /// payload. This will create a new outgoingGroupSession if necessary.
     352            3 :   Future<Map<String, dynamic>> encryptGroupMessagePayload(
     353              :     String roomId,
     354              :     Map<String, dynamic> payload, {
     355              :     String type = EventTypes.Message,
     356              :   }) async {
     357            3 :     payload = copyMap(payload);
     358            3 :     final Map<String, dynamic>? mRelatesTo = payload.remove('m.relates_to');
     359              : 
     360              :     // Events which only contain a m.relates_to like reactions don't need to
     361              :     // be encrypted.
     362            3 :     if (payload.isEmpty && mRelatesTo != null) {
     363            0 :       return {'m.relates_to': mRelatesTo};
     364              :     }
     365            6 :     final room = client.getRoomById(roomId);
     366            6 :     if (room == null || !room.encrypted || !enabled) {
     367              :       return payload;
     368              :     }
     369            6 :     if (room.encryptionAlgorithm != AlgorithmTypes.megolmV1AesSha2) {
     370              :       throw ('Unknown encryption algorithm');
     371              :     }
     372           11 :     if (keyManager.getOutboundGroupSession(roomId)?.isValid != true) {
     373            4 :       await keyManager.loadOutboundGroupSession(roomId);
     374              :     }
     375            6 :     await keyManager.clearOrUseOutboundGroupSession(roomId);
     376           11 :     if (keyManager.getOutboundGroupSession(roomId)?.isValid != true) {
     377            4 :       await keyManager.createOutboundGroupSession(roomId);
     378              :     }
     379            6 :     final sess = keyManager.getOutboundGroupSession(roomId);
     380            6 :     if (sess?.isValid != true) {
     381              :       throw ('Unable to create new outbound group session');
     382              :     }
     383              :     // we clone the payload as we do not want to remove 'm.relates_to' from the
     384              :     // original payload passed into this function
     385            3 :     payload = payload.copy();
     386            3 :     final payloadContent = {
     387              :       'content': payload,
     388              :       'type': type,
     389              :       'room_id': roomId,
     390              :     };
     391            3 :     final encryptedPayload = <String, dynamic>{
     392            3 :       'algorithm': AlgorithmTypes.megolmV1AesSha2,
     393            3 :       'ciphertext':
     394            9 :           sess!.outboundGroupSession!.encrypt(json.encode(payloadContent)),
     395              :       // device_id + sender_key should be removed at some point in future since
     396              :       // they're deprecated. Just left here for compatibility
     397            9 :       'device_id': client.deviceID,
     398            6 :       'sender_key': identityKey,
     399            9 :       'session_id': sess.outboundGroupSession!.sessionId,
     400            0 :       if (mRelatesTo != null) 'm.relates_to': mRelatesTo,
     401              :     };
     402            6 :     await keyManager.storeOutboundGroupSession(roomId, sess);
     403              :     return encryptedPayload;
     404              :   }
     405              : 
     406           10 :   Future<Map<String, Map<String, Map<String, dynamic>>>> encryptToDeviceMessage(
     407              :     List<DeviceKeys> deviceKeys,
     408              :     String type,
     409              :     Map<String, dynamic> payload,
     410              :   ) async {
     411           20 :     return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
     412              :   }
     413              : 
     414            0 :   Future<void> autovalidateMasterOwnKey() async {
     415              :     // check if we can set our own master key as verified, if it isn't yet
     416            0 :     final userId = client.userID;
     417            0 :     final masterKey = client.userDeviceKeys[userId]?.masterKey;
     418              :     if (masterKey != null &&
     419              :         userId != null &&
     420            0 :         !masterKey.directVerified &&
     421            0 :         masterKey.hasValidSignatureChain(onlyValidateUserIds: {userId})) {
     422            0 :       await masterKey.setVerified(true);
     423              :     }
     424              :   }
     425              : 
     426           22 :   Future<void> dispose() async {
     427           44 :     keyManager.dispose();
     428           44 :     await olmManager.dispose();
     429           44 :     keyVerificationManager.dispose();
     430              :   }
     431              : }
     432              : 
     433              : class DecryptException implements Exception {
     434              :   String cause;
     435              :   String? libolmMessage;
     436            9 :   DecryptException(this.cause, [this.libolmMessage]);
     437              : 
     438            8 :   @override
     439              :   String toString() =>
     440           26 :       cause + (libolmMessage != null ? ': $libolmMessage' : '');
     441              : 
     442              :   static const String notEnabled = 'Encryption is not enabled in your client.';
     443              :   static const String unknownAlgorithm = 'Unknown encryption algorithm.';
     444              :   static const String unknownSession =
     445              :       'The sender has not sent us the session key.';
     446              :   static const String channelCorrupted =
     447              :       'The secure channel with the sender was corrupted.';
     448              :   static const String unableToDecryptWithAnyOlmSession =
     449              :       'Unable to decrypt with any existing OLM session';
     450              :   static const String senderDoesntMatch =
     451              :       "Message was decrypted but sender doesn't match";
     452              :   static const String recipientDoesntMatch =
     453              :       "Message was decrypted but recipient doesn't match";
     454              :   static const String ownFingerprintDoesntMatch =
     455              :       "Message was decrypted but own fingerprint Key doesn't match";
     456              :   static const String isntSentForThisDevice =
     457              :       "The message isn't sent for this device";
     458              :   static const String unknownMessageType = 'Unknown message type';
     459              :   static const String decryptionFailed = 'Decryption failed';
     460              : }
        

Generated by: LCOV version 2.0-1