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.8 % 240 225
Test Date: 2025-02-14 21:16:54 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           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              : }
        

Generated by: LCOV version 2.0-1