LCOV - code coverage report
Current view: top level - lib/src/utils - device_keys_list.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 215 230 93.5 %
Date: 2024-09-04 20:26:16 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           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          30 :   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         150 :   String? get ed25519Key => keys['ed25519:$identifier'];
     153           9 :   bool get verified =>
     154          33 :       identifier != null && (directVerified || crossVerified) && !(blocked);
     155          60 :   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          60 :   bool get directVerified => _verified ?? false;
     171          16 :   bool get crossVerified => hasValidSignatureChain();
     172          20 :   bool get signed => hasValidSignatureChain(verifiedOnly: false);
     173             : 
     174          30 :   SignableKey.fromJson(Map<String, dynamic> super.json, this.client)
     175          30 :       : super.fromJson() {
     176          30 :     _verified = false;
     177          30 :     _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          30 :   bool _verifySignature(String pubKey, String signature,
     199             :       {bool isSignatureWithoutLibolmValid = false}) {
     200             :     olm.Utility olmutil;
     201             :     try {
     202          30 :       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           6 :             ?.tryGetMap<String, Object?>(otherUserId)
     284           6 :             ?.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          30 :   @override
     345             :   Map<String, dynamic> toJson() {
     346          60 :     final data = super.toJson().copy();
     347             :     // some old data may have the verified and blocked keys which are unneeded now
     348          30 :     data.remove('verified');
     349          30 :     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          60 :   String? get publicKey => identifier;
     370             :   late List<String> usage;
     371             : 
     372          30 :   bool get isValid =>
     373          60 :       userId.isNotEmpty &&
     374          30 :       publicKey != null &&
     375          60 :       keys.isNotEmpty &&
     376          30 :       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          30 :   CrossSigningKey.fromMatrixCrossSigningKey(
     399             :       MatrixCrossSigningKey key, Client client)
     400          90 :       : super.fromJson(key.toJson().copy(), client) {
     401          30 :     final json = toJson();
     402          60 :     identifier = key.publicKey;
     403          90 :     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          60 :   String? get deviceId => identifier;
     430             :   late List<String> algorithms;
     431             :   late DateTime lastActive;
     432             : 
     433         150 :   String? get curve25519Key => keys['curve25519:$deviceId'];
     434           0 :   String? get deviceDisplayName =>
     435           0 :       unsigned?.tryGet<String>('device_display_name');
     436             : 
     437             :   bool? _validSelfSignature;
     438          30 :   bool get selfSigned =>
     439          30 :       _validSelfSignature ??
     440          60 :       (_validSelfSignature = deviceId != null &&
     441          30 :           signatures
     442          60 :                   ?.tryGetMap<String, Object?>(userId)
     443          90 :                   ?.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          30 :           _verifySignature(
     448         210 :               ed25519Key!, signatures![userId]!['ed25519:$deviceId']!,
     449             :               isSignatureWithoutLibolmValid: true));
     450             : 
     451          30 :   @override
     452          60 :   bool get blocked => super.blocked || !selfSigned;
     453             : 
     454          30 :   bool get isValid =>
     455          30 :       deviceId != null &&
     456          60 :       keys.isNotEmpty &&
     457          30 :       curve25519Key != null &&
     458          30 :       ed25519Key != null &&
     459          30 :       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          30 :   DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys keys, Client client,
     484             :       [DateTime? lastActiveTs])
     485          90 :       : super.fromJson(keys.toJson().copy(), client) {
     486          30 :     final json = toJson();
     487          60 :     identifier = keys.deviceId;
     488          90 :     algorithms = json['algorithms'].cast<String>();
     489          60 :     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             : }

Generated by: LCOV version 1.14