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

Generated by: LCOV version 1.14