LCOV - code coverage report
Current view: top level - lib/src/utils - device_keys_list.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 93.5 % 232 217
Test Date: 2025-01-14 13:39:53 Functions: - 0 0

            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              : }
        

Generated by: LCOV version 2.0-1