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