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