refactor: nullsafe encryption

This commit is contained in:
Nicolas Werner 2021-09-10 01:32:44 +02:00
parent 259c9cade6
commit a196b53219
17 changed files with 695 additions and 608 deletions

View File

@ -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

View File

@ -1,4 +1,3 @@
// @dart=2.9
/*
* Famedly Matrix SDK
* Copyright (C) 2020, 2021 Famedly GmbH

View File

@ -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<String, dynamic> mRelatesTo = payload.remove('m.relates_to');
final Map<String, dynamic>? mRelatesTo = payload.remove('m.relates_to');
final payloadContent = {
'content': payload,
'type': type,
@ -348,7 +356,7 @@ class Encryption {
final encryptedPayload = <String, dynamic>{
'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

View File

@ -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<String, dynamic> content,
{bool forwarded = false,
Map<String, String> senderClaimedKeys,
void setInboundGroupSession(
String roomId,
String sessionId,
String senderKey,
Map<String, dynamic> content, {
bool forwarded = false,
Map<String, String>? senderClaimedKeys,
bool uploaded = false,
Map<String, Map<String, int>> allowedAtIndex}) {
senderClaimedKeys ??= <String, String>{};
if (!senderClaimedKeys.containsKey('ed25519')) {
Map<String, Map<String, int>>? allowedAtIndex,
}) {
final senderClaimedKeys_ = senderClaimedKeys ?? <String, String>{};
final allowedAtIndex_ = allowedAtIndex ?? <String, Map<String, int>>{};
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] = <String, SessionKey>{};
}
_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<SessionKey> loadInboundGroupSession(
Future<SessionKey?> 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] = <String, SessionKey>{};
}
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> deviceKeys) {
final deviceKeyIds = <String, Map<String, bool>>{};
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] = <String, bool>{};
}
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 = <DeviceKeys>[];
@ -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
final oldBlockedDevices = sess.devices.containsKey(userId)
? Set.from(sess.devices[userId]!.entries
.where((e) => e.value)
.map((e) => e.key));
final newBlockedDevices = Set.from(newDeviceKeyIds[userId]
.map((e) => e.key))
: <String>{};
final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
? Set.from(newDeviceKeyIds[userId]!
.entries
.where((e) => e.value)
.map((e) => e.key));
.map((e) => e.key))
: <String>{};
// 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
final oldDeviceIds = sess.devices.containsKey(userId)
? Set.from(sess.devices[userId]!.entries
.where((e) => !e.value)
.map((e) => e.key));
final newDeviceIds = Set.from(newDeviceKeyIds[userId]
.map((e) => e.key))
: <String>{};
final newDeviceIds = newDeviceKeyIds.containsKey(userId)
? Set.from(newDeviceKeyIds[userId]!
.entries
.where((e) => !e.value)
.map((e) => e.key));
.map((e) => e.key))
: <String>{};
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] ??= <String, int>{};
if (!inboundSess.allowedAtIndex[device.userId]
inboundSess!.allowedAtIndex[device.userId] ??= <String, int>{};
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<void> 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<OutboundGroupSession> 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 = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2,
@ -468,12 +488,16 @@ class KeyManager {
};
final allowedAtIndex = <String, Map<String, int>>{};
for (final device in deviceKeys) {
if (!device.isValid) {
Logs().e('Skipping invalid device');
continue;
}
allowedAtIndex[device.userId] ??= <String, int>{};
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<GetRoomKeysVersionCurrentResponse> 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<void> 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<String, dynamic> decrypted;
Map<String, dynamic>? 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<String, String>.from(decrypted['sender_claimed_keys'])
: null,
? Map<String, String>.from(
decrypted['sender_claimed_keys']!)
: <String, String>{},
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<Map<String, dynamic>>(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) =>
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'],
orElse: () => null);
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) =>
final device = request.devices.firstWhereOrNull((d) =>
d.userId == event.sender &&
d.curve25519Key == event.encryptedContent['sender_key'],
orElse: () => null);
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<DeviceKeys>? 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<void> forwardKey([int index]) async {
Future<void> 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<String>.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;

View File

@ -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<String, List<OlmSession>> get olmSessions => _olmSessions;
final Map<String, List<OlmSession>> _olmSessions = {};
Future<void> 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<void> 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<String, dynamic> signJson(Map<String, dynamic> payload) {
if (!enabled) throw ('Encryption is disabled');
final Map<String, dynamic> unsigned = payload['unsigned'];
final Map<String, dynamic> signatures = payload['signatures'];
final Map<String, dynamic>? unsigned = payload['unsigned'];
final Map<String, dynamic>? 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<String, dynamic> signedJson,
String userId, String deviceId) {
if (!enabled) throw ('Encryption is disabled');
final Map<String, dynamic> signatures = signedJson['signatures'];
final Map<String, dynamic>? 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<bool> 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 = <String, dynamic>{};
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<String, dynamic> 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 = <String, dynamic>{};
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<String, dynamic> 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<String, int> countJson, List<String> unusedFallbackKeyTypes) {
Map<String, int>? countJson, List<String>? 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<void> storeOlmSession(OlmSession session) async {
if (session.sessionId == null || session.pickledSession == null) {
return;
}
_olmSessions[session.identityKey] ??= <OlmSession>[];
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] ??= <OlmSession>[];
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<List<OlmSession>> 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<String, dynamic> 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<Map<String, dynamic>> encryptToDeviceMessagePayload(
DeviceKeys device, String type, Map<String, dynamic> 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 = <String, dynamic>{
'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<DeviceKeys>.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;

View File

@ -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 = <int>[...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<bool> 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<void> 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<OpenSSSS> createKey([String passphrase]) async {
Future<OpenSSSS> 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<String> getCached(String type) async {
Future<String?> 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<void> 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<String, dynamic> content;
Map<String, dynamic>? 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'] = <String, dynamic>{};
}
@ -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<String>.from(content['encrypted'].keys.where((k) => k != keyId));
content['encrypted'].removeWhere((k, v) => otherKeys.contains(k));
@ -387,7 +402,7 @@ class SSSS {
}
}
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
Future<void> maybeRequestAll([List<DeviceKeys>? devices]) async {
for (final type in cacheTypes) {
if (keyIdsFromType(type) != null) {
final secret = await getCached(type);
@ -398,7 +413,7 @@ class SSSS {
}
}
Future<void> request(String type, [List<DeviceKeys> devices]) async {
Future<void> request(String type, [List<DeviceKeys>? 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<void> 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) =>
final device = request.devices.firstWhereOrNull((d) =>
d.userId == event.sender &&
d.curve25519Key == event.encryptedContent['sender_key'],
orElse: () => null);
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<String> keyIdsFromType(String type) {
Set<String>? 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<DeviceKeys> 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<void> 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<String> 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<void> 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<void> 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<void> maybeCacheAll() async {
await ssss.maybeCacheAll(keyId, privateKey);
if (privateKey == null) {
throw Exception('SSSS not unlocked');
}
await ssss.maybeCacheAll(keyId, privateKey!);
}
Future<void> _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<Uint8List> _keyFromPassphrase(_KeyFromPassphraseArgs args) async {

View File

@ -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<String, OpenSSSS> oldSsssKeys;
OpenSSSS newSsssKey;
Map<String, String> secretMap;
Map<String, OpenSSSS>? oldSsssKeys;
OpenSSSS? newSsssKey;
Map<String, String>? 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<String, Set<String>> _secretsCache;
Map<String, Set<String>>? _secretsCache;
Map<String, Set<String>> analyzeSecrets() {
if (_secretsCache != null) {
// deep-copy so that we can do modifications
final newSecrets = <String, Set<String>>{};
for (final s in _secretsCache.entries) {
for (final s in _secretsCache!.entries) {
newSecrets[s.key] = Set<String>.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 = <String, OpenSSSS>{};
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<void> newSsss([String passphrase]) async {
Future<void> newSsss([String? passphrase]) async {
if (state != BootstrapState.askNewSsss) {
throw BootstrapBadStateException('Wrong State');
}
@ -275,7 +275,7 @@ class Bootstrap {
return s;
};
secretMap = <String, String>{};
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<void> 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 = <String, String>{};
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<void>(futures);
final keysToSign = <SignableKey>[];
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!();
}
}
}

View File

@ -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<String> _intersect(List<String> a, List<dynamic> b) =>
List<String> _intersect(List<String>? a, List<dynamic>? b) =>
(b == null || a == null) ? [] : a.where(b.contains).toList();
List<int> _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<String> possibleMethods;
Map<String, dynamic> startPaylaod;
String _nextAction;
List<SignableKey> _verifiedDevices;
_KeyVerificationMethod? method;
List<String> possibleMethods = [];
Map<String, dynamic>? startPayload;
String? _nextAction;
List<SignableKey> _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}) {
required this.userId,
String? deviceId,
this.onUpdate})
: _deviceId = deviceId,
lastActivity = DateTime.now();
_deviceId ??= deviceId;
}
void dispose() {
Logs().i('[Key Verification] disposing object...');
@ -188,7 +186,7 @@ class KeyVerification {
bool _handlePayloadLock = false;
Future<void> handlePayload(String type, Map<String, dynamic> 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<DeviceKeys>.from(
client.userDeviceKeys[userId].deviceKeys.values);
final devices = client.userDeviceKeys.containsKey(userId)
? List<DeviceKeys>.from(
client.userDeviceKeys[userId]!.deviceKeys.values)
: List<DeviceKeys>.from([]);
devices.removeWhere(
(d) => {deviceId, client.deviceID}.contains(d.deviceId));
final cancelPayload = <String, dynamic>{
@ -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<void> 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<String> 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<bool> verifyLastStep(List<String> checkLastStep) async {
Future<bool> verifyLastStep(List<String?> 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<void> handlePayload(String type, Map<String, dynamic> payload);
bool validateStart(Map<String, dynamic> payload) {
return false;
}
String _type;
late String _type;
String get type => _type;
Future<void> 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<String> authenticationTypes;
String startCanonicalJson;
String commitment;
String theirPublicKey;
Map<String, dynamic> macPayload;
olm.SAS sas;
String? keyAgreementProtocol;
String? hash;
String? messageAuthenticationCode;
List<String>? authenticationTypes;
late String startCanonicalJson;
String? commitment;
late String theirPublicKey;
Map<String, dynamic>? macPayload;
olm.SAS? sas;
@override
void dispose() {
@ -808,7 +809,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Future<void> _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<void> _sendKey() async {
await request.send('m.key.verification.key', {
'key': sas.get_pubkey(),
'key': sas!.get_pubkey(),
});
}
void _handleKey(Map<String, dynamic> 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<void> _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 = <String, String>{};
final keyList = <String>[];
@ -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<void> _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'] ?? '';
}

View File

@ -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<String, dynamic> dbEntry, String key) : key = key {
OlmSession.fromJson(Map<String, dynamic> 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();

View File

@ -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<String, dynamic> content;
Map<String, dynamic> content = <String, dynamic>{};
/// Map of stringified-index to event id, so that we can detect replay attacks
Map<String, String> indexes;
@ -34,7 +33,7 @@ class SessionKey {
Map<String, Map<String, int>> 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 {
<String>[];
/// Claimed keys of the original sender
Map<String, String> senderClaimedKeys;
late Map<String, String> 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<String, String> senderClaimedKeys}) {
_setSenderKey(senderKey);
_setSenderClaimedKeys(senderClaimedKeys);
indexes ??= <String, String>{};
allowedAtIndex ??= <String, Map<String, int>>{};
}
{required this.content,
required this.inboundGroupSession,
required this.key,
Map<String, String>? indexes,
Map<String, Map<String, int>>? allowedAtIndex,
required this.roomId,
required this.sessionId,
required this.senderKey,
required this.senderClaimedKeys})
: indexes = indexes ?? <String, String>{},
allowedAtIndex = allowedAtIndex ?? <String, Map<String, int>>{};
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<String, String>.from(Event.getMapFromPayload(dbEntry.indexes)),
allowedAtIndex = Map<String, Map<String, int>>.from(
Event.getMapFromPayload(dbEntry.allowedAtIndex)
.map((k, v) => MapEntry(k, Map<String, int>.from(v)))),
roomId = dbEntry.roomId,
sessionId = dbEntry.sessionId,
senderKey = dbEntry.senderKey,
inboundGroupSession = olm.InboundGroupSession() {
final parsedSenderClaimedKeys = Map<String, String>.from(
Event.getMapFromPayload(dbEntry.senderClaimedKeys));
// we need to try...catch as the map used to be <String, int> and that will throw an error.
try {
indexes = parsedIndexes != null
? Map<String, String>.from(parsedIndexes)
: <String, String>{};
} catch (e) {
indexes = <String, String>{};
}
try {
allowedAtIndex = parsedAllowedAtIndex != null
? Map<String, Map<String, int>>.from(parsedAllowedAtIndex
.map((k, v) => MapEntry(k, Map<String, int>.from(v))))
: <String, Map<String, int>>{};
} catch (e) {
allowedAtIndex = <String, Map<String, int>>{};
}
roomId = dbEntry.roomId;
sessionId = dbEntry.sessionId;
_setSenderKey(dbEntry.senderKey);
_setSenderClaimedKeys(Map<String, String>.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<String, String> keys) {
senderClaimedKeys = (keys != null && keys.isNotEmpty)
? keys
senderClaimedKeys = (parsedSenderClaimedKeys.isNotEmpty)
? parsedSenderClaimedKeys
: (content['sender_claimed_keys'] is Map
? Map<String, String>.from(content['sender_claimed_keys'])
: (content['sender_claimed_ed25519_key'] is String
@ -129,6 +97,13 @@ class SessionKey {
'ed25519': content['sender_claimed_ed25519_key']
}
: <String, String>{}));
try {
inboundGroupSession!.unpickle(key, dbEntry.pickle);
} catch (e, s) {
dispose();
Logs().e('[LibOlm] Unable to unpickle inboundGroupSession', e, s);
}
}
void dispose() {

View File

@ -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});

View File

@ -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);
}

View File

@ -1,4 +1,3 @@
// @dart=2.9
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH

View File

@ -1,4 +1,3 @@
// @dart=2.9
import 'dart:async';
import 'dart:typed_data';
import 'dart:ffi';

View File

@ -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<String, DeviceKeys> deviceKeys = {};
Map<String, CrossSigningKey> 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<String, dynamic> dbEntry,
List<Map<String, dynamic>> childEntries,
List<Map<String, dynamic>> 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<String, dynamic> json) : super.fromJson(json);
}
abstract class SignableKey extends MatrixSignableKey {
Client client;
Map<String, dynamic> validSignatures;
bool _verified;
bool blocked;
Map<String, dynamic>? 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 ??= <String, Map<String, String>>{};
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<String> visited,
Set<String> onlyValidateUserIds}) {
Set<String>? visited,
Set<String>? onlyValidateUserIds}) {
if (!client.encryptionEnabled) {
return false;
}
visited ??= <String>{};
onlyValidateUserIds ??= <String>{};
final visited_ = visited ?? <String>{};
final onlyValidateUserIds_ = onlyValidateUserIds ?? <String>{};
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 ??= <String, dynamic>{};
if (!validSignatures.containsKey(otherUserId)) {
validSignatures[otherUserId] = <String, dynamic>{};
if (!validSignatures!.containsKey(otherUserId)) {
validSignatures![otherUserId] = <String, dynamic>{};
}
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<void> setBlocked(bool newBlocked);
Future<void> /*!*/ setBlocked(bool newBlocked);
@override
Map<String, dynamic> toJson() {
@ -349,24 +359,36 @@ abstract class SignableKey extends MatrixSignableKey {
}
class CrossSigningKey extends SignableKey {
String get publicKey => identifier;
List<String> usage;
@override
String? identifier;
String? get publicKey => identifier;
late List<String> usage;
bool get isValid =>
userId != null && publicKey != null && keys != null && ed25519Key != null;
userId.isNotEmpty &&
publicKey != null &&
keys.isNotEmpty &&
ed25519Key != null;
@override
Future<void> 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<void> setBlocked(bool newBlocked) {
blocked = newBlocked;
return client.database
?.setBlockedUserCrossSigningKey(newBlocked, userId, publicKey);
Future<void> 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,32 +404,36 @@ class CrossSigningKey extends SignableKey {
identifier = dbEntry['public_key'];
usage = json['usage'].cast<String>();
_verified = dbEntry['verified'];
blocked = dbEntry['blocked'];
_blocked = dbEntry['blocked'];
}
CrossSigningKey.fromJson(Map<String, dynamic> json, Client cl)
: super.fromJson(json.copy(), cl) {
final json = toJson();
usage = json['usage'].cast<String>();
if (keys != null && keys.isNotEmpty) {
if (keys.isNotEmpty) {
identifier = keys.values.first;
}
}
}
class DeviceKeys extends SignableKey {
String get deviceId => identifier;
List<String> 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<String> 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
(_validSelfSignature = (deviceId != null &&
signatures
?.tryGet<Map<String, dynamic>>(userId)
?.tryGet<String>('ed25519:$deviceId') ==
null
@ -415,36 +441,43 @@ class DeviceKeys extends SignableKey {
// 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<void> 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<void> setBlocked(bool newBlocked) {
blocked = newBlocked;
return client?.database
?.setBlockedUserDeviceKey(newBlocked, userId, deviceId);
Future<void> 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<String>();
_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);

View File

@ -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

View File

@ -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 = <String, dynamic>{
'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({