diff --git a/analysis_options.yaml b/analysis_options.yaml index 3cf0b93f..9e295c27 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,6 +12,9 @@ analyzer: errors: todo: ignore import_of_legacy_library_into_null_safe: ignore + # ignore those until we are completely nullsafe + invalid_null_aware_operator: ignore + unnecessary_null_comparison: ignore exclude: - example/main.dart # needed until crypto packages upgrade diff --git a/lib/encryption.dart b/lib/encryption.dart index a71d1d70..f534f836 100644 --- a/lib/encryption.dart +++ b/lib/encryption.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2020, 2021 Famedly GmbH diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index 531ec4de..bffb0fc2 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2019, 2020, 2021 Famedly GmbH @@ -40,21 +39,21 @@ class Encryption { /// Returns the base64 encoded keys to store them in a store. /// This String should **never** leave the device! - String get pickledOlmAccount => olmManager.pickledOlmAccount; + String? get pickledOlmAccount => olmManager.pickledOlmAccount; - String get fingerprintKey => olmManager.fingerprintKey; - String get identityKey => olmManager.identityKey; + String? get fingerprintKey => olmManager.fingerprintKey; + String? get identityKey => olmManager.identityKey; - KeyManager keyManager; - OlmManager olmManager; - KeyVerificationManager keyVerificationManager; - CrossSigning crossSigning; - SSSS ssss; + late KeyManager keyManager; + late OlmManager olmManager; + late KeyVerificationManager keyVerificationManager; + late CrossSigning crossSigning; + late SSSS ssss; Encryption({ - this.client, - this.debug, - this.enableE2eeRecovery, + required this.client, + this.debug = false, + required this.enableE2eeRecovery, }) { ssss = SSSS(this); keyManager = KeyManager(this); @@ -81,7 +80,7 @@ class Encryption { } } - Bootstrap bootstrap({void Function() onUpdate}) => Bootstrap( + Bootstrap bootstrap({void Function()? onUpdate}) => Bootstrap( encryption: this, onUpdate: onUpdate, ); @@ -190,18 +189,24 @@ class Encryption { } final sessionId = content.sessionId; final senderKey = content.senderKey; + if (sessionId == null) { + throw DecryptException(DecryptException.unknownSession); + } + final inboundGroupSession = keyManager.getInboundGroupSession(roomId, sessionId, senderKey); - if (inboundGroupSession == null) { + if (!(inboundGroupSession?.isValid ?? false)) { canRequestSession = true; throw DecryptException(DecryptException.unknownSession); } + // decrypt errors here may mean we have a bad session key - others might have a better one canRequestSession = true; - final decryptResult = inboundGroupSession.inboundGroupSession - .decrypt(content.ciphertextMegolm); + final decryptResult = inboundGroupSession!.inboundGroupSession! + .decrypt(content.ciphertextMegolm!); canRequestSession = false; + // we can't have the key be an int, else json-serializing will fail, thus we need it to be a string final messageIndexKey = 'key-' + decryptResult.message_index.toString(); final messageIndexValue = event.eventId + @@ -214,6 +219,7 @@ class Encryption { Logs().e('[Decrypt] Could not decrypt due to a corrupted session.'); throw DecryptException(DecryptException.channelCorrupted); } + inboundGroupSession.indexes[messageIndexKey] = messageIndexValue; if (!haveIndex) { // now we persist the udpated indexes into the database. @@ -282,9 +288,11 @@ class Encryption { } try { if (client.database != null && - keyManager.getInboundGroupSession(roomId, event.content['session_id'], - event.content['sender_key']) == - null) { + !(keyManager + .getInboundGroupSession(roomId, event.content['session_id'], + event.content['sender_key']) + ?.isValid ?? + false)) { await keyManager.loadInboundGroupSession( roomId, event.content['session_id'], event.content['sender_key']); } @@ -325,21 +333,21 @@ class Encryption { if (room.encryptionAlgorithm != AlgorithmTypes.megolmV1AesSha2) { throw ('Unknown encryption algorithm'); } - if (keyManager.getOutboundGroupSession(roomId) == null) { + if (keyManager.getOutboundGroupSession(roomId)?.isValid != true) { await keyManager.loadOutboundGroupSession(roomId); } await keyManager.clearOrUseOutboundGroupSession(roomId); - if (keyManager.getOutboundGroupSession(roomId) == null) { + if (keyManager.getOutboundGroupSession(roomId)?.isValid != true) { await keyManager.createOutboundGroupSession(roomId); } final sess = keyManager.getOutboundGroupSession(roomId); - if (sess == null) { + if (sess?.isValid != true) { throw ('Unable to create new outbound group session'); } // we clone the payload as we do not want to remove 'm.relates_to' from the // original payload passed into this function payload = payload.copy(); - final Map mRelatesTo = payload.remove('m.relates_to'); + final Map? mRelatesTo = payload.remove('m.relates_to'); final payloadContent = { 'content': payload, 'type': type, @@ -348,7 +356,7 @@ class Encryption { final encryptedPayload = { 'algorithm': AlgorithmTypes.megolmV1AesSha2, 'ciphertext': - sess.outboundGroupSession.encrypt(json.encode(payloadContent)), + sess!.outboundGroupSession.encrypt(json.encode(payloadContent)), 'device_id': client.deviceID, 'sender_key': identityKey, 'session_id': sess.outboundGroupSession.session_id(), @@ -369,7 +377,7 @@ class Encryption { // check if we can set our own master key as verified, if it isn't yet if (client.database != null && client.userDeviceKeys.containsKey(client.userID)) { - final masterKey = client.userDeviceKeys[client.userID].masterKey; + final masterKey = client.userDeviceKeys[client.userID]!.masterKey; if (masterKey != null && !masterKey.directVerified && masterKey @@ -405,7 +413,7 @@ class Encryption { class DecryptException implements Exception { String cause; - String libolmMessage; + String? libolmMessage; DecryptException(this.cause, [this.libolmMessage]); @override diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 7d23f912..1023d080 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2019, 2020, 2021 Famedly GmbH @@ -21,6 +20,7 @@ import 'dart:convert'; import 'package:matrix/encryption/utils/stored_inbound_group_session.dart'; import 'package:olm/olm.dart' as olm; +import 'package:collection/collection.dart'; import './encryption.dart'; import './utils/outbound_group_session.dart'; @@ -83,17 +83,23 @@ class KeyManager { _inboundGroupSessions.clear(); } - void setInboundGroupSession(String roomId, String sessionId, String senderKey, - Map content, - {bool forwarded = false, - Map senderClaimedKeys, - bool uploaded = false, - Map> allowedAtIndex}) { - senderClaimedKeys ??= {}; - if (!senderClaimedKeys.containsKey('ed25519')) { + void setInboundGroupSession( + String roomId, + String sessionId, + String senderKey, + Map content, { + bool forwarded = false, + Map? senderClaimedKeys, + bool uploaded = false, + Map>? allowedAtIndex, + }) { + final senderClaimedKeys_ = senderClaimedKeys ?? {}; + final allowedAtIndex_ = allowedAtIndex ?? >{}; + + if (!senderClaimedKeys_.containsKey('ed25519')) { final device = client.getUserDeviceKeysByCurve25519Key(senderKey); - if (device != null) { - senderClaimedKeys['ed25519'] = device.ed25519Key; + if (device != null && device.ed25519Key != null) { + senderClaimedKeys_['ed25519'] = device.ed25519Key!; } } final oldSession = @@ -101,7 +107,7 @@ class KeyManager { if (content['algorithm'] != AlgorithmTypes.megolmV1AesSha2) { return; } - olm.InboundGroupSession inboundGroupSession; + late olm.InboundGroupSession inboundGroupSession; try { inboundGroupSession = olm.InboundGroupSession(); if (forwarded) { @@ -122,12 +128,12 @@ class KeyManager { sessionId: sessionId, key: client.userID, senderKey: senderKey, - senderClaimedKeys: senderClaimedKeys, - allowedAtIndex: allowedAtIndex, + senderClaimedKeys: senderClaimedKeys_, + allowedAtIndex: allowedAtIndex_, ); final oldFirstIndex = oldSession?.inboundGroupSession?.first_known_index() ?? 0; - final newFirstIndex = newSession.inboundGroupSession.first_known_index(); + final newFirstIndex = newSession.inboundGroupSession!.first_known_index(); if (oldSession == null || newFirstIndex < oldFirstIndex || (oldFirstIndex == newFirstIndex && @@ -143,7 +149,7 @@ class KeyManager { if (!_inboundGroupSessions.containsKey(roomId)) { _inboundGroupSessions[roomId] = {}; } - _inboundGroupSessions[roomId][sessionId] = newSession; + _inboundGroupSessions[roomId]![sessionId] = newSession; if (!client.isLogged() || client.encryption == null) { return; } @@ -154,11 +160,11 @@ class KeyManager { inboundGroupSession.pickle(client.userID), json.encode(content), json.encode({}), - json.encode(allowedAtIndex ?? {}), + json.encode(allowedAtIndex_), senderKey, - json.encode(senderClaimedKeys), + json.encode(senderClaimedKeys_), ) - ?.then((_) { + .then((_) { if (!client.isLogged() || client.encryption == null) { return; } @@ -180,12 +186,12 @@ class KeyManager { } } - SessionKey getInboundGroupSession( + SessionKey? getInboundGroupSession( String roomId, String sessionId, String senderKey, {bool otherRooms = true}) { if (_inboundGroupSessions.containsKey(roomId) && - _inboundGroupSessions[roomId].containsKey(sessionId)) { - final sess = _inboundGroupSessions[roomId][sessionId]; + _inboundGroupSessions[roomId]!.containsKey(sessionId)) { + final sess = _inboundGroupSessions[roomId]![sessionId]!; if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) { return null; } @@ -197,7 +203,7 @@ class KeyManager { // search if this session id is *somehow* found in another room for (final val in _inboundGroupSessions.values) { if (val.containsKey(sessionId)) { - final sess = val[sessionId]; + final sess = val[sessionId]!; if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) { return null; } @@ -223,14 +229,11 @@ class KeyManager { } /// Loads an inbound group session - Future loadInboundGroupSession( + Future loadInboundGroupSession( String roomId, String sessionId, String senderKey) async { - if (roomId == null || sessionId == null || senderKey == null) { - return null; - } if (_inboundGroupSessions.containsKey(roomId) && - _inboundGroupSessions[roomId].containsKey(sessionId)) { - final sess = _inboundGroupSessions[roomId][sessionId]; + _inboundGroupSessions[roomId]!.containsKey(sessionId)) { + final sess = _inboundGroupSessions[roomId]![sessionId]!; if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) { return null; // sender keys do not match....better not do anything } @@ -246,10 +249,11 @@ class KeyManager { _inboundGroupSessions[roomId] = {}; } if (!sess.isValid || - (sess.senderKey.isNotEmpty && sess.senderKey != senderKey)) { + sess.senderKey.isEmpty || + sess.senderKey != senderKey) { return null; } - _inboundGroupSessions[roomId][sessionId] = sess; + _inboundGroupSessions[roomId]![sessionId] = sess; return sess; } @@ -257,10 +261,14 @@ class KeyManager { List deviceKeys) { final deviceKeyIds = >{}; for (final device in deviceKeys) { + if (device.deviceId == null) { + Logs().w('[KeyManager] ignoring device without deviceid'); + continue; + } if (!deviceKeyIds.containsKey(device.userId)) { deviceKeyIds[device.userId] = {}; } - deviceKeyIds[device.userId][device.deviceId] = !device.encryptToDevice; + deviceKeyIds[device.userId]![device.deviceId!] = !device.encryptToDevice; } return deviceKeyIds; } @@ -280,6 +288,7 @@ class KeyManager { if (room == null || sess == null) { return true; } + if (!wipe) { // first check if it needs to be rotated final encryptionContent = @@ -294,8 +303,13 @@ class KeyManager { wipe = true; } } + final inboundSess = await loadInboundGroupSession(room.id, - sess.outboundGroupSession.session_id(), encryption.identityKey); + sess.outboundGroupSession.session_id(), encryption.identityKey!); + if (inboundSess == null) { + wipe = true; + } + if (!wipe) { // next check if the devices in the room changed final devicesToReceive = []; @@ -319,13 +333,17 @@ class KeyManager { // for this it is enough if we iterate over the old user Ids, as the new ones already have the needed keys in the list. // we also know that all the old user IDs appear in the old one, else we have already wiped the session for (final userId in oldUserIds) { - final oldBlockedDevices = Set.from(sess.devices[userId].entries - .where((e) => e.value) - .map((e) => e.key)); - final newBlockedDevices = Set.from(newDeviceKeyIds[userId] - .entries - .where((e) => e.value) - .map((e) => e.key)); + final oldBlockedDevices = sess.devices.containsKey(userId) + ? Set.from(sess.devices[userId]!.entries + .where((e) => e.value) + .map((e) => e.key)) + : {}; + final newBlockedDevices = newDeviceKeyIds.containsKey(userId) + ? Set.from(newDeviceKeyIds[userId]! + .entries + .where((e) => e.value) + .map((e) => e.key)) + : {}; // we don't really care about old devices that got dropped (deleted), we only care if new ones got added and if new ones got blocked // check if new devices got blocked if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) { @@ -333,13 +351,17 @@ class KeyManager { break; } // and now add all the new devices! - final oldDeviceIds = Set.from(sess.devices[userId].entries - .where((e) => !e.value) - .map((e) => e.key)); - final newDeviceIds = Set.from(newDeviceKeyIds[userId] - .entries - .where((e) => !e.value) - .map((e) => e.key)); + final oldDeviceIds = sess.devices.containsKey(userId) + ? Set.from(sess.devices[userId]!.entries + .where((e) => !e.value) + .map((e) => e.key)) + : {}; + final newDeviceIds = newDeviceKeyIds.containsKey(userId) + ? Set.from(newDeviceKeyIds[userId]! + .entries + .where((e) => !e.value) + .map((e) => e.key)) + : {}; final newDevices = newDeviceIds.difference(oldDeviceIds); if (newDeviceIds.isNotEmpty) { devicesToReceive.addAll(newDeviceKeys.where( @@ -366,20 +388,20 @@ class KeyManager { if (devicesToReceive.isNotEmpty) { // update allowedAtIndex for (final device in devicesToReceive) { - inboundSess.allowedAtIndex[device.userId] ??= {}; - if (!inboundSess.allowedAtIndex[device.userId] + inboundSess!.allowedAtIndex[device.userId] ??= {}; + if (!inboundSess.allowedAtIndex[device.userId]! .containsKey(device.curve25519Key) || - inboundSess.allowedAtIndex[device.userId] - [device.curve25519Key] > + inboundSess.allowedAtIndex[device.userId]![ + device.curve25519Key]! > sess.outboundGroupSession.message_index()) { - inboundSess.allowedAtIndex[device.userId] - [device.curve25519Key] = + inboundSess + .allowedAtIndex[device.userId]![device.curve25519Key!] = sess.outboundGroupSession.message_index(); } } if (client.database != null) { await client.database.updateInboundGroupSessionAllowedAtIndex( - json.encode(inboundSess.allowedAtIndex), + json.encode(inboundSess!.allowedAtIndex), room.id, sess.outboundGroupSession.session_id()); } @@ -405,9 +427,6 @@ class KeyManager { /// Store an outbound group session in the database Future storeOutboundGroupSession( String roomId, OutboundGroupSession sess) async { - if (sess == null) { - return; - } await client.database?.storeOutboundGroupSession( roomId, sess.outboundGroupSession.pickle(client.userID), @@ -422,12 +441,12 @@ class KeyManager { /// Creates an outbound group session for a given room id Future createOutboundGroupSession(String roomId) async { if (_pendingNewOutboundGroupSessions.containsKey(roomId)) { - return _pendingNewOutboundGroupSessions[roomId]; + return _pendingNewOutboundGroupSessions[roomId]!; } _pendingNewOutboundGroupSessions[roomId] = _createOutboundGroupSession(roomId); await _pendingNewOutboundGroupSessions[roomId]; - return _pendingNewOutboundGroupSessions.remove(roomId); + return _pendingNewOutboundGroupSessions.remove(roomId)!; } /// Prepares an outbound group session for a given room ID. That is, load it from @@ -447,7 +466,8 @@ class KeyManager { await clearOrUseOutboundGroupSession(roomId, wipe: true); final room = client.getRoomById(roomId); if (room == null) { - return null; + throw Exception( + 'Tried to create a megolm session in a non-existing room ($roomId)!'); } final deviceKeys = await room.getUserDeviceKeys(); final deviceKeyIds = _getDeviceKeyIdMap(deviceKeys); @@ -458,7 +478,7 @@ class KeyManager { } catch (e, s) { outboundGroupSession.free(); Logs().e('[LibOlm] Unable to create new outboundGroupSession', e, s); - return null; + rethrow; } final rawSession = { 'algorithm': AlgorithmTypes.megolmV1AesSha2, @@ -468,12 +488,16 @@ class KeyManager { }; final allowedAtIndex = >{}; for (final device in deviceKeys) { + if (!device.isValid) { + Logs().e('Skipping invalid device'); + continue; + } allowedAtIndex[device.userId] ??= {}; - allowedAtIndex[device.userId][device.curve25519Key] = + allowedAtIndex[device.userId]![device.curve25519Key!] = outboundGroupSession.message_index(); } setInboundGroupSession( - roomId, rawSession['session_id'], encryption.identityKey, rawSession, + roomId, rawSession['session_id'], encryption.identityKey!, rawSession, allowedAtIndex: allowedAtIndex); final sess = OutboundGroupSession( devices: deviceKeyIds, @@ -493,13 +517,13 @@ class KeyManager { e, s); sess.dispose(); - return null; + rethrow; } return sess; } /// Get an outbound group session for a room id - OutboundGroupSession getOutboundGroupSession(String roomId) { + OutboundGroupSession? getOutboundGroupSession(String roomId) { return _outboundGroupSessions[roomId]; } @@ -528,8 +552,8 @@ class KeyManager { return (await encryption.ssss.getCached(megolmKey)) != null; } - GetRoomKeysVersionCurrentResponse _roomKeysVersionCache; - DateTime _roomKeysVersionCacheDate; + GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache; + DateTime? _roomKeysVersionCacheDate; Future getRoomKeysBackupInfo( [bool useCache = true]) async { if (_roomKeysVersionCache != null && @@ -537,12 +561,12 @@ class KeyManager { useCache && DateTime.now() .subtract(Duration(minutes: 5)) - .isBefore(_roomKeysVersionCacheDate)) { - return _roomKeysVersionCache; + .isBefore(_roomKeysVersionCacheDate!)) { + return _roomKeysVersionCache!; } _roomKeysVersionCache = await client.getRoomKeysVersionCurrent(); _roomKeysVersionCacheDate = DateTime.now(); - return _roomKeysVersionCache; + return _roomKeysVersionCache!; } Future loadFromResponse(RoomKeys keys) async { @@ -550,15 +574,14 @@ class KeyManager { return; } final privateKey = - base64.decode(await encryption.ssss.getCached(megolmKey)); + base64.decode((await encryption.ssss.getCached(megolmKey))!); final decryption = olm.PkDecryption(); final info = await getRoomKeysBackupInfo(); String backupPubKey; try { backupPubKey = decryption.init_with_private_key(privateKey); - if (backupPubKey == null || - info.algorithm != BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 || + if (info.algorithm != BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 || info.authData['public_key'] != backupPubKey) { return; } @@ -567,17 +590,11 @@ class KeyManager { for (final sessionEntry in roomEntry.value.sessions.entries) { final sessionId = sessionEntry.key; final session = sessionEntry.value; - final firstMessageIndex = session.firstMessageIndex; - final forwardedCount = session.forwardedCount; - final isVerified = session.isVerified; final sessionData = session.sessionData; - if (firstMessageIndex == null || - forwardedCount == null || - isVerified == null || - !(sessionData is Map)) { + if (!(sessionData is Map)) { continue; } - Map decrypted; + Map? decrypted; try { decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], sessionData['mac'], sessionData['ciphertext'])); @@ -591,8 +608,9 @@ class KeyManager { roomId, sessionId, decrypted['sender_key'], decrypted, forwarded: true, senderClaimedKeys: decrypted['sender_claimed_keys'] != null - ? Map.from(decrypted['sender_claimed_keys']) - : null, + ? Map.from( + decrypted['sender_claimed_keys']!) + : {}, uploaded: true); } } @@ -702,7 +720,7 @@ class KeyManager { return; // nothing to do } final privateKey = - base64.decode(await encryption.ssss.getCached(megolmKey)); + base64.decode((await encryption.ssss.getCached(megolmKey))!); // decryption is needed to calculate the public key and thus see if the claimed information is in fact valid final decryption = olm.PkDecryption(); final info = await getRoomKeysBackupInfo(false); @@ -710,8 +728,7 @@ class KeyManager { try { backupPubKey = decryption.init_with_private_key(privateKey); - if (backupPubKey == null || - info.algorithm != + if (info.algorithm != BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 || info.authData['public_key'] != backupPubKey) { return; @@ -775,13 +792,13 @@ class KeyManager { return; // no body } if (!client.userDeviceKeys.containsKey(event.sender) || - !client.userDeviceKeys[event.sender].deviceKeys + !client.userDeviceKeys[event.sender]!.deviceKeys .containsKey(event.content['requesting_device_id'])) { Logs().i('[KeyManager] Device not found, doing nothing'); return; // device not found } - final device = client.userDeviceKeys[event.sender] - .deviceKeys[event.content['requesting_device_id']]; + final device = client.userDeviceKeys[event.sender]! + .deviceKeys[event.content['requesting_device_id']]!; if (device.userId == client.userID && device.deviceId == client.deviceID) { Logs().i('[KeyManager] Request is by ourself, ignoring'); @@ -824,13 +841,13 @@ class KeyManager { } else if (device.encryptToDevice && session.allowedAtIndex .tryGet>(device.userId) - ?.tryGet(device.curve25519Key) != + ?.tryGet(device.curve25519Key!) != null) { // if we know the user may see the message, then we can just forward the key. // we do not need to check if the device is verified, just if it is not blocked, // as that is the logic we already initially try to send out the room keys. final index = - session.allowedAtIndex[device.userId][device.curve25519Key]; + session.allowedAtIndex[device.userId]![device.curve25519Key]!; Logs().i( '[KeyManager] Valid foreign request, forwarding key at index $index...'); await roomKeyRequest.forwardKey(index); @@ -846,7 +863,7 @@ class KeyManager { return; // we don't know this request anyways } // alright, let's just cancel this request - final request = incomingShareRequests[event.content['request_id']]; + final request = incomingShareRequests[event.content['request_id']]!; request.canceled = true; incomingShareRequests.remove(request.requestId); } @@ -855,20 +872,16 @@ class KeyManager { if (event.encryptedContent == null) { return; // event wasn't encrypted, this is a security risk } - final request = outgoingShareRequests.values.firstWhere( - (r) => - r.room.id == event.content['room_id'] && - r.sessionId == event.content['session_id'] && - r.senderKey == event.content['sender_key'], - orElse: () => null); + final request = outgoingShareRequests.values.firstWhereOrNull((r) => + r.room.id == event.content['room_id'] && + r.sessionId == event.content['session_id'] && + r.senderKey == event.content['sender_key']); if (request == null || request.canceled) { return; // no associated request found or it got canceled } - final device = request.devices.firstWhere( - (d) => - d.userId == event.sender && - d.curve25519Key == event.encryptedContent['sender_key'], - orElse: () => null); + final device = request.devices.firstWhereOrNull((d) => + d.userId == event.sender && + d.curve25519Key == event.encryptedContent['sender_key']); if (device == null) { return; // someone we didn't send our request to replied....better ignore this } @@ -904,7 +917,7 @@ class KeyManager { if (!data.containsKey(device.userId)) { data[device.userId] = {}; } - data[device.userId][device.deviceId] = sendToDeviceMessage; + data[device.userId]![device.deviceId!] = sendToDeviceMessage; } await client.sendToDevice( EventTypes.RoomKeyRequest, @@ -921,11 +934,11 @@ class KeyManager { final String roomId = event.content['room_id']; final String sessionId = event.content['session_id']; if (client.userDeviceKeys.containsKey(event.sender) && - client.userDeviceKeys[event.sender].deviceKeys + client.userDeviceKeys[event.sender]!.deviceKeys .containsKey(event.content['requesting_device_id'])) { event.content['sender_claimed_ed25519_key'] = client - .userDeviceKeys[event.sender] - .deviceKeys[event.content['requesting_device_id']] + .userDeviceKeys[event.sender]! + .deviceKeys[event.content['requesting_device_id']]! .ed25519Key; } Logs().v('[KeyManager] Keeping room key'); @@ -956,21 +969,20 @@ class KeyManagerKeyShareRequest { bool canceled; KeyManagerKeyShareRequest( - {this.requestId, - this.devices, - this.room, - this.sessionId, - this.senderKey, - this.canceled = false}); + {required this.requestId, + List? devices, + required this.room, + required this.sessionId, + required this.senderKey, + this.canceled = false}) + : devices = devices ?? []; } class RoomKeyRequest extends ToDeviceEvent { KeyManager keyManager; KeyManagerKeyShareRequest request; - RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent, - KeyManager keyManager, KeyManagerKeyShareRequest request) { - this.keyManager = keyManager; - this.request = request; + RoomKeyRequest.fromToDeviceEvent( + ToDeviceEvent toDeviceEvent, this.keyManager, this.request) { sender = toDeviceEvent.sender; content = toDeviceEvent.content; type = toDeviceEvent.type; @@ -980,7 +992,7 @@ class RoomKeyRequest extends ToDeviceEvent { DeviceKeys get requestingDevice => request.devices.first; - Future forwardKey([int index]) async { + Future forwardKey([int? index]) async { if (request.canceled) { keyManager.incomingShareRequests.remove(request.requestId); return; // request is canceled, don't send anything @@ -988,21 +1000,28 @@ class RoomKeyRequest extends ToDeviceEvent { final room = this.room; final session = await keyManager.loadInboundGroupSession( room.id, request.sessionId, request.senderKey); + if (session == null) { + Logs().v("[KeyManager] Not forwarding key we don't have"); + return; + } + if (session.inboundGroupSession == null) { + Logs().v("[KeyManager] Not forwarding key we don't have"); + return; + } + final message = session.content.copy(); message['forwarding_curve25519_key_chain'] = List.from(session.forwardingCurve25519KeyChain); message['sender_key'] = - (session.senderKey != null && session.senderKey.isNotEmpty) - ? session.senderKey - : request.senderKey; + (session.senderKey.isNotEmpty) ? session.senderKey : request.senderKey; message['sender_claimed_ed25519_key'] = session.senderClaimedKeys['ed25519'] ?? (session.forwardingCurve25519KeyChain.isEmpty ? keyManager.encryption.fingerprintKey : null); - message['session_key'] = session.inboundGroupSession.export_session( - index ?? session.inboundGroupSession.first_known_index()); + message['session_key'] = session.inboundGroupSession!.export_session( + index ?? session.inboundGroupSession!.first_known_index()); // send the actual reply of the key back to the requester await keyManager.client.sendToDeviceEncrypted( [requestingDevice], @@ -1034,16 +1053,16 @@ RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) { 'forwarding_curve25519_key_chain': sess.forwardingCurve25519KeyChain, 'sender_key': sess.senderKey, 'sender_clencaimed_keys': sess.senderClaimedKeys, - 'session_key': sess.inboundGroupSession - .export_session(sess.inboundGroupSession.first_known_index()), + 'session_key': sess.inboundGroupSession! + .export_session(sess.inboundGroupSession!.first_known_index()), }; // encrypt the content final encrypted = enc.encrypt(json.encode(payload)); // fetch the device, if available... //final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey); // aaaand finally add the session key to our payload - roomKeys.rooms[sess.roomId].sessions[sess.sessionId] = KeyBackupData( - firstMessageIndex: sess.inboundGroupSession.first_known_index(), + roomKeys.rooms[sess.roomId]!.sessions[sess.sessionId] = KeyBackupData( + firstMessageIndex: sess.inboundGroupSession!.first_known_index(), forwardedCount: sess.forwardingCurve25519KeyChain.length, isVerified: dbSession.verified, //device?.verified ?? false, sessionData: { @@ -1063,14 +1082,16 @@ RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) { } class _DbInboundGroupSessionBundle { - _DbInboundGroupSessionBundle({this.dbSession, this.verified}); + _DbInboundGroupSessionBundle( + {required this.dbSession, required this.verified}); StoredInboundGroupSession dbSession; bool verified; } class _GenerateUploadKeysArgs { - _GenerateUploadKeysArgs({this.pubkey, this.dbSessions, this.userId}); + _GenerateUploadKeysArgs( + {required this.pubkey, required this.dbSessions, required this.userId}); String pubkey; List<_DbInboundGroupSessionBundle> dbSessions; diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index 85a34db1..a417b51c 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2019, 2020, 2021 Famedly GmbH @@ -20,6 +19,7 @@ import 'dart:convert'; import 'package:canonical_json/canonical_json.dart'; +import 'package:collection/collection.dart'; import 'package:matrix/matrix.dart'; import 'package:olm/olm.dart' as olm; @@ -31,16 +31,16 @@ import 'utils/olm_session.dart'; class OlmManager { final Encryption encryption; Client get client => encryption.client; - olm.Account _olmAccount; + olm.Account? _olmAccount; /// Returns the base64 encoded keys to store them in a store. /// This String should **never** leave the device! - String get pickledOlmAccount => - enabled ? _olmAccount.pickle(client.userID) : null; - String get fingerprintKey => - enabled ? json.decode(_olmAccount.identity_keys())['ed25519'] : null; - String get identityKey => - enabled ? json.decode(_olmAccount.identity_keys())['curve25519'] : null; + String? get pickledOlmAccount => + enabled ? _olmAccount!.pickle(client.userID) : null; + String? get fingerprintKey => + enabled ? json.decode(_olmAccount!.identity_keys())['ed25519'] : null; + String? get identityKey => + enabled ? json.decode(_olmAccount!.identity_keys())['curve25519'] : null; bool get enabled => _olmAccount != null; @@ -50,12 +50,13 @@ class OlmManager { Map> get olmSessions => _olmSessions; final Map> _olmSessions = {}; - Future init(String olmAccount) async { + // NOTE(Nico): Do we really want to create a new account on passing null instead of signing the user out? + Future init(String? olmAccount) async { if (olmAccount == null) { try { await olm.init(); _olmAccount = olm.Account(); - _olmAccount.create(); + _olmAccount!.create(); if (await uploadKeys(uploadDeviceKeys: true, updateDatabase: false) == false) { throw ('Upload key failed'); @@ -69,7 +70,7 @@ class OlmManager { try { await olm.init(); _olmAccount = olm.Account(); - _olmAccount.unpickle(client.userID, olmAccount); + _olmAccount!.unpickle(client.userID, olmAccount); } catch (_) { _olmAccount?.free(); _olmAccount = null; @@ -82,12 +83,12 @@ class OlmManager { /// json. Map signJson(Map payload) { if (!enabled) throw ('Encryption is disabled'); - final Map unsigned = payload['unsigned']; - final Map signatures = payload['signatures']; + final Map? unsigned = payload['unsigned']; + final Map? signatures = payload['signatures']; payload.remove('unsigned'); payload.remove('signatures'); final canonical = canonicalJson.encode(payload); - final signature = _olmAccount.sign(String.fromCharCodes(canonical)); + final signature = _olmAccount!.sign(String.fromCharCodes(canonical)); if (signatures != null) { payload['signatures'] = signatures; } else { @@ -105,7 +106,7 @@ class OlmManager { } String signString(String s) { - return _olmAccount.sign(s); + return _olmAccount!.sign(s); } /// Checks the signature of a signed json object. @@ -113,7 +114,7 @@ class OlmManager { bool checkJsonSignature(String key, Map signedJson, String userId, String deviceId) { if (!enabled) throw ('Encryption is disabled'); - final Map signatures = signedJson['signatures']; + final Map? signatures = signedJson['signatures']; if (signatures == null || !signatures.containsKey(userId)) return false; signedJson.remove('unsigned'); signedJson.remove('signatures'); @@ -140,9 +141,9 @@ class OlmManager { /// Generates new one time keys, signs everything and upload it to the server. Future uploadKeys({ bool uploadDeviceKeys = false, - int oldKeyCount = 0, + int? oldKeyCount = 0, bool updateDatabase = true, - bool unusedFallbackKey = false, + bool? unusedFallbackKey = false, }) async { if (!enabled) { return true; @@ -155,27 +156,27 @@ class OlmManager { try { final signedOneTimeKeys = {}; - int uploadedOneTimeKeysCount; + int? uploadedOneTimeKeysCount; if (oldKeyCount != null) { // check if we have OTKs that still need uploading. If we do, we don't try to generate new ones, // instead we try to upload the old ones first final oldOTKsNeedingUpload = json - .decode(_olmAccount.one_time_keys())['curve25519'] + .decode(_olmAccount!.one_time_keys())['curve25519'] .entries - .length; + .length as int; // generate one-time keys // we generate 2/3rds of max, so that other keys people may still have can // still be used final oneTimeKeysCount = - (_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() - + (_olmAccount!.max_number_of_one_time_keys() * 2 / 3).floor() - oldKeyCount - oldOTKsNeedingUpload; if (oneTimeKeysCount > 0) { - _olmAccount.generate_one_time_keys(oneTimeKeysCount); + _olmAccount!.generate_one_time_keys(oneTimeKeysCount); } uploadedOneTimeKeysCount = oneTimeKeysCount + oldOTKsNeedingUpload; final Map oneTimeKeys = - json.decode(_olmAccount.one_time_keys()); + json.decode(_olmAccount!.one_time_keys()); // now sign all the one-time keys for (final entry in oneTimeKeys['curve25519'].entries) { @@ -190,8 +191,8 @@ class OlmManager { final signedFallbackKeys = {}; if (encryption.isMinOlmVersion(3, 2, 0) && unusedFallbackKey == false) { // we don't have an unused fallback key uploaded....so let's change that! - _olmAccount.generate_fallback_key(); - final fallbackKey = json.decode(_olmAccount.fallback_key()); + _olmAccount!.generate_fallback_key(); + final fallbackKey = json.decode(_olmAccount!.fallback_key()); // now sign all the fallback keys for (final entry in fallbackKey['curve25519'].entries) { final key = entry.key; @@ -218,7 +219,7 @@ class OlmManager { }; if (uploadDeviceKeys) { final Map keys = - json.decode(_olmAccount.identity_keys()); + json.decode(_olmAccount!.identity_keys()); for (final entry in keys.entries) { final algorithm = entry.key; final value = entry.value; @@ -234,7 +235,7 @@ class OlmManager { // we can still re-try later if (updateDatabase) { await client.database?.updateClientKeys( - pickledOlmAccount, + pickledOlmAccount!, ); } final response = await client.uploadKeys( @@ -245,9 +246,9 @@ class OlmManager { fallbackKeys: signedFallbackKeys, ); // mark the OTKs as published and save that to datbase - _olmAccount.mark_keys_as_published(); + _olmAccount!.mark_keys_as_published(); if (updateDatabase) { - await client.database?.updateClientKeys(pickledOlmAccount); + await client.database?.updateClientKeys(pickledOlmAccount!); } return (uploadedOneTimeKeysCount != null && response['signed_curve25519'] == uploadedOneTimeKeysCount) || @@ -258,7 +259,7 @@ class OlmManager { } void handleDeviceOneTimeKeysCount( - Map countJson, List unusedFallbackKeyTypes) { + Map? countJson, List? unusedFallbackKeyTypes) { if (!enabled) { return; } @@ -277,7 +278,7 @@ class OlmManager { } // fixup accidental too many uploads. We delete only one of them so that the server has time to update the counts and because we will get rate limited anyway. - if (keyCount > _olmAccount.max_number_of_one_time_keys()) { + if (keyCount > _olmAccount!.max_number_of_one_time_keys()) { final requestingKeysFrom = { client.userID: {client.deviceID: 'signed_curve25519'} }; @@ -285,10 +286,10 @@ class OlmManager { } // Only upload keys if they are less than half of the max or we have no unused fallback key - if (keyCount < (_olmAccount.max_number_of_one_time_keys() / 2) || + if (keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) || !unusedFallbackKey) { uploadKeys( - oldKeyCount: keyCount < (_olmAccount.max_number_of_one_time_keys() / 2) + oldKeyCount: keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) ? keyCount : null, unusedFallbackKey: haveFallbackKeys ? unusedFallbackKey : null, @@ -297,24 +298,29 @@ class OlmManager { } Future storeOlmSession(OlmSession session) async { + if (session.sessionId == null || session.pickledSession == null) { + return; + } + _olmSessions[session.identityKey] ??= []; - final ix = _olmSessions[session.identityKey] + final ix = _olmSessions[session.identityKey]! .indexWhere((s) => s.sessionId == session.sessionId); if (ix == -1) { // add a new session - _olmSessions[session.identityKey].add(session); + _olmSessions[session.identityKey]!.add(session); } else { // update an existing session - _olmSessions[session.identityKey][ix] = session; + _olmSessions[session.identityKey]![ix] = session; } if (client.database == null) { return; } await client.database.storeOlmSession( session.identityKey, - session.sessionId, - session.pickledSession, - session.lastReceived.millisecondsSinceEpoch); + session.sessionId!, + session.pickledSession!, + session.lastReceived?.millisecondsSinceEpoch ?? + DateTime.now().millisecondsSinceEpoch); } ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) { @@ -325,20 +331,21 @@ class OlmManager { if (content.algorithm != AlgorithmTypes.olmV1Curve25519AesSha2) { throw DecryptException(DecryptException.unknownAlgorithm); } - if (!content.ciphertextOlm.containsKey(identityKey)) { + if (content.ciphertextOlm == null || + !content.ciphertextOlm!.containsKey(identityKey)) { throw DecryptException(DecryptException.isntSentForThisDevice); } - String plaintext; + String? plaintext; final senderKey = content.senderKey; - final body = content.ciphertextOlm[identityKey].body; - final type = content.ciphertextOlm[identityKey].type; + final body = content.ciphertextOlm![identityKey]!.body; + final type = content.ciphertextOlm![identityKey]!.type; if (type != 0 && type != 1) { throw DecryptException(DecryptException.unknownMessageType); } - final device = client.userDeviceKeys[event.sender]?.deviceKeys?.values - ?.firstWhere((d) => d.curve25519Key == senderKey, orElse: () => null); + final device = client.userDeviceKeys[event.sender]?.deviceKeys.values + .firstWhereOrNull((d) => d.curve25519Key == senderKey); final existingSessions = olmSessions[senderKey]; - final updateSessionUsage = ([OlmSession session]) => runInRoot(() async { + final updateSessionUsage = ([OlmSession? session]) => runInRoot(() async { if (session != null) { session.lastReceived = DateTime.now(); await storeOlmSession(session); @@ -348,14 +355,17 @@ class OlmManager { await client.database?.setLastActiveUserDeviceKey( device.lastActive.millisecondsSinceEpoch, device.userId, - device.deviceId); + device.deviceId!); } }); if (existingSessions != null) { for (final session in existingSessions) { - if (type == 0 && session.session.matches_inbound(body) == true) { + if (session.session == null) { + continue; + } + if (type == 0 && session.session!.matches_inbound(body) == true) { try { - plaintext = session.session.decrypt(type, body); + plaintext = session.session!.decrypt(type, body); } catch (e) { // The message was encrypted during this session, but is unable to decrypt throw DecryptException( @@ -365,7 +375,7 @@ class OlmManager { break; } else if (type == 1) { try { - plaintext = session.session.decrypt(type, body); + plaintext = session.session!.decrypt(type, body); updateSessionUsage(session); break; } catch (_) { @@ -381,10 +391,10 @@ class OlmManager { if (plaintext == null) { final newSession = olm.Session(); try { - newSession.create_inbound_from(_olmAccount, senderKey, body); - _olmAccount.remove_one_time_keys(newSession); + newSession.create_inbound_from(_olmAccount!, senderKey, body); + _olmAccount!.remove_one_time_keys(newSession); client.database?.updateClientKeys( - pickledOlmAccount, + pickledOlmAccount!, ); plaintext = newSession.decrypt(type, body); runInRoot(() => storeOlmSession(OlmSession( @@ -396,7 +406,7 @@ class OlmManager { ))); updateSessionUsage(); } catch (e) { - newSession?.free(); + newSession.free(); throw DecryptException(DecryptException.decryptionFailed, e.toString()); } } @@ -444,7 +454,7 @@ class OlmManager { for (final sess in rows) { res[sess.identityKey] ??= []; if (sess.isValid) { - res[sess.identityKey].add(sess); + res[sess.identityKey]!.add(sess); } } for (final entry in res.entries) { @@ -455,7 +465,7 @@ class OlmManager { Future> getOlmSessions(String senderKey, {bool getFromDb = true}) async { var sess = olmSessions[senderKey]; - if ((getFromDb ?? true) && (sess == null || sess.isEmpty)) { + if ((getFromDb) && (sess == null || sess.isEmpty)) { final sessions = await getOlmSessionsFromDatabase(senderKey); if (sessions.isEmpty) { return []; @@ -466,8 +476,9 @@ class OlmManager { return []; } sess.sort((a, b) => a.lastReceived == b.lastReceived - ? a.sessionId.compareTo(b.sessionId) - : b.lastReceived.compareTo(a.lastReceived)); + ? (a.sessionId ?? '').compareTo(b.sessionId ?? '') + : (b.lastReceived ?? DateTime(0)) + .compareTo(a.lastReceived ?? DateTime(0))); return sess; } @@ -477,8 +488,8 @@ class OlmManager { if (!client.userDeviceKeys.containsKey(userId)) { return; } - final device = client.userDeviceKeys[userId].deviceKeys.values - .firstWhere((d) => d.curve25519Key == senderKey, orElse: () => null); + final device = client.userDeviceKeys[userId]!.deviceKeys.values + .firstWhereOrNull((d) => d.curve25519Key == senderKey); if (device == null) { return; } @@ -487,7 +498,7 @@ class OlmManager { if (_restoredOlmSessionsTime.containsKey(mapKey) && DateTime.now() .subtract(Duration(hours: 1)) - .isBefore(_restoredOlmSessionsTime[mapKey])) { + .isBefore(_restoredOlmSessionsTime[mapKey]!)) { return; } _restoredOlmSessionsTime[mapKey] = DateTime.now(); @@ -532,7 +543,8 @@ class OlmManager { if (requestingKeysFrom[device.userId] == null) { requestingKeysFrom[device.userId] = {}; } - requestingKeysFrom[device.userId][device.deviceId] = 'signed_curve25519'; + requestingKeysFrom[device.userId]![device.deviceId!] = + 'signed_curve25519'; } final response = await client.claimKeys(requestingKeysFrom, timeout: 10000); @@ -542,9 +554,9 @@ class OlmManager { for (final deviceKeysEntry in userKeysEntry.value.entries) { final deviceId = deviceKeysEntry.key; final fingerprintKey = - client.userDeviceKeys[userId].deviceKeys[deviceId].ed25519Key; + client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.ed25519Key; final identityKey = - client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key; + client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.curve25519Key; for (final Map deviceKey in deviceKeysEntry.value.values) { if (!deviceKey.checkJsonSignature(fingerprintKey, userId, deviceId)) { @@ -553,7 +565,8 @@ class OlmManager { Logs().v('[OlmManager] Starting session with $userId:$deviceId'); final session = olm.Session(); try { - session.create_outbound(_olmAccount, identityKey, deviceKey['key']); + session.create_outbound( + _olmAccount!, identityKey!, deviceKey['key']); await storeOlmSession(OlmSession( key: client.userID, identityKey: identityKey, @@ -574,9 +587,9 @@ class OlmManager { Future> encryptToDeviceMessagePayload( DeviceKeys device, String type, Map payload, - {bool getFromDb}) async { + {bool getFromDb = true}) async { final sess = - await getOlmSessions(device.curve25519Key, getFromDb: getFromDb); + await getOlmSessions(device.curve25519Key!, getFromDb: getFromDb); if (sess.isEmpty) { throw ('No olm session found for ${device.userId}:${device.deviceId}'); } @@ -588,7 +601,7 @@ class OlmManager { 'recipient': device.userId, 'recipient_keys': {'ed25519': device.ed25519Key}, }; - final encryptResult = sess.first.session.encrypt(json.encode(fullPayload)); + final encryptResult = sess.first.session!.encrypt(json.encode(fullPayload)); await storeOlmSession(sess.first); if (client.database != null) { // ignore: unawaited_futures @@ -598,7 +611,7 @@ class OlmManager { 'content': payload, }), device.userId, - device.deviceId)); + device.deviceId!)); } final encryptedBody = { 'algorithm': AlgorithmTypes.olmV1Curve25519AesSha2, @@ -620,12 +633,12 @@ class OlmManager { // first check if any of our sessions we want to encrypt for are in the database if (client.database != null) { await getOlmSessionsForDevicesFromDatabase( - deviceKeys.map((d) => d.curve25519Key).toList()); + deviceKeys.map((d) => d.curve25519Key!).toList()); } final deviceKeysWithoutSession = List.from(deviceKeys); deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) => olmSessions.containsKey(deviceKeys.curve25519Key) && - olmSessions[deviceKeys.curve25519Key].isNotEmpty); + olmSessions[deviceKeys.curve25519Key]!.isNotEmpty); if (deviceKeysWithoutSession.isNotEmpty) { await startOutgoingOlmSessions(deviceKeysWithoutSession); } @@ -634,7 +647,7 @@ class OlmManager { data[device.userId] = {}; } try { - data[device.userId][device.deviceId] = + data[device.userId]![device.deviceId!] = await encryptToDeviceMessagePayload(device, type, payload, getFromDb: false); } catch (e, s) { @@ -660,7 +673,7 @@ class OlmManager { Logs().v( '[OlmManager] Device ${device.userId}:${device.deviceId} generated a new olm session, replaying last sent message...'); final lastSentMessageRes = await client.database - .getLastSentMessageUserDeviceKey(device.userId, device.deviceId); + .getLastSentMessageUserDeviceKey(device.userId, device.deviceId!); if (lastSentMessageRes.isEmpty || (lastSentMessageRes.first?.isEmpty ?? true)) { return; diff --git a/lib/encryption/ssss.dart b/lib/encryption/ssss.dart index 2c6d3ece..fc1eb2c7 100644 --- a/lib/encryption/ssss.dart +++ b/lib/encryption/ssss.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2020, 2021 Famedly GmbH @@ -24,6 +23,7 @@ import 'dart:typed_data'; import 'package:base58check/base58.dart'; import 'package:crypto/crypto.dart'; +import 'package:collection/collection.dart'; import '../matrix.dart'; import '../src/utils/crypto/crypto.dart' as uc; @@ -76,11 +76,13 @@ class SSSS { b[0] = 2; final hmacKey = Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b); - return _DerivedKeys(aesKey: aesKey.bytes, hmacKey: hmacKey.bytes); + return _DerivedKeys( + aesKey: Uint8List.fromList(aesKey.bytes), + hmacKey: Uint8List.fromList(hmacKey.bytes)); } static Future<_Encrypted> encryptAes(String data, Uint8List key, String name, - [String ivStr]) async { + [String? ivStr]) async { Uint8List iv; if (ivStr != null) { iv = base64.decode(ivStr); @@ -121,7 +123,7 @@ class SSSS { static Uint8List decodeRecoveryKey(String recoveryKey) { final result = base58.decode(recoveryKey.replaceAll(' ', '')); - final parity = result.fold(0, (a, b) => a ^ b); + final parity = result.fold(0, (a, b) => (a as int) ^ b); if (parity != 0) { throw Exception('Incorrect parity'); } @@ -142,7 +144,7 @@ class SSSS { static String encodeRecoveryKey(Uint8List recoveryKey) { final keyToEncode = [...olmRecoveryKeyPrefix, ...recoveryKey]; - final parity = keyToEncode.fold(0, (a, b) => a ^ b); + final parity = keyToEncode.fold(0, (a, b) => (a as int) ^ b); keyToEncode.add(parity); // base58-encode and add a space every four chars return base58 @@ -156,8 +158,18 @@ class SSSS { if (info.algorithm != AlgorithmTypes.pbkdf2) { throw Exception('Unknown algorithm'); } - return await uc.pbkdf2(utf8.encode(passphrase), utf8.encode(info.salt), - uc.sha512, info.iterations, info.bits ?? 256); + if (info.iterations == null) { + throw Exception('Passphrase info without iterations'); + } + if (info.salt == null) { + throw Exception('Passphrase info without salt'); + } + return await uc.pbkdf2( + Uint8List.fromList(utf8.encode(passphrase)), + Uint8List.fromList(utf8.encode(info.salt!)), + uc.sha512, + info.iterations!, + info.bits ?? 256); } void setValidator(String type, FutureOr Function(String) validator) { @@ -168,10 +180,10 @@ class SSSS { _cacheCallbacks[type] = callback; } - String get defaultKeyId => client + String? get defaultKeyId => client .accountData[EventTypes.SecretStorageDefaultKey] ?.parsedSecretStorageDefaultKeyContent - ?.key; + .key; Future setDefaultKeyId(String keyId) async { await client.setAccountData( @@ -181,7 +193,7 @@ class SSSS { ); } - SecretStorageKeyContent getKey(String keyId) { + SecretStorageKeyContent? getKey(String keyId) { return client.accountData[EventTypes.secretStorageKey(keyId)] ?.parsedSecretStorageKeyContent; } @@ -191,7 +203,7 @@ class SSSS { /// Creates a new secret storage key, optional encrypts it with [passphrase] /// and stores it in the user's `accountData`. - Future createKey([String passphrase]) async { + Future createKey([String? passphrase]) async { Uint8List privateKey; final content = SecretStorageKeyContent(); if (passphrase != null) { @@ -207,7 +219,7 @@ class SSSS { _keyFromPassphrase, _KeyFromPassphraseArgs( passphrase: passphrase, - info: content.passphrase, + info: content.passphrase!, ), ) .timeout(Duration(seconds: 10)); @@ -235,7 +247,7 @@ class SSSS { // noooow we set the account data final waitForAccountData = client.onSync.stream.firstWhere((syncUpdate) => syncUpdate.accountData != null && - syncUpdate.accountData + syncUpdate.accountData! .any((accountData) => accountData.type == accountDataType)); await client.setAccountData( client.userID, accountDataType, content.toJson()); @@ -250,7 +262,7 @@ class SSSS { if (info.algorithm == AlgorithmTypes.secretStorageV1AesHmcSha2) { if ((info.mac is String) && (info.iv is String)) { final encrypted = await encryptAes(zeroStr, key, '', info.iv); - return info.mac.replaceAll(RegExp(r'=+$'), '') == + return info.mac!.replaceAll(RegExp(r'=+$'), '') == encrypted.mac.replaceAll(RegExp(r'=+$'), ''); } else { // no real information about the key, assume it is valid @@ -263,9 +275,9 @@ class SSSS { bool isSecret(String type) => client.accountData[type] != null && - client.accountData[type].content['encrypted'] is Map; + client.accountData[type]!.content['encrypted'] is Map; - Future getCached(String type) async { + Future getCached(String type) async { if (client.database == null) { return null; } @@ -276,11 +288,12 @@ class SSSS { } final isValid = (dbEntry) => keys.contains(dbEntry.keyId) && - client.accountData[type].content['encrypted'][dbEntry.keyId] + dbEntry.ciphertext != null && + client.accountData[type]?.content['encrypted'][dbEntry.keyId] ['ciphertext'] == dbEntry.ciphertext; if (_cache.containsKey(type) && isValid(_cache[type])) { - return _cache[type].content; + return _cache[type]?.content; } final ret = await client.database.getSSSSCache(type); if (ret == null) { @@ -313,7 +326,7 @@ class SSSS { await client.database .storeSSSSCache(type, keyId, enc['ciphertext'], decrypted); if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) { - _cacheCallbacks[type](decrypted); + _cacheCallbacks[type]!(decrypted); } } return decrypted; @@ -321,12 +334,10 @@ class SSSS { Future store(String type, String secret, String keyId, Uint8List key, {bool add = false}) async { - final triggerCacheCallback = - _cacheCallbacks.containsKey(type) && await getCached(type) == null; final encrypted = await encryptAes(secret, key, type); - Map content; + Map? content; if (add && client.accountData[type] != null) { - content = client.accountData[type].content.copy(); + content = client.accountData[type]!.content.copy(); if (!(content['encrypted'] is Map)) { content['encrypted'] = {}; } @@ -345,8 +356,8 @@ class SSSS { // cache the thing await client.database .storeSSSSCache(type, keyId, encrypted.ciphertext, secret); - if (triggerCacheCallback) { - _cacheCallbacks[type](secret); + if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) { + _cacheCallbacks[type]!(secret); } } } @@ -357,7 +368,11 @@ class SSSS { throw Exception('Secrets do not match up!'); } // now remove all other keys - final content = client.accountData[type].content.copy(); + final content = client.accountData[type]?.content.copy(); + if (content == null) { + throw Exception('Key has no content!'); + } + final otherKeys = Set.from(content['encrypted'].keys.where((k) => k != keyId)); content['encrypted'].removeWhere((k, v) => otherKeys.contains(k)); @@ -387,7 +402,7 @@ class SSSS { } } - Future maybeRequestAll([List devices]) async { + Future maybeRequestAll([List? devices]) async { for (final type in cacheTypes) { if (keyIdsFromType(type) != null) { final secret = await getCached(type); @@ -398,7 +413,7 @@ class SSSS { } } - Future request(String type, [List devices]) async { + Future request(String type, [List? devices]) async { // only send to own, verified devices Logs().i('[SSSS] Requesting type $type...'); if (devices == null || devices.isEmpty) { @@ -406,7 +421,8 @@ class SSSS { Logs().w('[SSSS] User does not have any devices'); return; } - devices = client.userDeviceKeys[client.userID].deviceKeys.values.toList(); + devices = + client.userDeviceKeys[client.userID]!.deviceKeys.values.toList(); } devices.removeWhere((DeviceKeys d) => d.userId != client.userID || @@ -432,7 +448,7 @@ class SSSS { }); } - DateTime _lastCacheRequest; + DateTime? _lastCacheRequest; bool _isPeriodicallyRequestingMissingCache = false; Future periodicallyRequestMissingCache() async { @@ -440,7 +456,7 @@ class SSSS { (_lastCacheRequest != null && DateTime.now() .subtract(Duration(minutes: 15)) - .isBefore(_lastCacheRequest)) || + .isBefore(_lastCacheRequest!)) || client.isUnknownSession) { // we are already requesting right now or we attempted to within the last 15 min return; @@ -467,7 +483,7 @@ class SSSS { Logs().i('[SSSS] it is actually a cancelation'); return; // not actually requesting, so ignore } - final device = client.userDeviceKeys[client.userID] + final device = client.userDeviceKeys[client.userID]! .deviceKeys[event.content['requesting_device_id']]; if (device == null || !device.verified || device.blocked) { Logs().i('[SSSS] Unknown / unverified devices, ignoring'); @@ -499,13 +515,11 @@ class SSSS { Logs().i('[SSSS] Not by us or unknown request'); return; // we have no idea what we just received } - final request = pendingShareRequests[event.content['request_id']]; + final request = pendingShareRequests[event.content['request_id']]!; // alright, as we received a known request id, let's check if the sender is valid - final device = request.devices.firstWhere( - (d) => - d.userId == event.sender && - d.curve25519Key == event.encryptedContent['sender_key'], - orElse: () => null); + final device = request.devices.firstWhereOrNull((d) => + d.userId == event.sender && + d.curve25519Key == event.encryptedContent['sender_key']); if (device == null) { Logs().i('[SSSS] Someone else replied?'); return; // someone replied whom we didn't send the share request to @@ -517,7 +531,7 @@ class SSSS { } // let's validate if the secret is, well, valid if (_validators.containsKey(request.type) && - !(await _validators[request.type](secret))) { + !(await _validators[request.type]!(secret))) { Logs().i('[SSSS] The received secret was invalid'); return; // didn't pass the validator } @@ -530,19 +544,19 @@ class SSSS { if (client.database != null) { final keyId = keyIdFromType(request.type); if (keyId != null) { - final ciphertext = client.accountData[request.type] + final ciphertext = client.accountData[request.type]! .content['encrypted'][keyId]['ciphertext']; await client.database .storeSSSSCache(request.type, keyId, ciphertext, secret); if (_cacheCallbacks.containsKey(request.type)) { - _cacheCallbacks[request.type](secret); + _cacheCallbacks[request.type]!(secret); } } } } } - Set keyIdsFromType(String type) { + Set? keyIdsFromType(String type) { final data = client.accountData[type]; if (data == null) { return null; @@ -553,7 +567,7 @@ class SSSS { return null; } - String keyIdFromType(String type) { + String? keyIdFromType(String type) { final keys = keyIdsFromType(type); if (keys == null || keys.isEmpty) { return null; @@ -564,15 +578,12 @@ class SSSS { return keys.first; } - OpenSSSS open([String identifier]) { + OpenSSSS open([String? identifier]) { identifier ??= defaultKeyId; if (identifier == null) { throw Exception('Dont know what to open'); } final keyToOpen = keyIdFromType(identifier) ?? identifier; - if (keyToOpen == null) { - throw Exception('No key found to open'); - } final key = getKey(keyToOpen); if (key == null) { throw Exception('Unknown key to open'); @@ -587,7 +598,8 @@ class _ShareRequest { final List devices; final DateTime start; - _ShareRequest({this.requestId, this.type, this.devices}) + _ShareRequest( + {required this.requestId, required this.type, required this.devices}) : start = DateTime.now(); } @@ -596,14 +608,14 @@ class _Encrypted { final String ciphertext; final String mac; - _Encrypted({this.iv, this.ciphertext, this.mac}); + _Encrypted({required this.iv, required this.ciphertext, required this.mac}); } class _DerivedKeys { final Uint8List aesKey; final Uint8List hmacKey; - _DerivedKeys({this.aesKey, this.hmacKey}); + _DerivedKeys({required this.aesKey, required this.hmacKey}); } class OpenSSSS { @@ -611,21 +623,21 @@ class OpenSSSS { final String keyId; final SecretStorageKeyContent keyData; - OpenSSSS({this.ssss, this.keyId, this.keyData}); + OpenSSSS({required this.ssss, required this.keyId, required this.keyData}); - Uint8List privateKey; + Uint8List? privateKey; bool get isUnlocked => privateKey != null; bool get hasPassphrase => keyData.passphrase != null; - String get recoveryKey => - isUnlocked ? SSSS.encodeRecoveryKey(privateKey) : null; + String? get recoveryKey => + isUnlocked ? SSSS.encodeRecoveryKey(privateKey!) : null; Future unlock( - {String passphrase, - String recoveryKey, - String keyOrPassphrase, + {String? passphrase, + String? recoveryKey, + String? keyOrPassphrase, bool postUnlock = true}) async { if (keyOrPassphrase != null) { try { @@ -648,7 +660,7 @@ class OpenSSSS { _keyFromPassphrase, _KeyFromPassphraseArgs( passphrase: passphrase, - info: keyData.passphrase, + info: keyData.passphrase!, ), ) .timeout(Duration(seconds: 10)); @@ -658,7 +670,7 @@ class OpenSSSS { throw Exception('Nothing specified'); } // verify the validity of the key - if (!await ssss.checkKey(privateKey, keyData)) { + if (!await ssss.checkKey(privateKey!, keyData)) { privateKey = null; throw Exception('Inalid key'); } @@ -675,19 +687,31 @@ class OpenSSSS { } Future getStored(String type) async { - return await ssss.getStored(type, keyId, privateKey); + if (privateKey == null) { + throw Exception('SSSS not unlocked'); + } + return await ssss.getStored(type, keyId, privateKey!); } Future store(String type, String secret, {bool add = false}) async { - await ssss.store(type, secret, keyId, privateKey, add: add); + if (privateKey == null) { + throw Exception('SSSS not unlocked'); + } + await ssss.store(type, secret, keyId, privateKey!, add: add); } Future validateAndStripOtherKeys(String type, String secret) async { - await ssss.validateAndStripOtherKeys(type, secret, keyId, privateKey); + if (privateKey == null) { + throw Exception('SSSS not unlocked'); + } + await ssss.validateAndStripOtherKeys(type, secret, keyId, privateKey!); } Future maybeCacheAll() async { - await ssss.maybeCacheAll(keyId, privateKey); + if (privateKey == null) { + throw Exception('SSSS not unlocked'); + } + await ssss.maybeCacheAll(keyId, privateKey!); } Future _postUnlock() async { @@ -701,8 +725,9 @@ class OpenSSSS { ?.contains(keyId) ?? false) && (ssss.client.isUnknownSession || - !ssss.client.userDeviceKeys[ssss.client.userID].masterKey - .directVerified)) { + ssss.client.userDeviceKeys[ssss.client.userID]!.masterKey + ?.directVerified != + true)) { try { await ssss.encryption.crossSigning.selfSign(openSsss: this); } catch (e, s) { @@ -716,7 +741,7 @@ class _KeyFromPassphraseArgs { final String passphrase; final PassphraseInfo info; - _KeyFromPassphraseArgs({this.passphrase, this.info}); + _KeyFromPassphraseArgs({required this.passphrase, required this.info}); } Future _keyFromPassphrase(_KeyFromPassphraseArgs args) async { diff --git a/lib/encryption/utils/bootstrap.dart b/lib/encryption/utils/bootstrap.dart index 8d8791c8..3b3f6da9 100644 --- a/lib/encryption/utils/bootstrap.dart +++ b/lib/encryption/utils/bootstrap.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2020, 2021 Famedly GmbH @@ -73,14 +72,14 @@ enum BootstrapState { class Bootstrap { final Encryption encryption; Client get client => encryption.client; - void Function() onUpdate; + void Function()? onUpdate; BootstrapState get state => _state; BootstrapState _state = BootstrapState.loading; - Map oldSsssKeys; - OpenSSSS newSsssKey; - Map secretMap; + Map? oldSsssKeys; + OpenSSSS? newSsssKey; + Map? secretMap; - Bootstrap({this.encryption, this.onUpdate}) { + Bootstrap({required this.encryption, this.onUpdate}) { if (analyzeSecrets().isNotEmpty) { state = BootstrapState.askWipeSsss; } else { @@ -89,12 +88,12 @@ class Bootstrap { } // cache the secret analyzing so that we don't drop stuff a different client sets during bootstrapping - Map> _secretsCache; + Map>? _secretsCache; Map> analyzeSecrets() { if (_secretsCache != null) { // deep-copy so that we can do modifications final newSecrets = >{}; - for (final s in _secretsCache.entries) { + for (final s in _secretsCache!.entries) { newSecrets[s.key] = Set.from(s.value); } return newSecrets; @@ -149,9 +148,10 @@ class Bootstrap { for (final keys in secrets.values) { for (final key in keys) { if (!usage.containsKey(key)) { - usage[key] = 0; + usage[key] = 1; + } else { + usage[key] = usage[key]! + 1; } - usage[key]++; } } final entriesList = usage.entries.toList(); @@ -192,7 +192,7 @@ class Bootstrap { if (wipe) { state = BootstrapState.askNewSsss; } else if (encryption.ssss.defaultKeyId != null && - encryption.ssss.isKeyValid(encryption.ssss.defaultKeyId)) { + encryption.ssss.isKeyValid(encryption.ssss.defaultKeyId!)) { state = BootstrapState.askUseExistingSsss; } else if (badSecrets().isNotEmpty) { state = BootstrapState.askBadSsss; @@ -238,7 +238,7 @@ class Bootstrap { oldSsssKeys = {}; try { for (final key in keys) { - oldSsssKeys[key] = encryption.ssss.open(key); + oldSsssKeys![key] = encryption.ssss.open(key); } } catch (e, s) { Logs().e('[Bootstrapping] Error construction ssss key', e, s); @@ -255,7 +255,7 @@ class Bootstrap { state = BootstrapState.askNewSsss; } - Future newSsss([String passphrase]) async { + Future newSsss([String? passphrase]) async { if (state != BootstrapState.askNewSsss) { throw BootstrapBadStateException('Wrong State'); } @@ -275,7 +275,7 @@ class Bootstrap { return s; }; secretMap = {}; - for (final entry in oldSsssKeys.entries) { + for (final entry in oldSsssKeys!.entries) { final key = entry.value; final keyId = entry.key; if (!key.isUnlocked) { @@ -283,26 +283,26 @@ class Bootstrap { } for (final s in removeKey(keyId)) { Logs().v('Get stored key of type $s...'); - secretMap[s] = await key.getStored(s); + secretMap![s] = await key.getStored(s); Logs().v('Store new secret with this key...'); - await newSsssKey.store(s, secretMap[s], add: true); + await newSsssKey!.store(s, secretMap![s]!, add: true); } } // alright, we re-encrypted all the secrets. We delete the dead weight only *after* we set our key to the default key } final updatedAccountData = client.onSync.stream.firstWhere((syncUpdate) => syncUpdate.accountData != null && - syncUpdate.accountData.any((accountData) => + syncUpdate.accountData!.any((accountData) => accountData.type == EventTypes.SecretStorageDefaultKey)); - await encryption.ssss.setDefaultKeyId(newSsssKey.keyId); + await encryption.ssss.setDefaultKeyId(newSsssKey!.keyId); await updatedAccountData; if (oldSsssKeys != null) { - for (final entry in secretMap.entries) { + for (final entry in secretMap!.entries) { Logs().v('Validate and stripe other keys ${entry.key}...'); - await newSsssKey.validateAndStripOtherKeys(entry.key, entry.value); + await newSsssKey!.validateAndStripOtherKeys(entry.key, entry.value); } Logs().v('And make super sure we have everything cached...'); - await newSsssKey.maybeCacheAll(); + await newSsssKey!.maybeCacheAll(); } } catch (e, s) { Logs().e('[Bootstrapping] Error trying to migrate old secrets', e, s); @@ -315,14 +315,14 @@ class Bootstrap { } Future openExistingSsss() async { - if (state != BootstrapState.openExistingSsss) { + if (state != BootstrapState.openExistingSsss || newSsssKey == null) { throw BootstrapBadStateException(); } - if (!newSsssKey.isUnlocked) { + if (!newSsssKey!.isUnlocked) { throw BootstrapBadStateException('Key not unlocked'); } Logs().v('Maybe cache all...'); - await newSsssKey.maybeCacheAll(); + await newSsssKey!.maybeCacheAll(); checkCrossSigning(); } @@ -362,10 +362,10 @@ class Bootstrap { try { Uint8List masterSigningKey; final secretsToStore = {}; - MatrixCrossSigningKey masterKey; - MatrixCrossSigningKey selfSigningKey; - MatrixCrossSigningKey userSigningKey; - String masterPub; + MatrixCrossSigningKey? masterKey; + MatrixCrossSigningKey? selfSigningKey; + MatrixCrossSigningKey? userSigningKey; + String? masterPub; if (setupMasterKey) { final master = olm.PkSigning(); try { @@ -387,8 +387,9 @@ class Bootstrap { } else { Logs().v('Get stored key...'); masterSigningKey = base64.decode( - await newSsssKey.getStored(EventTypes.CrossSigningMasterKey) ?? ''); - if (masterSigningKey == null || masterSigningKey.isEmpty) { + await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ?? + ''); + if (masterSigningKey.isEmpty) { // no master signing key :( throw BootstrapBadStateException('No master key'); } @@ -477,9 +478,11 @@ class Bootstrap { client.onSync.stream .firstWhere((syncUpdate) => client.userDeviceKeys.containsKey(client.userID) && - client.userDeviceKeys[client.userID].masterKey != null && - client.userDeviceKeys[client.userID].masterKey.ed25519Key == - masterKey.publicKey) + client.userDeviceKeys[client.userID]!.masterKey != null && + client.userDeviceKeys[client.userID]!.masterKey!.ed25519Key != + null && + client.userDeviceKeys[client.userID]!.masterKey!.ed25519Key == + masterKey!.publicKey) .then((_) => Logs().v('New Master Key was created')), ); } @@ -488,32 +491,32 @@ class Bootstrap { client.onSync.stream .firstWhere((syncUpdate) => syncUpdate.accountData != null && - syncUpdate.accountData + syncUpdate.accountData! .any((accountData) => accountData.type == entry.key)) .then((_) => Logs().v('New Key with type ${entry.key} was created')), ); Logs().v('Store new SSSS key ${entry.key}...'); - await newSsssKey.store(entry.key, entry.value); + await newSsssKey?.store(entry.key, entry.value); } Logs().v( 'Wait for MasterKey and ${secretsToStore.entries.length} keys to be created'); await Future.wait(futures); final keysToSign = []; if (masterKey != null) { - if (client.userDeviceKeys[client.userID].masterKey.ed25519Key != + if (client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key != masterKey.publicKey) { throw BootstrapBadStateException( 'ERROR: New master key does not match up!'); } Logs().v('Set own master key to verified...'); - await client.userDeviceKeys[client.userID].masterKey + await client.userDeviceKeys[client.userID]!.masterKey! .setVerified(true, false); - keysToSign.add(client.userDeviceKeys[client.userID].masterKey); + keysToSign.add(client.userDeviceKeys[client.userID]!.masterKey!); } if (selfSigningKey != null) { keysToSign.add( - client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]); + client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!); } Logs().v('Sign ourself...'); await encryption.crossSigning.sign(keysToSign); @@ -572,7 +575,7 @@ class Bootstrap { }, ); Logs().v('Store the secret...'); - await newSsssKey.store(megolmKey, base64.encode(privKey)); + await newSsssKey?.store(megolmKey, base64.encode(privKey)); Logs().v( 'And finally set all megolm keys as needing to be uploaded again...'); await client.database?.markInboundGroupSessionsAsNeedingUpload(); @@ -593,7 +596,7 @@ class Bootstrap { _state = newState; } if (onUpdate != null) { - onUpdate(); + onUpdate!(); } } } diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index 8eac5639..91317a08 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2020, 2021 Famedly GmbH @@ -74,7 +73,7 @@ enum KeyVerificationState { enum KeyVerificationMethod { emoji, numbers } -List _intersect(List a, List b) => +List _intersect(List? a, List? b) => (b == null || a == null) ? [] : a.where(b.contains).toList(); List _bytesToInt(Uint8List bytes, int totalBits) { @@ -104,41 +103,40 @@ _KeyVerificationMethod _makeVerificationMethod( } class KeyVerification { - String transactionId; + String? transactionId; final Encryption encryption; Client get client => encryption.client; - final Room room; + final Room? room; final String userId; - void Function() onUpdate; - String get deviceId => _deviceId; - String _deviceId; + void Function()? onUpdate; + String? get deviceId => _deviceId; + String? _deviceId; bool startedVerification = false; - _KeyVerificationMethod method; - List possibleMethods; - Map startPaylaod; - String _nextAction; - List _verifiedDevices; + _KeyVerificationMethod? method; + List possibleMethods = []; + Map? startPayload; + String? _nextAction; + List _verifiedDevices = []; DateTime lastActivity; - String lastStep; + String? lastStep; KeyVerificationState state = KeyVerificationState.waitingAccept; bool canceled = false; - String canceledCode; - String canceledReason; + String? canceledCode; + String? canceledReason; bool get isDone => canceled || {KeyVerificationState.error, KeyVerificationState.done}.contains(state); KeyVerification( - {this.encryption, + {required this.encryption, this.room, - this.userId, - String deviceId, - this.onUpdate}) { - lastActivity = DateTime.now(); - _deviceId ??= deviceId; - } + required this.userId, + String? deviceId, + this.onUpdate}) + : _deviceId = deviceId, + lastActivity = DateTime.now(); void dispose() { Logs().i('[Key Verification] disposing object...'); @@ -188,7 +186,7 @@ class KeyVerification { bool _handlePayloadLock = false; Future handlePayload(String type, Map payload, - [String eventId]) async { + [String? eventId]) async { if (isDone) { return; // no need to do anything with already canceled requests } @@ -229,8 +227,10 @@ class KeyVerification { if (deviceId == '*') { _deviceId = payload['from_device']; // gotta set the real device id // and broadcast the cancel to the other devices - final devices = List.from( - client.userDeviceKeys[userId].deviceKeys.values); + final devices = client.userDeviceKeys.containsKey(userId) + ? List.from( + client.userDeviceKeys[userId]!.deviceKeys.values) + : List.from([]); devices.removeWhere( (d) => {deviceId, client.deviceID}.contains(d.deviceId)); final cancelPayload = { @@ -254,7 +254,7 @@ class KeyVerification { lastStep = type; // TODO: Pick method? method = _makeVerificationMethod(possibleMethods.first, this); - await method.sendStart(); + await method!.sendStart(); setState(KeyVerificationState.waitingAccept); break; case EventTypes.KeyVerificationStart: @@ -262,7 +262,7 @@ class KeyVerification { transactionId ??= eventId ?? payload['transaction_id']; if (method != null) { // the other side sent us a start, even though we already sent one - if (payload['method'] == method.type) { + if (payload['method'] == method?.type) { // same method. Determine priority final ourEntry = '${client.userID}|${client.deviceID}'; final entries = [ourEntry, '$userId|$deviceId']; @@ -275,7 +275,7 @@ class KeyVerification { startedVerification = false; // it is now as if they started thisLastStep = lastStep = EventTypes.KeyVerificationRequest; // we fake the last step - method.dispose(); // in case anything got created already + method?.dispose(); // in case anything got created already } } else { // methods don't match up, let's cancel this @@ -300,15 +300,15 @@ class KeyVerification { return; } // validate the specific payload - if (!method.validateStart(payload)) { + if (!method!.validateStart(payload)) { await cancel('m.unknown_method'); return; } - startPaylaod = payload; + startPayload = payload; setState(KeyVerificationState.askAccept); } else { Logs().i('handling start in method.....'); - await method.handlePayload(type, payload); + await method!.handlePayload(type, payload); } break; case EventTypes.KeyVerificationDone: @@ -322,7 +322,7 @@ class KeyVerification { break; default: if (method != null) { - await method.handlePayload(type, payload); + await method!.handlePayload(type, payload); } else { await cancel('m.invalid_message'); } @@ -347,9 +347,9 @@ class KeyVerification { } Future openSSSS( - {String passphrase, - String recoveryKey, - String keyOrPassphrase, + {String? passphrase, + String? recoveryKey, + String? keyOrPassphrase, bool skip = false}) async { final next = () { if (_nextAction == 'request') { @@ -391,7 +391,8 @@ class KeyVerification { }); } else { // we need to send an accept event - await method.handlePayload(EventTypes.KeyVerificationStart, startPaylaod); + await method! + .handlePayload(EventTypes.KeyVerificationStart, startPayload!); } } @@ -432,7 +433,7 @@ class KeyVerification { List get sasTypes { if (method is _KeyVerificationMethodSas) { - return (method as _KeyVerificationMethodSas).authenticationTypes; + return (method as _KeyVerificationMethodSas).authenticationTypes ?? []; } return []; } @@ -479,7 +480,7 @@ class KeyVerification { final keyId = entry.key; final verifyDeviceId = keyId.substring('ed25519:'.length); final keyInfo = entry.value; - final key = client.userDeviceKeys[userId].getKey(verifyDeviceId); + final key = client.userDeviceKeys[userId]!.getKey(verifyDeviceId); if (key != null) { if (!(await verifier(keyInfo, key))) { await cancel('m.key_mismatch'); @@ -536,7 +537,7 @@ class KeyVerification { return false; } - Future verifyLastStep(List checkLastStep) async { + Future verifyLastStep(List checkLastStep) async { if (!(await verifyActivity())) { return false; } @@ -577,7 +578,7 @@ class KeyVerification { makePayload(payload); Logs().i('[Key Verification] Sending type $type: ' + payload.toString()); if (room != null) { - Logs().i('[Key Verification] Sending to $userId in room ${room.id}...'); + Logs().i('[Key Verification] Sending to $userId in room ${room!.id}...'); if ({EventTypes.KeyVerificationRequest}.contains(type)) { payload['msgtype'] = type; payload['to'] = userId; @@ -585,7 +586,7 @@ class KeyVerification { 'Attempting verification request. ($type) Apparently your client doesn\'t support this'; type = EventTypes.Message; } - final newTransactionId = await room.sendEvent(payload, type: type); + final newTransactionId = await room!.sendEvent(payload, type: type); if (transactionId == null) { transactionId = newTransactionId; encryption.keyVerificationManager.addRequest(this); @@ -603,11 +604,11 @@ class KeyVerification { '[Key Verification] Tried to broadcast and un-broadcastable type: $type'); } } else { - if (client.userDeviceKeys[userId].deviceKeys[deviceId] == null) { + if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] == null) { Logs().e('[Key Verification] Unknown device'); } await client.sendToDeviceEncrypted( - [client.userDeviceKeys[userId].deviceKeys[deviceId]], + [client.userDeviceKeys[userId]!.deviceKeys[deviceId]!], type, payload); } @@ -619,7 +620,7 @@ class KeyVerification { state = newState; } if (onUpdate != null) { - onUpdate(); + onUpdate!(); } } } @@ -628,14 +629,14 @@ abstract class _KeyVerificationMethod { KeyVerification request; Encryption get encryption => request.encryption; Client get client => request.client; - _KeyVerificationMethod({this.request}); + _KeyVerificationMethod({required this.request}); Future handlePayload(String type, Map payload); bool validateStart(Map payload) { return false; } - String _type; + late String _type; String get type => _type; Future sendStart(); @@ -647,21 +648,21 @@ const knownHashes = ['sha256']; const knownHashesAuthentificationCodes = ['hkdf-hmac-sha256']; class _KeyVerificationMethodSas extends _KeyVerificationMethod { - _KeyVerificationMethodSas({KeyVerification request}) + _KeyVerificationMethodSas({required KeyVerification request}) : super(request: request); @override final _type = 'm.sas.v1'; - String keyAgreementProtocol; - String hash; - String messageAuthenticationCode; - List authenticationTypes; - String startCanonicalJson; - String commitment; - String theirPublicKey; - Map macPayload; - olm.SAS sas; + String? keyAgreementProtocol; + String? hash; + String? messageAuthenticationCode; + List? authenticationTypes; + late String startCanonicalJson; + String? commitment; + late String theirPublicKey; + Map? macPayload; + olm.SAS? sas; @override void dispose() { @@ -808,7 +809,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { Future _sendAccept() async { sas = olm.SAS(); - commitment = _makeCommitment(sas.get_pubkey(), startCanonicalJson); + commitment = _makeCommitment(sas!.get_pubkey(), startCanonicalJson); await request.send(EventTypes.KeyVerificationAccept, { 'method': type, 'key_agreement_protocol': keyAgreementProtocol, @@ -847,13 +848,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { Future _sendKey() async { await request.send('m.key.verification.key', { - 'key': sas.get_pubkey(), + 'key': sas!.get_pubkey(), }); } void _handleKey(Map payload) { theirPublicKey = payload['key']; - sas.set_their_key(payload['key']); + sas!.set_their_key(payload['key']); } bool _validateCommitment() { @@ -865,26 +866,26 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { var sasInfo = ''; if (keyAgreementProtocol == 'curve25519-hkdf-sha256') { final ourInfo = - '${client.userID}|${client.deviceID}|${sas.get_pubkey()}|'; + '${client.userID}|${client.deviceID}|${sas!.get_pubkey()}|'; final theirInfo = '${request.userId}|${request.deviceId}|$theirPublicKey|'; sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + - request.transactionId; + request.transactionId!; } else if (keyAgreementProtocol == 'curve25519') { final ourInfo = client.userID + client.deviceID; - final theirInfo = request.userId + request.deviceId; + final theirInfo = request.userId + request.deviceId!; sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + - request.transactionId; + request.transactionId!; } else { throw Exception('Unknown key agreement protocol'); } - return sas.generate_bytes(sasInfo, bytes); + return sas!.generate_bytes(sasInfo, bytes); } Future _sendMac() async { @@ -892,8 +893,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { client.userID + client.deviceID + request.userId + - request.deviceId + - request.transactionId; + request.deviceId! + + request.transactionId!; final mac = {}; final keyList = []; @@ -902,17 +903,17 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { // we would also add the cross signing key here final deviceKeyId = 'ed25519:${client.deviceID}'; mac[deviceKeyId] = - _calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId); + _calculateMac(encryption.fingerprintKey!, baseInfo + deviceKeyId); keyList.add(deviceKeyId); final masterKey = client.userDeviceKeys.containsKey(client.userID) - ? client.userDeviceKeys[client.userID].masterKey + ? client.userDeviceKeys[client.userID]!.masterKey : null; if (masterKey != null && masterKey.verified) { // we have our own master key verified, let's send it! final masterKeyId = 'ed25519:${masterKey.publicKey}'; mac[masterKeyId] = - _calculateMac(masterKey.publicKey, baseInfo + masterKeyId); + _calculateMac(masterKey.publicKey!, baseInfo + masterKeyId); keyList.add(masterKeyId); } @@ -925,13 +926,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { } Future _processMac() async { - final payload = macPayload; + final payload = macPayload!; final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + request.userId + - request.deviceId + + request.deviceId! + client.userID + client.deviceID + - request.transactionId; + request.transactionId!; final keyList = payload['mac'].keys.toList(); keyList.sort(); @@ -953,7 +954,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { } await request.verifyKeys(mac, (String mac, SignableKey key) async { return mac == - _calculateMac(key.ed25519Key, baseInfo + 'ed25519:' + key.identifier); + _calculateMac( + key.ed25519Key!, baseInfo + 'ed25519:' + key.identifier!); }); } @@ -969,7 +971,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { String _calculateMac(String input, String info) { if (messageAuthenticationCode == 'hkdf-hmac-sha256') { - return sas.calculate_mac(input, info); + return sas!.calculate_mac(input, info); } else { throw Exception('Unknown message authentification code'); } @@ -1239,6 +1241,6 @@ class KeyVerificationEmoji { final int number; KeyVerificationEmoji(this.number); - String get emoji => _emojiMap[number]['emoji']; - String get name => _emojiMap[number]['name']; + String get emoji => _emojiMap[number]['emoji'] ?? ''; + String get name => _emojiMap[number]['name'] ?? ''; } diff --git a/lib/encryption/utils/olm_session.dart b/lib/encryption/utils/olm_session.dart index 5d97e95a..2d66b3ae 100644 --- a/lib/encryption/utils/olm_session.dart +++ b/lib/encryption/utils/olm_session.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2020, 2021 Famedly GmbH @@ -23,31 +22,32 @@ import '../../matrix.dart'; class OlmSession { String identityKey; - String sessionId; - olm.Session session; - DateTime lastReceived; + String? sessionId; + olm.Session? session; + DateTime? lastReceived; final String key; - String get pickledSession => session.pickle(key); + String? get pickledSession => session?.pickle(key); bool get isValid => session != null; OlmSession({ - this.key, - this.identityKey, - this.sessionId, - this.session, - this.lastReceived, + required this.key, + required this.identityKey, + required this.sessionId, + required this.session, + required this.lastReceived, }); - OlmSession.fromJson(Map dbEntry, String key) : key = key { + OlmSession.fromJson(Map dbEntry, String key) + : key = key, + identityKey = dbEntry['identity_key'] ?? '' { session = olm.Session(); try { - session.unpickle(key, dbEntry['pickle']); - identityKey = dbEntry['identity_key']; + session!.unpickle(key, dbEntry['pickle']); sessionId = dbEntry['session_id']; lastReceived = DateTime.fromMillisecondsSinceEpoch(dbEntry['last_received'] ?? 0); - assert(sessionId == session.session_id()); + assert(sessionId == session!.session_id()); } catch (e, s) { Logs().e('[LibOlm] Could not unpickle olm session', e, s); dispose(); diff --git a/lib/encryption/utils/session_key.dart b/lib/encryption/utils/session_key.dart index c9e3be80..752d0e51 100644 --- a/lib/encryption/utils/session_key.dart +++ b/lib/encryption/utils/session_key.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2019, 2020, 2021 Famedly GmbH @@ -24,7 +23,7 @@ import '../../matrix.dart'; class SessionKey { /// The raw json content of the key - Map content; + Map content = {}; /// Map of stringified-index to event id, so that we can detect replay attacks Map indexes; @@ -34,7 +33,7 @@ class SessionKey { Map> allowedAtIndex; /// Underlying olm [InboundGroupSession] object - olm.InboundGroupSession inboundGroupSession; + olm.InboundGroupSession? inboundGroupSession; /// Key for libolm pickle / unpickle final String key; @@ -47,10 +46,10 @@ class SessionKey { []; /// Claimed keys of the original sender - Map senderClaimedKeys; + late Map senderClaimedKeys; /// Sender curve25519 key - String senderKey; + late String senderKey; /// Is this session valid? bool get isValid => inboundGroupSession != null; @@ -62,66 +61,35 @@ class SessionKey { String sessionId; SessionKey( - {this.content, - this.inboundGroupSession, - this.key, - this.indexes, - this.allowedAtIndex, - this.roomId, - this.sessionId, - String senderKey, - Map senderClaimedKeys}) { - _setSenderKey(senderKey); - _setSenderClaimedKeys(senderClaimedKeys); - indexes ??= {}; - allowedAtIndex ??= >{}; - } + {required this.content, + required this.inboundGroupSession, + required this.key, + Map? indexes, + Map>? allowedAtIndex, + required this.roomId, + required this.sessionId, + required this.senderKey, + required this.senderClaimedKeys}) + : indexes = indexes ?? {}, + allowedAtIndex = allowedAtIndex ?? >{}; - SessionKey.fromDb(StoredInboundGroupSession dbEntry, String key) : key = key { - final parsedContent = Event.getMapFromPayload(dbEntry.content); - final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes); - final parsedAllowedAtIndex = - Event.getMapFromPayload(dbEntry.allowedAtIndex); - final parsedSenderClaimedKeys = - Event.getMapFromPayload(dbEntry.senderClaimedKeys); - content = parsedContent; + SessionKey.fromDb(StoredInboundGroupSession dbEntry, String key) + : key = key, + content = Event.getMapFromPayload(dbEntry.content), + indexes = + Map.from(Event.getMapFromPayload(dbEntry.indexes)), + allowedAtIndex = Map>.from( + Event.getMapFromPayload(dbEntry.allowedAtIndex) + .map((k, v) => MapEntry(k, Map.from(v)))), + roomId = dbEntry.roomId, + sessionId = dbEntry.sessionId, + senderKey = dbEntry.senderKey, + inboundGroupSession = olm.InboundGroupSession() { + final parsedSenderClaimedKeys = Map.from( + Event.getMapFromPayload(dbEntry.senderClaimedKeys)); // we need to try...catch as the map used to be and that will throw an error. - try { - indexes = parsedIndexes != null - ? Map.from(parsedIndexes) - : {}; - } catch (e) { - indexes = {}; - } - try { - allowedAtIndex = parsedAllowedAtIndex != null - ? Map>.from(parsedAllowedAtIndex - .map((k, v) => MapEntry(k, Map.from(v)))) - : >{}; - } catch (e) { - allowedAtIndex = >{}; - } - roomId = dbEntry.roomId; - sessionId = dbEntry.sessionId; - _setSenderKey(dbEntry.senderKey); - _setSenderClaimedKeys(Map.from(parsedSenderClaimedKeys)); - - inboundGroupSession = olm.InboundGroupSession(); - try { - inboundGroupSession.unpickle(key, dbEntry.pickle); - } catch (e, s) { - dispose(); - Logs().e('[LibOlm] Unable to unpickle inboundGroupSession', e, s); - } - } - - void _setSenderKey(String key) { - senderKey = key ?? content['sender_key'] ?? ''; - } - - void _setSenderClaimedKeys(Map keys) { - senderClaimedKeys = (keys != null && keys.isNotEmpty) - ? keys + senderClaimedKeys = (parsedSenderClaimedKeys.isNotEmpty) + ? parsedSenderClaimedKeys : (content['sender_claimed_keys'] is Map ? Map.from(content['sender_claimed_keys']) : (content['sender_claimed_ed25519_key'] is String @@ -129,6 +97,13 @@ class SessionKey { 'ed25519': content['sender_claimed_ed25519_key'] } : {})); + + try { + inboundGroupSession!.unpickle(key, dbEntry.pickle); + } catch (e, s) { + dispose(); + Logs().e('[LibOlm] Unable to unpickle inboundGroupSession', e, s); + } } void dispose() { diff --git a/lib/encryption/utils/ssss_cache.dart b/lib/encryption/utils/ssss_cache.dart index e14aad77..7c9eb321 100644 --- a/lib/encryption/utils/ssss_cache.dart +++ b/lib/encryption/utils/ssss_cache.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2021 Famedly GmbH @@ -18,11 +17,11 @@ */ class SSSSCache { - final int clientId; - final String type; - final String keyId; - final String ciphertext; - final String content; + final int? clientId; + final String? type; + final String? keyId; + final String? ciphertext; + final String? content; const SSSSCache( {this.clientId, this.type, this.keyId, this.ciphertext, this.content}); diff --git a/lib/src/database/hive_database.dart b/lib/src/database/hive_database.dart index 88101ac6..a68e7ce1 100644 --- a/lib/src/database/hive_database.dart +++ b/lib/src/database/hive_database.dart @@ -231,10 +231,11 @@ class FamedlySdkHiveDatabase extends DatabaseApi { convertToJson(raw), Client(''), ); - await addSeenDeviceId(deviceKeys.userId, deviceKeys.deviceId, - deviceKeys.curve25519Key + deviceKeys.ed25519Key); - await addSeenPublicKey(deviceKeys.ed25519Key, deviceKeys.deviceId); - await addSeenPublicKey(deviceKeys.curve25519Key, deviceKeys.deviceId); + await addSeenDeviceId(deviceKeys.userId, deviceKeys.deviceId!, + deviceKeys.curve25519Key! + deviceKeys.ed25519Key!); + await addSeenPublicKey(deviceKeys.ed25519Key!, deviceKeys.deviceId!); + await addSeenPublicKey( + deviceKeys.curve25519Key!, deviceKeys.deviceId!); } catch (e) { Logs().w('Can not migrate device $key', e); } diff --git a/lib/src/utils/crypto/crypto.dart b/lib/src/utils/crypto/crypto.dart index a0cef89e..7bbddf80 100644 --- a/lib/src/utils/crypto/crypto.dart +++ b/lib/src/utils/crypto/crypto.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2019, 2020, 2021 Famedly GmbH diff --git a/lib/src/utils/crypto/native.dart b/lib/src/utils/crypto/native.dart index 476f2207..ce317859 100644 --- a/lib/src/utils/crypto/native.dart +++ b/lib/src/utils/crypto/native.dart @@ -1,4 +1,3 @@ -// @dart=2.9 import 'dart:async'; import 'dart:typed_data'; import 'dart:ffi'; diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 86ad1992..ce4c01de 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2020, 2021 Famedly GmbH @@ -20,6 +19,7 @@ import 'dart:convert'; import 'package:canonical_json/canonical_json.dart'; +import 'package:collection/collection.dart' show IterableExtension; import 'package:matrix/matrix.dart'; import 'package:olm/olm.dart' as olm; @@ -37,7 +37,7 @@ class DeviceKeysList { Map deviceKeys = {}; Map crossSigningKeys = {}; - SignableKey getKey(String id) { + SignableKey? getKey(String id) { if (deviceKeys.containsKey(id)) { return deviceKeys[id]; } @@ -47,18 +47,18 @@ class DeviceKeysList { return null; } - CrossSigningKey getCrossSigningKey(String type) => crossSigningKeys.values - .firstWhere((k) => k.usage.contains(type), orElse: () => null); + CrossSigningKey? getCrossSigningKey(String type) => + crossSigningKeys.values.firstWhereOrNull((k) => k.usage.contains(type)); - CrossSigningKey get masterKey => getCrossSigningKey('master'); - CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing'); - CrossSigningKey get userSigningKey => getCrossSigningKey('user_signing'); + CrossSigningKey? get masterKey => getCrossSigningKey('master'); + CrossSigningKey? get selfSigningKey => getCrossSigningKey('self_signing'); + CrossSigningKey? get userSigningKey => getCrossSigningKey('user_signing'); UserVerifiedStatus get verified { if (masterKey == null) { return UserVerifiedStatus.unknown; } - if (masterKey.verified) { + if (masterKey!.verified) { for (final key in deviceKeys.values) { if (!key.verified) { return UserVerifiedStatus.unknownDevice; @@ -79,7 +79,8 @@ class DeviceKeysList { if (userId != client.userID) { // in-room verification with someone else final roomId = await client.startDirectChat(userId); - if (roomId == null) { + if (roomId == + null /* can be null as long as startDirectChat is not migrated */) { throw Exception('Unable to start new room'); } final room = @@ -104,9 +105,9 @@ class DeviceKeysList { Map dbEntry, List> childEntries, List> crossSigningEntries, - Client cl) { - client = cl; - userId = dbEntry['user_id']; + Client cl) + : client = cl, + userId = dbEntry['user_id'] ?? '' { outdated = dbEntry['outdated']; deviceKeys = {}; for (final childEntry in childEntries) { @@ -132,24 +133,27 @@ class DeviceKeysList { class SimpleSignableKey extends MatrixSignableKey { @override - String identifier; + String? identifier; SimpleSignableKey.fromJson(Map json) : super.fromJson(json); } abstract class SignableKey extends MatrixSignableKey { Client client; - Map validSignatures; - bool _verified; - bool blocked; + Map? validSignatures; + bool? _verified; + bool? _blocked; - @override - String identifier; + String? get ed25519Key => keys['ed25519:$identifier']; + bool get verified => + identifier != null && (directVerified || crossVerified) && !(blocked); + bool get blocked => _blocked ?? false; + set blocked(bool b) => _blocked = b; - String get ed25519Key => keys['ed25519:$identifier']; - bool get verified => (directVerified || crossVerified) && !blocked; bool get encryptToDevice => - !blocked && + !(blocked) && + identifier != null && + ed25519Key != null && (client.userDeviceKeys[userId]?.masterKey?.verified ?? false ? verified : true); @@ -158,7 +162,7 @@ abstract class SignableKey extends MatrixSignableKey { _verified = v; } - bool get directVerified => _verified; + bool get directVerified => _verified ?? false; bool get crossVerified => hasValidSignatureChain(); bool get signed => hasValidSignatureChain(verifiedOnly: false); @@ -166,14 +170,14 @@ abstract class SignableKey extends MatrixSignableKey { : client = cl, super.fromJson(json) { _verified = false; - blocked = false; + _blocked = false; } SimpleSignableKey cloneForSigning() { final newKey = SimpleSignableKey.fromJson(toJson().copy()); newKey.identifier = identifier; newKey.signatures ??= >{}; - newKey.signatures.clear(); + newKey.signatures!.clear(); return newKey; } @@ -188,7 +192,7 @@ abstract class SignableKey extends MatrixSignableKey { return String.fromCharCodes(canonicalJson.encode(data)); } - bool _verifySignature(String pubKey, String signature, + bool _verifySignature(String /*!*/ pubKey, String /*!*/ signature, {bool isSignatureWithoutLibolmValid = false}) { olm.Utility olmutil; try { @@ -214,22 +218,26 @@ abstract class SignableKey extends MatrixSignableKey { bool hasValidSignatureChain( {bool verifiedOnly = true, - Set visited, - Set onlyValidateUserIds}) { + Set? visited, + Set? onlyValidateUserIds}) { if (!client.encryptionEnabled) { return false; } - visited ??= {}; - onlyValidateUserIds ??= {}; + + final visited_ = visited ?? {}; + final onlyValidateUserIds_ = onlyValidateUserIds ?? {}; + final setKey = '$userId;$identifier'; - if (visited.contains(setKey) || - (onlyValidateUserIds.isNotEmpty && - !onlyValidateUserIds.contains(userId))) { + if (visited_.contains(setKey) || + (onlyValidateUserIds_.isNotEmpty && + !onlyValidateUserIds_.contains(userId))) { return false; // prevent recursion & validate hasValidSignatureChain } - visited.add(setKey); + visited_.add(setKey); + if (signatures == null) return false; - for (final signatureEntries in signatures.entries) { + + for (final signatureEntries in signatures!.entries) { final otherUserId = signatureEntries.key; if (!(signatureEntries.value is Map) || !client.userDeviceKeys.containsKey(otherUserId)) { @@ -250,18 +258,20 @@ abstract class SignableKey extends MatrixSignableKey { if (otherUserId == userId && keyId == identifier) { continue; } - SignableKey key; - if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) { - key = client.userDeviceKeys[otherUserId].deviceKeys[keyId]; - } else if (client.userDeviceKeys[otherUserId].crossSigningKeys + SignableKey? key; + if (client.userDeviceKeys[otherUserId]!.deviceKeys.containsKey(keyId)) { + key = client.userDeviceKeys[otherUserId]!.deviceKeys[keyId]; + } else if (client.userDeviceKeys[otherUserId]!.crossSigningKeys .containsKey(keyId)) { - key = client.userDeviceKeys[otherUserId].crossSigningKeys[keyId]; - } else { + key = client.userDeviceKeys[otherUserId]!.crossSigningKeys[keyId]; + } + + if (key == null) { continue; } - if (onlyValidateUserIds.isNotEmpty && - !onlyValidateUserIds.contains(key.userId)) { + if (onlyValidateUserIds_.isNotEmpty && + !onlyValidateUserIds_.contains(key.userId)) { // we don't want to verify keys from this user continue; } @@ -272,24 +282,24 @@ abstract class SignableKey extends MatrixSignableKey { var haveValidSignature = false; var gotSignatureFromCache = false; if (validSignatures != null && - validSignatures.containsKey(otherUserId) && - validSignatures[otherUserId].containsKey(fullKeyId)) { - if (validSignatures[otherUserId][fullKeyId] == true) { + validSignatures!.containsKey(otherUserId) && + validSignatures![otherUserId].containsKey(fullKeyId)) { + if (validSignatures![otherUserId][fullKeyId] == true) { haveValidSignature = true; gotSignatureFromCache = true; - } else if (validSignatures[otherUserId][fullKeyId] == false) { + } else if (validSignatures![otherUserId][fullKeyId] == false) { haveValidSignature = false; gotSignatureFromCache = true; } } - if (!gotSignatureFromCache) { + if (!gotSignatureFromCache && key.ed25519Key != null) { // validate the signature manually - haveValidSignature = _verifySignature(key.ed25519Key, signature); + haveValidSignature = _verifySignature(key.ed25519Key!, signature); validSignatures ??= {}; - if (!validSignatures.containsKey(otherUserId)) { - validSignatures[otherUserId] = {}; + if (!validSignatures!.containsKey(otherUserId)) { + validSignatures![otherUserId] = {}; } - validSignatures[otherUserId][fullKeyId] = haveValidSignature; + validSignatures![otherUserId][fullKeyId] = haveValidSignature; } if (!haveValidSignature) { // no valid signature, this key is useless @@ -328,7 +338,7 @@ abstract class SignableKey extends MatrixSignableKey { } } - Future setBlocked(bool newBlocked); + Future /*!*/ setBlocked(bool newBlocked); @override Map toJson() { @@ -349,24 +359,36 @@ abstract class SignableKey extends MatrixSignableKey { } class CrossSigningKey extends SignableKey { - String get publicKey => identifier; - List usage; + @override + String? identifier; + + String? get publicKey => identifier; + late List usage; bool get isValid => - userId != null && publicKey != null && keys != null && ed25519Key != null; + userId.isNotEmpty && + publicKey != null && + keys.isNotEmpty && + ed25519Key != null; @override Future setVerified(bool newVerified, [bool sign = true]) async { + if (!isValid) { + throw Exception('setVerified called on invalid key'); + } await super.setVerified(newVerified, sign); - return client.database - ?.setVerifiedUserCrossSigningKey(newVerified, userId, publicKey); + await client.database + ?.setVerifiedUserCrossSigningKey(newVerified, userId, publicKey!); } @override - Future setBlocked(bool newBlocked) { - blocked = newBlocked; - return client.database - ?.setBlockedUserCrossSigningKey(newBlocked, userId, publicKey); + Future setBlocked(bool newBlocked) async { + if (!isValid) { + throw Exception('setBlocked called on invalid key'); + } + _blocked = newBlocked; + await client.database + ?.setBlockedUserCrossSigningKey(newBlocked, userId, publicKey!); } CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl) @@ -382,69 +404,80 @@ class CrossSigningKey extends SignableKey { identifier = dbEntry['public_key']; usage = json['usage'].cast(); _verified = dbEntry['verified']; - blocked = dbEntry['blocked']; + _blocked = dbEntry['blocked']; } CrossSigningKey.fromJson(Map json, Client cl) : super.fromJson(json.copy(), cl) { final json = toJson(); usage = json['usage'].cast(); - if (keys != null && keys.isNotEmpty) { + if (keys.isNotEmpty) { identifier = keys.values.first; } } } class DeviceKeys extends SignableKey { - String get deviceId => identifier; - List algorithms; - DateTime lastActive; + @override + String? identifier; - String get curve25519Key => keys['curve25519:$deviceId']; - String get deviceDisplayName => - unsigned != null ? unsigned['device_display_name'] : null; + String? get deviceId => identifier; + late List algorithms; + late DateTime lastActive; - bool _validSelfSignature; + String? get curve25519Key => keys['curve25519:$deviceId']; + String? get deviceDisplayName => + unsigned != null ? unsigned!['device_display_name'] : null; + + bool? _validSelfSignature; bool get selfSigned => _validSelfSignature ?? - (_validSelfSignature = (signatures - ?.tryGet>(userId) - ?.tryGet('ed25519:$deviceId') == - null + (_validSelfSignature = (deviceId != null && + signatures + ?.tryGet>(userId) + ?.tryGet('ed25519:$deviceId') == + null ? false // without libolm we still want to be able to add devices. In that case we ofc just can't // verify the signature : _verifySignature( - ed25519Key, signatures[userId]['ed25519:$deviceId'], + ed25519Key!, signatures![userId]!['ed25519:$deviceId']!, isSignatureWithoutLibolmValid: true))); @override bool get blocked => super.blocked || !selfSigned; bool get isValid => - userId != null && deviceId != null && - keys != null && + keys.isNotEmpty && curve25519Key != null && ed25519Key != null && selfSigned; @override Future setVerified(bool newVerified, [bool sign = true]) async { + if (!isValid) { + //throw Exception('setVerified called on invalid key'); + return; + } await super.setVerified(newVerified, sign); - return client?.database - ?.setVerifiedUserDeviceKey(newVerified, userId, deviceId); + await client.database + ?.setVerifiedUserDeviceKey(newVerified, userId, deviceId!); } @override - Future setBlocked(bool newBlocked) { - blocked = newBlocked; - return client?.database - ?.setBlockedUserDeviceKey(newBlocked, userId, deviceId); + Future setBlocked(bool newBlocked) async { + if (!isValid) { + //throw Exception('setBlocked called on invalid key'); + return; + } + _blocked = newBlocked; + await client.database + ?.setBlockedUserDeviceKey(newBlocked, userId, deviceId!); } DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl, - [DateTime lastActiveTs]) + [DateTime? lastActiveTs]) : super.fromJson(k.toJson().copy(), cl) { final json = toJson(); identifier = k.deviceId; @@ -458,7 +491,7 @@ class DeviceKeys extends SignableKey { identifier = dbEntry['device_id']; algorithms = json['algorithms'].cast(); _verified = dbEntry['verified']; - blocked = dbEntry['blocked']; + _blocked = dbEntry['blocked']; lastActive = DateTime.fromMillisecondsSinceEpoch(dbEntry['last_active'] ?? 0); } @@ -472,8 +505,11 @@ class DeviceKeys extends SignableKey { } KeyVerification startVerification() { + if (!isValid) { + throw Exception('setVerification called on invalid key'); + } final request = KeyVerification( - encryption: client.encryption, userId: userId, deviceId: deviceId); + encryption: client.encryption, userId: userId, deviceId: deviceId!); request.start(); client.encryption.keyVerificationManager.addRequest(request); diff --git a/pubspec.yaml b/pubspec.yaml index 459113e2..998892f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: js: ^0.6.3 slugify: ^2.0.0 html: ^0.15.0 + collection: ^1.15.0-nullsafety.4 dev_dependencies: pedantic: ^1.11.0 diff --git a/test/device_keys_list_test.dart b/test/device_keys_list_test.dart index a1aa9ad7..ec8a176e 100644 --- a/test/device_keys_list_test.dart +++ b/test/device_keys_list_test.dart @@ -31,7 +31,27 @@ void main() { /// All Tests related to device keys group('Device keys', () { Logs().level = Level.error; + + var olmEnabled = true; + + Client client; + + test('setupClient', () async { + try { + await olm.init(); + olm.get_library_version(); + } catch (e) { + olmEnabled = false; + Logs().w('[LibOlm] Failed to load LibOlm', e); + } + Logs().i('[LibOlm] Enabled: $olmEnabled'); + if (!olmEnabled) return; + + client = await getClient(); + }); + test('fromJson', () async { + if (!olmEnabled) return; var rawJson = { 'user_id': '@alice:example.com', 'device_id': 'JLAFKJWSCS', @@ -53,7 +73,8 @@ void main() { 'unsigned': {'device_display_name': "Alice's mobile phone"}, }; - final key = DeviceKeys.fromJson(rawJson, null); + final key = DeviceKeys.fromJson(rawJson, client); + // NOTE(Nico): this actually doesn't do anything, because the device signature is invalid... await key.setVerified(false, false); await key.setBlocked(true); expect(json.encode(key.toJson()), json.encode(rawJson)); @@ -69,29 +90,11 @@ void main() { }, 'signatures': {}, }; - final crossKey = CrossSigningKey.fromJson(rawJson, null); + final crossKey = CrossSigningKey.fromJson(rawJson, client); expect(json.encode(crossKey.toJson()), json.encode(rawJson)); expect(crossKey.usage.first, 'master'); }); - var olmEnabled = true; - - Client client; - - test('setupClient', () async { - try { - await olm.init(); - olm.get_library_version(); - } catch (e) { - olmEnabled = false; - Logs().w('[LibOlm] Failed to load LibOlm', e); - } - Logs().i('[LibOlm] Enabled: $olmEnabled'); - if (!olmEnabled) return; - - client = await getClient(); - }); - test('reject devices without self-signature', () async { if (!olmEnabled) return; var key = DeviceKeys.fromJson({