LCOV - code coverage report
Current view: top level - lib/src/utils - device_keys_list.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 217 232 93.5 %
Date: 2024-11-12 07:37:08 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 1.14