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: errors:
todo: ignore todo: ignore
import_of_legacy_library_into_null_safe: 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: exclude:
- example/main.dart - example/main.dart
# needed until crypto packages upgrade # needed until crypto packages upgrade

View File

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

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH * Copyright (C) 2019, 2020, 2021 Famedly GmbH
@ -40,21 +39,21 @@ class Encryption {
/// Returns the base64 encoded keys to store them in a store. /// Returns the base64 encoded keys to store them in a store.
/// This String should **never** leave the device! /// This String should **never** leave the device!
String get pickledOlmAccount => olmManager.pickledOlmAccount; String? get pickledOlmAccount => olmManager.pickledOlmAccount;
String get fingerprintKey => olmManager.fingerprintKey; String? get fingerprintKey => olmManager.fingerprintKey;
String get identityKey => olmManager.identityKey; String? get identityKey => olmManager.identityKey;
KeyManager keyManager; late KeyManager keyManager;
OlmManager olmManager; late OlmManager olmManager;
KeyVerificationManager keyVerificationManager; late KeyVerificationManager keyVerificationManager;
CrossSigning crossSigning; late CrossSigning crossSigning;
SSSS ssss; late SSSS ssss;
Encryption({ Encryption({
this.client, required this.client,
this.debug, this.debug = false,
this.enableE2eeRecovery, required this.enableE2eeRecovery,
}) { }) {
ssss = SSSS(this); ssss = SSSS(this);
keyManager = KeyManager(this); keyManager = KeyManager(this);
@ -81,7 +80,7 @@ class Encryption {
} }
} }
Bootstrap bootstrap({void Function() onUpdate}) => Bootstrap( Bootstrap bootstrap({void Function()? onUpdate}) => Bootstrap(
encryption: this, encryption: this,
onUpdate: onUpdate, onUpdate: onUpdate,
); );
@ -190,18 +189,24 @@ class Encryption {
} }
final sessionId = content.sessionId; final sessionId = content.sessionId;
final senderKey = content.senderKey; final senderKey = content.senderKey;
if (sessionId == null) {
throw DecryptException(DecryptException.unknownSession);
}
final inboundGroupSession = final inboundGroupSession =
keyManager.getInboundGroupSession(roomId, sessionId, senderKey); keyManager.getInboundGroupSession(roomId, sessionId, senderKey);
if (inboundGroupSession == null) { if (!(inboundGroupSession?.isValid ?? false)) {
canRequestSession = true; canRequestSession = true;
throw DecryptException(DecryptException.unknownSession); throw DecryptException(DecryptException.unknownSession);
} }
// decrypt errors here may mean we have a bad session key - others might have a better one // decrypt errors here may mean we have a bad session key - others might have a better one
canRequestSession = true; canRequestSession = true;
final decryptResult = inboundGroupSession.inboundGroupSession final decryptResult = inboundGroupSession!.inboundGroupSession!
.decrypt(content.ciphertextMegolm); .decrypt(content.ciphertextMegolm!);
canRequestSession = false; canRequestSession = false;
// we can't have the key be an int, else json-serializing will fail, thus we need it to be a string // 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 messageIndexKey = 'key-' + decryptResult.message_index.toString();
final messageIndexValue = event.eventId + final messageIndexValue = event.eventId +
@ -214,6 +219,7 @@ class Encryption {
Logs().e('[Decrypt] Could not decrypt due to a corrupted session.'); Logs().e('[Decrypt] Could not decrypt due to a corrupted session.');
throw DecryptException(DecryptException.channelCorrupted); throw DecryptException(DecryptException.channelCorrupted);
} }
inboundGroupSession.indexes[messageIndexKey] = messageIndexValue; inboundGroupSession.indexes[messageIndexKey] = messageIndexValue;
if (!haveIndex) { if (!haveIndex) {
// now we persist the udpated indexes into the database. // now we persist the udpated indexes into the database.
@ -282,9 +288,11 @@ class Encryption {
} }
try { try {
if (client.database != null && if (client.database != null &&
keyManager.getInboundGroupSession(roomId, event.content['session_id'], !(keyManager
event.content['sender_key']) == .getInboundGroupSession(roomId, event.content['session_id'],
null) { event.content['sender_key'])
?.isValid ??
false)) {
await keyManager.loadInboundGroupSession( await keyManager.loadInboundGroupSession(
roomId, event.content['session_id'], event.content['sender_key']); roomId, event.content['session_id'], event.content['sender_key']);
} }
@ -325,21 +333,21 @@ class Encryption {
if (room.encryptionAlgorithm != AlgorithmTypes.megolmV1AesSha2) { if (room.encryptionAlgorithm != AlgorithmTypes.megolmV1AesSha2) {
throw ('Unknown encryption algorithm'); throw ('Unknown encryption algorithm');
} }
if (keyManager.getOutboundGroupSession(roomId) == null) { if (keyManager.getOutboundGroupSession(roomId)?.isValid != true) {
await keyManager.loadOutboundGroupSession(roomId); await keyManager.loadOutboundGroupSession(roomId);
} }
await keyManager.clearOrUseOutboundGroupSession(roomId); await keyManager.clearOrUseOutboundGroupSession(roomId);
if (keyManager.getOutboundGroupSession(roomId) == null) { if (keyManager.getOutboundGroupSession(roomId)?.isValid != true) {
await keyManager.createOutboundGroupSession(roomId); await keyManager.createOutboundGroupSession(roomId);
} }
final sess = keyManager.getOutboundGroupSession(roomId); final sess = keyManager.getOutboundGroupSession(roomId);
if (sess == null) { if (sess?.isValid != true) {
throw ('Unable to create new outbound group session'); throw ('Unable to create new outbound group session');
} }
// we clone the payload as we do not want to remove 'm.relates_to' from the // we clone the payload as we do not want to remove 'm.relates_to' from the
// original payload passed into this function // original payload passed into this function
payload = payload.copy(); 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 = { final payloadContent = {
'content': payload, 'content': payload,
'type': type, 'type': type,
@ -348,7 +356,7 @@ class Encryption {
final encryptedPayload = <String, dynamic>{ final encryptedPayload = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2, 'algorithm': AlgorithmTypes.megolmV1AesSha2,
'ciphertext': 'ciphertext':
sess.outboundGroupSession.encrypt(json.encode(payloadContent)), sess!.outboundGroupSession.encrypt(json.encode(payloadContent)),
'device_id': client.deviceID, 'device_id': client.deviceID,
'sender_key': identityKey, 'sender_key': identityKey,
'session_id': sess.outboundGroupSession.session_id(), '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 // check if we can set our own master key as verified, if it isn't yet
if (client.database != null && if (client.database != null &&
client.userDeviceKeys.containsKey(client.userID)) { client.userDeviceKeys.containsKey(client.userID)) {
final masterKey = client.userDeviceKeys[client.userID].masterKey; final masterKey = client.userDeviceKeys[client.userID]!.masterKey;
if (masterKey != null && if (masterKey != null &&
!masterKey.directVerified && !masterKey.directVerified &&
masterKey masterKey
@ -405,7 +413,7 @@ class Encryption {
class DecryptException implements Exception { class DecryptException implements Exception {
String cause; String cause;
String libolmMessage; String? libolmMessage;
DecryptException(this.cause, [this.libolmMessage]); DecryptException(this.cause, [this.libolmMessage]);
@override @override

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH * 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:matrix/encryption/utils/stored_inbound_group_session.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import 'package:collection/collection.dart';
import './encryption.dart'; import './encryption.dart';
import './utils/outbound_group_session.dart'; import './utils/outbound_group_session.dart';
@ -83,17 +83,23 @@ class KeyManager {
_inboundGroupSessions.clear(); _inboundGroupSessions.clear();
} }
void setInboundGroupSession(String roomId, String sessionId, String senderKey, void setInboundGroupSession(
Map<String, dynamic> content, String roomId,
{bool forwarded = false, String sessionId,
Map<String, String> senderClaimedKeys, String senderKey,
bool uploaded = false, Map<String, dynamic> content, {
Map<String, Map<String, int>> allowedAtIndex}) { bool forwarded = false,
senderClaimedKeys ??= <String, String>{}; Map<String, String>? senderClaimedKeys,
if (!senderClaimedKeys.containsKey('ed25519')) { bool uploaded = false,
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); final device = client.getUserDeviceKeysByCurve25519Key(senderKey);
if (device != null) { if (device != null && device.ed25519Key != null) {
senderClaimedKeys['ed25519'] = device.ed25519Key; senderClaimedKeys_['ed25519'] = device.ed25519Key!;
} }
} }
final oldSession = final oldSession =
@ -101,7 +107,7 @@ class KeyManager {
if (content['algorithm'] != AlgorithmTypes.megolmV1AesSha2) { if (content['algorithm'] != AlgorithmTypes.megolmV1AesSha2) {
return; return;
} }
olm.InboundGroupSession inboundGroupSession; late olm.InboundGroupSession inboundGroupSession;
try { try {
inboundGroupSession = olm.InboundGroupSession(); inboundGroupSession = olm.InboundGroupSession();
if (forwarded) { if (forwarded) {
@ -122,12 +128,12 @@ class KeyManager {
sessionId: sessionId, sessionId: sessionId,
key: client.userID, key: client.userID,
senderKey: senderKey, senderKey: senderKey,
senderClaimedKeys: senderClaimedKeys, senderClaimedKeys: senderClaimedKeys_,
allowedAtIndex: allowedAtIndex, allowedAtIndex: allowedAtIndex_,
); );
final oldFirstIndex = final oldFirstIndex =
oldSession?.inboundGroupSession?.first_known_index() ?? 0; oldSession?.inboundGroupSession?.first_known_index() ?? 0;
final newFirstIndex = newSession.inboundGroupSession.first_known_index(); final newFirstIndex = newSession.inboundGroupSession!.first_known_index();
if (oldSession == null || if (oldSession == null ||
newFirstIndex < oldFirstIndex || newFirstIndex < oldFirstIndex ||
(oldFirstIndex == newFirstIndex && (oldFirstIndex == newFirstIndex &&
@ -143,7 +149,7 @@ class KeyManager {
if (!_inboundGroupSessions.containsKey(roomId)) { if (!_inboundGroupSessions.containsKey(roomId)) {
_inboundGroupSessions[roomId] = <String, SessionKey>{}; _inboundGroupSessions[roomId] = <String, SessionKey>{};
} }
_inboundGroupSessions[roomId][sessionId] = newSession; _inboundGroupSessions[roomId]![sessionId] = newSession;
if (!client.isLogged() || client.encryption == null) { if (!client.isLogged() || client.encryption == null) {
return; return;
} }
@ -154,11 +160,11 @@ class KeyManager {
inboundGroupSession.pickle(client.userID), inboundGroupSession.pickle(client.userID),
json.encode(content), json.encode(content),
json.encode({}), json.encode({}),
json.encode(allowedAtIndex ?? {}), json.encode(allowedAtIndex_),
senderKey, senderKey,
json.encode(senderClaimedKeys), json.encode(senderClaimedKeys_),
) )
?.then((_) { .then((_) {
if (!client.isLogged() || client.encryption == null) { if (!client.isLogged() || client.encryption == null) {
return; return;
} }
@ -180,12 +186,12 @@ class KeyManager {
} }
} }
SessionKey getInboundGroupSession( SessionKey? getInboundGroupSession(
String roomId, String sessionId, String senderKey, String roomId, String sessionId, String senderKey,
{bool otherRooms = true}) { {bool otherRooms = true}) {
if (_inboundGroupSessions.containsKey(roomId) && if (_inboundGroupSessions.containsKey(roomId) &&
_inboundGroupSessions[roomId].containsKey(sessionId)) { _inboundGroupSessions[roomId]!.containsKey(sessionId)) {
final sess = _inboundGroupSessions[roomId][sessionId]; final sess = _inboundGroupSessions[roomId]![sessionId]!;
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) { if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
return null; return null;
} }
@ -197,7 +203,7 @@ class KeyManager {
// search if this session id is *somehow* found in another room // search if this session id is *somehow* found in another room
for (final val in _inboundGroupSessions.values) { for (final val in _inboundGroupSessions.values) {
if (val.containsKey(sessionId)) { if (val.containsKey(sessionId)) {
final sess = val[sessionId]; final sess = val[sessionId]!;
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) { if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
return null; return null;
} }
@ -223,14 +229,11 @@ class KeyManager {
} }
/// Loads an inbound group session /// Loads an inbound group session
Future<SessionKey> loadInboundGroupSession( Future<SessionKey?> loadInboundGroupSession(
String roomId, String sessionId, String senderKey) async { String roomId, String sessionId, String senderKey) async {
if (roomId == null || sessionId == null || senderKey == null) {
return null;
}
if (_inboundGroupSessions.containsKey(roomId) && if (_inboundGroupSessions.containsKey(roomId) &&
_inboundGroupSessions[roomId].containsKey(sessionId)) { _inboundGroupSessions[roomId]!.containsKey(sessionId)) {
final sess = _inboundGroupSessions[roomId][sessionId]; final sess = _inboundGroupSessions[roomId]![sessionId]!;
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) { if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
return null; // sender keys do not match....better not do anything return null; // sender keys do not match....better not do anything
} }
@ -246,10 +249,11 @@ class KeyManager {
_inboundGroupSessions[roomId] = <String, SessionKey>{}; _inboundGroupSessions[roomId] = <String, SessionKey>{};
} }
if (!sess.isValid || if (!sess.isValid ||
(sess.senderKey.isNotEmpty && sess.senderKey != senderKey)) { sess.senderKey.isEmpty ||
sess.senderKey != senderKey) {
return null; return null;
} }
_inboundGroupSessions[roomId][sessionId] = sess; _inboundGroupSessions[roomId]![sessionId] = sess;
return sess; return sess;
} }
@ -257,10 +261,14 @@ class KeyManager {
List<DeviceKeys> deviceKeys) { List<DeviceKeys> deviceKeys) {
final deviceKeyIds = <String, Map<String, bool>>{}; final deviceKeyIds = <String, Map<String, bool>>{};
for (final device in deviceKeys) { for (final device in deviceKeys) {
if (device.deviceId == null) {
Logs().w('[KeyManager] ignoring device without deviceid');
continue;
}
if (!deviceKeyIds.containsKey(device.userId)) { if (!deviceKeyIds.containsKey(device.userId)) {
deviceKeyIds[device.userId] = <String, bool>{}; deviceKeyIds[device.userId] = <String, bool>{};
} }
deviceKeyIds[device.userId][device.deviceId] = !device.encryptToDevice; deviceKeyIds[device.userId]![device.deviceId!] = !device.encryptToDevice;
} }
return deviceKeyIds; return deviceKeyIds;
} }
@ -280,6 +288,7 @@ class KeyManager {
if (room == null || sess == null) { if (room == null || sess == null) {
return true; return true;
} }
if (!wipe) { if (!wipe) {
// first check if it needs to be rotated // first check if it needs to be rotated
final encryptionContent = final encryptionContent =
@ -294,8 +303,13 @@ class KeyManager {
wipe = true; wipe = true;
} }
} }
final inboundSess = await loadInboundGroupSession(room.id, 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) { if (!wipe) {
// next check if the devices in the room changed // next check if the devices in the room changed
final devicesToReceive = <DeviceKeys>[]; 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. // 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 // 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) { for (final userId in oldUserIds) {
final oldBlockedDevices = Set.from(sess.devices[userId].entries final oldBlockedDevices = sess.devices.containsKey(userId)
.where((e) => e.value) ? Set.from(sess.devices[userId]!.entries
.map((e) => e.key)); .where((e) => e.value)
final newBlockedDevices = Set.from(newDeviceKeyIds[userId] .map((e) => e.key))
.entries : <String>{};
.where((e) => e.value) final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
.map((e) => e.key)); ? Set.from(newDeviceKeyIds[userId]!
.entries
.where((e) => e.value)
.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 // 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 // check if new devices got blocked
if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) { if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) {
@ -333,13 +351,17 @@ class KeyManager {
break; break;
} }
// and now add all the new devices! // and now add all the new devices!
final oldDeviceIds = Set.from(sess.devices[userId].entries final oldDeviceIds = sess.devices.containsKey(userId)
.where((e) => !e.value) ? Set.from(sess.devices[userId]!.entries
.map((e) => e.key)); .where((e) => !e.value)
final newDeviceIds = Set.from(newDeviceKeyIds[userId] .map((e) => e.key))
.entries : <String>{};
.where((e) => !e.value) final newDeviceIds = newDeviceKeyIds.containsKey(userId)
.map((e) => e.key)); ? Set.from(newDeviceKeyIds[userId]!
.entries
.where((e) => !e.value)
.map((e) => e.key))
: <String>{};
final newDevices = newDeviceIds.difference(oldDeviceIds); final newDevices = newDeviceIds.difference(oldDeviceIds);
if (newDeviceIds.isNotEmpty) { if (newDeviceIds.isNotEmpty) {
devicesToReceive.addAll(newDeviceKeys.where( devicesToReceive.addAll(newDeviceKeys.where(
@ -366,20 +388,20 @@ class KeyManager {
if (devicesToReceive.isNotEmpty) { if (devicesToReceive.isNotEmpty) {
// update allowedAtIndex // update allowedAtIndex
for (final device in devicesToReceive) { for (final device in devicesToReceive) {
inboundSess.allowedAtIndex[device.userId] ??= <String, int>{}; inboundSess!.allowedAtIndex[device.userId] ??= <String, int>{};
if (!inboundSess.allowedAtIndex[device.userId] if (!inboundSess.allowedAtIndex[device.userId]!
.containsKey(device.curve25519Key) || .containsKey(device.curve25519Key) ||
inboundSess.allowedAtIndex[device.userId] inboundSess.allowedAtIndex[device.userId]![
[device.curve25519Key] > device.curve25519Key]! >
sess.outboundGroupSession.message_index()) { sess.outboundGroupSession.message_index()) {
inboundSess.allowedAtIndex[device.userId] inboundSess
[device.curve25519Key] = .allowedAtIndex[device.userId]![device.curve25519Key!] =
sess.outboundGroupSession.message_index(); sess.outboundGroupSession.message_index();
} }
} }
if (client.database != null) { if (client.database != null) {
await client.database.updateInboundGroupSessionAllowedAtIndex( await client.database.updateInboundGroupSessionAllowedAtIndex(
json.encode(inboundSess.allowedAtIndex), json.encode(inboundSess!.allowedAtIndex),
room.id, room.id,
sess.outboundGroupSession.session_id()); sess.outboundGroupSession.session_id());
} }
@ -405,9 +427,6 @@ class KeyManager {
/// Store an outbound group session in the database /// Store an outbound group session in the database
Future<void> storeOutboundGroupSession( Future<void> storeOutboundGroupSession(
String roomId, OutboundGroupSession sess) async { String roomId, OutboundGroupSession sess) async {
if (sess == null) {
return;
}
await client.database?.storeOutboundGroupSession( await client.database?.storeOutboundGroupSession(
roomId, roomId,
sess.outboundGroupSession.pickle(client.userID), sess.outboundGroupSession.pickle(client.userID),
@ -422,12 +441,12 @@ class KeyManager {
/// Creates an outbound group session for a given room id /// Creates an outbound group session for a given room id
Future<OutboundGroupSession> createOutboundGroupSession(String roomId) async { Future<OutboundGroupSession> createOutboundGroupSession(String roomId) async {
if (_pendingNewOutboundGroupSessions.containsKey(roomId)) { if (_pendingNewOutboundGroupSessions.containsKey(roomId)) {
return _pendingNewOutboundGroupSessions[roomId]; return _pendingNewOutboundGroupSessions[roomId]!;
} }
_pendingNewOutboundGroupSessions[roomId] = _pendingNewOutboundGroupSessions[roomId] =
_createOutboundGroupSession(roomId); _createOutboundGroupSession(roomId);
await _pendingNewOutboundGroupSessions[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 /// 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); await clearOrUseOutboundGroupSession(roomId, wipe: true);
final room = client.getRoomById(roomId); final room = client.getRoomById(roomId);
if (room == null) { 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 deviceKeys = await room.getUserDeviceKeys();
final deviceKeyIds = _getDeviceKeyIdMap(deviceKeys); final deviceKeyIds = _getDeviceKeyIdMap(deviceKeys);
@ -458,7 +478,7 @@ class KeyManager {
} catch (e, s) { } catch (e, s) {
outboundGroupSession.free(); outboundGroupSession.free();
Logs().e('[LibOlm] Unable to create new outboundGroupSession', e, s); Logs().e('[LibOlm] Unable to create new outboundGroupSession', e, s);
return null; rethrow;
} }
final rawSession = <String, dynamic>{ final rawSession = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2, 'algorithm': AlgorithmTypes.megolmV1AesSha2,
@ -468,12 +488,16 @@ class KeyManager {
}; };
final allowedAtIndex = <String, Map<String, int>>{}; final allowedAtIndex = <String, Map<String, int>>{};
for (final device in deviceKeys) { for (final device in deviceKeys) {
if (!device.isValid) {
Logs().e('Skipping invalid device');
continue;
}
allowedAtIndex[device.userId] ??= <String, int>{}; allowedAtIndex[device.userId] ??= <String, int>{};
allowedAtIndex[device.userId][device.curve25519Key] = allowedAtIndex[device.userId]![device.curve25519Key!] =
outboundGroupSession.message_index(); outboundGroupSession.message_index();
} }
setInboundGroupSession( setInboundGroupSession(
roomId, rawSession['session_id'], encryption.identityKey, rawSession, roomId, rawSession['session_id'], encryption.identityKey!, rawSession,
allowedAtIndex: allowedAtIndex); allowedAtIndex: allowedAtIndex);
final sess = OutboundGroupSession( final sess = OutboundGroupSession(
devices: deviceKeyIds, devices: deviceKeyIds,
@ -493,13 +517,13 @@ class KeyManager {
e, e,
s); s);
sess.dispose(); sess.dispose();
return null; rethrow;
} }
return sess; return sess;
} }
/// Get an outbound group session for a room id /// Get an outbound group session for a room id
OutboundGroupSession getOutboundGroupSession(String roomId) { OutboundGroupSession? getOutboundGroupSession(String roomId) {
return _outboundGroupSessions[roomId]; return _outboundGroupSessions[roomId];
} }
@ -528,8 +552,8 @@ class KeyManager {
return (await encryption.ssss.getCached(megolmKey)) != null; return (await encryption.ssss.getCached(megolmKey)) != null;
} }
GetRoomKeysVersionCurrentResponse _roomKeysVersionCache; GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
DateTime _roomKeysVersionCacheDate; DateTime? _roomKeysVersionCacheDate;
Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo( Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo(
[bool useCache = true]) async { [bool useCache = true]) async {
if (_roomKeysVersionCache != null && if (_roomKeysVersionCache != null &&
@ -537,12 +561,12 @@ class KeyManager {
useCache && useCache &&
DateTime.now() DateTime.now()
.subtract(Duration(minutes: 5)) .subtract(Duration(minutes: 5))
.isBefore(_roomKeysVersionCacheDate)) { .isBefore(_roomKeysVersionCacheDate!)) {
return _roomKeysVersionCache; return _roomKeysVersionCache!;
} }
_roomKeysVersionCache = await client.getRoomKeysVersionCurrent(); _roomKeysVersionCache = await client.getRoomKeysVersionCurrent();
_roomKeysVersionCacheDate = DateTime.now(); _roomKeysVersionCacheDate = DateTime.now();
return _roomKeysVersionCache; return _roomKeysVersionCache!;
} }
Future<void> loadFromResponse(RoomKeys keys) async { Future<void> loadFromResponse(RoomKeys keys) async {
@ -550,15 +574,14 @@ class KeyManager {
return; return;
} }
final privateKey = final privateKey =
base64.decode(await encryption.ssss.getCached(megolmKey)); base64.decode((await encryption.ssss.getCached(megolmKey))!);
final decryption = olm.PkDecryption(); final decryption = olm.PkDecryption();
final info = await getRoomKeysBackupInfo(); final info = await getRoomKeysBackupInfo();
String backupPubKey; String backupPubKey;
try { try {
backupPubKey = decryption.init_with_private_key(privateKey); backupPubKey = decryption.init_with_private_key(privateKey);
if (backupPubKey == null || if (info.algorithm != BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 ||
info.algorithm != BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 ||
info.authData['public_key'] != backupPubKey) { info.authData['public_key'] != backupPubKey) {
return; return;
} }
@ -567,17 +590,11 @@ class KeyManager {
for (final sessionEntry in roomEntry.value.sessions.entries) { for (final sessionEntry in roomEntry.value.sessions.entries) {
final sessionId = sessionEntry.key; final sessionId = sessionEntry.key;
final session = sessionEntry.value; final session = sessionEntry.value;
final firstMessageIndex = session.firstMessageIndex;
final forwardedCount = session.forwardedCount;
final isVerified = session.isVerified;
final sessionData = session.sessionData; final sessionData = session.sessionData;
if (firstMessageIndex == null || if (!(sessionData is Map)) {
forwardedCount == null ||
isVerified == null ||
!(sessionData is Map)) {
continue; continue;
} }
Map<String, dynamic> decrypted; Map<String, dynamic>? decrypted;
try { try {
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
sessionData['mac'], sessionData['ciphertext'])); sessionData['mac'], sessionData['ciphertext']));
@ -591,8 +608,9 @@ class KeyManager {
roomId, sessionId, decrypted['sender_key'], decrypted, roomId, sessionId, decrypted['sender_key'], decrypted,
forwarded: true, forwarded: true,
senderClaimedKeys: decrypted['sender_claimed_keys'] != null senderClaimedKeys: decrypted['sender_claimed_keys'] != null
? Map<String, String>.from(decrypted['sender_claimed_keys']) ? Map<String, String>.from(
: null, decrypted['sender_claimed_keys']!)
: <String, String>{},
uploaded: true); uploaded: true);
} }
} }
@ -702,7 +720,7 @@ class KeyManager {
return; // nothing to do return; // nothing to do
} }
final privateKey = 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 // decryption is needed to calculate the public key and thus see if the claimed information is in fact valid
final decryption = olm.PkDecryption(); final decryption = olm.PkDecryption();
final info = await getRoomKeysBackupInfo(false); final info = await getRoomKeysBackupInfo(false);
@ -710,8 +728,7 @@ class KeyManager {
try { try {
backupPubKey = decryption.init_with_private_key(privateKey); backupPubKey = decryption.init_with_private_key(privateKey);
if (backupPubKey == null || if (info.algorithm !=
info.algorithm !=
BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 || BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2 ||
info.authData['public_key'] != backupPubKey) { info.authData['public_key'] != backupPubKey) {
return; return;
@ -775,13 +792,13 @@ class KeyManager {
return; // no body return; // no body
} }
if (!client.userDeviceKeys.containsKey(event.sender) || if (!client.userDeviceKeys.containsKey(event.sender) ||
!client.userDeviceKeys[event.sender].deviceKeys !client.userDeviceKeys[event.sender]!.deviceKeys
.containsKey(event.content['requesting_device_id'])) { .containsKey(event.content['requesting_device_id'])) {
Logs().i('[KeyManager] Device not found, doing nothing'); Logs().i('[KeyManager] Device not found, doing nothing');
return; // device not found return; // device not found
} }
final device = client.userDeviceKeys[event.sender] final device = client.userDeviceKeys[event.sender]!
.deviceKeys[event.content['requesting_device_id']]; .deviceKeys[event.content['requesting_device_id']]!;
if (device.userId == client.userID && if (device.userId == client.userID &&
device.deviceId == client.deviceID) { device.deviceId == client.deviceID) {
Logs().i('[KeyManager] Request is by ourself, ignoring'); Logs().i('[KeyManager] Request is by ourself, ignoring');
@ -824,13 +841,13 @@ class KeyManager {
} else if (device.encryptToDevice && } else if (device.encryptToDevice &&
session.allowedAtIndex session.allowedAtIndex
.tryGet<Map<String, dynamic>>(device.userId) .tryGet<Map<String, dynamic>>(device.userId)
?.tryGet(device.curve25519Key) != ?.tryGet(device.curve25519Key!) !=
null) { null) {
// if we know the user may see the message, then we can just forward the key. // 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, // 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. // as that is the logic we already initially try to send out the room keys.
final index = final index =
session.allowedAtIndex[device.userId][device.curve25519Key]; session.allowedAtIndex[device.userId]![device.curve25519Key]!;
Logs().i( Logs().i(
'[KeyManager] Valid foreign request, forwarding key at index $index...'); '[KeyManager] Valid foreign request, forwarding key at index $index...');
await roomKeyRequest.forwardKey(index); await roomKeyRequest.forwardKey(index);
@ -846,7 +863,7 @@ class KeyManager {
return; // we don't know this request anyways return; // we don't know this request anyways
} }
// alright, let's just cancel this request // alright, let's just cancel this request
final request = incomingShareRequests[event.content['request_id']]; final request = incomingShareRequests[event.content['request_id']]!;
request.canceled = true; request.canceled = true;
incomingShareRequests.remove(request.requestId); incomingShareRequests.remove(request.requestId);
} }
@ -855,20 +872,16 @@ class KeyManager {
if (event.encryptedContent == null) { if (event.encryptedContent == null) {
return; // event wasn't encrypted, this is a security risk return; // event wasn't encrypted, this is a security risk
} }
final request = outgoingShareRequests.values.firstWhere( final request = outgoingShareRequests.values.firstWhereOrNull((r) =>
(r) => r.room.id == event.content['room_id'] &&
r.room.id == event.content['room_id'] && r.sessionId == event.content['session_id'] &&
r.sessionId == event.content['session_id'] && r.senderKey == event.content['sender_key']);
r.senderKey == event.content['sender_key'],
orElse: () => null);
if (request == null || request.canceled) { if (request == null || request.canceled) {
return; // no associated request found or it got canceled return; // no associated request found or it got canceled
} }
final device = request.devices.firstWhere( final device = request.devices.firstWhereOrNull((d) =>
(d) => d.userId == event.sender &&
d.userId == event.sender && d.curve25519Key == event.encryptedContent['sender_key']);
d.curve25519Key == event.encryptedContent['sender_key'],
orElse: () => null);
if (device == null) { if (device == null) {
return; // someone we didn't send our request to replied....better ignore this return; // someone we didn't send our request to replied....better ignore this
} }
@ -904,7 +917,7 @@ class KeyManager {
if (!data.containsKey(device.userId)) { if (!data.containsKey(device.userId)) {
data[device.userId] = {}; data[device.userId] = {};
} }
data[device.userId][device.deviceId] = sendToDeviceMessage; data[device.userId]![device.deviceId!] = sendToDeviceMessage;
} }
await client.sendToDevice( await client.sendToDevice(
EventTypes.RoomKeyRequest, EventTypes.RoomKeyRequest,
@ -921,11 +934,11 @@ class KeyManager {
final String roomId = event.content['room_id']; final String roomId = event.content['room_id'];
final String sessionId = event.content['session_id']; final String sessionId = event.content['session_id'];
if (client.userDeviceKeys.containsKey(event.sender) && if (client.userDeviceKeys.containsKey(event.sender) &&
client.userDeviceKeys[event.sender].deviceKeys client.userDeviceKeys[event.sender]!.deviceKeys
.containsKey(event.content['requesting_device_id'])) { .containsKey(event.content['requesting_device_id'])) {
event.content['sender_claimed_ed25519_key'] = client event.content['sender_claimed_ed25519_key'] = client
.userDeviceKeys[event.sender] .userDeviceKeys[event.sender]!
.deviceKeys[event.content['requesting_device_id']] .deviceKeys[event.content['requesting_device_id']]!
.ed25519Key; .ed25519Key;
} }
Logs().v('[KeyManager] Keeping room key'); Logs().v('[KeyManager] Keeping room key');
@ -956,21 +969,20 @@ class KeyManagerKeyShareRequest {
bool canceled; bool canceled;
KeyManagerKeyShareRequest( KeyManagerKeyShareRequest(
{this.requestId, {required this.requestId,
this.devices, List<DeviceKeys>? devices,
this.room, required this.room,
this.sessionId, required this.sessionId,
this.senderKey, required this.senderKey,
this.canceled = false}); this.canceled = false})
: devices = devices ?? [];
} }
class RoomKeyRequest extends ToDeviceEvent { class RoomKeyRequest extends ToDeviceEvent {
KeyManager keyManager; KeyManager keyManager;
KeyManagerKeyShareRequest request; KeyManagerKeyShareRequest request;
RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent, RoomKeyRequest.fromToDeviceEvent(
KeyManager keyManager, KeyManagerKeyShareRequest request) { ToDeviceEvent toDeviceEvent, this.keyManager, this.request) {
this.keyManager = keyManager;
this.request = request;
sender = toDeviceEvent.sender; sender = toDeviceEvent.sender;
content = toDeviceEvent.content; content = toDeviceEvent.content;
type = toDeviceEvent.type; type = toDeviceEvent.type;
@ -980,7 +992,7 @@ class RoomKeyRequest extends ToDeviceEvent {
DeviceKeys get requestingDevice => request.devices.first; DeviceKeys get requestingDevice => request.devices.first;
Future<void> forwardKey([int index]) async { Future<void> forwardKey([int? index]) async {
if (request.canceled) { if (request.canceled) {
keyManager.incomingShareRequests.remove(request.requestId); keyManager.incomingShareRequests.remove(request.requestId);
return; // request is canceled, don't send anything return; // request is canceled, don't send anything
@ -988,21 +1000,28 @@ class RoomKeyRequest extends ToDeviceEvent {
final room = this.room; final room = this.room;
final session = await keyManager.loadInboundGroupSession( final session = await keyManager.loadInboundGroupSession(
room.id, request.sessionId, request.senderKey); 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(); final message = session.content.copy();
message['forwarding_curve25519_key_chain'] = message['forwarding_curve25519_key_chain'] =
List<String>.from(session.forwardingCurve25519KeyChain); List<String>.from(session.forwardingCurve25519KeyChain);
message['sender_key'] = message['sender_key'] =
(session.senderKey != null && session.senderKey.isNotEmpty) (session.senderKey.isNotEmpty) ? session.senderKey : request.senderKey;
? session.senderKey
: request.senderKey;
message['sender_claimed_ed25519_key'] = message['sender_claimed_ed25519_key'] =
session.senderClaimedKeys['ed25519'] ?? session.senderClaimedKeys['ed25519'] ??
(session.forwardingCurve25519KeyChain.isEmpty (session.forwardingCurve25519KeyChain.isEmpty
? keyManager.encryption.fingerprintKey ? keyManager.encryption.fingerprintKey
: null); : null);
message['session_key'] = session.inboundGroupSession.export_session( message['session_key'] = session.inboundGroupSession!.export_session(
index ?? session.inboundGroupSession.first_known_index()); index ?? session.inboundGroupSession!.first_known_index());
// send the actual reply of the key back to the requester // send the actual reply of the key back to the requester
await keyManager.client.sendToDeviceEncrypted( await keyManager.client.sendToDeviceEncrypted(
[requestingDevice], [requestingDevice],
@ -1034,16 +1053,16 @@ RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) {
'forwarding_curve25519_key_chain': sess.forwardingCurve25519KeyChain, 'forwarding_curve25519_key_chain': sess.forwardingCurve25519KeyChain,
'sender_key': sess.senderKey, 'sender_key': sess.senderKey,
'sender_clencaimed_keys': sess.senderClaimedKeys, 'sender_clencaimed_keys': sess.senderClaimedKeys,
'session_key': sess.inboundGroupSession 'session_key': sess.inboundGroupSession!
.export_session(sess.inboundGroupSession.first_known_index()), .export_session(sess.inboundGroupSession!.first_known_index()),
}; };
// encrypt the content // encrypt the content
final encrypted = enc.encrypt(json.encode(payload)); final encrypted = enc.encrypt(json.encode(payload));
// fetch the device, if available... // fetch the device, if available...
//final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey); //final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey);
// aaaand finally add the session key to our payload // aaaand finally add the session key to our payload
roomKeys.rooms[sess.roomId].sessions[sess.sessionId] = KeyBackupData( roomKeys.rooms[sess.roomId]!.sessions[sess.sessionId] = KeyBackupData(
firstMessageIndex: sess.inboundGroupSession.first_known_index(), firstMessageIndex: sess.inboundGroupSession!.first_known_index(),
forwardedCount: sess.forwardingCurve25519KeyChain.length, forwardedCount: sess.forwardingCurve25519KeyChain.length,
isVerified: dbSession.verified, //device?.verified ?? false, isVerified: dbSession.verified, //device?.verified ?? false,
sessionData: { sessionData: {
@ -1063,14 +1082,16 @@ RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) {
} }
class _DbInboundGroupSessionBundle { class _DbInboundGroupSessionBundle {
_DbInboundGroupSessionBundle({this.dbSession, this.verified}); _DbInboundGroupSessionBundle(
{required this.dbSession, required this.verified});
StoredInboundGroupSession dbSession; StoredInboundGroupSession dbSession;
bool verified; bool verified;
} }
class _GenerateUploadKeysArgs { class _GenerateUploadKeysArgs {
_GenerateUploadKeysArgs({this.pubkey, this.dbSessions, this.userId}); _GenerateUploadKeysArgs(
{required this.pubkey, required this.dbSessions, required this.userId});
String pubkey; String pubkey;
List<_DbInboundGroupSessionBundle> dbSessions; List<_DbInboundGroupSessionBundle> dbSessions;

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH * Copyright (C) 2019, 2020, 2021 Famedly GmbH
@ -20,6 +19,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:canonical_json/canonical_json.dart'; import 'package:canonical_json/canonical_json.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
@ -31,16 +31,16 @@ import 'utils/olm_session.dart';
class OlmManager { class OlmManager {
final Encryption encryption; final Encryption encryption;
Client get client => encryption.client; Client get client => encryption.client;
olm.Account _olmAccount; olm.Account? _olmAccount;
/// Returns the base64 encoded keys to store them in a store. /// Returns the base64 encoded keys to store them in a store.
/// This String should **never** leave the device! /// This String should **never** leave the device!
String get pickledOlmAccount => String? get pickledOlmAccount =>
enabled ? _olmAccount.pickle(client.userID) : null; enabled ? _olmAccount!.pickle(client.userID) : null;
String get fingerprintKey => String? get fingerprintKey =>
enabled ? json.decode(_olmAccount.identity_keys())['ed25519'] : null; enabled ? json.decode(_olmAccount!.identity_keys())['ed25519'] : null;
String get identityKey => String? get identityKey =>
enabled ? json.decode(_olmAccount.identity_keys())['curve25519'] : null; enabled ? json.decode(_olmAccount!.identity_keys())['curve25519'] : null;
bool get enabled => _olmAccount != null; bool get enabled => _olmAccount != null;
@ -50,12 +50,13 @@ class OlmManager {
Map<String, List<OlmSession>> get olmSessions => _olmSessions; Map<String, List<OlmSession>> get olmSessions => _olmSessions;
final Map<String, List<OlmSession>> _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) { if (olmAccount == null) {
try { try {
await olm.init(); await olm.init();
_olmAccount = olm.Account(); _olmAccount = olm.Account();
_olmAccount.create(); _olmAccount!.create();
if (await uploadKeys(uploadDeviceKeys: true, updateDatabase: false) == if (await uploadKeys(uploadDeviceKeys: true, updateDatabase: false) ==
false) { false) {
throw ('Upload key failed'); throw ('Upload key failed');
@ -69,7 +70,7 @@ class OlmManager {
try { try {
await olm.init(); await olm.init();
_olmAccount = olm.Account(); _olmAccount = olm.Account();
_olmAccount.unpickle(client.userID, olmAccount); _olmAccount!.unpickle(client.userID, olmAccount);
} catch (_) { } catch (_) {
_olmAccount?.free(); _olmAccount?.free();
_olmAccount = null; _olmAccount = null;
@ -82,12 +83,12 @@ class OlmManager {
/// json. /// json.
Map<String, dynamic> signJson(Map<String, dynamic> payload) { Map<String, dynamic> signJson(Map<String, dynamic> payload) {
if (!enabled) throw ('Encryption is disabled'); if (!enabled) throw ('Encryption is disabled');
final Map<String, dynamic> unsigned = payload['unsigned']; final Map<String, dynamic>? unsigned = payload['unsigned'];
final Map<String, dynamic> signatures = payload['signatures']; final Map<String, dynamic>? signatures = payload['signatures'];
payload.remove('unsigned'); payload.remove('unsigned');
payload.remove('signatures'); payload.remove('signatures');
final canonical = canonicalJson.encode(payload); final canonical = canonicalJson.encode(payload);
final signature = _olmAccount.sign(String.fromCharCodes(canonical)); final signature = _olmAccount!.sign(String.fromCharCodes(canonical));
if (signatures != null) { if (signatures != null) {
payload['signatures'] = signatures; payload['signatures'] = signatures;
} else { } else {
@ -105,7 +106,7 @@ class OlmManager {
} }
String signString(String s) { String signString(String s) {
return _olmAccount.sign(s); return _olmAccount!.sign(s);
} }
/// Checks the signature of a signed json object. /// Checks the signature of a signed json object.
@ -113,7 +114,7 @@ class OlmManager {
bool checkJsonSignature(String key, Map<String, dynamic> signedJson, bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
String userId, String deviceId) { String userId, String deviceId) {
if (!enabled) throw ('Encryption is disabled'); 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; if (signatures == null || !signatures.containsKey(userId)) return false;
signedJson.remove('unsigned'); signedJson.remove('unsigned');
signedJson.remove('signatures'); signedJson.remove('signatures');
@ -140,9 +141,9 @@ class OlmManager {
/// Generates new one time keys, signs everything and upload it to the server. /// Generates new one time keys, signs everything and upload it to the server.
Future<bool> uploadKeys({ Future<bool> uploadKeys({
bool uploadDeviceKeys = false, bool uploadDeviceKeys = false,
int oldKeyCount = 0, int? oldKeyCount = 0,
bool updateDatabase = true, bool updateDatabase = true,
bool unusedFallbackKey = false, bool? unusedFallbackKey = false,
}) async { }) async {
if (!enabled) { if (!enabled) {
return true; return true;
@ -155,27 +156,27 @@ class OlmManager {
try { try {
final signedOneTimeKeys = <String, dynamic>{}; final signedOneTimeKeys = <String, dynamic>{};
int uploadedOneTimeKeysCount; int? uploadedOneTimeKeysCount;
if (oldKeyCount != null) { if (oldKeyCount != null) {
// check if we have OTKs that still need uploading. If we do, we don't try to generate new ones, // 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 // instead we try to upload the old ones first
final oldOTKsNeedingUpload = json final oldOTKsNeedingUpload = json
.decode(_olmAccount.one_time_keys())['curve25519'] .decode(_olmAccount!.one_time_keys())['curve25519']
.entries .entries
.length; .length as int;
// generate one-time keys // generate one-time keys
// we generate 2/3rds of max, so that other keys people may still have can // we generate 2/3rds of max, so that other keys people may still have can
// still be used // still be used
final oneTimeKeysCount = final oneTimeKeysCount =
(_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() - (_olmAccount!.max_number_of_one_time_keys() * 2 / 3).floor() -
oldKeyCount - oldKeyCount -
oldOTKsNeedingUpload; oldOTKsNeedingUpload;
if (oneTimeKeysCount > 0) { if (oneTimeKeysCount > 0) {
_olmAccount.generate_one_time_keys(oneTimeKeysCount); _olmAccount!.generate_one_time_keys(oneTimeKeysCount);
} }
uploadedOneTimeKeysCount = oneTimeKeysCount + oldOTKsNeedingUpload; uploadedOneTimeKeysCount = oneTimeKeysCount + oldOTKsNeedingUpload;
final Map<String, dynamic> oneTimeKeys = final Map<String, dynamic> oneTimeKeys =
json.decode(_olmAccount.one_time_keys()); json.decode(_olmAccount!.one_time_keys());
// now sign all the one-time keys // now sign all the one-time keys
for (final entry in oneTimeKeys['curve25519'].entries) { for (final entry in oneTimeKeys['curve25519'].entries) {
@ -190,8 +191,8 @@ class OlmManager {
final signedFallbackKeys = <String, dynamic>{}; final signedFallbackKeys = <String, dynamic>{};
if (encryption.isMinOlmVersion(3, 2, 0) && unusedFallbackKey == false) { if (encryption.isMinOlmVersion(3, 2, 0) && unusedFallbackKey == false) {
// we don't have an unused fallback key uploaded....so let's change that! // we don't have an unused fallback key uploaded....so let's change that!
_olmAccount.generate_fallback_key(); _olmAccount!.generate_fallback_key();
final fallbackKey = json.decode(_olmAccount.fallback_key()); final fallbackKey = json.decode(_olmAccount!.fallback_key());
// now sign all the fallback keys // now sign all the fallback keys
for (final entry in fallbackKey['curve25519'].entries) { for (final entry in fallbackKey['curve25519'].entries) {
final key = entry.key; final key = entry.key;
@ -218,7 +219,7 @@ class OlmManager {
}; };
if (uploadDeviceKeys) { if (uploadDeviceKeys) {
final Map<String, dynamic> keys = final Map<String, dynamic> keys =
json.decode(_olmAccount.identity_keys()); json.decode(_olmAccount!.identity_keys());
for (final entry in keys.entries) { for (final entry in keys.entries) {
final algorithm = entry.key; final algorithm = entry.key;
final value = entry.value; final value = entry.value;
@ -234,7 +235,7 @@ class OlmManager {
// we can still re-try later // we can still re-try later
if (updateDatabase) { if (updateDatabase) {
await client.database?.updateClientKeys( await client.database?.updateClientKeys(
pickledOlmAccount, pickledOlmAccount!,
); );
} }
final response = await client.uploadKeys( final response = await client.uploadKeys(
@ -245,9 +246,9 @@ class OlmManager {
fallbackKeys: signedFallbackKeys, fallbackKeys: signedFallbackKeys,
); );
// mark the OTKs as published and save that to datbase // mark the OTKs as published and save that to datbase
_olmAccount.mark_keys_as_published(); _olmAccount!.mark_keys_as_published();
if (updateDatabase) { if (updateDatabase) {
await client.database?.updateClientKeys(pickledOlmAccount); await client.database?.updateClientKeys(pickledOlmAccount!);
} }
return (uploadedOneTimeKeysCount != null && return (uploadedOneTimeKeysCount != null &&
response['signed_curve25519'] == uploadedOneTimeKeysCount) || response['signed_curve25519'] == uploadedOneTimeKeysCount) ||
@ -258,7 +259,7 @@ class OlmManager {
} }
void handleDeviceOneTimeKeysCount( void handleDeviceOneTimeKeysCount(
Map<String, int> countJson, List<String> unusedFallbackKeyTypes) { Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) {
if (!enabled) { if (!enabled) {
return; 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. // 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 = { final requestingKeysFrom = {
client.userID: {client.deviceID: 'signed_curve25519'} 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 // 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) { !unusedFallbackKey) {
uploadKeys( uploadKeys(
oldKeyCount: keyCount < (_olmAccount.max_number_of_one_time_keys() / 2) oldKeyCount: keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2)
? keyCount ? keyCount
: null, : null,
unusedFallbackKey: haveFallbackKeys ? unusedFallbackKey : null, unusedFallbackKey: haveFallbackKeys ? unusedFallbackKey : null,
@ -297,24 +298,29 @@ class OlmManager {
} }
Future<void> storeOlmSession(OlmSession session) async { Future<void> storeOlmSession(OlmSession session) async {
if (session.sessionId == null || session.pickledSession == null) {
return;
}
_olmSessions[session.identityKey] ??= <OlmSession>[]; _olmSessions[session.identityKey] ??= <OlmSession>[];
final ix = _olmSessions[session.identityKey] final ix = _olmSessions[session.identityKey]!
.indexWhere((s) => s.sessionId == session.sessionId); .indexWhere((s) => s.sessionId == session.sessionId);
if (ix == -1) { if (ix == -1) {
// add a new session // add a new session
_olmSessions[session.identityKey].add(session); _olmSessions[session.identityKey]!.add(session);
} else { } else {
// update an existing session // update an existing session
_olmSessions[session.identityKey][ix] = session; _olmSessions[session.identityKey]![ix] = session;
} }
if (client.database == null) { if (client.database == null) {
return; return;
} }
await client.database.storeOlmSession( await client.database.storeOlmSession(
session.identityKey, session.identityKey,
session.sessionId, session.sessionId!,
session.pickledSession, session.pickledSession!,
session.lastReceived.millisecondsSinceEpoch); session.lastReceived?.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch);
} }
ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) { ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) {
@ -325,20 +331,21 @@ class OlmManager {
if (content.algorithm != AlgorithmTypes.olmV1Curve25519AesSha2) { if (content.algorithm != AlgorithmTypes.olmV1Curve25519AesSha2) {
throw DecryptException(DecryptException.unknownAlgorithm); throw DecryptException(DecryptException.unknownAlgorithm);
} }
if (!content.ciphertextOlm.containsKey(identityKey)) { if (content.ciphertextOlm == null ||
!content.ciphertextOlm!.containsKey(identityKey)) {
throw DecryptException(DecryptException.isntSentForThisDevice); throw DecryptException(DecryptException.isntSentForThisDevice);
} }
String plaintext; String? plaintext;
final senderKey = content.senderKey; final senderKey = content.senderKey;
final body = content.ciphertextOlm[identityKey].body; final body = content.ciphertextOlm![identityKey]!.body;
final type = content.ciphertextOlm[identityKey].type; final type = content.ciphertextOlm![identityKey]!.type;
if (type != 0 && type != 1) { if (type != 0 && type != 1) {
throw DecryptException(DecryptException.unknownMessageType); throw DecryptException(DecryptException.unknownMessageType);
} }
final device = client.userDeviceKeys[event.sender]?.deviceKeys?.values final device = client.userDeviceKeys[event.sender]?.deviceKeys.values
?.firstWhere((d) => d.curve25519Key == senderKey, orElse: () => null); .firstWhereOrNull((d) => d.curve25519Key == senderKey);
final existingSessions = olmSessions[senderKey]; final existingSessions = olmSessions[senderKey];
final updateSessionUsage = ([OlmSession session]) => runInRoot(() async { final updateSessionUsage = ([OlmSession? session]) => runInRoot(() async {
if (session != null) { if (session != null) {
session.lastReceived = DateTime.now(); session.lastReceived = DateTime.now();
await storeOlmSession(session); await storeOlmSession(session);
@ -348,14 +355,17 @@ class OlmManager {
await client.database?.setLastActiveUserDeviceKey( await client.database?.setLastActiveUserDeviceKey(
device.lastActive.millisecondsSinceEpoch, device.lastActive.millisecondsSinceEpoch,
device.userId, device.userId,
device.deviceId); device.deviceId!);
} }
}); });
if (existingSessions != null) { if (existingSessions != null) {
for (final session in existingSessions) { 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 { try {
plaintext = session.session.decrypt(type, body); plaintext = session.session!.decrypt(type, body);
} catch (e) { } catch (e) {
// The message was encrypted during this session, but is unable to decrypt // The message was encrypted during this session, but is unable to decrypt
throw DecryptException( throw DecryptException(
@ -365,7 +375,7 @@ class OlmManager {
break; break;
} else if (type == 1) { } else if (type == 1) {
try { try {
plaintext = session.session.decrypt(type, body); plaintext = session.session!.decrypt(type, body);
updateSessionUsage(session); updateSessionUsage(session);
break; break;
} catch (_) { } catch (_) {
@ -381,10 +391,10 @@ class OlmManager {
if (plaintext == null) { if (plaintext == null) {
final newSession = olm.Session(); final newSession = olm.Session();
try { try {
newSession.create_inbound_from(_olmAccount, senderKey, body); newSession.create_inbound_from(_olmAccount!, senderKey, body);
_olmAccount.remove_one_time_keys(newSession); _olmAccount!.remove_one_time_keys(newSession);
client.database?.updateClientKeys( client.database?.updateClientKeys(
pickledOlmAccount, pickledOlmAccount!,
); );
plaintext = newSession.decrypt(type, body); plaintext = newSession.decrypt(type, body);
runInRoot(() => storeOlmSession(OlmSession( runInRoot(() => storeOlmSession(OlmSession(
@ -396,7 +406,7 @@ class OlmManager {
))); )));
updateSessionUsage(); updateSessionUsage();
} catch (e) { } catch (e) {
newSession?.free(); newSession.free();
throw DecryptException(DecryptException.decryptionFailed, e.toString()); throw DecryptException(DecryptException.decryptionFailed, e.toString());
} }
} }
@ -444,7 +454,7 @@ class OlmManager {
for (final sess in rows) { for (final sess in rows) {
res[sess.identityKey] ??= <OlmSession>[]; res[sess.identityKey] ??= <OlmSession>[];
if (sess.isValid) { if (sess.isValid) {
res[sess.identityKey].add(sess); res[sess.identityKey]!.add(sess);
} }
} }
for (final entry in res.entries) { for (final entry in res.entries) {
@ -455,7 +465,7 @@ class OlmManager {
Future<List<OlmSession>> getOlmSessions(String senderKey, Future<List<OlmSession>> getOlmSessions(String senderKey,
{bool getFromDb = true}) async { {bool getFromDb = true}) async {
var sess = olmSessions[senderKey]; var sess = olmSessions[senderKey];
if ((getFromDb ?? true) && (sess == null || sess.isEmpty)) { if ((getFromDb) && (sess == null || sess.isEmpty)) {
final sessions = await getOlmSessionsFromDatabase(senderKey); final sessions = await getOlmSessionsFromDatabase(senderKey);
if (sessions.isEmpty) { if (sessions.isEmpty) {
return []; return [];
@ -466,8 +476,9 @@ class OlmManager {
return []; return [];
} }
sess.sort((a, b) => a.lastReceived == b.lastReceived sess.sort((a, b) => a.lastReceived == b.lastReceived
? a.sessionId.compareTo(b.sessionId) ? (a.sessionId ?? '').compareTo(b.sessionId ?? '')
: b.lastReceived.compareTo(a.lastReceived)); : (b.lastReceived ?? DateTime(0))
.compareTo(a.lastReceived ?? DateTime(0)));
return sess; return sess;
} }
@ -477,8 +488,8 @@ class OlmManager {
if (!client.userDeviceKeys.containsKey(userId)) { if (!client.userDeviceKeys.containsKey(userId)) {
return; return;
} }
final device = client.userDeviceKeys[userId].deviceKeys.values final device = client.userDeviceKeys[userId]!.deviceKeys.values
.firstWhere((d) => d.curve25519Key == senderKey, orElse: () => null); .firstWhereOrNull((d) => d.curve25519Key == senderKey);
if (device == null) { if (device == null) {
return; return;
} }
@ -487,7 +498,7 @@ class OlmManager {
if (_restoredOlmSessionsTime.containsKey(mapKey) && if (_restoredOlmSessionsTime.containsKey(mapKey) &&
DateTime.now() DateTime.now()
.subtract(Duration(hours: 1)) .subtract(Duration(hours: 1))
.isBefore(_restoredOlmSessionsTime[mapKey])) { .isBefore(_restoredOlmSessionsTime[mapKey]!)) {
return; return;
} }
_restoredOlmSessionsTime[mapKey] = DateTime.now(); _restoredOlmSessionsTime[mapKey] = DateTime.now();
@ -532,7 +543,8 @@ class OlmManager {
if (requestingKeysFrom[device.userId] == null) { if (requestingKeysFrom[device.userId] == null) {
requestingKeysFrom[device.userId] = {}; 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); final response = await client.claimKeys(requestingKeysFrom, timeout: 10000);
@ -542,9 +554,9 @@ class OlmManager {
for (final deviceKeysEntry in userKeysEntry.value.entries) { for (final deviceKeysEntry in userKeysEntry.value.entries) {
final deviceId = deviceKeysEntry.key; final deviceId = deviceKeysEntry.key;
final fingerprintKey = final fingerprintKey =
client.userDeviceKeys[userId].deviceKeys[deviceId].ed25519Key; client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.ed25519Key;
final identityKey = final identityKey =
client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key; client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.curve25519Key;
for (final Map<String, dynamic> deviceKey for (final Map<String, dynamic> deviceKey
in deviceKeysEntry.value.values) { in deviceKeysEntry.value.values) {
if (!deviceKey.checkJsonSignature(fingerprintKey, userId, deviceId)) { if (!deviceKey.checkJsonSignature(fingerprintKey, userId, deviceId)) {
@ -553,7 +565,8 @@ class OlmManager {
Logs().v('[OlmManager] Starting session with $userId:$deviceId'); Logs().v('[OlmManager] Starting session with $userId:$deviceId');
final session = olm.Session(); final session = olm.Session();
try { try {
session.create_outbound(_olmAccount, identityKey, deviceKey['key']); session.create_outbound(
_olmAccount!, identityKey!, deviceKey['key']);
await storeOlmSession(OlmSession( await storeOlmSession(OlmSession(
key: client.userID, key: client.userID,
identityKey: identityKey, identityKey: identityKey,
@ -574,9 +587,9 @@ class OlmManager {
Future<Map<String, dynamic>> encryptToDeviceMessagePayload( Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
DeviceKeys device, String type, Map<String, dynamic> payload, DeviceKeys device, String type, Map<String, dynamic> payload,
{bool getFromDb}) async { {bool getFromDb = true}) async {
final sess = final sess =
await getOlmSessions(device.curve25519Key, getFromDb: getFromDb); await getOlmSessions(device.curve25519Key!, getFromDb: getFromDb);
if (sess.isEmpty) { if (sess.isEmpty) {
throw ('No olm session found for ${device.userId}:${device.deviceId}'); throw ('No olm session found for ${device.userId}:${device.deviceId}');
} }
@ -588,7 +601,7 @@ class OlmManager {
'recipient': device.userId, 'recipient': device.userId,
'recipient_keys': {'ed25519': device.ed25519Key}, '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); await storeOlmSession(sess.first);
if (client.database != null) { if (client.database != null) {
// ignore: unawaited_futures // ignore: unawaited_futures
@ -598,7 +611,7 @@ class OlmManager {
'content': payload, 'content': payload,
}), }),
device.userId, device.userId,
device.deviceId)); device.deviceId!));
} }
final encryptedBody = <String, dynamic>{ final encryptedBody = <String, dynamic>{
'algorithm': AlgorithmTypes.olmV1Curve25519AesSha2, '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 // first check if any of our sessions we want to encrypt for are in the database
if (client.database != null) { if (client.database != null) {
await getOlmSessionsForDevicesFromDatabase( await getOlmSessionsForDevicesFromDatabase(
deviceKeys.map((d) => d.curve25519Key).toList()); deviceKeys.map((d) => d.curve25519Key!).toList());
} }
final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys); final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) => deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
olmSessions.containsKey(deviceKeys.curve25519Key) && olmSessions.containsKey(deviceKeys.curve25519Key) &&
olmSessions[deviceKeys.curve25519Key].isNotEmpty); olmSessions[deviceKeys.curve25519Key]!.isNotEmpty);
if (deviceKeysWithoutSession.isNotEmpty) { if (deviceKeysWithoutSession.isNotEmpty) {
await startOutgoingOlmSessions(deviceKeysWithoutSession); await startOutgoingOlmSessions(deviceKeysWithoutSession);
} }
@ -634,7 +647,7 @@ class OlmManager {
data[device.userId] = {}; data[device.userId] = {};
} }
try { try {
data[device.userId][device.deviceId] = data[device.userId]![device.deviceId!] =
await encryptToDeviceMessagePayload(device, type, payload, await encryptToDeviceMessagePayload(device, type, payload,
getFromDb: false); getFromDb: false);
} catch (e, s) { } catch (e, s) {
@ -660,7 +673,7 @@ class OlmManager {
Logs().v( Logs().v(
'[OlmManager] Device ${device.userId}:${device.deviceId} generated a new olm session, replaying last sent message...'); '[OlmManager] Device ${device.userId}:${device.deviceId} generated a new olm session, replaying last sent message...');
final lastSentMessageRes = await client.database final lastSentMessageRes = await client.database
.getLastSentMessageUserDeviceKey(device.userId, device.deviceId); .getLastSentMessageUserDeviceKey(device.userId, device.deviceId!);
if (lastSentMessageRes.isEmpty || if (lastSentMessageRes.isEmpty ||
(lastSentMessageRes.first?.isEmpty ?? true)) { (lastSentMessageRes.first?.isEmpty ?? true)) {
return; return;

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2020, 2021 Famedly GmbH * Copyright (C) 2020, 2021 Famedly GmbH
@ -24,6 +23,7 @@ import 'dart:typed_data';
import 'package:base58check/base58.dart'; import 'package:base58check/base58.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:collection/collection.dart';
import '../matrix.dart'; import '../matrix.dart';
import '../src/utils/crypto/crypto.dart' as uc; import '../src/utils/crypto/crypto.dart' as uc;
@ -76,11 +76,13 @@ class SSSS {
b[0] = 2; b[0] = 2;
final hmacKey = final hmacKey =
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b); 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, static Future<_Encrypted> encryptAes(String data, Uint8List key, String name,
[String ivStr]) async { [String? ivStr]) async {
Uint8List iv; Uint8List iv;
if (ivStr != null) { if (ivStr != null) {
iv = base64.decode(ivStr); iv = base64.decode(ivStr);
@ -121,7 +123,7 @@ class SSSS {
static Uint8List decodeRecoveryKey(String recoveryKey) { static Uint8List decodeRecoveryKey(String recoveryKey) {
final result = base58.decode(recoveryKey.replaceAll(' ', '')); 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) { if (parity != 0) {
throw Exception('Incorrect parity'); throw Exception('Incorrect parity');
} }
@ -142,7 +144,7 @@ class SSSS {
static String encodeRecoveryKey(Uint8List recoveryKey) { static String encodeRecoveryKey(Uint8List recoveryKey) {
final keyToEncode = <int>[...olmRecoveryKeyPrefix, ...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); keyToEncode.add(parity);
// base58-encode and add a space every four chars // base58-encode and add a space every four chars
return base58 return base58
@ -156,8 +158,18 @@ class SSSS {
if (info.algorithm != AlgorithmTypes.pbkdf2) { if (info.algorithm != AlgorithmTypes.pbkdf2) {
throw Exception('Unknown algorithm'); throw Exception('Unknown algorithm');
} }
return await uc.pbkdf2(utf8.encode(passphrase), utf8.encode(info.salt), if (info.iterations == null) {
uc.sha512, info.iterations, info.bits ?? 256); 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) { void setValidator(String type, FutureOr<bool> Function(String) validator) {
@ -168,10 +180,10 @@ class SSSS {
_cacheCallbacks[type] = callback; _cacheCallbacks[type] = callback;
} }
String get defaultKeyId => client String? get defaultKeyId => client
.accountData[EventTypes.SecretStorageDefaultKey] .accountData[EventTypes.SecretStorageDefaultKey]
?.parsedSecretStorageDefaultKeyContent ?.parsedSecretStorageDefaultKeyContent
?.key; .key;
Future<void> setDefaultKeyId(String keyId) async { Future<void> setDefaultKeyId(String keyId) async {
await client.setAccountData( await client.setAccountData(
@ -181,7 +193,7 @@ class SSSS {
); );
} }
SecretStorageKeyContent getKey(String keyId) { SecretStorageKeyContent? getKey(String keyId) {
return client.accountData[EventTypes.secretStorageKey(keyId)] return client.accountData[EventTypes.secretStorageKey(keyId)]
?.parsedSecretStorageKeyContent; ?.parsedSecretStorageKeyContent;
} }
@ -191,7 +203,7 @@ class SSSS {
/// Creates a new secret storage key, optional encrypts it with [passphrase] /// Creates a new secret storage key, optional encrypts it with [passphrase]
/// and stores it in the user's `accountData`. /// and stores it in the user's `accountData`.
Future<OpenSSSS> createKey([String passphrase]) async { Future<OpenSSSS> createKey([String? passphrase]) async {
Uint8List privateKey; Uint8List privateKey;
final content = SecretStorageKeyContent(); final content = SecretStorageKeyContent();
if (passphrase != null) { if (passphrase != null) {
@ -207,7 +219,7 @@ class SSSS {
_keyFromPassphrase, _keyFromPassphrase,
_KeyFromPassphraseArgs( _KeyFromPassphraseArgs(
passphrase: passphrase, passphrase: passphrase,
info: content.passphrase, info: content.passphrase!,
), ),
) )
.timeout(Duration(seconds: 10)); .timeout(Duration(seconds: 10));
@ -235,7 +247,7 @@ class SSSS {
// noooow we set the account data // noooow we set the account data
final waitForAccountData = client.onSync.stream.firstWhere((syncUpdate) => final waitForAccountData = client.onSync.stream.firstWhere((syncUpdate) =>
syncUpdate.accountData != null && syncUpdate.accountData != null &&
syncUpdate.accountData syncUpdate.accountData!
.any((accountData) => accountData.type == accountDataType)); .any((accountData) => accountData.type == accountDataType));
await client.setAccountData( await client.setAccountData(
client.userID, accountDataType, content.toJson()); client.userID, accountDataType, content.toJson());
@ -250,7 +262,7 @@ class SSSS {
if (info.algorithm == AlgorithmTypes.secretStorageV1AesHmcSha2) { if (info.algorithm == AlgorithmTypes.secretStorageV1AesHmcSha2) {
if ((info.mac is String) && (info.iv is String)) { if ((info.mac is String) && (info.iv is String)) {
final encrypted = await encryptAes(zeroStr, key, '', info.iv); 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'=+$'), ''); encrypted.mac.replaceAll(RegExp(r'=+$'), '');
} else { } else {
// no real information about the key, assume it is valid // no real information about the key, assume it is valid
@ -263,9 +275,9 @@ class SSSS {
bool isSecret(String type) => bool isSecret(String type) =>
client.accountData[type] != null && 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) { if (client.database == null) {
return null; return null;
} }
@ -276,11 +288,12 @@ class SSSS {
} }
final isValid = (dbEntry) => final isValid = (dbEntry) =>
keys.contains(dbEntry.keyId) && keys.contains(dbEntry.keyId) &&
client.accountData[type].content['encrypted'][dbEntry.keyId] dbEntry.ciphertext != null &&
client.accountData[type]?.content['encrypted'][dbEntry.keyId]
['ciphertext'] == ['ciphertext'] ==
dbEntry.ciphertext; dbEntry.ciphertext;
if (_cache.containsKey(type) && isValid(_cache[type])) { if (_cache.containsKey(type) && isValid(_cache[type])) {
return _cache[type].content; return _cache[type]?.content;
} }
final ret = await client.database.getSSSSCache(type); final ret = await client.database.getSSSSCache(type);
if (ret == null) { if (ret == null) {
@ -313,7 +326,7 @@ class SSSS {
await client.database await client.database
.storeSSSSCache(type, keyId, enc['ciphertext'], decrypted); .storeSSSSCache(type, keyId, enc['ciphertext'], decrypted);
if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) { if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
_cacheCallbacks[type](decrypted); _cacheCallbacks[type]!(decrypted);
} }
} }
return decrypted; return decrypted;
@ -321,12 +334,10 @@ class SSSS {
Future<void> store(String type, String secret, String keyId, Uint8List key, Future<void> store(String type, String secret, String keyId, Uint8List key,
{bool add = false}) async { {bool add = false}) async {
final triggerCacheCallback =
_cacheCallbacks.containsKey(type) && await getCached(type) == null;
final encrypted = await encryptAes(secret, key, type); final encrypted = await encryptAes(secret, key, type);
Map<String, dynamic> content; Map<String, dynamic>? content;
if (add && client.accountData[type] != null) { if (add && client.accountData[type] != null) {
content = client.accountData[type].content.copy(); content = client.accountData[type]!.content.copy();
if (!(content['encrypted'] is Map)) { if (!(content['encrypted'] is Map)) {
content['encrypted'] = <String, dynamic>{}; content['encrypted'] = <String, dynamic>{};
} }
@ -345,8 +356,8 @@ class SSSS {
// cache the thing // cache the thing
await client.database await client.database
.storeSSSSCache(type, keyId, encrypted.ciphertext, secret); .storeSSSSCache(type, keyId, encrypted.ciphertext, secret);
if (triggerCacheCallback) { if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
_cacheCallbacks[type](secret); _cacheCallbacks[type]!(secret);
} }
} }
} }
@ -357,7 +368,11 @@ class SSSS {
throw Exception('Secrets do not match up!'); throw Exception('Secrets do not match up!');
} }
// now remove all other keys // 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 = final otherKeys =
Set<String>.from(content['encrypted'].keys.where((k) => k != keyId)); Set<String>.from(content['encrypted'].keys.where((k) => k != keyId));
content['encrypted'].removeWhere((k, v) => otherKeys.contains(k)); 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) { for (final type in cacheTypes) {
if (keyIdsFromType(type) != null) { if (keyIdsFromType(type) != null) {
final secret = await getCached(type); 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 // only send to own, verified devices
Logs().i('[SSSS] Requesting type $type...'); Logs().i('[SSSS] Requesting type $type...');
if (devices == null || devices.isEmpty) { if (devices == null || devices.isEmpty) {
@ -406,7 +421,8 @@ class SSSS {
Logs().w('[SSSS] User does not have any devices'); Logs().w('[SSSS] User does not have any devices');
return; return;
} }
devices = client.userDeviceKeys[client.userID].deviceKeys.values.toList(); devices =
client.userDeviceKeys[client.userID]!.deviceKeys.values.toList();
} }
devices.removeWhere((DeviceKeys d) => devices.removeWhere((DeviceKeys d) =>
d.userId != client.userID || d.userId != client.userID ||
@ -432,7 +448,7 @@ class SSSS {
}); });
} }
DateTime _lastCacheRequest; DateTime? _lastCacheRequest;
bool _isPeriodicallyRequestingMissingCache = false; bool _isPeriodicallyRequestingMissingCache = false;
Future<void> periodicallyRequestMissingCache() async { Future<void> periodicallyRequestMissingCache() async {
@ -440,7 +456,7 @@ class SSSS {
(_lastCacheRequest != null && (_lastCacheRequest != null &&
DateTime.now() DateTime.now()
.subtract(Duration(minutes: 15)) .subtract(Duration(minutes: 15))
.isBefore(_lastCacheRequest)) || .isBefore(_lastCacheRequest!)) ||
client.isUnknownSession) { client.isUnknownSession) {
// we are already requesting right now or we attempted to within the last 15 min // we are already requesting right now or we attempted to within the last 15 min
return; return;
@ -467,7 +483,7 @@ class SSSS {
Logs().i('[SSSS] it is actually a cancelation'); Logs().i('[SSSS] it is actually a cancelation');
return; // not actually requesting, so ignore return; // not actually requesting, so ignore
} }
final device = client.userDeviceKeys[client.userID] final device = client.userDeviceKeys[client.userID]!
.deviceKeys[event.content['requesting_device_id']]; .deviceKeys[event.content['requesting_device_id']];
if (device == null || !device.verified || device.blocked) { if (device == null || !device.verified || device.blocked) {
Logs().i('[SSSS] Unknown / unverified devices, ignoring'); Logs().i('[SSSS] Unknown / unverified devices, ignoring');
@ -499,13 +515,11 @@ class SSSS {
Logs().i('[SSSS] Not by us or unknown request'); Logs().i('[SSSS] Not by us or unknown request');
return; // we have no idea what we just received 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 // alright, as we received a known request id, let's check if the sender is valid
final device = request.devices.firstWhere( final device = request.devices.firstWhereOrNull((d) =>
(d) => d.userId == event.sender &&
d.userId == event.sender && d.curve25519Key == event.encryptedContent['sender_key']);
d.curve25519Key == event.encryptedContent['sender_key'],
orElse: () => null);
if (device == null) { if (device == null) {
Logs().i('[SSSS] Someone else replied?'); Logs().i('[SSSS] Someone else replied?');
return; // someone replied whom we didn't send the share request to 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 // let's validate if the secret is, well, valid
if (_validators.containsKey(request.type) && if (_validators.containsKey(request.type) &&
!(await _validators[request.type](secret))) { !(await _validators[request.type]!(secret))) {
Logs().i('[SSSS] The received secret was invalid'); Logs().i('[SSSS] The received secret was invalid');
return; // didn't pass the validator return; // didn't pass the validator
} }
@ -530,19 +544,19 @@ class SSSS {
if (client.database != null) { if (client.database != null) {
final keyId = keyIdFromType(request.type); final keyId = keyIdFromType(request.type);
if (keyId != null) { if (keyId != null) {
final ciphertext = client.accountData[request.type] final ciphertext = client.accountData[request.type]!
.content['encrypted'][keyId]['ciphertext']; .content['encrypted'][keyId]['ciphertext'];
await client.database await client.database
.storeSSSSCache(request.type, keyId, ciphertext, secret); .storeSSSSCache(request.type, keyId, ciphertext, secret);
if (_cacheCallbacks.containsKey(request.type)) { 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]; final data = client.accountData[type];
if (data == null) { if (data == null) {
return null; return null;
@ -553,7 +567,7 @@ class SSSS {
return null; return null;
} }
String keyIdFromType(String type) { String? keyIdFromType(String type) {
final keys = keyIdsFromType(type); final keys = keyIdsFromType(type);
if (keys == null || keys.isEmpty) { if (keys == null || keys.isEmpty) {
return null; return null;
@ -564,15 +578,12 @@ class SSSS {
return keys.first; return keys.first;
} }
OpenSSSS open([String identifier]) { OpenSSSS open([String? identifier]) {
identifier ??= defaultKeyId; identifier ??= defaultKeyId;
if (identifier == null) { if (identifier == null) {
throw Exception('Dont know what to open'); throw Exception('Dont know what to open');
} }
final keyToOpen = keyIdFromType(identifier) ?? identifier; final keyToOpen = keyIdFromType(identifier) ?? identifier;
if (keyToOpen == null) {
throw Exception('No key found to open');
}
final key = getKey(keyToOpen); final key = getKey(keyToOpen);
if (key == null) { if (key == null) {
throw Exception('Unknown key to open'); throw Exception('Unknown key to open');
@ -587,7 +598,8 @@ class _ShareRequest {
final List<DeviceKeys> devices; final List<DeviceKeys> devices;
final DateTime start; final DateTime start;
_ShareRequest({this.requestId, this.type, this.devices}) _ShareRequest(
{required this.requestId, required this.type, required this.devices})
: start = DateTime.now(); : start = DateTime.now();
} }
@ -596,14 +608,14 @@ class _Encrypted {
final String ciphertext; final String ciphertext;
final String mac; final String mac;
_Encrypted({this.iv, this.ciphertext, this.mac}); _Encrypted({required this.iv, required this.ciphertext, required this.mac});
} }
class _DerivedKeys { class _DerivedKeys {
final Uint8List aesKey; final Uint8List aesKey;
final Uint8List hmacKey; final Uint8List hmacKey;
_DerivedKeys({this.aesKey, this.hmacKey}); _DerivedKeys({required this.aesKey, required this.hmacKey});
} }
class OpenSSSS { class OpenSSSS {
@ -611,21 +623,21 @@ class OpenSSSS {
final String keyId; final String keyId;
final SecretStorageKeyContent keyData; 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 isUnlocked => privateKey != null;
bool get hasPassphrase => keyData.passphrase != null; bool get hasPassphrase => keyData.passphrase != null;
String get recoveryKey => String? get recoveryKey =>
isUnlocked ? SSSS.encodeRecoveryKey(privateKey) : null; isUnlocked ? SSSS.encodeRecoveryKey(privateKey!) : null;
Future<void> unlock( Future<void> unlock(
{String passphrase, {String? passphrase,
String recoveryKey, String? recoveryKey,
String keyOrPassphrase, String? keyOrPassphrase,
bool postUnlock = true}) async { bool postUnlock = true}) async {
if (keyOrPassphrase != null) { if (keyOrPassphrase != null) {
try { try {
@ -648,7 +660,7 @@ class OpenSSSS {
_keyFromPassphrase, _keyFromPassphrase,
_KeyFromPassphraseArgs( _KeyFromPassphraseArgs(
passphrase: passphrase, passphrase: passphrase,
info: keyData.passphrase, info: keyData.passphrase!,
), ),
) )
.timeout(Duration(seconds: 10)); .timeout(Duration(seconds: 10));
@ -658,7 +670,7 @@ class OpenSSSS {
throw Exception('Nothing specified'); throw Exception('Nothing specified');
} }
// verify the validity of the key // verify the validity of the key
if (!await ssss.checkKey(privateKey, keyData)) { if (!await ssss.checkKey(privateKey!, keyData)) {
privateKey = null; privateKey = null;
throw Exception('Inalid key'); throw Exception('Inalid key');
} }
@ -675,19 +687,31 @@ class OpenSSSS {
} }
Future<String> getStored(String type) async { 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 { 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 { 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 { 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 { Future<void> _postUnlock() async {
@ -701,8 +725,9 @@ class OpenSSSS {
?.contains(keyId) ?? ?.contains(keyId) ??
false) && false) &&
(ssss.client.isUnknownSession || (ssss.client.isUnknownSession ||
!ssss.client.userDeviceKeys[ssss.client.userID].masterKey ssss.client.userDeviceKeys[ssss.client.userID]!.masterKey
.directVerified)) { ?.directVerified !=
true)) {
try { try {
await ssss.encryption.crossSigning.selfSign(openSsss: this); await ssss.encryption.crossSigning.selfSign(openSsss: this);
} catch (e, s) { } catch (e, s) {
@ -716,7 +741,7 @@ class _KeyFromPassphraseArgs {
final String passphrase; final String passphrase;
final PassphraseInfo info; final PassphraseInfo info;
_KeyFromPassphraseArgs({this.passphrase, this.info}); _KeyFromPassphraseArgs({required this.passphrase, required this.info});
} }
Future<Uint8List> _keyFromPassphrase(_KeyFromPassphraseArgs args) async { Future<Uint8List> _keyFromPassphrase(_KeyFromPassphraseArgs args) async {

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2020, 2021 Famedly GmbH * Copyright (C) 2020, 2021 Famedly GmbH
@ -73,14 +72,14 @@ enum BootstrapState {
class Bootstrap { class Bootstrap {
final Encryption encryption; final Encryption encryption;
Client get client => encryption.client; Client get client => encryption.client;
void Function() onUpdate; void Function()? onUpdate;
BootstrapState get state => _state; BootstrapState get state => _state;
BootstrapState _state = BootstrapState.loading; BootstrapState _state = BootstrapState.loading;
Map<String, OpenSSSS> oldSsssKeys; Map<String, OpenSSSS>? oldSsssKeys;
OpenSSSS newSsssKey; OpenSSSS? newSsssKey;
Map<String, String> secretMap; Map<String, String>? secretMap;
Bootstrap({this.encryption, this.onUpdate}) { Bootstrap({required this.encryption, this.onUpdate}) {
if (analyzeSecrets().isNotEmpty) { if (analyzeSecrets().isNotEmpty) {
state = BootstrapState.askWipeSsss; state = BootstrapState.askWipeSsss;
} else { } else {
@ -89,12 +88,12 @@ class Bootstrap {
} }
// cache the secret analyzing so that we don't drop stuff a different client sets during bootstrapping // 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() { Map<String, Set<String>> analyzeSecrets() {
if (_secretsCache != null) { if (_secretsCache != null) {
// deep-copy so that we can do modifications // deep-copy so that we can do modifications
final newSecrets = <String, Set<String>>{}; 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); newSecrets[s.key] = Set<String>.from(s.value);
} }
return newSecrets; return newSecrets;
@ -149,9 +148,10 @@ class Bootstrap {
for (final keys in secrets.values) { for (final keys in secrets.values) {
for (final key in keys) { for (final key in keys) {
if (!usage.containsKey(key)) { if (!usage.containsKey(key)) {
usage[key] = 0; usage[key] = 1;
} else {
usage[key] = usage[key]! + 1;
} }
usage[key]++;
} }
} }
final entriesList = usage.entries.toList(); final entriesList = usage.entries.toList();
@ -192,7 +192,7 @@ class Bootstrap {
if (wipe) { if (wipe) {
state = BootstrapState.askNewSsss; state = BootstrapState.askNewSsss;
} else if (encryption.ssss.defaultKeyId != null && } else if (encryption.ssss.defaultKeyId != null &&
encryption.ssss.isKeyValid(encryption.ssss.defaultKeyId)) { encryption.ssss.isKeyValid(encryption.ssss.defaultKeyId!)) {
state = BootstrapState.askUseExistingSsss; state = BootstrapState.askUseExistingSsss;
} else if (badSecrets().isNotEmpty) { } else if (badSecrets().isNotEmpty) {
state = BootstrapState.askBadSsss; state = BootstrapState.askBadSsss;
@ -238,7 +238,7 @@ class Bootstrap {
oldSsssKeys = <String, OpenSSSS>{}; oldSsssKeys = <String, OpenSSSS>{};
try { try {
for (final key in keys) { for (final key in keys) {
oldSsssKeys[key] = encryption.ssss.open(key); oldSsssKeys![key] = encryption.ssss.open(key);
} }
} catch (e, s) { } catch (e, s) {
Logs().e('[Bootstrapping] Error construction ssss key', e, s); Logs().e('[Bootstrapping] Error construction ssss key', e, s);
@ -255,7 +255,7 @@ class Bootstrap {
state = BootstrapState.askNewSsss; state = BootstrapState.askNewSsss;
} }
Future<void> newSsss([String passphrase]) async { Future<void> newSsss([String? passphrase]) async {
if (state != BootstrapState.askNewSsss) { if (state != BootstrapState.askNewSsss) {
throw BootstrapBadStateException('Wrong State'); throw BootstrapBadStateException('Wrong State');
} }
@ -275,7 +275,7 @@ class Bootstrap {
return s; return s;
}; };
secretMap = <String, String>{}; secretMap = <String, String>{};
for (final entry in oldSsssKeys.entries) { for (final entry in oldSsssKeys!.entries) {
final key = entry.value; final key = entry.value;
final keyId = entry.key; final keyId = entry.key;
if (!key.isUnlocked) { if (!key.isUnlocked) {
@ -283,26 +283,26 @@ class Bootstrap {
} }
for (final s in removeKey(keyId)) { for (final s in removeKey(keyId)) {
Logs().v('Get stored key of type $s...'); 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...'); 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 // 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) => final updatedAccountData = client.onSync.stream.firstWhere((syncUpdate) =>
syncUpdate.accountData != null && syncUpdate.accountData != null &&
syncUpdate.accountData.any((accountData) => syncUpdate.accountData!.any((accountData) =>
accountData.type == EventTypes.SecretStorageDefaultKey)); accountData.type == EventTypes.SecretStorageDefaultKey));
await encryption.ssss.setDefaultKeyId(newSsssKey.keyId); await encryption.ssss.setDefaultKeyId(newSsssKey!.keyId);
await updatedAccountData; await updatedAccountData;
if (oldSsssKeys != null) { if (oldSsssKeys != null) {
for (final entry in secretMap.entries) { for (final entry in secretMap!.entries) {
Logs().v('Validate and stripe other keys ${entry.key}...'); 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...'); Logs().v('And make super sure we have everything cached...');
await newSsssKey.maybeCacheAll(); await newSsssKey!.maybeCacheAll();
} }
} catch (e, s) { } catch (e, s) {
Logs().e('[Bootstrapping] Error trying to migrate old secrets', e, s); Logs().e('[Bootstrapping] Error trying to migrate old secrets', e, s);
@ -315,14 +315,14 @@ class Bootstrap {
} }
Future<void> openExistingSsss() async { Future<void> openExistingSsss() async {
if (state != BootstrapState.openExistingSsss) { if (state != BootstrapState.openExistingSsss || newSsssKey == null) {
throw BootstrapBadStateException(); throw BootstrapBadStateException();
} }
if (!newSsssKey.isUnlocked) { if (!newSsssKey!.isUnlocked) {
throw BootstrapBadStateException('Key not unlocked'); throw BootstrapBadStateException('Key not unlocked');
} }
Logs().v('Maybe cache all...'); Logs().v('Maybe cache all...');
await newSsssKey.maybeCacheAll(); await newSsssKey!.maybeCacheAll();
checkCrossSigning(); checkCrossSigning();
} }
@ -362,10 +362,10 @@ class Bootstrap {
try { try {
Uint8List masterSigningKey; Uint8List masterSigningKey;
final secretsToStore = <String, String>{}; final secretsToStore = <String, String>{};
MatrixCrossSigningKey masterKey; MatrixCrossSigningKey? masterKey;
MatrixCrossSigningKey selfSigningKey; MatrixCrossSigningKey? selfSigningKey;
MatrixCrossSigningKey userSigningKey; MatrixCrossSigningKey? userSigningKey;
String masterPub; String? masterPub;
if (setupMasterKey) { if (setupMasterKey) {
final master = olm.PkSigning(); final master = olm.PkSigning();
try { try {
@ -387,8 +387,9 @@ class Bootstrap {
} else { } else {
Logs().v('Get stored key...'); Logs().v('Get stored key...');
masterSigningKey = base64.decode( masterSigningKey = base64.decode(
await newSsssKey.getStored(EventTypes.CrossSigningMasterKey) ?? ''); await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ??
if (masterSigningKey == null || masterSigningKey.isEmpty) { '');
if (masterSigningKey.isEmpty) {
// no master signing key :( // no master signing key :(
throw BootstrapBadStateException('No master key'); throw BootstrapBadStateException('No master key');
} }
@ -477,9 +478,11 @@ class Bootstrap {
client.onSync.stream client.onSync.stream
.firstWhere((syncUpdate) => .firstWhere((syncUpdate) =>
client.userDeviceKeys.containsKey(client.userID) && client.userDeviceKeys.containsKey(client.userID) &&
client.userDeviceKeys[client.userID].masterKey != null && client.userDeviceKeys[client.userID]!.masterKey != null &&
client.userDeviceKeys[client.userID].masterKey.ed25519Key == client.userDeviceKeys[client.userID]!.masterKey!.ed25519Key !=
masterKey.publicKey) null &&
client.userDeviceKeys[client.userID]!.masterKey!.ed25519Key ==
masterKey!.publicKey)
.then((_) => Logs().v('New Master Key was created')), .then((_) => Logs().v('New Master Key was created')),
); );
} }
@ -488,32 +491,32 @@ class Bootstrap {
client.onSync.stream client.onSync.stream
.firstWhere((syncUpdate) => .firstWhere((syncUpdate) =>
syncUpdate.accountData != null && syncUpdate.accountData != null &&
syncUpdate.accountData syncUpdate.accountData!
.any((accountData) => accountData.type == entry.key)) .any((accountData) => accountData.type == entry.key))
.then((_) => .then((_) =>
Logs().v('New Key with type ${entry.key} was created')), Logs().v('New Key with type ${entry.key} was created')),
); );
Logs().v('Store new SSSS key ${entry.key}...'); Logs().v('Store new SSSS key ${entry.key}...');
await newSsssKey.store(entry.key, entry.value); await newSsssKey?.store(entry.key, entry.value);
} }
Logs().v( Logs().v(
'Wait for MasterKey and ${secretsToStore.entries.length} keys to be created'); 'Wait for MasterKey and ${secretsToStore.entries.length} keys to be created');
await Future.wait<void>(futures); await Future.wait<void>(futures);
final keysToSign = <SignableKey>[]; final keysToSign = <SignableKey>[];
if (masterKey != null) { if (masterKey != null) {
if (client.userDeviceKeys[client.userID].masterKey.ed25519Key != if (client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key !=
masterKey.publicKey) { masterKey.publicKey) {
throw BootstrapBadStateException( throw BootstrapBadStateException(
'ERROR: New master key does not match up!'); 'ERROR: New master key does not match up!');
} }
Logs().v('Set own master key to verified...'); Logs().v('Set own master key to verified...');
await client.userDeviceKeys[client.userID].masterKey await client.userDeviceKeys[client.userID]!.masterKey!
.setVerified(true, false); .setVerified(true, false);
keysToSign.add(client.userDeviceKeys[client.userID].masterKey); keysToSign.add(client.userDeviceKeys[client.userID]!.masterKey!);
} }
if (selfSigningKey != null) { if (selfSigningKey != null) {
keysToSign.add( keysToSign.add(
client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]); client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!);
} }
Logs().v('Sign ourself...'); Logs().v('Sign ourself...');
await encryption.crossSigning.sign(keysToSign); await encryption.crossSigning.sign(keysToSign);
@ -572,7 +575,7 @@ class Bootstrap {
}, },
); );
Logs().v('Store the secret...'); Logs().v('Store the secret...');
await newSsssKey.store(megolmKey, base64.encode(privKey)); await newSsssKey?.store(megolmKey, base64.encode(privKey));
Logs().v( Logs().v(
'And finally set all megolm keys as needing to be uploaded again...'); 'And finally set all megolm keys as needing to be uploaded again...');
await client.database?.markInboundGroupSessionsAsNeedingUpload(); await client.database?.markInboundGroupSessionsAsNeedingUpload();
@ -593,7 +596,7 @@ class Bootstrap {
_state = newState; _state = newState;
} }
if (onUpdate != null) { if (onUpdate != null) {
onUpdate(); onUpdate!();
} }
} }
} }

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2020, 2021 Famedly GmbH * Copyright (C) 2020, 2021 Famedly GmbH
@ -74,7 +73,7 @@ enum KeyVerificationState {
enum KeyVerificationMethod { emoji, numbers } 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(); (b == null || a == null) ? [] : a.where(b.contains).toList();
List<int> _bytesToInt(Uint8List bytes, int totalBits) { List<int> _bytesToInt(Uint8List bytes, int totalBits) {
@ -104,41 +103,40 @@ _KeyVerificationMethod _makeVerificationMethod(
} }
class KeyVerification { class KeyVerification {
String transactionId; String? transactionId;
final Encryption encryption; final Encryption encryption;
Client get client => encryption.client; Client get client => encryption.client;
final Room room; final Room? room;
final String userId; final String userId;
void Function() onUpdate; void Function()? onUpdate;
String get deviceId => _deviceId; String? get deviceId => _deviceId;
String _deviceId; String? _deviceId;
bool startedVerification = false; bool startedVerification = false;
_KeyVerificationMethod method; _KeyVerificationMethod? method;
List<String> possibleMethods; List<String> possibleMethods = [];
Map<String, dynamic> startPaylaod; Map<String, dynamic>? startPayload;
String _nextAction; String? _nextAction;
List<SignableKey> _verifiedDevices; List<SignableKey> _verifiedDevices = [];
DateTime lastActivity; DateTime lastActivity;
String lastStep; String? lastStep;
KeyVerificationState state = KeyVerificationState.waitingAccept; KeyVerificationState state = KeyVerificationState.waitingAccept;
bool canceled = false; bool canceled = false;
String canceledCode; String? canceledCode;
String canceledReason; String? canceledReason;
bool get isDone => bool get isDone =>
canceled || canceled ||
{KeyVerificationState.error, KeyVerificationState.done}.contains(state); {KeyVerificationState.error, KeyVerificationState.done}.contains(state);
KeyVerification( KeyVerification(
{this.encryption, {required this.encryption,
this.room, this.room,
this.userId, required this.userId,
String deviceId, String? deviceId,
this.onUpdate}) { this.onUpdate})
lastActivity = DateTime.now(); : _deviceId = deviceId,
_deviceId ??= deviceId; lastActivity = DateTime.now();
}
void dispose() { void dispose() {
Logs().i('[Key Verification] disposing object...'); Logs().i('[Key Verification] disposing object...');
@ -188,7 +186,7 @@ class KeyVerification {
bool _handlePayloadLock = false; bool _handlePayloadLock = false;
Future<void> handlePayload(String type, Map<String, dynamic> payload, Future<void> handlePayload(String type, Map<String, dynamic> payload,
[String eventId]) async { [String? eventId]) async {
if (isDone) { if (isDone) {
return; // no need to do anything with already canceled requests return; // no need to do anything with already canceled requests
} }
@ -229,8 +227,10 @@ class KeyVerification {
if (deviceId == '*') { if (deviceId == '*') {
_deviceId = payload['from_device']; // gotta set the real device id _deviceId = payload['from_device']; // gotta set the real device id
// and broadcast the cancel to the other devices // and broadcast the cancel to the other devices
final devices = List<DeviceKeys>.from( final devices = client.userDeviceKeys.containsKey(userId)
client.userDeviceKeys[userId].deviceKeys.values); ? List<DeviceKeys>.from(
client.userDeviceKeys[userId]!.deviceKeys.values)
: List<DeviceKeys>.from([]);
devices.removeWhere( devices.removeWhere(
(d) => {deviceId, client.deviceID}.contains(d.deviceId)); (d) => {deviceId, client.deviceID}.contains(d.deviceId));
final cancelPayload = <String, dynamic>{ final cancelPayload = <String, dynamic>{
@ -254,7 +254,7 @@ class KeyVerification {
lastStep = type; lastStep = type;
// TODO: Pick method? // TODO: Pick method?
method = _makeVerificationMethod(possibleMethods.first, this); method = _makeVerificationMethod(possibleMethods.first, this);
await method.sendStart(); await method!.sendStart();
setState(KeyVerificationState.waitingAccept); setState(KeyVerificationState.waitingAccept);
break; break;
case EventTypes.KeyVerificationStart: case EventTypes.KeyVerificationStart:
@ -262,7 +262,7 @@ class KeyVerification {
transactionId ??= eventId ?? payload['transaction_id']; transactionId ??= eventId ?? payload['transaction_id'];
if (method != null) { if (method != null) {
// the other side sent us a start, even though we already sent one // 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 // same method. Determine priority
final ourEntry = '${client.userID}|${client.deviceID}'; final ourEntry = '${client.userID}|${client.deviceID}';
final entries = [ourEntry, '$userId|$deviceId']; final entries = [ourEntry, '$userId|$deviceId'];
@ -275,7 +275,7 @@ class KeyVerification {
startedVerification = false; // it is now as if they started startedVerification = false; // it is now as if they started
thisLastStep = lastStep = thisLastStep = lastStep =
EventTypes.KeyVerificationRequest; // we fake the last step EventTypes.KeyVerificationRequest; // we fake the last step
method.dispose(); // in case anything got created already method?.dispose(); // in case anything got created already
} }
} else { } else {
// methods don't match up, let's cancel this // methods don't match up, let's cancel this
@ -300,15 +300,15 @@ class KeyVerification {
return; return;
} }
// validate the specific payload // validate the specific payload
if (!method.validateStart(payload)) { if (!method!.validateStart(payload)) {
await cancel('m.unknown_method'); await cancel('m.unknown_method');
return; return;
} }
startPaylaod = payload; startPayload = payload;
setState(KeyVerificationState.askAccept); setState(KeyVerificationState.askAccept);
} else { } else {
Logs().i('handling start in method.....'); Logs().i('handling start in method.....');
await method.handlePayload(type, payload); await method!.handlePayload(type, payload);
} }
break; break;
case EventTypes.KeyVerificationDone: case EventTypes.KeyVerificationDone:
@ -322,7 +322,7 @@ class KeyVerification {
break; break;
default: default:
if (method != null) { if (method != null) {
await method.handlePayload(type, payload); await method!.handlePayload(type, payload);
} else { } else {
await cancel('m.invalid_message'); await cancel('m.invalid_message');
} }
@ -347,9 +347,9 @@ class KeyVerification {
} }
Future<void> openSSSS( Future<void> openSSSS(
{String passphrase, {String? passphrase,
String recoveryKey, String? recoveryKey,
String keyOrPassphrase, String? keyOrPassphrase,
bool skip = false}) async { bool skip = false}) async {
final next = () { final next = () {
if (_nextAction == 'request') { if (_nextAction == 'request') {
@ -391,7 +391,8 @@ class KeyVerification {
}); });
} else { } else {
// we need to send an accept event // 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 { List<String> get sasTypes {
if (method is _KeyVerificationMethodSas) { if (method is _KeyVerificationMethodSas) {
return (method as _KeyVerificationMethodSas).authenticationTypes; return (method as _KeyVerificationMethodSas).authenticationTypes ?? [];
} }
return []; return [];
} }
@ -479,7 +480,7 @@ class KeyVerification {
final keyId = entry.key; final keyId = entry.key;
final verifyDeviceId = keyId.substring('ed25519:'.length); final verifyDeviceId = keyId.substring('ed25519:'.length);
final keyInfo = entry.value; final keyInfo = entry.value;
final key = client.userDeviceKeys[userId].getKey(verifyDeviceId); final key = client.userDeviceKeys[userId]!.getKey(verifyDeviceId);
if (key != null) { if (key != null) {
if (!(await verifier(keyInfo, key))) { if (!(await verifier(keyInfo, key))) {
await cancel('m.key_mismatch'); await cancel('m.key_mismatch');
@ -536,7 +537,7 @@ class KeyVerification {
return false; return false;
} }
Future<bool> verifyLastStep(List<String> checkLastStep) async { Future<bool> verifyLastStep(List<String?> checkLastStep) async {
if (!(await verifyActivity())) { if (!(await verifyActivity())) {
return false; return false;
} }
@ -577,7 +578,7 @@ class KeyVerification {
makePayload(payload); makePayload(payload);
Logs().i('[Key Verification] Sending type $type: ' + payload.toString()); Logs().i('[Key Verification] Sending type $type: ' + payload.toString());
if (room != null) { 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)) { if ({EventTypes.KeyVerificationRequest}.contains(type)) {
payload['msgtype'] = type; payload['msgtype'] = type;
payload['to'] = userId; payload['to'] = userId;
@ -585,7 +586,7 @@ class KeyVerification {
'Attempting verification request. ($type) Apparently your client doesn\'t support this'; 'Attempting verification request. ($type) Apparently your client doesn\'t support this';
type = EventTypes.Message; type = EventTypes.Message;
} }
final newTransactionId = await room.sendEvent(payload, type: type); final newTransactionId = await room!.sendEvent(payload, type: type);
if (transactionId == null) { if (transactionId == null) {
transactionId = newTransactionId; transactionId = newTransactionId;
encryption.keyVerificationManager.addRequest(this); encryption.keyVerificationManager.addRequest(this);
@ -603,11 +604,11 @@ class KeyVerification {
'[Key Verification] Tried to broadcast and un-broadcastable type: $type'); '[Key Verification] Tried to broadcast and un-broadcastable type: $type');
} }
} else { } else {
if (client.userDeviceKeys[userId].deviceKeys[deviceId] == null) { if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] == null) {
Logs().e('[Key Verification] Unknown device'); Logs().e('[Key Verification] Unknown device');
} }
await client.sendToDeviceEncrypted( await client.sendToDeviceEncrypted(
[client.userDeviceKeys[userId].deviceKeys[deviceId]], [client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
type, type,
payload); payload);
} }
@ -619,7 +620,7 @@ class KeyVerification {
state = newState; state = newState;
} }
if (onUpdate != null) { if (onUpdate != null) {
onUpdate(); onUpdate!();
} }
} }
} }
@ -628,14 +629,14 @@ abstract class _KeyVerificationMethod {
KeyVerification request; KeyVerification request;
Encryption get encryption => request.encryption; Encryption get encryption => request.encryption;
Client get client => request.client; Client get client => request.client;
_KeyVerificationMethod({this.request}); _KeyVerificationMethod({required this.request});
Future<void> handlePayload(String type, Map<String, dynamic> payload); Future<void> handlePayload(String type, Map<String, dynamic> payload);
bool validateStart(Map<String, dynamic> payload) { bool validateStart(Map<String, dynamic> payload) {
return false; return false;
} }
String _type; late String _type;
String get type => _type; String get type => _type;
Future<void> sendStart(); Future<void> sendStart();
@ -647,21 +648,21 @@ const knownHashes = ['sha256'];
const knownHashesAuthentificationCodes = ['hkdf-hmac-sha256']; const knownHashesAuthentificationCodes = ['hkdf-hmac-sha256'];
class _KeyVerificationMethodSas extends _KeyVerificationMethod { class _KeyVerificationMethodSas extends _KeyVerificationMethod {
_KeyVerificationMethodSas({KeyVerification request}) _KeyVerificationMethodSas({required KeyVerification request})
: super(request: request); : super(request: request);
@override @override
final _type = 'm.sas.v1'; final _type = 'm.sas.v1';
String keyAgreementProtocol; String? keyAgreementProtocol;
String hash; String? hash;
String messageAuthenticationCode; String? messageAuthenticationCode;
List<String> authenticationTypes; List<String>? authenticationTypes;
String startCanonicalJson; late String startCanonicalJson;
String commitment; String? commitment;
String theirPublicKey; late String theirPublicKey;
Map<String, dynamic> macPayload; Map<String, dynamic>? macPayload;
olm.SAS sas; olm.SAS? sas;
@override @override
void dispose() { void dispose() {
@ -808,7 +809,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Future<void> _sendAccept() async { Future<void> _sendAccept() async {
sas = olm.SAS(); sas = olm.SAS();
commitment = _makeCommitment(sas.get_pubkey(), startCanonicalJson); commitment = _makeCommitment(sas!.get_pubkey(), startCanonicalJson);
await request.send(EventTypes.KeyVerificationAccept, { await request.send(EventTypes.KeyVerificationAccept, {
'method': type, 'method': type,
'key_agreement_protocol': keyAgreementProtocol, 'key_agreement_protocol': keyAgreementProtocol,
@ -847,13 +848,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Future<void> _sendKey() async { Future<void> _sendKey() async {
await request.send('m.key.verification.key', { await request.send('m.key.verification.key', {
'key': sas.get_pubkey(), 'key': sas!.get_pubkey(),
}); });
} }
void _handleKey(Map<String, dynamic> payload) { void _handleKey(Map<String, dynamic> payload) {
theirPublicKey = payload['key']; theirPublicKey = payload['key'];
sas.set_their_key(payload['key']); sas!.set_their_key(payload['key']);
} }
bool _validateCommitment() { bool _validateCommitment() {
@ -865,26 +866,26 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
var sasInfo = ''; var sasInfo = '';
if (keyAgreementProtocol == 'curve25519-hkdf-sha256') { if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
final ourInfo = final ourInfo =
'${client.userID}|${client.deviceID}|${sas.get_pubkey()}|'; '${client.userID}|${client.deviceID}|${sas!.get_pubkey()}|';
final theirInfo = final theirInfo =
'${request.userId}|${request.deviceId}|$theirPublicKey|'; '${request.userId}|${request.deviceId}|$theirPublicKey|';
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' + sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' +
(request.startedVerification (request.startedVerification
? ourInfo + theirInfo ? ourInfo + theirInfo
: theirInfo + ourInfo) + : theirInfo + ourInfo) +
request.transactionId; request.transactionId!;
} else if (keyAgreementProtocol == 'curve25519') { } else if (keyAgreementProtocol == 'curve25519') {
final ourInfo = client.userID + client.deviceID; final ourInfo = client.userID + client.deviceID;
final theirInfo = request.userId + request.deviceId; final theirInfo = request.userId + request.deviceId!;
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' + sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' +
(request.startedVerification (request.startedVerification
? ourInfo + theirInfo ? ourInfo + theirInfo
: theirInfo + ourInfo) + : theirInfo + ourInfo) +
request.transactionId; request.transactionId!;
} else { } else {
throw Exception('Unknown key agreement protocol'); throw Exception('Unknown key agreement protocol');
} }
return sas.generate_bytes(sasInfo, bytes); return sas!.generate_bytes(sasInfo, bytes);
} }
Future<void> _sendMac() async { Future<void> _sendMac() async {
@ -892,8 +893,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
client.userID + client.userID +
client.deviceID + client.deviceID +
request.userId + request.userId +
request.deviceId + request.deviceId! +
request.transactionId; request.transactionId!;
final mac = <String, String>{}; final mac = <String, String>{};
final keyList = <String>[]; final keyList = <String>[];
@ -902,17 +903,17 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
// we would also add the cross signing key here // we would also add the cross signing key here
final deviceKeyId = 'ed25519:${client.deviceID}'; final deviceKeyId = 'ed25519:${client.deviceID}';
mac[deviceKeyId] = mac[deviceKeyId] =
_calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId); _calculateMac(encryption.fingerprintKey!, baseInfo + deviceKeyId);
keyList.add(deviceKeyId); keyList.add(deviceKeyId);
final masterKey = client.userDeviceKeys.containsKey(client.userID) final masterKey = client.userDeviceKeys.containsKey(client.userID)
? client.userDeviceKeys[client.userID].masterKey ? client.userDeviceKeys[client.userID]!.masterKey
: null; : null;
if (masterKey != null && masterKey.verified) { if (masterKey != null && masterKey.verified) {
// we have our own master key verified, let's send it! // we have our own master key verified, let's send it!
final masterKeyId = 'ed25519:${masterKey.publicKey}'; final masterKeyId = 'ed25519:${masterKey.publicKey}';
mac[masterKeyId] = mac[masterKeyId] =
_calculateMac(masterKey.publicKey, baseInfo + masterKeyId); _calculateMac(masterKey.publicKey!, baseInfo + masterKeyId);
keyList.add(masterKeyId); keyList.add(masterKeyId);
} }
@ -925,13 +926,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
} }
Future<void> _processMac() async { Future<void> _processMac() async {
final payload = macPayload; final payload = macPayload!;
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
request.userId + request.userId +
request.deviceId + request.deviceId! +
client.userID + client.userID +
client.deviceID + client.deviceID +
request.transactionId; request.transactionId!;
final keyList = payload['mac'].keys.toList(); final keyList = payload['mac'].keys.toList();
keyList.sort(); keyList.sort();
@ -953,7 +954,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
} }
await request.verifyKeys(mac, (String mac, SignableKey key) async { await request.verifyKeys(mac, (String mac, SignableKey key) async {
return mac == 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) { String _calculateMac(String input, String info) {
if (messageAuthenticationCode == 'hkdf-hmac-sha256') { if (messageAuthenticationCode == 'hkdf-hmac-sha256') {
return sas.calculate_mac(input, info); return sas!.calculate_mac(input, info);
} else { } else {
throw Exception('Unknown message authentification code'); throw Exception('Unknown message authentification code');
} }
@ -1239,6 +1241,6 @@ class KeyVerificationEmoji {
final int number; final int number;
KeyVerificationEmoji(this.number); KeyVerificationEmoji(this.number);
String get emoji => _emojiMap[number]['emoji']; String get emoji => _emojiMap[number]['emoji'] ?? '';
String get name => _emojiMap[number]['name']; String get name => _emojiMap[number]['name'] ?? '';
} }

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2020, 2021 Famedly GmbH * Copyright (C) 2020, 2021 Famedly GmbH
@ -23,31 +22,32 @@ import '../../matrix.dart';
class OlmSession { class OlmSession {
String identityKey; String identityKey;
String sessionId; String? sessionId;
olm.Session session; olm.Session? session;
DateTime lastReceived; DateTime? lastReceived;
final String key; final String key;
String get pickledSession => session.pickle(key); String? get pickledSession => session?.pickle(key);
bool get isValid => session != null; bool get isValid => session != null;
OlmSession({ OlmSession({
this.key, required this.key,
this.identityKey, required this.identityKey,
this.sessionId, required this.sessionId,
this.session, required this.session,
this.lastReceived, 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(); session = olm.Session();
try { try {
session.unpickle(key, dbEntry['pickle']); session!.unpickle(key, dbEntry['pickle']);
identityKey = dbEntry['identity_key'];
sessionId = dbEntry['session_id']; sessionId = dbEntry['session_id'];
lastReceived = lastReceived =
DateTime.fromMillisecondsSinceEpoch(dbEntry['last_received'] ?? 0); DateTime.fromMillisecondsSinceEpoch(dbEntry['last_received'] ?? 0);
assert(sessionId == session.session_id()); assert(sessionId == session!.session_id());
} catch (e, s) { } catch (e, s) {
Logs().e('[LibOlm] Could not unpickle olm session', e, s); Logs().e('[LibOlm] Could not unpickle olm session', e, s);
dispose(); dispose();

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH * Copyright (C) 2019, 2020, 2021 Famedly GmbH
@ -24,7 +23,7 @@ import '../../matrix.dart';
class SessionKey { class SessionKey {
/// The raw json content of the key /// 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 of stringified-index to event id, so that we can detect replay attacks
Map<String, String> indexes; Map<String, String> indexes;
@ -34,7 +33,7 @@ class SessionKey {
Map<String, Map<String, int>> allowedAtIndex; Map<String, Map<String, int>> allowedAtIndex;
/// Underlying olm [InboundGroupSession] object /// Underlying olm [InboundGroupSession] object
olm.InboundGroupSession inboundGroupSession; olm.InboundGroupSession? inboundGroupSession;
/// Key for libolm pickle / unpickle /// Key for libolm pickle / unpickle
final String key; final String key;
@ -47,10 +46,10 @@ class SessionKey {
<String>[]; <String>[];
/// Claimed keys of the original sender /// Claimed keys of the original sender
Map<String, String> senderClaimedKeys; late Map<String, String> senderClaimedKeys;
/// Sender curve25519 key /// Sender curve25519 key
String senderKey; late String senderKey;
/// Is this session valid? /// Is this session valid?
bool get isValid => inboundGroupSession != null; bool get isValid => inboundGroupSession != null;
@ -62,66 +61,35 @@ class SessionKey {
String sessionId; String sessionId;
SessionKey( SessionKey(
{this.content, {required this.content,
this.inboundGroupSession, required this.inboundGroupSession,
this.key, required this.key,
this.indexes, Map<String, String>? indexes,
this.allowedAtIndex, Map<String, Map<String, int>>? allowedAtIndex,
this.roomId, required this.roomId,
this.sessionId, required this.sessionId,
String senderKey, required this.senderKey,
Map<String, String> senderClaimedKeys}) { required this.senderClaimedKeys})
_setSenderKey(senderKey); : indexes = indexes ?? <String, String>{},
_setSenderClaimedKeys(senderClaimedKeys); allowedAtIndex = allowedAtIndex ?? <String, Map<String, int>>{};
indexes ??= <String, String>{};
allowedAtIndex ??= <String, Map<String, int>>{};
}
SessionKey.fromDb(StoredInboundGroupSession dbEntry, String key) : key = key { SessionKey.fromDb(StoredInboundGroupSession dbEntry, String key)
final parsedContent = Event.getMapFromPayload(dbEntry.content); : key = key,
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes); content = Event.getMapFromPayload(dbEntry.content),
final parsedAllowedAtIndex = indexes =
Event.getMapFromPayload(dbEntry.allowedAtIndex); Map<String, String>.from(Event.getMapFromPayload(dbEntry.indexes)),
final parsedSenderClaimedKeys = allowedAtIndex = Map<String, Map<String, int>>.from(
Event.getMapFromPayload(dbEntry.senderClaimedKeys); Event.getMapFromPayload(dbEntry.allowedAtIndex)
content = parsedContent; .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. // we need to try...catch as the map used to be <String, int> and that will throw an error.
try { senderClaimedKeys = (parsedSenderClaimedKeys.isNotEmpty)
indexes = parsedIndexes != null ? parsedSenderClaimedKeys
? 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
: (content['sender_claimed_keys'] is Map : (content['sender_claimed_keys'] is Map
? Map<String, String>.from(content['sender_claimed_keys']) ? Map<String, String>.from(content['sender_claimed_keys'])
: (content['sender_claimed_ed25519_key'] is String : (content['sender_claimed_ed25519_key'] is String
@ -129,6 +97,13 @@ class SessionKey {
'ed25519': content['sender_claimed_ed25519_key'] 'ed25519': content['sender_claimed_ed25519_key']
} }
: <String, String>{})); : <String, String>{}));
try {
inboundGroupSession!.unpickle(key, dbEntry.pickle);
} catch (e, s) {
dispose();
Logs().e('[LibOlm] Unable to unpickle inboundGroupSession', e, s);
}
} }
void dispose() { void dispose() {

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2021 Famedly GmbH * Copyright (C) 2021 Famedly GmbH
@ -18,11 +17,11 @@
*/ */
class SSSSCache { class SSSSCache {
final int clientId; final int? clientId;
final String type; final String? type;
final String keyId; final String? keyId;
final String ciphertext; final String? ciphertext;
final String content; final String? content;
const SSSSCache( const SSSSCache(
{this.clientId, this.type, this.keyId, this.ciphertext, this.content}); {this.clientId, this.type, this.keyId, this.ciphertext, this.content});

View File

@ -231,10 +231,11 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
convertToJson(raw), convertToJson(raw),
Client(''), Client(''),
); );
await addSeenDeviceId(deviceKeys.userId, deviceKeys.deviceId, await addSeenDeviceId(deviceKeys.userId, deviceKeys.deviceId!,
deviceKeys.curve25519Key + deviceKeys.ed25519Key); deviceKeys.curve25519Key! + deviceKeys.ed25519Key!);
await addSeenPublicKey(deviceKeys.ed25519Key, deviceKeys.deviceId); await addSeenPublicKey(deviceKeys.ed25519Key!, deviceKeys.deviceId!);
await addSeenPublicKey(deviceKeys.curve25519Key, deviceKeys.deviceId); await addSeenPublicKey(
deviceKeys.curve25519Key!, deviceKeys.deviceId!);
} catch (e) { } catch (e) {
Logs().w('Can not migrate device $key', e); Logs().w('Can not migrate device $key', e);
} }

View File

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

View File

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

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2020, 2021 Famedly GmbH * Copyright (C) 2020, 2021 Famedly GmbH
@ -20,6 +19,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:canonical_json/canonical_json.dart'; import 'package:canonical_json/canonical_json.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
@ -37,7 +37,7 @@ class DeviceKeysList {
Map<String, DeviceKeys> deviceKeys = {}; Map<String, DeviceKeys> deviceKeys = {};
Map<String, CrossSigningKey> crossSigningKeys = {}; Map<String, CrossSigningKey> crossSigningKeys = {};
SignableKey getKey(String id) { SignableKey? getKey(String id) {
if (deviceKeys.containsKey(id)) { if (deviceKeys.containsKey(id)) {
return deviceKeys[id]; return deviceKeys[id];
} }
@ -47,18 +47,18 @@ class DeviceKeysList {
return null; return null;
} }
CrossSigningKey getCrossSigningKey(String type) => crossSigningKeys.values CrossSigningKey? getCrossSigningKey(String type) =>
.firstWhere((k) => k.usage.contains(type), orElse: () => null); crossSigningKeys.values.firstWhereOrNull((k) => k.usage.contains(type));
CrossSigningKey get masterKey => getCrossSigningKey('master'); CrossSigningKey? get masterKey => getCrossSigningKey('master');
CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing'); CrossSigningKey? get selfSigningKey => getCrossSigningKey('self_signing');
CrossSigningKey get userSigningKey => getCrossSigningKey('user_signing'); CrossSigningKey? get userSigningKey => getCrossSigningKey('user_signing');
UserVerifiedStatus get verified { UserVerifiedStatus get verified {
if (masterKey == null) { if (masterKey == null) {
return UserVerifiedStatus.unknown; return UserVerifiedStatus.unknown;
} }
if (masterKey.verified) { if (masterKey!.verified) {
for (final key in deviceKeys.values) { for (final key in deviceKeys.values) {
if (!key.verified) { if (!key.verified) {
return UserVerifiedStatus.unknownDevice; return UserVerifiedStatus.unknownDevice;
@ -79,7 +79,8 @@ class DeviceKeysList {
if (userId != client.userID) { if (userId != client.userID) {
// in-room verification with someone else // in-room verification with someone else
final roomId = await client.startDirectChat(userId); 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'); throw Exception('Unable to start new room');
} }
final room = final room =
@ -104,9 +105,9 @@ class DeviceKeysList {
Map<String, dynamic> dbEntry, Map<String, dynamic> dbEntry,
List<Map<String, dynamic>> childEntries, List<Map<String, dynamic>> childEntries,
List<Map<String, dynamic>> crossSigningEntries, List<Map<String, dynamic>> crossSigningEntries,
Client cl) { Client cl)
client = cl; : client = cl,
userId = dbEntry['user_id']; userId = dbEntry['user_id'] ?? '' {
outdated = dbEntry['outdated']; outdated = dbEntry['outdated'];
deviceKeys = {}; deviceKeys = {};
for (final childEntry in childEntries) { for (final childEntry in childEntries) {
@ -132,24 +133,27 @@ class DeviceKeysList {
class SimpleSignableKey extends MatrixSignableKey { class SimpleSignableKey extends MatrixSignableKey {
@override @override
String identifier; String? identifier;
SimpleSignableKey.fromJson(Map<String, dynamic> json) : super.fromJson(json); SimpleSignableKey.fromJson(Map<String, dynamic> json) : super.fromJson(json);
} }
abstract class SignableKey extends MatrixSignableKey { abstract class SignableKey extends MatrixSignableKey {
Client client; Client client;
Map<String, dynamic> validSignatures; Map<String, dynamic>? validSignatures;
bool _verified; bool? _verified;
bool blocked; bool? _blocked;
@override String? get ed25519Key => keys['ed25519:$identifier'];
String 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 => bool get encryptToDevice =>
!blocked && !(blocked) &&
identifier != null &&
ed25519Key != null &&
(client.userDeviceKeys[userId]?.masterKey?.verified ?? false (client.userDeviceKeys[userId]?.masterKey?.verified ?? false
? verified ? verified
: true); : true);
@ -158,7 +162,7 @@ abstract class SignableKey extends MatrixSignableKey {
_verified = v; _verified = v;
} }
bool get directVerified => _verified; bool get directVerified => _verified ?? false;
bool get crossVerified => hasValidSignatureChain(); bool get crossVerified => hasValidSignatureChain();
bool get signed => hasValidSignatureChain(verifiedOnly: false); bool get signed => hasValidSignatureChain(verifiedOnly: false);
@ -166,14 +170,14 @@ abstract class SignableKey extends MatrixSignableKey {
: client = cl, : client = cl,
super.fromJson(json) { super.fromJson(json) {
_verified = false; _verified = false;
blocked = false; _blocked = false;
} }
SimpleSignableKey cloneForSigning() { SimpleSignableKey cloneForSigning() {
final newKey = SimpleSignableKey.fromJson(toJson().copy()); final newKey = SimpleSignableKey.fromJson(toJson().copy());
newKey.identifier = identifier; newKey.identifier = identifier;
newKey.signatures ??= <String, Map<String, String>>{}; newKey.signatures ??= <String, Map<String, String>>{};
newKey.signatures.clear(); newKey.signatures!.clear();
return newKey; return newKey;
} }
@ -188,7 +192,7 @@ abstract class SignableKey extends MatrixSignableKey {
return String.fromCharCodes(canonicalJson.encode(data)); return String.fromCharCodes(canonicalJson.encode(data));
} }
bool _verifySignature(String pubKey, String signature, bool _verifySignature(String /*!*/ pubKey, String /*!*/ signature,
{bool isSignatureWithoutLibolmValid = false}) { {bool isSignatureWithoutLibolmValid = false}) {
olm.Utility olmutil; olm.Utility olmutil;
try { try {
@ -214,22 +218,26 @@ abstract class SignableKey extends MatrixSignableKey {
bool hasValidSignatureChain( bool hasValidSignatureChain(
{bool verifiedOnly = true, {bool verifiedOnly = true,
Set<String> visited, Set<String>? visited,
Set<String> onlyValidateUserIds}) { Set<String>? onlyValidateUserIds}) {
if (!client.encryptionEnabled) { if (!client.encryptionEnabled) {
return false; return false;
} }
visited ??= <String>{};
onlyValidateUserIds ??= <String>{}; final visited_ = visited ?? <String>{};
final onlyValidateUserIds_ = onlyValidateUserIds ?? <String>{};
final setKey = '$userId;$identifier'; final setKey = '$userId;$identifier';
if (visited.contains(setKey) || if (visited_.contains(setKey) ||
(onlyValidateUserIds.isNotEmpty && (onlyValidateUserIds_.isNotEmpty &&
!onlyValidateUserIds.contains(userId))) { !onlyValidateUserIds_.contains(userId))) {
return false; // prevent recursion & validate hasValidSignatureChain return false; // prevent recursion & validate hasValidSignatureChain
} }
visited.add(setKey); visited_.add(setKey);
if (signatures == null) return false; if (signatures == null) return false;
for (final signatureEntries in signatures.entries) {
for (final signatureEntries in signatures!.entries) {
final otherUserId = signatureEntries.key; final otherUserId = signatureEntries.key;
if (!(signatureEntries.value is Map) || if (!(signatureEntries.value is Map) ||
!client.userDeviceKeys.containsKey(otherUserId)) { !client.userDeviceKeys.containsKey(otherUserId)) {
@ -250,18 +258,20 @@ abstract class SignableKey extends MatrixSignableKey {
if (otherUserId == userId && keyId == identifier) { if (otherUserId == userId && keyId == identifier) {
continue; continue;
} }
SignableKey key; SignableKey? key;
if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) { if (client.userDeviceKeys[otherUserId]!.deviceKeys.containsKey(keyId)) {
key = client.userDeviceKeys[otherUserId].deviceKeys[keyId]; key = client.userDeviceKeys[otherUserId]!.deviceKeys[keyId];
} else if (client.userDeviceKeys[otherUserId].crossSigningKeys } else if (client.userDeviceKeys[otherUserId]!.crossSigningKeys
.containsKey(keyId)) { .containsKey(keyId)) {
key = client.userDeviceKeys[otherUserId].crossSigningKeys[keyId]; key = client.userDeviceKeys[otherUserId]!.crossSigningKeys[keyId];
} else { }
if (key == null) {
continue; continue;
} }
if (onlyValidateUserIds.isNotEmpty && if (onlyValidateUserIds_.isNotEmpty &&
!onlyValidateUserIds.contains(key.userId)) { !onlyValidateUserIds_.contains(key.userId)) {
// we don't want to verify keys from this user // we don't want to verify keys from this user
continue; continue;
} }
@ -272,24 +282,24 @@ abstract class SignableKey extends MatrixSignableKey {
var haveValidSignature = false; var haveValidSignature = false;
var gotSignatureFromCache = false; var gotSignatureFromCache = false;
if (validSignatures != null && if (validSignatures != null &&
validSignatures.containsKey(otherUserId) && validSignatures!.containsKey(otherUserId) &&
validSignatures[otherUserId].containsKey(fullKeyId)) { validSignatures![otherUserId].containsKey(fullKeyId)) {
if (validSignatures[otherUserId][fullKeyId] == true) { if (validSignatures![otherUserId][fullKeyId] == true) {
haveValidSignature = true; haveValidSignature = true;
gotSignatureFromCache = true; gotSignatureFromCache = true;
} else if (validSignatures[otherUserId][fullKeyId] == false) { } else if (validSignatures![otherUserId][fullKeyId] == false) {
haveValidSignature = false; haveValidSignature = false;
gotSignatureFromCache = true; gotSignatureFromCache = true;
} }
} }
if (!gotSignatureFromCache) { if (!gotSignatureFromCache && key.ed25519Key != null) {
// validate the signature manually // validate the signature manually
haveValidSignature = _verifySignature(key.ed25519Key, signature); haveValidSignature = _verifySignature(key.ed25519Key!, signature);
validSignatures ??= <String, dynamic>{}; validSignatures ??= <String, dynamic>{};
if (!validSignatures.containsKey(otherUserId)) { if (!validSignatures!.containsKey(otherUserId)) {
validSignatures[otherUserId] = <String, dynamic>{}; validSignatures![otherUserId] = <String, dynamic>{};
} }
validSignatures[otherUserId][fullKeyId] = haveValidSignature; validSignatures![otherUserId][fullKeyId] = haveValidSignature;
} }
if (!haveValidSignature) { if (!haveValidSignature) {
// no valid signature, this key is useless // 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 @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -349,24 +359,36 @@ abstract class SignableKey extends MatrixSignableKey {
} }
class CrossSigningKey extends SignableKey { class CrossSigningKey extends SignableKey {
String get publicKey => identifier; @override
List<String> usage; String? identifier;
String? get publicKey => identifier;
late List<String> usage;
bool get isValid => bool get isValid =>
userId != null && publicKey != null && keys != null && ed25519Key != null; userId.isNotEmpty &&
publicKey != null &&
keys.isNotEmpty &&
ed25519Key != null;
@override @override
Future<void> setVerified(bool newVerified, [bool sign = true]) async { Future<void> setVerified(bool newVerified, [bool sign = true]) async {
if (!isValid) {
throw Exception('setVerified called on invalid key');
}
await super.setVerified(newVerified, sign); await super.setVerified(newVerified, sign);
return client.database await client.database
?.setVerifiedUserCrossSigningKey(newVerified, userId, publicKey); ?.setVerifiedUserCrossSigningKey(newVerified, userId, publicKey!);
} }
@override @override
Future<void> setBlocked(bool newBlocked) { Future<void> setBlocked(bool newBlocked) async {
blocked = newBlocked; if (!isValid) {
return client.database throw Exception('setBlocked called on invalid key');
?.setBlockedUserCrossSigningKey(newBlocked, userId, publicKey); }
_blocked = newBlocked;
await client.database
?.setBlockedUserCrossSigningKey(newBlocked, userId, publicKey!);
} }
CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl) CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl)
@ -382,69 +404,80 @@ class CrossSigningKey extends SignableKey {
identifier = dbEntry['public_key']; identifier = dbEntry['public_key'];
usage = json['usage'].cast<String>(); usage = json['usage'].cast<String>();
_verified = dbEntry['verified']; _verified = dbEntry['verified'];
blocked = dbEntry['blocked']; _blocked = dbEntry['blocked'];
} }
CrossSigningKey.fromJson(Map<String, dynamic> json, Client cl) CrossSigningKey.fromJson(Map<String, dynamic> json, Client cl)
: super.fromJson(json.copy(), cl) { : super.fromJson(json.copy(), cl) {
final json = toJson(); final json = toJson();
usage = json['usage'].cast<String>(); usage = json['usage'].cast<String>();
if (keys != null && keys.isNotEmpty) { if (keys.isNotEmpty) {
identifier = keys.values.first; identifier = keys.values.first;
} }
} }
} }
class DeviceKeys extends SignableKey { class DeviceKeys extends SignableKey {
String get deviceId => identifier; @override
List<String> algorithms; String? identifier;
DateTime lastActive;
String get curve25519Key => keys['curve25519:$deviceId']; String? get deviceId => identifier;
String get deviceDisplayName => late List<String> algorithms;
unsigned != null ? unsigned['device_display_name'] : null; 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 => bool get selfSigned =>
_validSelfSignature ?? _validSelfSignature ??
(_validSelfSignature = (signatures (_validSelfSignature = (deviceId != null &&
?.tryGet<Map<String, dynamic>>(userId) signatures
?.tryGet<String>('ed25519:$deviceId') == ?.tryGet<Map<String, dynamic>>(userId)
null ?.tryGet<String>('ed25519:$deviceId') ==
null
? false ? false
// without libolm we still want to be able to add devices. In that case we ofc just can't // without libolm we still want to be able to add devices. In that case we ofc just can't
// verify the signature // verify the signature
: _verifySignature( : _verifySignature(
ed25519Key, signatures[userId]['ed25519:$deviceId'], ed25519Key!, signatures![userId]!['ed25519:$deviceId']!,
isSignatureWithoutLibolmValid: true))); isSignatureWithoutLibolmValid: true)));
@override @override
bool get blocked => super.blocked || !selfSigned; bool get blocked => super.blocked || !selfSigned;
bool get isValid => bool get isValid =>
userId != null &&
deviceId != null && deviceId != null &&
keys != null && keys.isNotEmpty &&
curve25519Key != null && curve25519Key != null &&
ed25519Key != null && ed25519Key != null &&
selfSigned; selfSigned;
@override @override
Future<void> setVerified(bool newVerified, [bool sign = true]) async { Future<void> setVerified(bool newVerified, [bool sign = true]) async {
if (!isValid) {
//throw Exception('setVerified called on invalid key');
return;
}
await super.setVerified(newVerified, sign); await super.setVerified(newVerified, sign);
return client?.database await client.database
?.setVerifiedUserDeviceKey(newVerified, userId, deviceId); ?.setVerifiedUserDeviceKey(newVerified, userId, deviceId!);
} }
@override @override
Future<void> setBlocked(bool newBlocked) { Future<void> setBlocked(bool newBlocked) async {
blocked = newBlocked; if (!isValid) {
return client?.database //throw Exception('setBlocked called on invalid key');
?.setBlockedUserDeviceKey(newBlocked, userId, deviceId); return;
}
_blocked = newBlocked;
await client.database
?.setBlockedUserDeviceKey(newBlocked, userId, deviceId!);
} }
DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl, DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl,
[DateTime lastActiveTs]) [DateTime? lastActiveTs])
: super.fromJson(k.toJson().copy(), cl) { : super.fromJson(k.toJson().copy(), cl) {
final json = toJson(); final json = toJson();
identifier = k.deviceId; identifier = k.deviceId;
@ -458,7 +491,7 @@ class DeviceKeys extends SignableKey {
identifier = dbEntry['device_id']; identifier = dbEntry['device_id'];
algorithms = json['algorithms'].cast<String>(); algorithms = json['algorithms'].cast<String>();
_verified = dbEntry['verified']; _verified = dbEntry['verified'];
blocked = dbEntry['blocked']; _blocked = dbEntry['blocked'];
lastActive = lastActive =
DateTime.fromMillisecondsSinceEpoch(dbEntry['last_active'] ?? 0); DateTime.fromMillisecondsSinceEpoch(dbEntry['last_active'] ?? 0);
} }
@ -472,8 +505,11 @@ class DeviceKeys extends SignableKey {
} }
KeyVerification startVerification() { KeyVerification startVerification() {
if (!isValid) {
throw Exception('setVerification called on invalid key');
}
final request = KeyVerification( final request = KeyVerification(
encryption: client.encryption, userId: userId, deviceId: deviceId); encryption: client.encryption, userId: userId, deviceId: deviceId!);
request.start(); request.start();
client.encryption.keyVerificationManager.addRequest(request); client.encryption.keyVerificationManager.addRequest(request);

View File

@ -22,6 +22,7 @@ dependencies:
js: ^0.6.3 js: ^0.6.3
slugify: ^2.0.0 slugify: ^2.0.0
html: ^0.15.0 html: ^0.15.0
collection: ^1.15.0-nullsafety.4
dev_dependencies: dev_dependencies:
pedantic: ^1.11.0 pedantic: ^1.11.0

View File

@ -31,7 +31,27 @@ void main() {
/// All Tests related to device keys /// All Tests related to device keys
group('Device keys', () { group('Device keys', () {
Logs().level = Level.error; 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 { test('fromJson', () async {
if (!olmEnabled) return;
var rawJson = <String, dynamic>{ var rawJson = <String, dynamic>{
'user_id': '@alice:example.com', 'user_id': '@alice:example.com',
'device_id': 'JLAFKJWSCS', 'device_id': 'JLAFKJWSCS',
@ -53,7 +73,8 @@ void main() {
'unsigned': {'device_display_name': "Alice's mobile phone"}, '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.setVerified(false, false);
await key.setBlocked(true); await key.setBlocked(true);
expect(json.encode(key.toJson()), json.encode(rawJson)); expect(json.encode(key.toJson()), json.encode(rawJson));
@ -69,29 +90,11 @@ void main() {
}, },
'signatures': {}, 'signatures': {},
}; };
final crossKey = CrossSigningKey.fromJson(rawJson, null); final crossKey = CrossSigningKey.fromJson(rawJson, client);
expect(json.encode(crossKey.toJson()), json.encode(rawJson)); expect(json.encode(crossKey.toJson()), json.encode(rawJson));
expect(crossKey.usage.first, 'master'); 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 { test('reject devices without self-signature', () async {
if (!olmEnabled) return; if (!olmEnabled) return;
var key = DeviceKeys.fromJson({ var key = DeviceKeys.fromJson({