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

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

Generated by: LCOV version 2.0-1