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 : }
|