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