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:collection/collection.dart';
23 : import 'package:olm/olm.dart' as olm;
24 :
25 : import 'package:matrix/encryption/encryption.dart';
26 : import 'package:matrix/encryption/utils/base64_unpadded.dart';
27 : import 'package:matrix/encryption/utils/outbound_group_session.dart';
28 : import 'package:matrix/encryption/utils/session_key.dart';
29 : import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
30 : import 'package:matrix/matrix.dart';
31 : import 'package:matrix/src/utils/run_in_root.dart';
32 :
33 : const megolmKey = EventTypes.MegolmBackup;
34 :
35 : class KeyManager {
36 : final Encryption encryption;
37 :
38 75 : Client get client => encryption.client;
39 : final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
40 : final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
41 : final _inboundGroupSessions = <String, Map<String, SessionKey>>{};
42 : final _outboundGroupSessions = <String, OutboundGroupSession>{};
43 : final Set<String> _loadedOutboundGroupSessions = <String>{};
44 : final Set<String> _requestedSessionIds = <String>{};
45 :
46 25 : KeyManager(this.encryption) {
47 76 : encryption.ssss.setValidator(megolmKey, (String secret) async {
48 1 : final keyObj = olm.PkDecryption();
49 : try {
50 1 : final info = await getRoomKeysBackupInfo(false);
51 2 : if (info.algorithm !=
52 : BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2) {
53 : return false;
54 : }
55 3 : return keyObj.init_with_private_key(base64decodeUnpadded(secret)) ==
56 2 : info.authData['public_key'];
57 : } catch (_) {
58 : return false;
59 : } finally {
60 1 : keyObj.free();
61 : }
62 : });
63 76 : encryption.ssss.setCacheCallback(megolmKey, (String secret) {
64 : // we got a megolm key cached, clear our requested keys and try to re-decrypt
65 : // last events
66 2 : _requestedSessionIds.clear();
67 3 : for (final room in client.rooms) {
68 1 : final lastEvent = room.lastEvent;
69 : if (lastEvent != null &&
70 2 : lastEvent.type == EventTypes.Encrypted &&
71 0 : lastEvent.content['can_request_session'] == true) {
72 0 : final sessionId = lastEvent.content.tryGet<String>('session_id');
73 0 : final senderKey = lastEvent.content.tryGet<String>('sender_key');
74 : if (sessionId != null && senderKey != null) {
75 0 : maybeAutoRequest(
76 0 : room.id,
77 : sessionId,
78 : senderKey,
79 : );
80 : }
81 : }
82 : }
83 : });
84 : }
85 :
86 100 : bool get enabled => encryption.ssss.isSecret(megolmKey);
87 :
88 : /// clear all cached inbound group sessions. useful for testing
89 4 : void clearInboundGroupSessions() {
90 8 : _inboundGroupSessions.clear();
91 : }
92 :
93 24 : Future<void> setInboundGroupSession(
94 : String roomId,
95 : String sessionId,
96 : String senderKey,
97 : Map<String, dynamic> content, {
98 : bool forwarded = false,
99 : Map<String, String>? senderClaimedKeys,
100 : bool uploaded = false,
101 : Map<String, Map<String, int>>? allowedAtIndex,
102 : }) async {
103 24 : final senderClaimedKeys_ = senderClaimedKeys ?? <String, String>{};
104 24 : final allowedAtIndex_ = allowedAtIndex ?? <String, Map<String, int>>{};
105 48 : final userId = client.userID;
106 0 : if (userId == null) return Future.value();
107 :
108 24 : if (!senderClaimedKeys_.containsKey('ed25519')) {
109 48 : final device = client.getUserDeviceKeysByCurve25519Key(senderKey);
110 6 : if (device != null && device.ed25519Key != null) {
111 12 : senderClaimedKeys_['ed25519'] = device.ed25519Key!;
112 : }
113 : }
114 24 : final oldSession = getInboundGroupSession(
115 : roomId,
116 : sessionId,
117 : );
118 48 : if (content['algorithm'] != AlgorithmTypes.megolmV1AesSha2) {
119 : return;
120 : }
121 : late olm.InboundGroupSession inboundGroupSession;
122 : try {
123 24 : inboundGroupSession = olm.InboundGroupSession();
124 : if (forwarded) {
125 6 : inboundGroupSession.import_session(content['session_key']);
126 : } else {
127 48 : inboundGroupSession.create(content['session_key']);
128 : }
129 : } catch (e, s) {
130 0 : inboundGroupSession.free();
131 0 : Logs().e('[LibOlm] Could not create new InboundGroupSession', e, s);
132 0 : return Future.value();
133 : }
134 24 : final newSession = SessionKey(
135 : content: content,
136 : inboundGroupSession: inboundGroupSession,
137 24 : indexes: {},
138 : roomId: roomId,
139 : sessionId: sessionId,
140 : key: userId,
141 : senderKey: senderKey,
142 : senderClaimedKeys: senderClaimedKeys_,
143 : allowedAtIndex: allowedAtIndex_,
144 : );
145 : final oldFirstIndex =
146 2 : oldSession?.inboundGroupSession?.first_known_index() ?? 0;
147 48 : final newFirstIndex = newSession.inboundGroupSession!.first_known_index();
148 : if (oldSession == null ||
149 1 : newFirstIndex < oldFirstIndex ||
150 1 : (oldFirstIndex == newFirstIndex &&
151 3 : newSession.forwardingCurve25519KeyChain.length <
152 2 : oldSession.forwardingCurve25519KeyChain.length)) {
153 : // use new session
154 1 : oldSession?.dispose();
155 : } else {
156 : // we are gonna keep our old session
157 1 : newSession.dispose();
158 : return;
159 : }
160 :
161 : final roomInboundGroupSessions =
162 72 : _inboundGroupSessions[roomId] ??= <String, SessionKey>{};
163 24 : roomInboundGroupSessions[sessionId] = newSession;
164 96 : if (!client.isLogged() || client.encryption == null) {
165 : return;
166 : }
167 :
168 48 : final storeFuture = client.database
169 24 : .storeInboundGroupSession(
170 : roomId,
171 : sessionId,
172 24 : inboundGroupSession.pickle(userId),
173 24 : json.encode(content),
174 48 : json.encode({}),
175 24 : json.encode(allowedAtIndex_),
176 : senderKey,
177 24 : json.encode(senderClaimedKeys_),
178 : )
179 48 : .then((_) async {
180 96 : if (!client.isLogged() || client.encryption == null) {
181 : return;
182 : }
183 : if (uploaded) {
184 2 : await client.database
185 1 : .markInboundGroupSessionAsUploaded(roomId, sessionId);
186 : }
187 : });
188 48 : final room = client.getRoomById(roomId);
189 : if (room != null) {
190 : // attempt to decrypt the last event
191 7 : final event = room.lastEvent;
192 : if (event != null &&
193 14 : event.type == EventTypes.Encrypted &&
194 6 : event.content['session_id'] == sessionId) {
195 4 : final decrypted = encryption.decryptRoomEventSync(event);
196 4 : if (decrypted.type != EventTypes.Encrypted) {
197 : // Update the last event in memory first
198 2 : room.lastEvent = decrypted;
199 :
200 : // To persist it in database and trigger UI updates:
201 8 : await client.database.transaction(() async {
202 4 : await client.handleSync(
203 2 : SyncUpdate(
204 : nextBatch: '',
205 2 : rooms: switch (room.membership) {
206 2 : Membership.join =>
207 4 : RoomsUpdate(join: {room.id: JoinedRoomUpdate()}),
208 1 : Membership.ban ||
209 1 : Membership.leave =>
210 4 : RoomsUpdate(leave: {room.id: LeftRoomUpdate()}),
211 0 : Membership.invite =>
212 0 : RoomsUpdate(invite: {room.id: InvitedRoomUpdate()}),
213 0 : Membership.knock =>
214 0 : RoomsUpdate(knock: {room.id: KnockRoomUpdate()}),
215 : },
216 : ),
217 : );
218 : });
219 : }
220 : }
221 : // and finally broadcast the new session
222 14 : room.onSessionKeyReceived.add(sessionId);
223 : }
224 :
225 : return storeFuture;
226 : }
227 :
228 24 : SessionKey? getInboundGroupSession(String roomId, String sessionId) {
229 53 : final sess = _inboundGroupSessions[roomId]?[sessionId];
230 : if (sess != null) {
231 10 : if (sess.sessionId != sessionId && sess.sessionId.isNotEmpty) {
232 : return null;
233 : }
234 : return sess;
235 : }
236 : return null;
237 : }
238 :
239 : /// Attempt auto-request for a key
240 3 : void maybeAutoRequest(
241 : String roomId,
242 : String sessionId,
243 : String? senderKey, {
244 : bool tryOnlineBackup = true,
245 : bool onlineKeyBackupOnly = true,
246 : }) {
247 6 : final room = client.getRoomById(roomId);
248 3 : final requestIdent = '$roomId|$sessionId';
249 : if (room != null &&
250 4 : !_requestedSessionIds.contains(requestIdent) &&
251 4 : !client.isUnknownSession) {
252 : // do e2ee recovery
253 0 : _requestedSessionIds.add(requestIdent);
254 :
255 0 : runInRoot(
256 0 : () async => request(
257 : room,
258 : sessionId,
259 : senderKey,
260 : tryOnlineBackup: tryOnlineBackup,
261 : onlineKeyBackupOnly: onlineKeyBackupOnly,
262 : ),
263 : );
264 : }
265 : }
266 :
267 : /// Loads an inbound group session
268 8 : Future<SessionKey?> loadInboundGroupSession(
269 : String roomId,
270 : String sessionId,
271 : ) async {
272 21 : final sess = _inboundGroupSessions[roomId]?[sessionId];
273 : if (sess != null) {
274 10 : if (sess.sessionId != sessionId && sess.sessionId.isNotEmpty) {
275 : return null; // session_id does not match....better not do anything
276 : }
277 : return sess; // nothing to do
278 : }
279 : final session =
280 15 : await client.database.getInboundGroupSession(roomId, sessionId);
281 : if (session == null) return null;
282 4 : final userID = client.userID;
283 : if (userID == null) return null;
284 2 : final dbSess = SessionKey.fromDb(session, userID);
285 : final roomInboundGroupSessions =
286 6 : _inboundGroupSessions[roomId] ??= <String, SessionKey>{};
287 2 : if (!dbSess.isValid ||
288 4 : dbSess.sessionId.isEmpty ||
289 4 : dbSess.sessionId != sessionId) {
290 : return null;
291 : }
292 2 : return roomInboundGroupSessions[sessionId] = dbSess;
293 : }
294 :
295 5 : Map<String, Map<String, bool>> _getDeviceKeyIdMap(
296 : List<DeviceKeys> deviceKeys,
297 : ) {
298 5 : final deviceKeyIds = <String, Map<String, bool>>{};
299 8 : for (final device in deviceKeys) {
300 3 : final deviceId = device.deviceId;
301 : if (deviceId == null) {
302 0 : Logs().w('[KeyManager] ignoring device without deviceid');
303 : continue;
304 : }
305 9 : final userDeviceKeyIds = deviceKeyIds[device.userId] ??= <String, bool>{};
306 6 : userDeviceKeyIds[deviceId] = !device.encryptToDevice;
307 : }
308 : return deviceKeyIds;
309 : }
310 :
311 : /// clear all cached inbound group sessions. useful for testing
312 3 : void clearOutboundGroupSessions() {
313 6 : _outboundGroupSessions.clear();
314 : }
315 :
316 : /// Clears the existing outboundGroupSession but first checks if the participating
317 : /// devices have been changed. Returns false if the session has not been cleared because
318 : /// it wasn't necessary. Otherwise returns true.
319 5 : Future<bool> clearOrUseOutboundGroupSession(
320 : String roomId, {
321 : bool wipe = false,
322 : bool use = true,
323 : }) async {
324 10 : final room = client.getRoomById(roomId);
325 5 : final sess = getOutboundGroupSession(roomId);
326 4 : if (room == null || sess == null || sess.outboundGroupSession == null) {
327 : return true;
328 : }
329 :
330 : if (!wipe) {
331 : // first check if it needs to be rotated
332 : final encryptionContent =
333 6 : room.getState(EventTypes.Encryption)?.parsedRoomEncryptionContent;
334 3 : final maxMessages = encryptionContent?.rotationPeriodMsgs ?? 100;
335 3 : final maxAge = encryptionContent?.rotationPeriodMs ??
336 : 604800000; // default of one week
337 6 : if ((sess.sentMessages ?? maxMessages) >= maxMessages ||
338 3 : sess.creationTime
339 6 : .add(Duration(milliseconds: maxAge))
340 6 : .isBefore(DateTime.now())) {
341 : wipe = true;
342 : }
343 : }
344 :
345 4 : final inboundSess = await loadInboundGroupSession(
346 4 : room.id,
347 8 : sess.outboundGroupSession!.session_id(),
348 : );
349 : if (inboundSess == null) {
350 0 : Logs().w('No inbound megolm session found for outbound session!');
351 0 : assert(inboundSess != null);
352 : wipe = true;
353 : }
354 :
355 : if (!wipe) {
356 : // next check if the devices in the room changed
357 3 : final devicesToReceive = <DeviceKeys>[];
358 3 : final newDeviceKeys = await room.getUserDeviceKeys();
359 3 : final newDeviceKeyIds = _getDeviceKeyIdMap(newDeviceKeys);
360 : // first check for user differences
361 9 : final oldUserIds = sess.devices.keys.toSet();
362 6 : final newUserIds = newDeviceKeyIds.keys.toSet();
363 6 : if (oldUserIds.difference(newUserIds).isNotEmpty) {
364 : // a user left the room, we must wipe the session
365 : wipe = true;
366 : } else {
367 3 : final newUsers = newUserIds.difference(oldUserIds);
368 3 : if (newUsers.isNotEmpty) {
369 : // new user! Gotta send the megolm session to them
370 : devicesToReceive
371 5 : .addAll(newDeviceKeys.where((d) => newUsers.contains(d.userId)));
372 : }
373 : // okay, now we must test all the individual user devices, if anything new got blocked
374 : // or if we need to send to any new devices.
375 : // for this it is enough if we iterate over the old user Ids, as the new ones already have the needed keys in the list.
376 : // we also know that all the old user IDs appear in the old one, else we have already wiped the session
377 5 : for (final userId in oldUserIds) {
378 4 : final oldBlockedDevices = sess.devices.containsKey(userId)
379 6 : ? sess.devices[userId]!.entries
380 6 : .where((e) => e.value)
381 2 : .map((e) => e.key)
382 2 : .toSet()
383 : : <String>{};
384 2 : final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
385 2 : ? newDeviceKeyIds[userId]!
386 2 : .entries
387 6 : .where((e) => e.value)
388 4 : .map((e) => e.key)
389 2 : .toSet()
390 : : <String>{};
391 : // we don't really care about old devices that got dropped (deleted), we only care if new ones got added and if new ones got blocked
392 : // check if new devices got blocked
393 4 : if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) {
394 : wipe = true;
395 : break;
396 : }
397 : // and now add all the new devices!
398 4 : final oldDeviceIds = sess.devices.containsKey(userId)
399 6 : ? sess.devices[userId]!.entries
400 6 : .where((e) => !e.value)
401 6 : .map((e) => e.key)
402 2 : .toSet()
403 : : <String>{};
404 2 : final newDeviceIds = newDeviceKeyIds.containsKey(userId)
405 2 : ? newDeviceKeyIds[userId]!
406 2 : .entries
407 6 : .where((e) => !e.value)
408 6 : .map((e) => e.key)
409 2 : .toSet()
410 : : <String>{};
411 :
412 : // check if a device got removed
413 4 : if (oldDeviceIds.difference(newDeviceIds).isNotEmpty) {
414 : wipe = true;
415 : break;
416 : }
417 :
418 : // check if any new devices need keys
419 2 : final newDevices = newDeviceIds.difference(oldDeviceIds);
420 2 : if (newDeviceIds.isNotEmpty) {
421 2 : devicesToReceive.addAll(
422 2 : newDeviceKeys.where(
423 10 : (d) => d.userId == userId && newDevices.contains(d.deviceId),
424 : ),
425 : );
426 : }
427 : }
428 : }
429 :
430 : if (!wipe) {
431 : if (!use) {
432 : return false;
433 : }
434 : // okay, we use the outbound group session!
435 3 : sess.devices = newDeviceKeyIds;
436 3 : final rawSession = <String, dynamic>{
437 : 'algorithm': AlgorithmTypes.megolmV1AesSha2,
438 3 : 'room_id': room.id,
439 6 : 'session_id': sess.outboundGroupSession!.session_id(),
440 6 : 'session_key': sess.outboundGroupSession!.session_key(),
441 : };
442 : try {
443 5 : devicesToReceive.removeWhere((k) => !k.encryptToDevice);
444 3 : if (devicesToReceive.isNotEmpty) {
445 : // update allowedAtIndex
446 2 : for (final device in devicesToReceive) {
447 4 : inboundSess!.allowedAtIndex[device.userId] ??= <String, int>{};
448 3 : if (!inboundSess.allowedAtIndex[device.userId]!
449 2 : .containsKey(device.curve25519Key) ||
450 0 : inboundSess.allowedAtIndex[device.userId]![
451 0 : device.curve25519Key]! >
452 0 : sess.outboundGroupSession!.message_index()) {
453 : inboundSess
454 5 : .allowedAtIndex[device.userId]![device.curve25519Key!] =
455 2 : sess.outboundGroupSession!.message_index();
456 : }
457 : }
458 3 : await client.database.updateInboundGroupSessionAllowedAtIndex(
459 2 : json.encode(inboundSess!.allowedAtIndex),
460 1 : room.id,
461 2 : sess.outboundGroupSession!.session_id(),
462 : );
463 : // send out the key
464 2 : await client.sendToDeviceEncryptedChunked(
465 : devicesToReceive,
466 : EventTypes.RoomKey,
467 : rawSession,
468 : );
469 : }
470 : } catch (e, s) {
471 0 : Logs().e(
472 : '[LibOlm] Unable to re-send the session key at later index to new devices',
473 : e,
474 : s,
475 : );
476 : }
477 : return false;
478 : }
479 : }
480 2 : sess.dispose();
481 4 : _outboundGroupSessions.remove(roomId);
482 6 : await client.database.removeOutboundGroupSession(roomId);
483 : return true;
484 : }
485 :
486 : /// Store an outbound group session in the database
487 5 : Future<void> storeOutboundGroupSession(
488 : String roomId,
489 : OutboundGroupSession sess,
490 : ) async {
491 10 : final userID = client.userID;
492 : if (userID == null) return;
493 15 : await client.database.storeOutboundGroupSession(
494 : roomId,
495 10 : sess.outboundGroupSession!.pickle(userID),
496 10 : json.encode(sess.devices),
497 10 : sess.creationTime.millisecondsSinceEpoch,
498 : );
499 : }
500 :
501 : final Map<String, Future<OutboundGroupSession>>
502 : _pendingNewOutboundGroupSessions = {};
503 :
504 : /// Creates an outbound group session for a given room id
505 5 : Future<OutboundGroupSession> createOutboundGroupSession(String roomId) async {
506 10 : final sess = _pendingNewOutboundGroupSessions[roomId];
507 : if (sess != null) {
508 : return sess;
509 : }
510 10 : final newSess = _pendingNewOutboundGroupSessions[roomId] =
511 5 : _createOutboundGroupSession(roomId);
512 :
513 : try {
514 : await newSess;
515 : } finally {
516 5 : _pendingNewOutboundGroupSessions
517 15 : .removeWhere((_, value) => value == newSess);
518 : }
519 :
520 : return newSess;
521 : }
522 :
523 : /// Prepares an outbound group session for a given room ID. That is, load it from
524 : /// the database, cycle it if needed and create it if absent.
525 1 : Future<void> prepareOutboundGroupSession(String roomId) async {
526 1 : if (getOutboundGroupSession(roomId) == null) {
527 0 : await loadOutboundGroupSession(roomId);
528 : }
529 1 : await clearOrUseOutboundGroupSession(roomId, use: false);
530 1 : if (getOutboundGroupSession(roomId) == null) {
531 1 : await createOutboundGroupSession(roomId);
532 : }
533 : }
534 :
535 5 : Future<OutboundGroupSession> _createOutboundGroupSession(
536 : String roomId,
537 : ) async {
538 5 : await clearOrUseOutboundGroupSession(roomId, wipe: true);
539 10 : await client.firstSyncReceived;
540 10 : final room = client.getRoomById(roomId);
541 : if (room == null) {
542 0 : throw Exception(
543 0 : 'Tried to create a megolm session in a non-existing room ($roomId)!',
544 : );
545 : }
546 10 : final userID = client.userID;
547 : if (userID == null) {
548 0 : throw Exception(
549 : 'Tried to create a megolm session without being logged in!',
550 : );
551 : }
552 :
553 5 : final deviceKeys = await room.getUserDeviceKeys();
554 5 : final deviceKeyIds = _getDeviceKeyIdMap(deviceKeys);
555 11 : deviceKeys.removeWhere((k) => !k.encryptToDevice);
556 5 : final outboundGroupSession = olm.OutboundGroupSession();
557 : try {
558 5 : outboundGroupSession.create();
559 : } catch (e, s) {
560 0 : outboundGroupSession.free();
561 0 : Logs().e('[LibOlm] Unable to create new outboundGroupSession', e, s);
562 : rethrow;
563 : }
564 5 : final rawSession = <String, dynamic>{
565 : 'algorithm': AlgorithmTypes.megolmV1AesSha2,
566 5 : 'room_id': room.id,
567 5 : 'session_id': outboundGroupSession.session_id(),
568 5 : 'session_key': outboundGroupSession.session_key(),
569 : };
570 5 : final allowedAtIndex = <String, Map<String, int>>{};
571 8 : for (final device in deviceKeys) {
572 3 : if (!device.isValid) {
573 0 : Logs().e('Skipping invalid device');
574 : continue;
575 : }
576 9 : allowedAtIndex[device.userId] ??= <String, int>{};
577 12 : allowedAtIndex[device.userId]![device.curve25519Key!] =
578 3 : outboundGroupSession.message_index();
579 : }
580 5 : await setInboundGroupSession(
581 : roomId,
582 5 : rawSession['session_id'],
583 10 : encryption.identityKey!,
584 : rawSession,
585 : allowedAtIndex: allowedAtIndex,
586 : );
587 5 : final sess = OutboundGroupSession(
588 : devices: deviceKeyIds,
589 5 : creationTime: DateTime.now(),
590 : outboundGroupSession: outboundGroupSession,
591 : key: userID,
592 : );
593 : try {
594 10 : await client.sendToDeviceEncryptedChunked(
595 : deviceKeys,
596 : EventTypes.RoomKey,
597 : rawSession,
598 : );
599 5 : await storeOutboundGroupSession(roomId, sess);
600 10 : _outboundGroupSessions[roomId] = sess;
601 : } catch (e, s) {
602 0 : Logs().e(
603 : '[LibOlm] Unable to send the session key to the participating devices',
604 : e,
605 : s,
606 : );
607 0 : sess.dispose();
608 : rethrow;
609 : }
610 : return sess;
611 : }
612 :
613 : /// Get an outbound group session for a room id
614 5 : OutboundGroupSession? getOutboundGroupSession(String roomId) {
615 10 : return _outboundGroupSessions[roomId];
616 : }
617 :
618 : /// Load an outbound group session from database
619 3 : Future<void> loadOutboundGroupSession(String roomId) async {
620 6 : final database = client.database;
621 6 : final userID = client.userID;
622 6 : if (_loadedOutboundGroupSessions.contains(roomId) ||
623 6 : _outboundGroupSessions.containsKey(roomId) ||
624 : userID == null) {
625 : return; // nothing to do
626 : }
627 6 : _loadedOutboundGroupSessions.add(roomId);
628 3 : final sess = await database.getOutboundGroupSession(
629 : roomId,
630 : userID,
631 : );
632 1 : if (sess == null || !sess.isValid) {
633 : return;
634 : }
635 2 : _outboundGroupSessions[roomId] = sess;
636 : }
637 :
638 25 : Future<bool> isCached() async {
639 50 : await client.accountDataLoading;
640 25 : if (!enabled) {
641 : return false;
642 : }
643 50 : await client.userDeviceKeysLoading;
644 75 : return (await encryption.ssss.getCached(megolmKey)) != null;
645 : }
646 :
647 : GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
648 : DateTime? _roomKeysVersionCacheDate;
649 :
650 5 : Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo([
651 : bool useCache = true,
652 : ]) async {
653 5 : if (_roomKeysVersionCache != null &&
654 3 : _roomKeysVersionCacheDate != null &&
655 : useCache &&
656 1 : DateTime.now()
657 2 : .subtract(Duration(minutes: 5))
658 2 : .isBefore(_roomKeysVersionCacheDate!)) {
659 1 : return _roomKeysVersionCache!;
660 : }
661 15 : _roomKeysVersionCache = await client.getRoomKeysVersionCurrent();
662 10 : _roomKeysVersionCacheDate = DateTime.now();
663 5 : return _roomKeysVersionCache!;
664 : }
665 :
666 1 : Future<void> loadFromResponse(RoomKeys keys) async {
667 1 : if (!(await isCached())) {
668 : return;
669 : }
670 : final privateKey =
671 4 : base64decodeUnpadded((await encryption.ssss.getCached(megolmKey))!);
672 1 : final decryption = olm.PkDecryption();
673 1 : final info = await getRoomKeysBackupInfo();
674 : String backupPubKey;
675 : try {
676 1 : backupPubKey = decryption.init_with_private_key(privateKey);
677 :
678 2 : if (info.algorithm != BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 ||
679 3 : info.authData['public_key'] != backupPubKey) {
680 : return;
681 : }
682 3 : for (final roomEntry in keys.rooms.entries) {
683 1 : final roomId = roomEntry.key;
684 4 : for (final sessionEntry in roomEntry.value.sessions.entries) {
685 1 : final sessionId = sessionEntry.key;
686 1 : final session = sessionEntry.value;
687 1 : final sessionData = session.sessionData;
688 : Map<String, Object?>? decrypted;
689 : try {
690 1 : decrypted = json.decode(
691 1 : decryption.decrypt(
692 1 : sessionData['ephemeral'] as String,
693 1 : sessionData['mac'] as String,
694 1 : sessionData['ciphertext'] as String,
695 : ),
696 : );
697 : } catch (e, s) {
698 0 : Logs().e('[LibOlm] Error decrypting room key', e, s);
699 : }
700 1 : final senderKey = decrypted?.tryGet<String>('sender_key');
701 : if (decrypted != null && senderKey != null) {
702 1 : decrypted['session_id'] = sessionId;
703 1 : decrypted['room_id'] = roomId;
704 1 : await setInboundGroupSession(
705 : roomId,
706 : sessionId,
707 : senderKey,
708 : decrypted,
709 : forwarded: true,
710 : senderClaimedKeys:
711 1 : decrypted.tryGetMap<String, String>('sender_claimed_keys') ??
712 0 : <String, String>{},
713 : uploaded: true,
714 : );
715 : }
716 : }
717 : }
718 : } finally {
719 1 : decryption.free();
720 : }
721 : }
722 :
723 : /// Loads and stores all keys from the online key backup. This may take a
724 : /// while for older and big accounts.
725 1 : Future<void> loadAllKeys() async {
726 1 : final info = await getRoomKeysBackupInfo();
727 3 : final ret = await client.getRoomKeys(info.version);
728 1 : await loadFromResponse(ret);
729 : }
730 :
731 : /// Loads all room keys for a single room and stores them. This may take a
732 : /// while for older and big rooms.
733 1 : Future<void> loadAllKeysFromRoom(String roomId) async {
734 1 : final info = await getRoomKeysBackupInfo();
735 3 : final ret = await client.getRoomKeysByRoomId(roomId, info.version);
736 2 : final keys = RoomKeys.fromJson({
737 1 : 'rooms': {
738 1 : roomId: {
739 5 : 'sessions': ret.sessions.map((k, s) => MapEntry(k, s.toJson())),
740 : },
741 : },
742 : });
743 1 : await loadFromResponse(keys);
744 : }
745 :
746 : /// Loads a single key for the specified room from the online key backup
747 : /// and stores it.
748 1 : Future<void> loadSingleKey(String roomId, String sessionId) async {
749 1 : final info = await getRoomKeysBackupInfo();
750 : final ret =
751 3 : await client.getRoomKeyBySessionId(roomId, sessionId, info.version);
752 2 : final keys = RoomKeys.fromJson({
753 1 : 'rooms': {
754 1 : roomId: {
755 1 : 'sessions': {
756 1 : sessionId: ret.toJson(),
757 : },
758 : },
759 : },
760 : });
761 1 : await loadFromResponse(keys);
762 : }
763 :
764 : /// Request a certain key from another device
765 3 : Future<void> request(
766 : Room room,
767 : String sessionId,
768 : String? senderKey, {
769 : bool tryOnlineBackup = true,
770 : bool onlineKeyBackupOnly = false,
771 : }) async {
772 2 : if (tryOnlineBackup && await isCached()) {
773 : // let's first check our online key backup store thingy...
774 2 : final hadPreviously = getInboundGroupSession(room.id, sessionId) != null;
775 : try {
776 2 : await loadSingleKey(room.id, sessionId);
777 : } catch (err, stacktrace) {
778 0 : if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
779 0 : Logs().i(
780 : '[KeyManager] Key not in online key backup, requesting it from other devices...',
781 : );
782 : } else {
783 0 : Logs().e(
784 : '[KeyManager] Failed to access online key backup',
785 : err,
786 : stacktrace,
787 : );
788 : }
789 : }
790 : // TODO: also don't request from others if we have an index of 0 now
791 : if (!hadPreviously &&
792 2 : getInboundGroupSession(room.id, sessionId) != null) {
793 : return; // we managed to load the session from online backup, no need to care about it now
794 : }
795 : }
796 : if (onlineKeyBackupOnly) {
797 : return; // we only want to do the online key backup
798 : }
799 : try {
800 : // while we just send the to-device event to '*', we still need to save the
801 : // devices themself to know where to send the cancel to after receiving a reply
802 2 : final devices = await room.getUserDeviceKeys();
803 4 : final requestId = client.generateUniqueTransactionId();
804 2 : final request = KeyManagerKeyShareRequest(
805 : requestId: requestId,
806 : devices: devices,
807 : room: room,
808 : sessionId: sessionId,
809 : );
810 2 : final userList = await room.requestParticipants();
811 4 : await client.sendToDevicesOfUserIds(
812 6 : userList.map<String>((u) => u.id).toSet(),
813 : EventTypes.RoomKeyRequest,
814 2 : {
815 : 'action': 'request',
816 2 : 'body': {
817 2 : 'algorithm': AlgorithmTypes.megolmV1AesSha2,
818 4 : 'room_id': room.id,
819 2 : 'session_id': sessionId,
820 2 : if (senderKey != null) 'sender_key': senderKey,
821 : },
822 : 'request_id': requestId,
823 4 : 'requesting_device_id': client.deviceID,
824 : },
825 : );
826 6 : outgoingShareRequests[request.requestId] = request;
827 : } catch (e, s) {
828 0 : Logs().e('[Key Manager] Sending key verification request failed', e, s);
829 : }
830 : }
831 :
832 : Future<void>? _uploadingFuture;
833 :
834 25 : void startAutoUploadKeys() {
835 150 : _uploadKeysOnSync = encryption.client.onSync.stream.listen(
836 50 : (_) async => uploadInboundGroupSessions(skipIfInProgress: true),
837 : );
838 : }
839 :
840 : /// This task should be performed after sync processing but should not block
841 : /// the sync. To make sure that it never gets executed multiple times, it is
842 : /// skipped when an upload task is already in progress. Set `skipIfInProgress`
843 : /// to `false` to await the pending upload task instead.
844 25 : Future<void> uploadInboundGroupSessions({
845 : bool skipIfInProgress = false,
846 : }) async {
847 50 : final database = client.database;
848 50 : final userID = client.userID;
849 : if (userID == null) {
850 : return;
851 : }
852 :
853 : // Make sure to not run in parallel
854 25 : if (_uploadingFuture != null) {
855 : if (skipIfInProgress) return;
856 : try {
857 1 : await _uploadingFuture;
858 : } finally {
859 : // shouldn't be necessary, since it will be unset already by the other process that started it, but just to be safe, also unset the future here
860 1 : _uploadingFuture = null;
861 : }
862 : }
863 :
864 25 : Future<void> uploadInternal() async {
865 : try {
866 50 : await client.userDeviceKeysLoading;
867 :
868 25 : if (!(await isCached())) {
869 : return; // we can't backup anyways
870 : }
871 5 : final dbSessions = await database.getInboundGroupSessionsToUpload();
872 5 : if (dbSessions.isEmpty) {
873 : return; // nothing to do
874 : }
875 : final privateKey =
876 20 : base64decodeUnpadded((await encryption.ssss.getCached(megolmKey))!);
877 : // decryption is needed to calculate the public key and thus see if the claimed information is in fact valid
878 5 : final decryption = olm.PkDecryption();
879 5 : final info = await getRoomKeysBackupInfo(false);
880 : String backupPubKey;
881 : try {
882 5 : backupPubKey = decryption.init_with_private_key(privateKey);
883 :
884 10 : if (info.algorithm !=
885 : BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 ||
886 15 : info.authData['public_key'] != backupPubKey) {
887 1 : decryption.free();
888 : return;
889 : }
890 4 : final args = GenerateUploadKeysArgs(
891 : pubkey: backupPubKey,
892 4 : dbSessions: <DbInboundGroupSessionBundle>[],
893 : userId: userID,
894 : );
895 : // we need to calculate verified beforehand, as else we pass a closure to an isolate
896 : // with 500 keys they do, however, noticably block the UI, which is why we give brief async suspentions in here
897 : // so that the event loop can progress
898 : var i = 0;
899 8 : for (final dbSession in dbSessions) {
900 : final device =
901 12 : client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
902 8 : args.dbSessions.add(
903 4 : DbInboundGroupSessionBundle(
904 : dbSession: dbSession,
905 4 : verified: device?.verified ?? false,
906 : ),
907 : );
908 4 : i++;
909 4 : if (i > 10) {
910 0 : await Future.delayed(Duration(milliseconds: 1));
911 : i = 0;
912 : }
913 : }
914 : final roomKeys =
915 12 : await client.nativeImplementations.generateUploadKeys(args);
916 16 : Logs().i('[Key Manager] Uploading ${dbSessions.length} room keys...');
917 : // upload the payload...
918 12 : await client.putRoomKeys(info.version, roomKeys);
919 : // and now finally mark all the keys as uploaded
920 : // no need to optimze this, as we only run it so seldomly and almost never with many keys at once
921 8 : for (final dbSession in dbSessions) {
922 4 : await database.markInboundGroupSessionAsUploaded(
923 4 : dbSession.roomId,
924 4 : dbSession.sessionId,
925 : );
926 : }
927 : } finally {
928 5 : decryption.free();
929 : }
930 : } catch (e, s) {
931 4 : Logs().e('[Key Manager] Error uploading room keys', e, s);
932 : }
933 : }
934 :
935 50 : _uploadingFuture = uploadInternal();
936 : try {
937 25 : await _uploadingFuture;
938 : } finally {
939 25 : _uploadingFuture = null;
940 : }
941 : }
942 :
943 : /// Handle an incoming to_device event that is related to key sharing
944 24 : Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
945 48 : if (event.type == EventTypes.RoomKeyRequest) {
946 3 : if (event.content['request_id'] is! String) {
947 : return; // invalid event
948 : }
949 3 : if (event.content['action'] == 'request') {
950 : // we are *receiving* a request
951 2 : Logs().i(
952 4 : '[KeyManager] Received key sharing request from ${event.sender}:${event.content['requesting_device_id']}...',
953 : );
954 2 : if (!event.content.containsKey('body')) {
955 2 : Logs().w('[KeyManager] No body, doing nothing');
956 : return; // no body
957 : }
958 2 : final body = event.content.tryGetMap<String, Object?>('body');
959 : if (body == null) {
960 0 : Logs().w('[KeyManager] Wrong type for body, doing nothing');
961 : return; // wrong type for body
962 : }
963 1 : final roomId = body.tryGet<String>('room_id');
964 : if (roomId == null) {
965 0 : Logs().w(
966 : '[KeyManager] Wrong type for room_id or no room_id, doing nothing',
967 : );
968 : return; // wrong type for roomId or no roomId found
969 : }
970 4 : final device = client.userDeviceKeys[event.sender]
971 4 : ?.deviceKeys[event.content['requesting_device_id']];
972 : if (device == null) {
973 2 : Logs().w('[KeyManager] Device not found, doing nothing');
974 : return; // device not found
975 : }
976 4 : if (device.userId == client.userID &&
977 4 : device.deviceId == client.deviceID) {
978 0 : Logs().i('[KeyManager] Request is by ourself, ignoring');
979 : return; // ignore requests by ourself
980 : }
981 2 : final room = client.getRoomById(roomId);
982 : if (room == null) {
983 2 : Logs().i('[KeyManager] Unknown room, ignoring');
984 : return; // unknown room
985 : }
986 1 : final sessionId = body.tryGet<String>('session_id');
987 : if (sessionId == null) {
988 0 : Logs().w(
989 : '[KeyManager] Wrong type for session_id or no session_id, doing nothing',
990 : );
991 : return; // wrong type for session_id
992 : }
993 : // okay, let's see if we have this session at all
994 2 : final session = await loadInboundGroupSession(room.id, sessionId);
995 : if (session == null) {
996 2 : Logs().i('[KeyManager] Unknown session, ignoring');
997 : return; // we don't have this session anyways
998 : }
999 3 : if (event.content['request_id'] is! String) {
1000 0 : Logs().w(
1001 : '[KeyManager] Wrong type for request_id or no request_id, doing nothing',
1002 : );
1003 : return; // wrong type for request_id
1004 : }
1005 1 : final request = KeyManagerKeyShareRequest(
1006 2 : requestId: event.content.tryGet<String>('request_id')!,
1007 1 : devices: [device],
1008 : room: room,
1009 : sessionId: sessionId,
1010 : );
1011 3 : if (incomingShareRequests.containsKey(request.requestId)) {
1012 0 : Logs().i('[KeyManager] Already processed this request, ignoring');
1013 : return; // we don't want to process one and the same request multiple times
1014 : }
1015 3 : incomingShareRequests[request.requestId] = request;
1016 : final roomKeyRequest =
1017 1 : RoomKeyRequest.fromToDeviceEvent(event, this, request);
1018 4 : if (device.userId == client.userID &&
1019 1 : device.verified &&
1020 1 : !device.blocked) {
1021 2 : Logs().i('[KeyManager] All checks out, forwarding key...');
1022 : // alright, we can forward the key
1023 1 : await roomKeyRequest.forwardKey();
1024 1 : } else if (device.encryptToDevice &&
1025 1 : session.allowedAtIndex
1026 2 : .tryGet<Map<String, Object?>>(device.userId)
1027 2 : ?.tryGet(device.curve25519Key!) !=
1028 : null) {
1029 : // if we know the user may see the message, then we can just forward the key.
1030 : // we do not need to check if the device is verified, just if it is not blocked,
1031 : // as that is the logic we already initially try to send out the room keys.
1032 : final index =
1033 5 : session.allowedAtIndex[device.userId]![device.curve25519Key]!;
1034 2 : Logs().i(
1035 1 : '[KeyManager] Valid foreign request, forwarding key at index $index...',
1036 : );
1037 1 : await roomKeyRequest.forwardKey(index);
1038 : } else {
1039 1 : Logs()
1040 1 : .i('[KeyManager] Asking client, if the key should be forwarded');
1041 2 : client.onRoomKeyRequest
1042 1 : .add(roomKeyRequest); // let the client handle this
1043 : }
1044 0 : } else if (event.content['action'] == 'request_cancellation') {
1045 : // we got told to cancel an incoming request
1046 0 : if (!incomingShareRequests.containsKey(event.content['request_id'])) {
1047 : return; // we don't know this request anyways
1048 : }
1049 : // alright, let's just cancel this request
1050 0 : final request = incomingShareRequests[event.content['request_id']]!;
1051 0 : request.canceled = true;
1052 0 : incomingShareRequests.remove(request.requestId);
1053 : }
1054 48 : } else if (event.type == EventTypes.ForwardedRoomKey) {
1055 : // we *received* an incoming key request
1056 1 : final encryptedContent = event.encryptedContent;
1057 : if (encryptedContent == null) {
1058 2 : Logs().w(
1059 : 'Ignoring an unencrypted forwarded key from a to device message',
1060 1 : event.toJson(),
1061 : );
1062 : return;
1063 : }
1064 3 : final request = outgoingShareRequests.values.firstWhereOrNull(
1065 1 : (r) =>
1066 5 : r.room.id == event.content['room_id'] &&
1067 4 : r.sessionId == event.content['session_id'],
1068 : );
1069 1 : if (request == null || request.canceled) {
1070 : return; // no associated request found or it got canceled
1071 : }
1072 2 : final device = request.devices.firstWhereOrNull(
1073 1 : (d) =>
1074 3 : d.userId == event.sender &&
1075 3 : d.curve25519Key == encryptedContent['sender_key'],
1076 : );
1077 : if (device == null) {
1078 : return; // someone we didn't send our request to replied....better ignore this
1079 : }
1080 : // we add the sender key to the forwarded key chain
1081 3 : if (event.content['forwarding_curve25519_key_chain'] is! List) {
1082 0 : event.content['forwarding_curve25519_key_chain'] = <String>[];
1083 : }
1084 2 : (event.content['forwarding_curve25519_key_chain'] as List)
1085 2 : .add(encryptedContent['sender_key']);
1086 3 : if (event.content['sender_claimed_ed25519_key'] is! String) {
1087 0 : Logs().w('sender_claimed_ed255519_key has wrong type');
1088 : return; // wrong type
1089 : }
1090 : // TODO: verify that the keys work to decrypt a message
1091 : // alright, all checks out, let's go ahead and store this session
1092 1 : await setInboundGroupSession(
1093 2 : request.room.id,
1094 1 : request.sessionId,
1095 1 : device.curve25519Key!,
1096 1 : event.content,
1097 : forwarded: true,
1098 1 : senderClaimedKeys: {
1099 2 : 'ed25519': event.content['sender_claimed_ed25519_key'] as String,
1100 : },
1101 : );
1102 2 : request.devices.removeWhere(
1103 7 : (k) => k.userId == device.userId && k.deviceId == device.deviceId,
1104 : );
1105 3 : outgoingShareRequests.remove(request.requestId);
1106 : // send cancel to all other devices
1107 2 : if (request.devices.isEmpty) {
1108 : return; // no need to send any cancellation
1109 : }
1110 : // Send with send-to-device messaging
1111 1 : final sendToDeviceMessage = {
1112 : 'action': 'request_cancellation',
1113 1 : 'request_id': request.requestId,
1114 2 : 'requesting_device_id': client.deviceID,
1115 : };
1116 1 : final data = <String, Map<String, Map<String, dynamic>>>{};
1117 2 : for (final device in request.devices) {
1118 3 : final userData = data[device.userId] ??= {};
1119 2 : userData[device.deviceId!] = sendToDeviceMessage;
1120 : }
1121 2 : await client.sendToDevice(
1122 : EventTypes.RoomKeyRequest,
1123 2 : client.generateUniqueTransactionId(),
1124 : data,
1125 : );
1126 48 : } else if (event.type == EventTypes.RoomKey) {
1127 48 : Logs().v(
1128 72 : '[KeyManager] Received room key with session ${event.content['session_id']}',
1129 : );
1130 24 : final encryptedContent = event.encryptedContent;
1131 : if (encryptedContent == null) {
1132 2 : Logs().v('[KeyManager] not encrypted, ignoring...');
1133 : return; // the event wasn't encrypted, this is a security risk;
1134 : }
1135 48 : final roomId = event.content.tryGet<String>('room_id');
1136 48 : final sessionId = event.content.tryGet<String>('session_id');
1137 : if (roomId == null || sessionId == null) {
1138 0 : Logs().w(
1139 : 'Either room_id or session_id are not the expected type or missing',
1140 : );
1141 : return;
1142 : }
1143 96 : final sender_ed25519 = client.userDeviceKeys[event.sender]
1144 4 : ?.deviceKeys[event.content['requesting_device_id']]?.ed25519Key;
1145 : if (sender_ed25519 != null) {
1146 0 : event.content['sender_claimed_ed25519_key'] = sender_ed25519;
1147 : }
1148 48 : Logs().v('[KeyManager] Keeping room key');
1149 24 : await setInboundGroupSession(
1150 : roomId,
1151 : sessionId,
1152 24 : encryptedContent['sender_key'],
1153 24 : event.content,
1154 : forwarded: false,
1155 : );
1156 : }
1157 : }
1158 :
1159 : StreamSubscription<SyncUpdate>? _uploadKeysOnSync;
1160 :
1161 22 : void dispose() {
1162 : // ignore: discarded_futures
1163 44 : _uploadKeysOnSync?.cancel();
1164 48 : for (final sess in _outboundGroupSessions.values) {
1165 4 : sess.dispose();
1166 : }
1167 65 : for (final entries in _inboundGroupSessions.values) {
1168 42 : for (final sess in entries.values) {
1169 21 : sess.dispose();
1170 : }
1171 : }
1172 : }
1173 : }
1174 :
1175 : class KeyManagerKeyShareRequest {
1176 : final String requestId;
1177 : final List<DeviceKeys> devices;
1178 : final Room room;
1179 : final String sessionId;
1180 : bool canceled;
1181 :
1182 2 : KeyManagerKeyShareRequest({
1183 : required this.requestId,
1184 : List<DeviceKeys>? devices,
1185 : required this.room,
1186 : required this.sessionId,
1187 : this.canceled = false,
1188 0 : }) : devices = devices ?? [];
1189 : }
1190 :
1191 : class RoomKeyRequest extends ToDeviceEvent {
1192 : KeyManager keyManager;
1193 : KeyManagerKeyShareRequest request;
1194 :
1195 1 : RoomKeyRequest.fromToDeviceEvent(
1196 : ToDeviceEvent toDeviceEvent,
1197 : this.keyManager,
1198 : this.request,
1199 1 : ) : super(
1200 1 : sender: toDeviceEvent.sender,
1201 1 : content: toDeviceEvent.content,
1202 1 : type: toDeviceEvent.type,
1203 : );
1204 :
1205 3 : Room get room => request.room;
1206 :
1207 4 : DeviceKeys get requestingDevice => request.devices.first;
1208 :
1209 1 : Future<void> forwardKey([int? index]) async {
1210 2 : if (request.canceled) {
1211 0 : keyManager.incomingShareRequests.remove(request.requestId);
1212 : return; // request is canceled, don't send anything
1213 : }
1214 1 : final room = this.room;
1215 : final session =
1216 5 : await keyManager.loadInboundGroupSession(room.id, request.sessionId);
1217 1 : if (session?.inboundGroupSession == null) {
1218 0 : Logs().v("[KeyManager] Not forwarding key we don't have");
1219 : return;
1220 : }
1221 :
1222 2 : final message = session!.content.copy();
1223 1 : message['forwarding_curve25519_key_chain'] =
1224 2 : List<String>.from(session.forwardingCurve25519KeyChain);
1225 :
1226 2 : if (session.senderKey.isNotEmpty) {
1227 2 : message['sender_key'] = session.senderKey;
1228 : }
1229 1 : message['sender_claimed_ed25519_key'] =
1230 2 : session.senderClaimedKeys['ed25519'] ??
1231 2 : (session.forwardingCurve25519KeyChain.isEmpty
1232 3 : ? keyManager.encryption.fingerprintKey
1233 : : null);
1234 3 : message['session_key'] = session.inboundGroupSession!.export_session(
1235 2 : index ?? session.inboundGroupSession!.first_known_index(),
1236 : );
1237 : // send the actual reply of the key back to the requester
1238 3 : await keyManager.client.sendToDeviceEncrypted(
1239 2 : [requestingDevice],
1240 : EventTypes.ForwardedRoomKey,
1241 : message,
1242 : );
1243 5 : keyManager.incomingShareRequests.remove(request.requestId);
1244 : }
1245 : }
1246 :
1247 : /// you would likely want to use [NativeImplementations] and
1248 : /// [Client.nativeImplementations] instead
1249 4 : RoomKeys generateUploadKeysImplementation(GenerateUploadKeysArgs args) {
1250 4 : final enc = olm.PkEncryption();
1251 : try {
1252 8 : enc.set_recipient_key(args.pubkey);
1253 : // first we generate the payload to upload all the session keys in this chunk
1254 8 : final roomKeys = RoomKeys(rooms: {});
1255 8 : for (final dbSession in args.dbSessions) {
1256 12 : final sess = SessionKey.fromDb(dbSession.dbSession, args.userId);
1257 4 : if (!sess.isValid) {
1258 : continue;
1259 : }
1260 : // create the room if it doesn't exist
1261 : final roomKeyBackup =
1262 20 : roomKeys.rooms[sess.roomId] ??= RoomKeyBackup(sessions: {});
1263 : // generate the encrypted content
1264 4 : final payload = <String, dynamic>{
1265 : 'algorithm': AlgorithmTypes.megolmV1AesSha2,
1266 4 : 'forwarding_curve25519_key_chain': sess.forwardingCurve25519KeyChain,
1267 4 : 'sender_key': sess.senderKey,
1268 4 : 'sender_claimed_keys': sess.senderClaimedKeys,
1269 4 : 'session_key': sess.inboundGroupSession!
1270 12 : .export_session(sess.inboundGroupSession!.first_known_index()),
1271 : };
1272 : // encrypt the content
1273 8 : final encrypted = enc.encrypt(json.encode(payload));
1274 : // fetch the device, if available...
1275 : //final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey);
1276 : // aaaand finally add the session key to our payload
1277 16 : roomKeyBackup.sessions[sess.sessionId] = KeyBackupData(
1278 8 : firstMessageIndex: sess.inboundGroupSession!.first_known_index(),
1279 8 : forwardedCount: sess.forwardingCurve25519KeyChain.length,
1280 4 : isVerified: dbSession.verified, //device?.verified ?? false,
1281 4 : sessionData: {
1282 4 : 'ephemeral': encrypted.ephemeral,
1283 4 : 'ciphertext': encrypted.ciphertext,
1284 4 : 'mac': encrypted.mac,
1285 : },
1286 : );
1287 : }
1288 4 : enc.free();
1289 : return roomKeys;
1290 : } catch (e, s) {
1291 0 : Logs().e('[Key Manager] Error generating payload', e, s);
1292 0 : enc.free();
1293 : rethrow;
1294 : }
1295 : }
1296 :
1297 : class DbInboundGroupSessionBundle {
1298 4 : DbInboundGroupSessionBundle({
1299 : required this.dbSession,
1300 : required this.verified,
1301 : });
1302 :
1303 0 : factory DbInboundGroupSessionBundle.fromJson(Map<dynamic, dynamic> json) =>
1304 0 : DbInboundGroupSessionBundle(
1305 : dbSession:
1306 0 : StoredInboundGroupSession.fromJson(Map.from(json['dbSession'])),
1307 0 : verified: json['verified'],
1308 : );
1309 :
1310 0 : Map<String, Object> toJson() => {
1311 0 : 'dbSession': dbSession.toJson(),
1312 0 : 'verified': verified,
1313 : };
1314 : StoredInboundGroupSession dbSession;
1315 : bool verified;
1316 : }
1317 :
1318 : class GenerateUploadKeysArgs {
1319 4 : GenerateUploadKeysArgs({
1320 : required this.pubkey,
1321 : required this.dbSessions,
1322 : required this.userId,
1323 : });
1324 :
1325 0 : factory GenerateUploadKeysArgs.fromJson(Map<dynamic, dynamic> json) =>
1326 0 : GenerateUploadKeysArgs(
1327 0 : pubkey: json['pubkey'],
1328 0 : dbSessions: (json['dbSessions'] as Iterable)
1329 0 : .map((e) => DbInboundGroupSessionBundle.fromJson(e))
1330 0 : .toList(),
1331 0 : userId: json['userId'],
1332 : );
1333 :
1334 0 : Map<String, Object> toJson() => {
1335 0 : 'pubkey': pubkey,
1336 0 : 'dbSessions': dbSessions.map((e) => e.toJson()).toList(),
1337 0 : 'userId': userId,
1338 : };
1339 :
1340 : String pubkey;
1341 : List<DbInboundGroupSessionBundle> dbSessions;
1342 : String userId;
1343 : }
|