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