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:vodozemac/vodozemac.dart' as vod;
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 33 : CrossSigningKey? getCrossSigningKey(String type) => crossSigningKeys.values
40 38 : .firstWhereOrNull((key) => key.usage.contains(type));
41 :
42 22 : CrossSigningKey? get masterKey => getCrossSigningKey('master');
43 12 : CrossSigningKey? get selfSigningKey => getCrossSigningKey('self_signing');
44 8 : CrossSigningKey? get userSigningKey => getCrossSigningKey('user_signing');
45 :
46 2 : UserVerifiedStatus get verified {
47 2 : if (masterKey == null) {
48 : return UserVerifiedStatus.unknown;
49 : }
50 4 : 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 6 : for (final key in deviceKeys.values) {
59 2 : 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 2 : DeviceKeysList.fromDbJson(
136 : Map<String, dynamic> dbEntry,
137 : List<Map<String, dynamic>> childEntries,
138 : List<Map<String, dynamic>> crossSigningEntries,
139 : this.client,
140 2 : ) : userId = dbEntry['user_id'] ?? '' {
141 4 : outdated = dbEntry['outdated'];
142 4 : deviceKeys = {};
143 3 : 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 4 : for (final crossSigningEntry in crossSigningEntries) {
154 : try {
155 4 : final entry = CrossSigningKey.fromDbJson(crossSigningEntry, client);
156 2 : if (!entry.isValid) throw Exception('Invalid device keys');
157 6 : 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 40 : 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 200 : String? get ed25519Key => keys['ed25519:$identifier'];
183 8 : bool get verified =>
184 30 : identifier != null && (directVerified || crossVerified) && !(blocked);
185 80 : 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 27 : void setDirectVerified(bool isVerified) {
207 27 : _verified = isVerified;
208 : }
209 :
210 80 : bool get directVerified => _verified ?? false;
211 14 : bool get crossVerified => hasValidSignatureChain();
212 22 : bool get signed => hasValidSignatureChain(verifiedOnly: false);
213 :
214 40 : SignableKey.fromJson(Map<String, dynamic> super.json, this.client)
215 40 : : super.fromJson() {
216 40 : _verified = false;
217 40 : _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 28 : String get signingContent {
228 56 : final data = super.toJson().copy();
229 : // some old data might have the custom verified and blocked keys
230 28 : data.remove('verified');
231 28 : data.remove('blocked');
232 : // remove the keys not needed for signing
233 28 : data.remove('unsigned');
234 28 : data.remove('signatures');
235 56 : return String.fromCharCodes(canonicalJson.encode(data));
236 : }
237 :
238 40 : bool _verifySignature(
239 : String pubKey,
240 : String signature, {
241 : bool isSignatureWithoutLibolmValid = false,
242 : }) {
243 : var valid = false;
244 : try {
245 68 : vod.Ed25519PublicKey.fromBase64(pubKey).verify(
246 28 : message: signingContent,
247 28 : signature: vod.Ed25519Signature.fromBase64(signature),
248 : );
249 : valid = true;
250 : } catch (e) {
251 26 : Logs().d('Invalid Ed25519 signature', e);
252 : // bad signature
253 : valid = false;
254 : }
255 : return valid;
256 : }
257 :
258 12 : bool hasValidSignatureChain({
259 : bool verifiedOnly = true,
260 : Set<String>? visited,
261 : Set<String>? onlyValidateUserIds,
262 :
263 : /// Only check if this key is verified by their Master key.
264 : bool verifiedByTheirMasterKey = false,
265 : }) {
266 24 : if (!client.encryptionEnabled) {
267 : return false;
268 : }
269 :
270 : final visited_ = visited ?? <String>{};
271 : final onlyValidateUserIds_ = onlyValidateUserIds ?? <String>{};
272 :
273 36 : final setKey = '$userId;$identifier';
274 12 : if (visited_.contains(setKey) ||
275 12 : (onlyValidateUserIds_.isNotEmpty &&
276 0 : !onlyValidateUserIds_.contains(userId))) {
277 : return false; // prevent recursion & validate hasValidSignatureChain
278 : }
279 12 : visited_.add(setKey);
280 :
281 12 : if (signatures == null) return false;
282 :
283 36 : for (final signatureEntries in signatures!.entries) {
284 12 : final otherUserId = signatureEntries.key;
285 36 : if (!client.userDeviceKeys.containsKey(otherUserId)) {
286 : continue;
287 : }
288 : // we don't allow transitive trust unless it is for ourself
289 24 : if (otherUserId != userId && otherUserId != client.userID) {
290 : continue;
291 : }
292 36 : for (final signatureEntry in signatureEntries.value.entries) {
293 12 : final fullKeyId = signatureEntry.key;
294 12 : final signature = signatureEntry.value;
295 24 : final keyId = fullKeyId.substring('ed25519:'.length);
296 : // we ignore self-signatures here
297 48 : if (otherUserId == userId && keyId == identifier) {
298 : continue;
299 : }
300 :
301 60 : final key = client.userDeviceKeys[otherUserId]?.deviceKeys[keyId] ??
302 60 : client.userDeviceKeys[otherUserId]?.crossSigningKeys[keyId];
303 : if (key == null) {
304 : continue;
305 : }
306 :
307 11 : if (onlyValidateUserIds_.isNotEmpty &&
308 0 : !onlyValidateUserIds_.contains(key.userId)) {
309 : // we don't want to verify keys from this user
310 : continue;
311 : }
312 :
313 11 : if (key.blocked) {
314 : continue; // we can't be bothered about this keys signatures
315 : }
316 : var haveValidSignature = false;
317 : var gotSignatureFromCache = false;
318 11 : final fullKeyIdBool = validSignatures
319 7 : ?.tryGetMap<String, Object?>(otherUserId)
320 7 : ?.tryGet<bool>(fullKeyId);
321 11 : if (fullKeyIdBool == true) {
322 : haveValidSignature = true;
323 : gotSignatureFromCache = true;
324 11 : } else if (fullKeyIdBool == false) {
325 : haveValidSignature = false;
326 : gotSignatureFromCache = true;
327 : }
328 :
329 11 : if (!gotSignatureFromCache && key.ed25519Key != null) {
330 : // validate the signature manually
331 22 : haveValidSignature = _verifySignature(key.ed25519Key!, signature);
332 22 : final validSignatures = this.validSignatures ??= <String, dynamic>{};
333 11 : if (!validSignatures.containsKey(otherUserId)) {
334 22 : validSignatures[otherUserId] = <String, dynamic>{};
335 : }
336 22 : validSignatures[otherUserId][fullKeyId] = haveValidSignature;
337 : }
338 : if (!haveValidSignature) {
339 : // no valid signature, this key is useless
340 : continue;
341 : }
342 :
343 5 : if ((verifiedOnly && key.directVerified) ||
344 11 : (key is CrossSigningKey &&
345 22 : key.usage.contains('master') &&
346 : (verifiedByTheirMasterKey ||
347 26 : (key.directVerified && key.userId == client.userID)))) {
348 : return true; // we verified this key and it is valid...all checks out!
349 : }
350 : // or else we just recurse into that key and check if it works out
351 11 : final haveChain = key.hasValidSignatureChain(
352 : verifiedOnly: verifiedOnly,
353 : visited: visited_,
354 : onlyValidateUserIds: onlyValidateUserIds,
355 : verifiedByTheirMasterKey: verifiedByTheirMasterKey,
356 : );
357 : if (haveChain) {
358 : return true;
359 : }
360 : }
361 : }
362 : return false;
363 : }
364 :
365 7 : Future<void> setVerified(bool newVerified, [bool sign = true]) async {
366 7 : _verified = newVerified;
367 14 : final encryption = client.encryption;
368 : if (newVerified &&
369 : sign &&
370 : encryption != null &&
371 4 : client.encryptionEnabled &&
372 6 : encryption.crossSigning.signable([this])) {
373 : // sign the key!
374 : // ignore: unawaited_futures
375 6 : encryption.crossSigning.sign([this]);
376 : }
377 : }
378 :
379 : Future<void> setBlocked(bool newBlocked);
380 :
381 40 : @override
382 : Map<String, dynamic> toJson() {
383 80 : final data = super.toJson().copy();
384 : // some old data may have the verified and blocked keys which are unneeded now
385 40 : data.remove('verified');
386 40 : data.remove('blocked');
387 : return data;
388 : }
389 :
390 0 : @override
391 0 : String toString() => json.encode(toJson());
392 :
393 9 : @override
394 9 : bool operator ==(Object other) => (other is SignableKey &&
395 27 : other.userId == userId &&
396 27 : other.identifier == identifier);
397 :
398 9 : @override
399 27 : int get hashCode => Object.hash(userId, identifier);
400 : }
401 :
402 : class CrossSigningKey extends SignableKey {
403 : @override
404 : String? identifier;
405 :
406 80 : String? get publicKey => identifier;
407 : late List<String> usage;
408 :
409 40 : bool get isValid =>
410 80 : userId.isNotEmpty &&
411 40 : publicKey != null &&
412 80 : keys.isNotEmpty &&
413 40 : ed25519Key != null;
414 :
415 5 : @override
416 : Future<void> setVerified(bool newVerified, [bool sign = true]) async {
417 5 : if (!isValid) {
418 0 : throw Exception('setVerified called on invalid key');
419 : }
420 5 : await super.setVerified(newVerified, sign);
421 10 : await client.database
422 15 : .setVerifiedUserCrossSigningKey(newVerified, userId, publicKey!);
423 : }
424 :
425 2 : @override
426 : Future<void> setBlocked(bool newBlocked) async {
427 2 : if (!isValid) {
428 0 : throw Exception('setBlocked called on invalid key');
429 : }
430 2 : _blocked = newBlocked;
431 4 : await client.database
432 6 : .setBlockedUserCrossSigningKey(newBlocked, userId, publicKey!);
433 : }
434 :
435 40 : CrossSigningKey.fromMatrixCrossSigningKey(
436 : MatrixCrossSigningKey key,
437 : Client client,
438 120 : ) : super.fromJson(key.toJson().copy(), client) {
439 40 : final json = toJson();
440 80 : identifier = key.publicKey;
441 120 : usage = json['usage'].cast<String>();
442 : }
443 :
444 2 : CrossSigningKey.fromDbJson(Map<String, dynamic> dbEntry, Client client)
445 6 : : super.fromJson(Event.getMapFromPayload(dbEntry['content']), client) {
446 2 : final json = toJson();
447 4 : identifier = dbEntry['public_key'];
448 6 : usage = json['usage'].cast<String>();
449 4 : _verified = dbEntry['verified'];
450 4 : _blocked = dbEntry['blocked'];
451 : }
452 :
453 2 : CrossSigningKey.fromJson(Map<String, dynamic> json, Client client)
454 4 : : super.fromJson(json.copy(), client) {
455 2 : final json = toJson();
456 6 : usage = json['usage'].cast<String>();
457 4 : if (keys.isNotEmpty) {
458 8 : identifier = keys.values.first;
459 : }
460 : }
461 : }
462 :
463 : class DeviceKeys extends SignableKey {
464 : @override
465 : String? identifier;
466 :
467 80 : String? get deviceId => identifier;
468 : late List<String> algorithms;
469 : late DateTime lastActive;
470 :
471 200 : String? get curve25519Key => keys['curve25519:$deviceId'];
472 0 : String? get deviceDisplayName =>
473 0 : unsigned?.tryGet<String>('device_display_name');
474 :
475 : bool? _validSelfSignature;
476 40 : bool get selfSigned =>
477 40 : _validSelfSignature ??
478 80 : (_validSelfSignature = deviceId != null &&
479 40 : signatures
480 80 : ?.tryGetMap<String, Object?>(userId)
481 120 : ?.tryGet<String>('ed25519:$deviceId') !=
482 : null &&
483 : // without libolm we still want to be able to add devices. In that case we ofc just can't
484 : // verify the signature
485 40 : _verifySignature(
486 40 : ed25519Key!,
487 240 : signatures![userId]!['ed25519:$deviceId']!,
488 : isSignatureWithoutLibolmValid: true,
489 : ));
490 :
491 28 : @override
492 56 : bool get blocked => super.blocked || !selfSigned;
493 :
494 40 : bool get isValid =>
495 40 : deviceId != null &&
496 80 : keys.isNotEmpty &&
497 40 : curve25519Key != null &&
498 40 : ed25519Key != null &&
499 40 : selfSigned;
500 :
501 3 : @override
502 : Future<void> setVerified(bool newVerified, [bool sign = true]) async {
503 3 : if (!isValid) {
504 : //throw Exception('setVerified called on invalid key');
505 : return;
506 : }
507 3 : await super.setVerified(newVerified, sign);
508 6 : await client.database
509 9 : .setVerifiedUserDeviceKey(newVerified, userId, deviceId!);
510 : }
511 :
512 2 : @override
513 : Future<void> setBlocked(bool newBlocked) async {
514 2 : if (!isValid) {
515 : //throw Exception('setBlocked called on invalid key');
516 : return;
517 : }
518 2 : _blocked = newBlocked;
519 4 : await client.database
520 6 : .setBlockedUserDeviceKey(newBlocked, userId, deviceId!);
521 : }
522 :
523 40 : DeviceKeys.fromMatrixDeviceKeys(
524 : MatrixDeviceKeys keys,
525 : Client client, [
526 : DateTime? lastActiveTs,
527 120 : ]) : super.fromJson(keys.toJson().copy(), client) {
528 40 : final json = toJson();
529 80 : identifier = keys.deviceId;
530 120 : algorithms = json['algorithms'].cast<String>();
531 80 : lastActive = lastActiveTs ?? DateTime.now();
532 : }
533 :
534 1 : DeviceKeys.fromDb(Map<String, dynamic> dbEntry, Client client)
535 3 : : super.fromJson(Event.getMapFromPayload(dbEntry['content']), client) {
536 1 : final json = toJson();
537 2 : identifier = dbEntry['device_id'];
538 3 : algorithms = json['algorithms'].cast<String>();
539 2 : _verified = dbEntry['verified'];
540 2 : _blocked = dbEntry['blocked'];
541 1 : lastActive =
542 2 : DateTime.fromMillisecondsSinceEpoch(dbEntry['last_active'] ?? 0);
543 : }
544 :
545 4 : DeviceKeys.fromJson(Map<String, dynamic> json, Client client)
546 8 : : super.fromJson(json.copy(), client) {
547 4 : final json = toJson();
548 8 : identifier = json['device_id'];
549 12 : algorithms = json['algorithms'].cast<String>();
550 8 : lastActive = DateTime.fromMillisecondsSinceEpoch(0);
551 : }
552 :
553 1 : Future<KeyVerification> startVerification() async {
554 1 : if (!isValid) {
555 0 : throw Exception('setVerification called on invalid key');
556 : }
557 2 : final encryption = client.encryption;
558 : if (encryption == null) {
559 0 : throw Exception('setVerification called with disabled encryption');
560 : }
561 :
562 1 : final request = KeyVerification(
563 : encryption: encryption,
564 1 : userId: userId,
565 1 : deviceId: deviceId!,
566 : );
567 :
568 1 : await request.start();
569 2 : encryption.keyVerificationManager.addRequest(request);
570 : return request;
571 : }
572 : }
|