LCOV - code coverage report
Current view: top level - lib/encryption - encryption.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 153 184 83.2 %
Date: 2024-11-12 07:37:08 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(String roomId, 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          10 :           keyManager.getInboundGroupSession(roomId, 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             :               roomId,
     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           1 :                       .getOutboundGroupSession(roomId)
     264           0 :                       ?.outboundGroupSession
     265           0 :                       ?.session_id() ??
     266           1 :                   '') ==
     267           1 :               content.sessionId) {
     268           0 :         runInRoot(
     269           0 :           () async =>
     270           0 :               keyManager.clearOrUseOutboundGroupSession(roomId, wipe: true),
     271             :         );
     272             :       }
     273             :       if (canRequestSession) {
     274           3 :         decryptedPayload = {
     275           3 :           'content': event.content,
     276             :           'type': EventTypes.Encrypted,
     277             :         };
     278           9 :         decryptedPayload['content']['body'] = exception.toString();
     279           6 :         decryptedPayload['content']['msgtype'] = MessageTypes.BadEncrypted;
     280           6 :         decryptedPayload['content']['can_request_session'] = true;
     281             :       } else {
     282           0 :         decryptedPayload = {
     283           0 :           'content': <String, dynamic>{
     284             :             'msgtype': MessageTypes.BadEncrypted,
     285           0 :             'body': exception.toString(),
     286             :           },
     287             :           'type': EventTypes.Encrypted,
     288             :         };
     289             :       }
     290             :     }
     291          10 :     if (event.content['m.relates_to'] != null) {
     292           0 :       decryptedPayload['content']['m.relates_to'] =
     293           0 :           event.content['m.relates_to'];
     294             :     }
     295           5 :     return Event(
     296           5 :       content: decryptedPayload['content'],
     297           5 :       type: decryptedPayload['type'],
     298           5 :       senderId: event.senderId,
     299           5 :       eventId: event.eventId,
     300           5 :       room: event.room,
     301           5 :       originServerTs: event.originServerTs,
     302           5 :       unsigned: event.unsigned,
     303           5 :       stateKey: event.stateKey,
     304           5 :       prevContent: event.prevContent,
     305           5 :       status: event.status,
     306             :       originalSource: event,
     307             :     );
     308             :   }
     309             : 
     310           5 :   Future<Event> decryptRoomEvent(
     311             :     String roomId,
     312             :     Event event, {
     313             :     bool store = false,
     314             :     EventUpdateType updateType = EventUpdateType.timeline,
     315             :   }) async {
     316          15 :     if (event.type != EventTypes.Encrypted || event.redacted) {
     317             :       return event;
     318             :     }
     319           5 :     final content = event.parsedRoomEncryptedContent;
     320           5 :     final sessionId = content.sessionId;
     321             :     try {
     322          10 :       if (client.database != null &&
     323             :           sessionId != null &&
     324           4 :           !(keyManager
     325           4 :                   .getInboundGroupSession(
     326             :                     roomId,
     327             :                     sessionId,
     328             :                   )
     329           1 :                   ?.isValid ??
     330             :               false)) {
     331           8 :         await keyManager.loadInboundGroupSession(
     332             :           roomId,
     333             :           sessionId,
     334             :         );
     335             :       }
     336           5 :       event = decryptRoomEventSync(roomId, event);
     337          10 :       if (event.type == EventTypes.Encrypted &&
     338          12 :           event.content['can_request_session'] == true &&
     339             :           sessionId != null) {
     340           6 :         keyManager.maybeAutoRequest(
     341             :           roomId,
     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 :           EventUpdate(
     352           0 :             content: event.toJson(),
     353             :             roomID: roomId,
     354             :             type: updateType,
     355             :           ),
     356           0 :           client,
     357             :         );
     358             :       }
     359             :       return event;
     360             :     } catch (e, s) {
     361           2 :       Logs().e('[Decrypt] Could not decrpyt event', e, s);
     362             :       return event;
     363             :     }
     364             :   }
     365             : 
     366             :   /// Encrypts the given json payload and creates a send-ready m.room.encrypted
     367             :   /// payload. This will create a new outgoingGroupSession if necessary.
     368           3 :   Future<Map<String, dynamic>> encryptGroupMessagePayload(
     369             :     String roomId,
     370             :     Map<String, dynamic> payload, {
     371             :     String type = EventTypes.Message,
     372             :   }) async {
     373           3 :     payload = copyMap(payload);
     374           3 :     final Map<String, dynamic>? mRelatesTo = payload.remove('m.relates_to');
     375             : 
     376             :     // Events which only contain a m.relates_to like reactions don't need to
     377             :     // be encrypted.
     378           3 :     if (payload.isEmpty && mRelatesTo != null) {
     379           0 :       return {'m.relates_to': mRelatesTo};
     380             :     }
     381           6 :     final room = client.getRoomById(roomId);
     382           6 :     if (room == null || !room.encrypted || !enabled) {
     383             :       return payload;
     384             :     }
     385           6 :     if (room.encryptionAlgorithm != AlgorithmTypes.megolmV1AesSha2) {
     386             :       throw ('Unknown encryption algorithm');
     387             :     }
     388          11 :     if (keyManager.getOutboundGroupSession(roomId)?.isValid != true) {
     389           4 :       await keyManager.loadOutboundGroupSession(roomId);
     390             :     }
     391           6 :     await keyManager.clearOrUseOutboundGroupSession(roomId);
     392          11 :     if (keyManager.getOutboundGroupSession(roomId)?.isValid != true) {
     393           4 :       await keyManager.createOutboundGroupSession(roomId);
     394             :     }
     395           6 :     final sess = keyManager.getOutboundGroupSession(roomId);
     396           6 :     if (sess?.isValid != true) {
     397             :       throw ('Unable to create new outbound group session');
     398             :     }
     399             :     // we clone the payload as we do not want to remove 'm.relates_to' from the
     400             :     // original payload passed into this function
     401           3 :     payload = payload.copy();
     402           3 :     final payloadContent = {
     403             :       'content': payload,
     404             :       'type': type,
     405             :       'room_id': roomId,
     406             :     };
     407           3 :     final encryptedPayload = <String, dynamic>{
     408           3 :       'algorithm': AlgorithmTypes.megolmV1AesSha2,
     409           3 :       'ciphertext':
     410           9 :           sess!.outboundGroupSession!.encrypt(json.encode(payloadContent)),
     411             :       // device_id + sender_key should be removed at some point in future since
     412             :       // they're deprecated. Just left here for compatibility
     413           9 :       'device_id': client.deviceID,
     414           6 :       'sender_key': identityKey,
     415           9 :       'session_id': sess.outboundGroupSession!.session_id(),
     416           0 :       if (mRelatesTo != null) 'm.relates_to': mRelatesTo,
     417             :     };
     418           6 :     await keyManager.storeOutboundGroupSession(roomId, sess);
     419             :     return encryptedPayload;
     420             :   }
     421             : 
     422          10 :   Future<Map<String, Map<String, Map<String, dynamic>>>> encryptToDeviceMessage(
     423             :     List<DeviceKeys> deviceKeys,
     424             :     String type,
     425             :     Map<String, dynamic> payload,
     426             :   ) async {
     427          20 :     return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
     428             :   }
     429             : 
     430           0 :   Future<void> autovalidateMasterOwnKey() async {
     431             :     // check if we can set our own master key as verified, if it isn't yet
     432           0 :     final userId = client.userID;
     433           0 :     final masterKey = client.userDeviceKeys[userId]?.masterKey;
     434           0 :     if (client.database != null &&
     435             :         masterKey != null &&
     436             :         userId != null &&
     437           0 :         !masterKey.directVerified &&
     438           0 :         masterKey.hasValidSignatureChain(onlyValidateUserIds: {userId})) {
     439           0 :       await masterKey.setVerified(true);
     440             :     }
     441             :   }
     442             : 
     443          21 :   Future<void> dispose() async {
     444          42 :     keyManager.dispose();
     445          42 :     await olmManager.dispose();
     446          42 :     keyVerificationManager.dispose();
     447             :   }
     448             : }
     449             : 
     450             : class DecryptException implements Exception {
     451             :   String cause;
     452             :   String? libolmMessage;
     453           9 :   DecryptException(this.cause, [this.libolmMessage]);
     454             : 
     455           7 :   @override
     456             :   String toString() =>
     457          23 :       cause + (libolmMessage != null ? ': $libolmMessage' : '');
     458             : 
     459             :   static const String notEnabled = 'Encryption is not enabled in your client.';
     460             :   static const String unknownAlgorithm = 'Unknown encryption algorithm.';
     461             :   static const String unknownSession =
     462             :       'The sender has not sent us the session key.';
     463             :   static const String channelCorrupted =
     464             :       'The secure channel with the sender was corrupted.';
     465             :   static const String unableToDecryptWithAnyOlmSession =
     466             :       'Unable to decrypt with any existing OLM session';
     467             :   static const String senderDoesntMatch =
     468             :       "Message was decrypted but sender doesn't match";
     469             :   static const String recipientDoesntMatch =
     470             :       "Message was decrypted but recipient doesn't match";
     471             :   static const String ownFingerprintDoesntMatch =
     472             :       "Message was decrypted but own fingerprint Key doesn't match";
     473             :   static const String isntSentForThisDevice =
     474             :       "The message isn't sent for this device";
     475             :   static const String unknownMessageType = 'Unknown message type';
     476             :   static const String decryptionFailed = 'Decryption failed';
     477             : }

Generated by: LCOV version 1.14