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

Generated by: LCOV version 1.14