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