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