Line data Source code
1 : /*
2 : * Famedly Matrix SDK
3 : * Copyright (C) 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:convert';
20 :
21 : import 'package:canonical_json/canonical_json.dart';
22 : import 'package:collection/collection.dart' show IterableExtension;
23 : import 'package:olm/olm.dart' as olm;
24 :
25 : import 'package:matrix/encryption.dart';
26 : import 'package:matrix/matrix.dart';
27 :
28 : enum UserVerifiedStatus { verified, unknown, unknownDevice }
29 :
30 : class DeviceKeysList {
31 : Client client;
32 : String userId;
33 : bool outdated = true;
34 : Map<String, DeviceKeys> deviceKeys = {};
35 : Map<String, CrossSigningKey> crossSigningKeys = {};
36 :
37 13 : SignableKey? getKey(String id) => deviceKeys[id] ?? crossSigningKeys[id];
38 :
39 36 : CrossSigningKey? getCrossSigningKey(String type) => crossSigningKeys.values
40 42 : .firstWhereOrNull((key) => key.usage.contains(type));
41 :
42 24 : CrossSigningKey? get masterKey => getCrossSigningKey('master');
43 12 : CrossSigningKey? get selfSigningKey => getCrossSigningKey('self_signing');
44 8 : CrossSigningKey? get userSigningKey => getCrossSigningKey('user_signing');
45 :
46 3 : UserVerifiedStatus get verified {
47 3 : if (masterKey == null) {
48 : return UserVerifiedStatus.unknown;
49 : }
50 6 : if (masterKey!.verified) {
51 3 : for (final key in deviceKeys.values) {
52 1 : if (!key.verified) {
53 : return UserVerifiedStatus.unknownDevice;
54 : }
55 : }
56 : return UserVerifiedStatus.verified;
57 : } else {
58 9 : for (final key in deviceKeys.values) {
59 3 : if (!key.verified) {
60 : return UserVerifiedStatus.unknown;
61 : }
62 : }
63 : return UserVerifiedStatus.verified;
64 : }
65 : }
66 :
67 : /// Starts a verification with this device. This might need to create a new
68 : /// direct chat to send the verification request over this room. For this you
69 : /// can set parameters here.
70 3 : Future<KeyVerification> startVerification({
71 : bool? newDirectChatEnableEncryption,
72 : List<StateEvent>? newDirectChatInitialState,
73 : }) async {
74 6 : final encryption = client.encryption;
75 : if (encryption == null) {
76 0 : throw Exception('Encryption not enabled');
77 : }
78 12 : if (userId != client.userID) {
79 : // in-room verification with someone else
80 : Room? room;
81 : // we check if there's already a direct chat with the user
82 13 : for (final directChatRoomId in client.directChats[userId] ?? []) {
83 2 : final tempRoom = client.getRoomById(directChatRoomId);
84 : if (tempRoom != null &&
85 : // check if the room is a direct chat and has less than 2 members
86 : // (including the invited users)
87 3 : (tempRoom.summary.mInvitedMemberCount ?? 0) +
88 3 : (tempRoom.summary.mJoinedMemberCount ?? 1) <=
89 : 2) {
90 : // Now we check if the users in the room are none other than the current
91 : // user and the user we want to verify
92 2 : final members = tempRoom.getParticipants([
93 : Membership.invite,
94 : Membership.join,
95 : ]);
96 7 : if (members.every((m) => {userId, client.userID}.contains(m.id))) {
97 : // if so, we use that room
98 : room = tempRoom;
99 : break;
100 : }
101 : }
102 : }
103 : // if there's no direct chat that satisfies the conditions, we create a new one
104 : if (room == null) {
105 4 : final newRoomId = await client.startDirectChat(
106 2 : userId,
107 : enableEncryption: newDirectChatEnableEncryption,
108 : initialState: newDirectChatInitialState,
109 : waitForSync: false,
110 : skipExistingChat: true, // to create a new room directly
111 : );
112 4 : room = client.getRoomById(newRoomId) ??
113 4 : Room(id: newRoomId, client: client);
114 : }
115 :
116 : final request =
117 4 : KeyVerification(encryption: encryption, room: room, userId: userId);
118 2 : await request.start();
119 : // no need to add to the request client object. As we are doing a room
120 : // verification request that'll happen automatically once we know the transaction id
121 : return request;
122 : } else {
123 : // start verification with verified devices
124 1 : final request = KeyVerification(
125 : encryption: encryption,
126 1 : userId: userId,
127 : deviceId: '*',
128 : );
129 1 : await request.start();
130 2 : encryption.keyVerificationManager.addRequest(request);
131 : return request;
132 : }
133 : }
134 :
135 1 : DeviceKeysList.fromDbJson(
136 : Map<String, dynamic> dbEntry,
137 : List<Map<String, dynamic>> childEntries,
138 : List<Map<String, dynamic>> crossSigningEntries,
139 : this.client,
140 1 : ) : userId = dbEntry['user_id'] ?? '' {
141 2 : outdated = dbEntry['outdated'];
142 2 : deviceKeys = {};
143 2 : for (final childEntry in childEntries) {
144 : try {
145 2 : final entry = DeviceKeys.fromDb(childEntry, client);
146 1 : if (!entry.isValid) throw Exception('Invalid device keys');
147 3 : deviceKeys[childEntry['device_id']] = entry;
148 : } catch (e, s) {
149 0 : Logs().w('Skipping invalid user device key', e, s);
150 0 : outdated = true;
151 : }
152 : }
153 2 : for (final crossSigningEntry in crossSigningEntries) {
154 : try {
155 2 : final entry = CrossSigningKey.fromDbJson(crossSigningEntry, client);
156 1 : if (!entry.isValid) throw Exception('Invalid device keys');
157 3 : crossSigningKeys[crossSigningEntry['public_key']] = entry;
158 : } catch (e, s) {
159 0 : Logs().w('Skipping invalid cross siging key', e, s);
160 0 : outdated = true;
161 : }
162 : }
163 : }
164 :
165 31 : DeviceKeysList(this.userId, this.client);
166 : }
167 :
168 : class SimpleSignableKey extends MatrixSignableKey {
169 : @override
170 : String? identifier;
171 :
172 7 : SimpleSignableKey.fromJson(Map<String, dynamic> super.json)
173 7 : : super.fromJson();
174 : }
175 :
176 : abstract class SignableKey extends MatrixSignableKey {
177 : Client client;
178 : Map<String, dynamic>? validSignatures;
179 : bool? _verified;
180 : bool? _blocked;
181 :
182 155 : String? get ed25519Key => keys['ed25519:$identifier'];
183 9 : bool get verified =>
184 33 : identifier != null && (directVerified || crossVerified) && !(blocked);
185 62 : bool get blocked => _blocked ?? false;
186 6 : set blocked(bool isBlocked) => _blocked = isBlocked;
187 :
188 5 : bool get encryptToDevice {
189 5 : if (blocked) return false;
190 :
191 10 : if (identifier == null || ed25519Key == null) return false;
192 :
193 10 : switch (client.shareKeysWith) {
194 5 : case ShareKeysWith.all:
195 : return true;
196 5 : case ShareKeysWith.crossVerifiedIfEnabled:
197 24 : if (client.userDeviceKeys[userId]?.masterKey == null) return true;
198 2 : return hasValidSignatureChain(verifiedByTheirMasterKey: true);
199 1 : case ShareKeysWith.crossVerified:
200 1 : return hasValidSignatureChain(verifiedByTheirMasterKey: true);
201 1 : case ShareKeysWith.directlyVerifiedOnly:
202 1 : return directVerified;
203 : }
204 : }
205 :
206 23 : void setDirectVerified(bool isVerified) {
207 23 : _verified = isVerified;
208 : }
209 :
210 62 : bool get directVerified => _verified ?? false;
211 16 : bool get crossVerified => hasValidSignatureChain();
212 20 : bool get signed => hasValidSignatureChain(verifiedOnly: false);
213 :
214 31 : SignableKey.fromJson(Map<String, dynamic> super.json, this.client)
215 31 : : super.fromJson() {
216 31 : _verified = false;
217 31 : _blocked = false;
218 : }
219 :
220 7 : SimpleSignableKey cloneForSigning() {
221 21 : final newKey = SimpleSignableKey.fromJson(toJson().copy());
222 14 : newKey.identifier = identifier;
223 14 : (newKey.signatures ??= {}).clear();
224 : return newKey;
225 : }
226 :
227 23 : String get signingContent {
228 46 : final data = super.toJson().copy();
229 : // some old data might have the custom verified and blocked keys
230 23 : data.remove('verified');
231 23 : data.remove('blocked');
232 : // remove the keys not needed for signing
233 23 : data.remove('unsigned');
234 23 : data.remove('signatures');
235 46 : return String.fromCharCodes(canonicalJson.encode(data));
236 : }
237 :
238 31 : bool _verifySignature(
239 : String pubKey,
240 : String signature, {
241 : bool isSignatureWithoutLibolmValid = false,
242 : }) {
243 : olm.Utility olmutil;
244 : try {
245 31 : olmutil = olm.Utility();
246 : } catch (e) {
247 : // if no libolm is present we land in this catch block, and return the default
248 : // set if no libolm is there. Some signatures should be assumed-valid while others
249 : // should be assumed-invalid
250 : return isSignatureWithoutLibolmValid;
251 : }
252 : var valid = false;
253 : try {
254 46 : olmutil.ed25519_verify(pubKey, signingContent, signature);
255 : valid = true;
256 : } catch (_) {
257 : // bad signature
258 : valid = false;
259 : } finally {
260 23 : olmutil.free();
261 : }
262 : return valid;
263 : }
264 :
265 12 : bool hasValidSignatureChain({
266 : bool verifiedOnly = true,
267 : Set<String>? visited,
268 : Set<String>? onlyValidateUserIds,
269 :
270 : /// Only check if this key is verified by their Master key.
271 : bool verifiedByTheirMasterKey = false,
272 : }) {
273 24 : if (!client.encryptionEnabled) {
274 : return false;
275 : }
276 :
277 : final visited_ = visited ?? <String>{};
278 : final onlyValidateUserIds_ = onlyValidateUserIds ?? <String>{};
279 :
280 33 : final setKey = '$userId;$identifier';
281 11 : if (visited_.contains(setKey) ||
282 11 : (onlyValidateUserIds_.isNotEmpty &&
283 0 : !onlyValidateUserIds_.contains(userId))) {
284 : return false; // prevent recursion & validate hasValidSignatureChain
285 : }
286 11 : visited_.add(setKey);
287 :
288 11 : if (signatures == null) return false;
289 :
290 33 : for (final signatureEntries in signatures!.entries) {
291 11 : final otherUserId = signatureEntries.key;
292 33 : if (!client.userDeviceKeys.containsKey(otherUserId)) {
293 : continue;
294 : }
295 : // we don't allow transitive trust unless it is for ourself
296 22 : if (otherUserId != userId && otherUserId != client.userID) {
297 : continue;
298 : }
299 33 : for (final signatureEntry in signatureEntries.value.entries) {
300 11 : final fullKeyId = signatureEntry.key;
301 11 : final signature = signatureEntry.value;
302 22 : final keyId = fullKeyId.substring('ed25519:'.length);
303 : // we ignore self-signatures here
304 44 : if (otherUserId == userId && keyId == identifier) {
305 : continue;
306 : }
307 :
308 55 : final key = client.userDeviceKeys[otherUserId]?.deviceKeys[keyId] ??
309 55 : client.userDeviceKeys[otherUserId]?.crossSigningKeys[keyId];
310 : if (key == null) {
311 : continue;
312 : }
313 :
314 10 : if (onlyValidateUserIds_.isNotEmpty &&
315 0 : !onlyValidateUserIds_.contains(key.userId)) {
316 : // we don't want to verify keys from this user
317 : continue;
318 : }
319 :
320 10 : if (key.blocked) {
321 : continue; // we can't be bothered about this keys signatures
322 : }
323 : var haveValidSignature = false;
324 : var gotSignatureFromCache = false;
325 10 : final fullKeyIdBool = validSignatures
326 7 : ?.tryGetMap<String, Object?>(otherUserId)
327 7 : ?.tryGet<bool>(fullKeyId);
328 10 : if (fullKeyIdBool == true) {
329 : haveValidSignature = true;
330 : gotSignatureFromCache = true;
331 10 : } else if (fullKeyIdBool == false) {
332 : haveValidSignature = false;
333 : gotSignatureFromCache = true;
334 : }
335 :
336 10 : if (!gotSignatureFromCache && key.ed25519Key != null) {
337 : // validate the signature manually
338 20 : haveValidSignature = _verifySignature(key.ed25519Key!, signature);
339 20 : final validSignatures = this.validSignatures ??= <String, dynamic>{};
340 10 : if (!validSignatures.containsKey(otherUserId)) {
341 20 : validSignatures[otherUserId] = <String, dynamic>{};
342 : }
343 20 : validSignatures[otherUserId][fullKeyId] = haveValidSignature;
344 : }
345 : if (!haveValidSignature) {
346 : // no valid signature, this key is useless
347 : continue;
348 : }
349 :
350 5 : if ((verifiedOnly && key.directVerified) ||
351 10 : (key is CrossSigningKey &&
352 20 : key.usage.contains('master') &&
353 : (verifiedByTheirMasterKey ||
354 25 : (key.directVerified && key.userId == client.userID)))) {
355 : return true; // we verified this key and it is valid...all checks out!
356 : }
357 : // or else we just recurse into that key and check if it works out
358 10 : final haveChain = key.hasValidSignatureChain(
359 : verifiedOnly: verifiedOnly,
360 : visited: visited_,
361 : onlyValidateUserIds: onlyValidateUserIds,
362 : verifiedByTheirMasterKey: verifiedByTheirMasterKey,
363 : );
364 : if (haveChain) {
365 : return true;
366 : }
367 : }
368 : }
369 : return false;
370 : }
371 :
372 7 : Future<void> setVerified(bool newVerified, [bool sign = true]) async {
373 7 : _verified = newVerified;
374 14 : final encryption = client.encryption;
375 : if (newVerified &&
376 : sign &&
377 : encryption != null &&
378 4 : client.encryptionEnabled &&
379 6 : encryption.crossSigning.signable([this])) {
380 : // sign the key!
381 : // ignore: unawaited_futures
382 6 : encryption.crossSigning.sign([this]);
383 : }
384 : }
385 :
386 : Future<void> setBlocked(bool newBlocked);
387 :
388 31 : @override
389 : Map<String, dynamic> toJson() {
390 62 : final data = super.toJson().copy();
391 : // some old data may have the verified and blocked keys which are unneeded now
392 31 : data.remove('verified');
393 31 : data.remove('blocked');
394 : return data;
395 : }
396 :
397 0 : @override
398 0 : String toString() => json.encode(toJson());
399 :
400 9 : @override
401 9 : bool operator ==(Object other) => (other is SignableKey &&
402 27 : other.userId == userId &&
403 27 : other.identifier == identifier);
404 :
405 9 : @override
406 27 : int get hashCode => Object.hash(userId, identifier);
407 : }
408 :
409 : class CrossSigningKey extends SignableKey {
410 : @override
411 : String? identifier;
412 :
413 62 : String? get publicKey => identifier;
414 : late List<String> usage;
415 :
416 31 : bool get isValid =>
417 62 : userId.isNotEmpty &&
418 31 : publicKey != null &&
419 62 : keys.isNotEmpty &&
420 31 : ed25519Key != null;
421 :
422 5 : @override
423 : Future<void> setVerified(bool newVerified, [bool sign = true]) async {
424 5 : if (!isValid) {
425 0 : throw Exception('setVerified called on invalid key');
426 : }
427 5 : await super.setVerified(newVerified, sign);
428 10 : await client.database
429 15 : ?.setVerifiedUserCrossSigningKey(newVerified, userId, publicKey!);
430 : }
431 :
432 2 : @override
433 : Future<void> setBlocked(bool newBlocked) async {
434 2 : if (!isValid) {
435 0 : throw Exception('setBlocked called on invalid key');
436 : }
437 2 : _blocked = newBlocked;
438 4 : await client.database
439 6 : ?.setBlockedUserCrossSigningKey(newBlocked, userId, publicKey!);
440 : }
441 :
442 31 : CrossSigningKey.fromMatrixCrossSigningKey(
443 : MatrixCrossSigningKey key,
444 : Client client,
445 93 : ) : super.fromJson(key.toJson().copy(), client) {
446 31 : final json = toJson();
447 62 : identifier = key.publicKey;
448 93 : usage = json['usage'].cast<String>();
449 : }
450 :
451 1 : CrossSigningKey.fromDbJson(Map<String, dynamic> dbEntry, Client client)
452 3 : : super.fromJson(Event.getMapFromPayload(dbEntry['content']), client) {
453 1 : final json = toJson();
454 2 : identifier = dbEntry['public_key'];
455 3 : usage = json['usage'].cast<String>();
456 2 : _verified = dbEntry['verified'];
457 2 : _blocked = dbEntry['blocked'];
458 : }
459 :
460 2 : CrossSigningKey.fromJson(Map<String, dynamic> json, Client client)
461 4 : : super.fromJson(json.copy(), client) {
462 2 : final json = toJson();
463 6 : usage = json['usage'].cast<String>();
464 4 : if (keys.isNotEmpty) {
465 8 : identifier = keys.values.first;
466 : }
467 : }
468 : }
469 :
470 : class DeviceKeys extends SignableKey {
471 : @override
472 : String? identifier;
473 :
474 62 : String? get deviceId => identifier;
475 : late List<String> algorithms;
476 : late DateTime lastActive;
477 :
478 155 : String? get curve25519Key => keys['curve25519:$deviceId'];
479 0 : String? get deviceDisplayName =>
480 0 : unsigned?.tryGet<String>('device_display_name');
481 :
482 : bool? _validSelfSignature;
483 31 : bool get selfSigned =>
484 31 : _validSelfSignature ??
485 62 : (_validSelfSignature = deviceId != null &&
486 31 : signatures
487 62 : ?.tryGetMap<String, Object?>(userId)
488 93 : ?.tryGet<String>('ed25519:$deviceId') !=
489 : null &&
490 : // without libolm we still want to be able to add devices. In that case we ofc just can't
491 : // verify the signature
492 31 : _verifySignature(
493 31 : ed25519Key!,
494 186 : signatures![userId]!['ed25519:$deviceId']!,
495 : isSignatureWithoutLibolmValid: true,
496 : ));
497 :
498 31 : @override
499 62 : bool get blocked => super.blocked || !selfSigned;
500 :
501 31 : bool get isValid =>
502 31 : deviceId != null &&
503 62 : keys.isNotEmpty &&
504 31 : curve25519Key != null &&
505 31 : ed25519Key != null &&
506 31 : selfSigned;
507 :
508 3 : @override
509 : Future<void> setVerified(bool newVerified, [bool sign = true]) async {
510 3 : if (!isValid) {
511 : //throw Exception('setVerified called on invalid key');
512 : return;
513 : }
514 3 : await super.setVerified(newVerified, sign);
515 6 : await client.database
516 9 : ?.setVerifiedUserDeviceKey(newVerified, userId, deviceId!);
517 : }
518 :
519 2 : @override
520 : Future<void> setBlocked(bool newBlocked) async {
521 2 : if (!isValid) {
522 : //throw Exception('setBlocked called on invalid key');
523 : return;
524 : }
525 2 : _blocked = newBlocked;
526 4 : await client.database
527 6 : ?.setBlockedUserDeviceKey(newBlocked, userId, deviceId!);
528 : }
529 :
530 31 : DeviceKeys.fromMatrixDeviceKeys(
531 : MatrixDeviceKeys keys,
532 : Client client, [
533 : DateTime? lastActiveTs,
534 93 : ]) : super.fromJson(keys.toJson().copy(), client) {
535 31 : final json = toJson();
536 62 : identifier = keys.deviceId;
537 93 : algorithms = json['algorithms'].cast<String>();
538 62 : lastActive = lastActiveTs ?? DateTime.now();
539 : }
540 :
541 1 : DeviceKeys.fromDb(Map<String, dynamic> dbEntry, Client client)
542 3 : : super.fromJson(Event.getMapFromPayload(dbEntry['content']), client) {
543 1 : final json = toJson();
544 2 : identifier = dbEntry['device_id'];
545 3 : algorithms = json['algorithms'].cast<String>();
546 2 : _verified = dbEntry['verified'];
547 2 : _blocked = dbEntry['blocked'];
548 1 : lastActive =
549 2 : DateTime.fromMillisecondsSinceEpoch(dbEntry['last_active'] ?? 0);
550 : }
551 :
552 4 : DeviceKeys.fromJson(Map<String, dynamic> json, Client client)
553 8 : : super.fromJson(json.copy(), client) {
554 4 : final json = toJson();
555 8 : identifier = json['device_id'];
556 12 : algorithms = json['algorithms'].cast<String>();
557 8 : lastActive = DateTime.fromMillisecondsSinceEpoch(0);
558 : }
559 :
560 1 : Future<KeyVerification> startVerification() async {
561 1 : if (!isValid) {
562 0 : throw Exception('setVerification called on invalid key');
563 : }
564 2 : final encryption = client.encryption;
565 : if (encryption == null) {
566 0 : throw Exception('setVerification called with disabled encryption');
567 : }
568 :
569 1 : final request = KeyVerification(
570 : encryption: encryption,
571 1 : userId: userId,
572 1 : deviceId: deviceId!,
573 : );
574 :
575 1 : await request.start();
576 2 : encryption.keyVerificationManager.addRequest(request);
577 : return request;
578 : }
579 : }
|