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