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