migrate to new thingy!
This commit is contained in:
parent
d29fb9abfe
commit
4c60369b8d
|
@ -1,19 +1,38 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
import 'client.dart';
|
||||
import 'utils/device_keys_list.dart';
|
||||
import 'encryption.dart';
|
||||
|
||||
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
|
||||
const USER_SIGNING_KEY = 'm.cross_signing.user_signing';
|
||||
const MASTER_KEY = 'm.cross_signing.master';
|
||||
|
||||
class CrossSigning {
|
||||
final Client client;
|
||||
CrossSigning(this.client) {
|
||||
client.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async {
|
||||
final Encryption encryption;
|
||||
Client get client => encryption.client;
|
||||
CrossSigning(this.encryption) {
|
||||
encryption.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async {
|
||||
final keyObj = olm.PkSigning();
|
||||
try {
|
||||
return keyObj.init_with_seed(base64.decode(secret)) ==
|
||||
|
@ -24,7 +43,7 @@ class CrossSigning {
|
|||
keyObj.free();
|
||||
}
|
||||
});
|
||||
client.ssss.setValidator(USER_SIGNING_KEY, (String secret) async {
|
||||
encryption.ssss.setValidator(USER_SIGNING_KEY, (String secret) async {
|
||||
final keyObj = olm.PkSigning();
|
||||
try {
|
||||
return keyObj.init_with_seed(base64.decode(secret)) ==
|
||||
|
@ -46,12 +65,12 @@ class CrossSigning {
|
|||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
return (await client.ssss.getCached(SELF_SIGNING_KEY)) != null &&
|
||||
(await client.ssss.getCached(USER_SIGNING_KEY)) != null;
|
||||
return (await encryption.ssss.getCached(SELF_SIGNING_KEY)) != null &&
|
||||
(await encryption.ssss.getCached(USER_SIGNING_KEY)) != null;
|
||||
}
|
||||
|
||||
Future<void> selfSign({String password, String recoveryKey}) async {
|
||||
final handle = client.ssss.open(MASTER_KEY);
|
||||
final handle = encryption.ssss.open(MASTER_KEY);
|
||||
await handle.unlock(password: password, recoveryKey: recoveryKey);
|
||||
await handle.maybeCacheAll();
|
||||
final masterPrivateKey = base64.decode(await handle.getStored(MASTER_KEY));
|
||||
|
@ -133,7 +152,7 @@ class CrossSigning {
|
|||
if (key is CrossSigningKey) {
|
||||
if (key.usage.contains('master')) {
|
||||
// okay, we'll sign our own master key
|
||||
final signature = client.signString(key.signingContent);
|
||||
final signature = encryption.olmManager.signString(key.signingContent);
|
||||
addSignature(
|
||||
key,
|
||||
client
|
||||
|
@ -144,8 +163,8 @@ class CrossSigning {
|
|||
} else {
|
||||
// okay, we'll sign a device key with our self signing key
|
||||
selfSigningKey ??= base64
|
||||
.decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? '');
|
||||
if (selfSigningKey != null) {
|
||||
.decode(await encryption.ssss.getCached(SELF_SIGNING_KEY) ?? '');
|
||||
if (selfSigningKey.isNotEmpty) {
|
||||
final signature = _sign(key.signingContent, selfSigningKey);
|
||||
addSignature(key,
|
||||
client.userDeviceKeys[client.userID].selfSigningKey, signature);
|
||||
|
@ -154,8 +173,8 @@ class CrossSigning {
|
|||
} else if (key is CrossSigningKey && key.usage.contains('master')) {
|
||||
// we are signing someone elses master key
|
||||
userSigningKey ??=
|
||||
base64.decode(await client.ssss.getCached(USER_SIGNING_KEY) ?? '');
|
||||
if (userSigningKey != null) {
|
||||
base64.decode(await encryption.ssss.getCached(USER_SIGNING_KEY) ?? '');
|
||||
if (userSigningKey.isNotEmpty) {
|
||||
final signature = _sign(key.signingContent, userSigningKey);
|
||||
addSignature(key, client.userDeviceKeys[client.userID].userSigningKey,
|
||||
signature);
|
||||
|
@ -165,7 +184,7 @@ class CrossSigning {
|
|||
if (signedKey) {
|
||||
// post our new keys!
|
||||
await client.jsonRequest(
|
||||
type: HTTPType.POST,
|
||||
type: RequestType.POST,
|
||||
action: '/client/r0/keys/signatures/upload',
|
||||
data: signatures,
|
||||
);
|
|
@ -24,6 +24,8 @@ import 'package:pedantic/pedantic.dart';
|
|||
import 'key_manager.dart';
|
||||
import 'olm_manager.dart';
|
||||
import 'key_verification_manager.dart';
|
||||
import 'cross_signing.dart';
|
||||
import 'ssss.dart';
|
||||
|
||||
class Encryption {
|
||||
final Client client;
|
||||
|
@ -42,15 +44,19 @@ class Encryption {
|
|||
KeyManager keyManager;
|
||||
OlmManager olmManager;
|
||||
KeyVerificationManager keyVerificationManager;
|
||||
CrossSigning crossSigning;
|
||||
SSSS ssss;
|
||||
|
||||
Encryption({
|
||||
this.client,
|
||||
this.debug,
|
||||
this.enableE2eeRecovery,
|
||||
}) {
|
||||
ssss = SSSS(this);
|
||||
keyManager = KeyManager(this);
|
||||
olmManager = OlmManager(this);
|
||||
keyVerificationManager = KeyVerificationManager(this);
|
||||
crossSigning = CrossSigning(this);
|
||||
}
|
||||
|
||||
Future<void> init(String olmAccount) async {
|
||||
|
@ -79,6 +85,16 @@ class Encryption {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> handleEventUpdate(EventUpdate update) async {
|
||||
if (update.type == 'ephemeral') {
|
||||
return;
|
||||
}
|
||||
if (update.eventType.startsWith('m.key.verification.') || (update.eventType == 'm.room.message' && (update.content['content']['msgtype'] is String) && update.content['content']['msgtype'].startsWith('m.key.verification.'))) {
|
||||
// "just" key verification, no need to do this in sync
|
||||
unawaited(keyVerificationManager.handleEventUpdate(update));
|
||||
}
|
||||
}
|
||||
|
||||
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
|
||||
return await olmManager.decryptToDeviceEvent(event);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import './encryption.dart';
|
|||
import './utils/session_key.dart';
|
||||
import './utils/outbound_group_session.dart';
|
||||
|
||||
const MEGOLM_KEY = 'm.megolm_backup.v1';
|
||||
|
||||
class KeyManager {
|
||||
final Encryption encryption;
|
||||
Client get client => encryption.client;
|
||||
|
@ -37,7 +39,22 @@ class KeyManager {
|
|||
final Set<String> _loadedOutboundGroupSessions = <String>{};
|
||||
final Set<String> _requestedSessionIds = <String>{};
|
||||
|
||||
KeyManager(this.encryption);
|
||||
KeyManager(this.encryption) {
|
||||
encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async {
|
||||
final keyObj = olm.PkDecryption();
|
||||
try {
|
||||
final info = await getRoomKeysInfo();
|
||||
return keyObj.init_with_private_key(base64.decode(secret)) ==
|
||||
info['auth_data']['public_key'];
|
||||
} catch (_) {
|
||||
return false;
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool get enabled => client.accountData[MEGOLM_KEY] != null;
|
||||
|
||||
/// clear all cached inbound group sessions. useful for testing
|
||||
void clearInboundGroupSessions() {
|
||||
|
@ -283,8 +300,120 @@ class KeyManager {
|
|||
_outboundGroupSessions[roomId] = sess;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getRoomKeysInfo() async {
|
||||
return await client.jsonRequest(
|
||||
type: RequestType.GET,
|
||||
action: '/client/r0/room_keys/version',
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> isCached() async {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
return (await encryption.ssss.getCached(MEGOLM_KEY)) != null;
|
||||
}
|
||||
|
||||
Future<void> loadFromResponse(Map<String, dynamic> payload) async {
|
||||
if (!(await isCached())) {
|
||||
return;
|
||||
}
|
||||
if (!(payload['rooms'] is Map)) {
|
||||
return;
|
||||
}
|
||||
final privateKey = base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
|
||||
final decryption = olm.PkDecryption();
|
||||
final info = await getRoomKeysInfo();
|
||||
String backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privateKey);
|
||||
|
||||
if (backupPubKey == null ||
|
||||
!info.containsKey('auth_data') ||
|
||||
!(info['auth_data'] is Map) ||
|
||||
info['auth_data']['public_key'] != backupPubKey) {
|
||||
return;
|
||||
}
|
||||
for (final roomEntries in payload['rooms'].entries) {
|
||||
final roomId = roomEntries.key;
|
||||
if (!(roomEntries.value is Map) ||
|
||||
!(roomEntries.value['sessions'] is Map)) {
|
||||
continue;
|
||||
}
|
||||
for (final sessionEntries in roomEntries.value['sessions'].entries) {
|
||||
final sessionId = sessionEntries.key;
|
||||
final rawEncryptedSession = sessionEntries.value;
|
||||
if (!(rawEncryptedSession is Map)) {
|
||||
continue;
|
||||
}
|
||||
final firstMessageIndex =
|
||||
rawEncryptedSession['first_message_index'] is int
|
||||
? rawEncryptedSession['first_message_index']
|
||||
: null;
|
||||
final forwardedCount = rawEncryptedSession['forwarded_count'] is int
|
||||
? rawEncryptedSession['forwarded_count']
|
||||
: null;
|
||||
final isVerified = rawEncryptedSession['is_verified'] is bool
|
||||
? rawEncryptedSession['is_verified']
|
||||
: null;
|
||||
final sessionData = rawEncryptedSession['session_data'];
|
||||
if (firstMessageIndex == null ||
|
||||
forwardedCount == null ||
|
||||
isVerified == null ||
|
||||
!(sessionData is Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, dynamic> decrypted;
|
||||
try {
|
||||
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
|
||||
sessionData['mac'], sessionData['ciphertext']));
|
||||
} catch (err) {
|
||||
print('[LibOlm] Error decrypting room key: ' + err.toString());
|
||||
}
|
||||
if (decrypted != null) {
|
||||
decrypted['session_id'] = sessionId;
|
||||
decrypted['room_id'] = roomId;
|
||||
setInboundGroupSession(roomId, sessionId, decrypted['sender_key'], decrypted, forwarded: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadSingleKey(String roomId, String sessionId) async {
|
||||
final info = await getRoomKeysInfo();
|
||||
final ret = await client.jsonRequest(
|
||||
type: RequestType.GET,
|
||||
action:
|
||||
'/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}',
|
||||
);
|
||||
await loadFromResponse({
|
||||
'rooms': {
|
||||
roomId: {
|
||||
'sessions': {
|
||||
sessionId: ret,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// Request a certain key from another device
|
||||
Future<void> request(Room room, String sessionId, String senderKey) async {
|
||||
// let's first check our online key backup store thingy...
|
||||
var hadPreviously = getInboundGroupSession(room.id, sessionId, senderKey) != null;
|
||||
try {
|
||||
await loadSingleKey(room.id, sessionId);
|
||||
} catch (err, stacktrace) {
|
||||
print('++++++++++++++++++');
|
||||
print(err.toString());
|
||||
print(stacktrace);
|
||||
}
|
||||
if (!hadPreviously && getInboundGroupSession(room.id, sessionId, senderKey) != null) {
|
||||
return; // we managed to load the session from online backup, no need to care about it now
|
||||
}
|
||||
// while we just send the to-device event to '*', we still need to save the
|
||||
// devices themself to know where to send the cancel to after receiving a reply
|
||||
final devices = await room.getUserDeviceKeys();
|
||||
|
@ -500,7 +629,6 @@ class RoomKeyRequest extends ToDeviceEvent {
|
|||
for (final key in session.forwardingCurve25519KeyChain) {
|
||||
forwardedKeys.add(key);
|
||||
}
|
||||
await requestingDevice.setVerified(true, keyManager.client);
|
||||
var message = session.content;
|
||||
message['forwarding_curve25519_key_chain'] = forwardedKeys;
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ class KeyVerificationManager {
|
|||
}
|
||||
|
||||
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||
if (!event.type.startsWith('m.key.verification')) {
|
||||
if (!event.type.startsWith('m.key.verification') || client.verificationMethods.isEmpty) {
|
||||
return;
|
||||
}
|
||||
// we have key verification going on!
|
||||
|
@ -75,6 +75,52 @@ class KeyVerificationManager {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> handleEventUpdate(EventUpdate update) async {
|
||||
final event = update.content;
|
||||
final type = event['type'].startsWith('m.key.verification.')
|
||||
? event['type']
|
||||
: event['content']['msgtype'];
|
||||
if (type == null || !type.startsWith('m.key.verification.') || client.verificationMethods.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (type == 'm.key.verification.request') {
|
||||
event['content']['timestamp'] = event['origin_server_ts'];
|
||||
}
|
||||
|
||||
final transactionId =
|
||||
KeyVerification.getTransactionId(event['content']) ?? event['event_id'];
|
||||
|
||||
if (_requests.containsKey(transactionId)) {
|
||||
final req = _requests[transactionId];
|
||||
if (event['sender'] != client.userID) {
|
||||
req.handlePayload(type, event['content'], event['event_id']);
|
||||
} else if (req.userId == client.userID && req.deviceId == null) {
|
||||
// okay, maybe another of our devices answered
|
||||
await req.handlePayload(type, event['content'], event['event_id']);
|
||||
if (req.deviceId != client.deviceID) {
|
||||
req.otherDeviceAccepted();
|
||||
req.dispose();
|
||||
_requests.remove(transactionId);
|
||||
}
|
||||
}
|
||||
} else if (event['sender'] != client.userID) {
|
||||
final room =
|
||||
client.getRoomById(update.roomID) ?? Room(id: update.roomID, client: client);
|
||||
final newKeyRequest =
|
||||
KeyVerification(encryption: encryption, userId: event['sender'], room: room);
|
||||
await newKeyRequest
|
||||
.handlePayload(type, event['content'], event['event_id']);
|
||||
if (newKeyRequest.state != KeyVerificationState.askAccept) {
|
||||
// something went wrong, let's just dispose the request
|
||||
newKeyRequest.dispose();
|
||||
} else {
|
||||
// new request! Let's notify it and stuff
|
||||
_requests[transactionId] = newKeyRequest;
|
||||
client.onKeyVerificationRequest.add(newKeyRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
for (final req in _requests.values) {
|
||||
req.dispose();
|
||||
|
|
|
@ -96,6 +96,10 @@ class OlmManager {
|
|||
return payload;
|
||||
}
|
||||
|
||||
String signString(String s) {
|
||||
return _olmAccount.sign(s);
|
||||
}
|
||||
|
||||
/// Checks the signature of a signed json object.
|
||||
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
|
||||
String userId, String deviceId) {
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
|
||||
|
@ -5,11 +23,10 @@ import 'package:encrypt/encrypt.dart';
|
|||
import 'package:crypto/crypto.dart';
|
||||
import 'package:base58check/base58.dart';
|
||||
import 'package:password_hash/password_hash.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
|
||||
import 'client.dart';
|
||||
import 'account_data.dart';
|
||||
import 'utils/device_keys_list.dart';
|
||||
import 'utils/to_device_event.dart';
|
||||
import 'encryption.dart';
|
||||
|
||||
const CACHE_TYPES = <String>[
|
||||
'm.cross_signing.self_signing',
|
||||
|
@ -25,10 +42,11 @@ const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
|||
const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm
|
||||
|
||||
class SSSS {
|
||||
final Client client;
|
||||
final Encryption encryption;
|
||||
Client get client => encryption.client;
|
||||
final pendingShareRequests = <String, _ShareRequest>{};
|
||||
final _validators = <String, Future<bool> Function(String)>{};
|
||||
SSSS(this.client);
|
||||
SSSS(this.encryption);
|
||||
|
||||
static _DerivedKeys deriveKeys(Uint8List key, String name) {
|
||||
final zerosalt = Uint8List(8);
|
||||
|
@ -129,11 +147,11 @@ class SSSS {
|
|||
return keyData.content['key'];
|
||||
}
|
||||
|
||||
AccountData getKey(String keyId) {
|
||||
BasicEvent getKey(String keyId) {
|
||||
return client.accountData['m.secret_storage.key.${keyId}'];
|
||||
}
|
||||
|
||||
bool checkKey(Uint8List key, AccountData keyData) {
|
||||
bool checkKey(Uint8List key, BasicEvent keyData) {
|
||||
final info = keyData.content;
|
||||
if (info['algorithm'] == 'm.secret_storage.v1.aes-hmac-sha2') {
|
||||
if ((info['mac'] is String) && (info['iv'] is String)) {
|
||||
|
@ -200,7 +218,7 @@ class SSSS {
|
|||
};
|
||||
// store the thing in your account data
|
||||
await client.jsonRequest(
|
||||
type: HTTPType.PUT,
|
||||
type: RequestType.PUT,
|
||||
action: '/client/r0/user/${client.userID}/account_data/${type}',
|
||||
data: content,
|
||||
);
|
||||
|
@ -421,7 +439,7 @@ class _PasswordInfo {
|
|||
class OpenSSSS {
|
||||
final SSSS ssss;
|
||||
final String keyId;
|
||||
final AccountData keyData;
|
||||
final BasicEvent keyData;
|
||||
OpenSSSS({this.ssss, this.keyId, this.keyData});
|
||||
Uint8List privateKey;
|
||||
|
|
@ -184,8 +184,8 @@ class KeyVerification {
|
|||
if (room == null) {
|
||||
transactionId = client.generateUniqueTransactionId();
|
||||
}
|
||||
if (client.crossSigning.enabled &&
|
||||
!(await client.crossSigning.isCached()) &&
|
||||
if (encryption.crossSigning.enabled &&
|
||||
!(await encryption.crossSigning.isCached()) &&
|
||||
!client.isUnknownSession) {
|
||||
setState(KeyVerificationState.askSSSS);
|
||||
_nextAction = 'request';
|
||||
|
@ -241,7 +241,6 @@ class KeyVerification {
|
|||
print('Setting device id start: ' + _deviceId.toString());
|
||||
transactionId ??= eventId ?? payload['transaction_id'];
|
||||
if (method != null) {
|
||||
print('DUPLICATE START');
|
||||
// the other side sent us a start, even though we already sent one
|
||||
if (payload['method'] == method.type) {
|
||||
// same method. Determine priority
|
||||
|
@ -250,10 +249,8 @@ class KeyVerification {
|
|||
entries.sort();
|
||||
if (entries.first == ourEntry) {
|
||||
// our start won, nothing to do
|
||||
print('we won, nothing to do');
|
||||
return;
|
||||
} else {
|
||||
print('They won, handing off');
|
||||
// the other start won, let's hand off
|
||||
startedVerification = false; // it is now as if they started
|
||||
lastStep =
|
||||
|
@ -324,7 +321,7 @@ class KeyVerification {
|
|||
} else if (_nextAction == 'done') {
|
||||
if (_verifiedDevices != null) {
|
||||
// and now let's sign them all in the background
|
||||
client.crossSigning.sign(_verifiedDevices);
|
||||
encryption.crossSigning.sign(_verifiedDevices);
|
||||
}
|
||||
setState(KeyVerificationState.done);
|
||||
}
|
||||
|
@ -333,7 +330,7 @@ class KeyVerification {
|
|||
next();
|
||||
return;
|
||||
}
|
||||
final handle = client.ssss.open('m.cross_signing.user_signing');
|
||||
final handle = encryption.ssss.open('m.cross_signing.user_signing');
|
||||
await handle.unlock(password: password, recoveryKey: recoveryKey);
|
||||
await handle.maybeCacheAll();
|
||||
next();
|
||||
|
@ -437,18 +434,18 @@ class KeyVerification {
|
|||
if (verifiedMasterKey && userId == client.userID) {
|
||||
// it was our own master key, let's request the cross signing keys
|
||||
// we do it in the background, thus no await needed here
|
||||
unawaited(client.ssss
|
||||
unawaited(encryption.ssss
|
||||
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()));
|
||||
}
|
||||
await send('m.key.verification.done', {});
|
||||
|
||||
var askingSSSS = false;
|
||||
if (client.crossSigning.enabled &&
|
||||
client.crossSigning.signable(_verifiedDevices)) {
|
||||
if (encryption.crossSigning.enabled &&
|
||||
encryption.crossSigning.signable(_verifiedDevices)) {
|
||||
// these keys can be signed! Let's do so
|
||||
if (await client.crossSigning.isCached()) {
|
||||
if (await encryption.crossSigning.isCached()) {
|
||||
// and now let's sign them all in the background
|
||||
unawaited(client.crossSigning.sign(_verifiedDevices));
|
||||
unawaited(encryption.crossSigning.sign(_verifiedDevices));
|
||||
} else if (!wasUnknownSession) {
|
||||
askingSSSS = true;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export 'package:famedlysdk/matrix_api/model/filter.dart';
|
|||
export 'package:famedlysdk/matrix_api/model/keys_query_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/login_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/login_types.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/matrix_cross_signing_key.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/matrix_device_keys.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/matrix_exception.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/message_types.dart';
|
||||
|
|
|
@ -17,10 +17,14 @@
|
|||
*/
|
||||
|
||||
import 'matrix_device_keys.dart';
|
||||
import 'matrix_cross_signing_key.dart';
|
||||
|
||||
class KeysQueryResponse {
|
||||
Map<String, dynamic> failures;
|
||||
Map<String, Map<String, MatrixDeviceKeys>> deviceKeys;
|
||||
Map<String, MatrixCrossSigningKey> masterKeys;
|
||||
Map<String, MatrixCrossSigningKey> selfSigningKeys;
|
||||
Map<String, MatrixCrossSigningKey> userSigningKeys;
|
||||
|
||||
KeysQueryResponse.fromJson(Map<String, dynamic> json) {
|
||||
failures = Map<String, dynamic>.from(json['failures']);
|
||||
|
@ -37,6 +41,32 @@ class KeysQueryResponse {
|
|||
),
|
||||
)
|
||||
: null;
|
||||
masterKeys = json['master_keys'] != null ?
|
||||
(json['master_keys'] as Map).map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
MatrixCrossSigningKey.fromJson(v),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
selfSigningKeys = json['self_signing_keys'] != null ?
|
||||
(json['self_signing_keys'] as Map).map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
MatrixCrossSigningKey.fromJson(v),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
userSigningKeys = json['user_signing_keys'] != null ?
|
||||
(json['user_signing_keys'] as Map).map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
MatrixCrossSigningKey.fromJson(v),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
@ -57,6 +87,30 @@ class KeysQueryResponse {
|
|||
),
|
||||
);
|
||||
}
|
||||
if (masterKeys != null) {
|
||||
data['master_keys'] = masterKeys.map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
v.toJson(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (selfSigningKeys != null) {
|
||||
data['self_signing_keys'] = selfSigningKeys.map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
v.toJson(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (userSigningKeys != null) {
|
||||
data['user_signing_keys'] = userSigningKeys.map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
v.toJson(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
65
lib/matrix_api/model/matrix_cross_signing_key.dart
Normal file
65
lib/matrix_api/model/matrix_cross_signing_key.dart
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
class MatrixCrossSigningKey {
|
||||
String userId;
|
||||
List<String> usage;
|
||||
Map<String, String> keys;
|
||||
Map<String, Map<String, String>> signatures;
|
||||
Map<String, dynamic> unsigned;
|
||||
String get publicKey => keys?.values?.first;
|
||||
|
||||
MatrixCrossSigningKey(
|
||||
this.userId,
|
||||
this.usage,
|
||||
this.keys,
|
||||
this.signatures, {
|
||||
this.unsigned,
|
||||
});
|
||||
|
||||
// This object is used for signing so we need the raw json too
|
||||
Map<String, dynamic> _json;
|
||||
|
||||
MatrixCrossSigningKey.fromJson(Map<String, dynamic> json) {
|
||||
_json = json;
|
||||
userId = json['user_id'];
|
||||
usage = List<String>.from(json['usage']);
|
||||
keys = Map<String, String>.from(json['keys']);
|
||||
signatures = Map<String, Map<String, String>>.from(
|
||||
(json['signatures'] as Map)
|
||||
.map((k, v) => MapEntry(k, Map<String, String>.from(v))));
|
||||
unsigned = json['unsigned'] != null
|
||||
? Map<String, dynamic>.from(json['unsigned'])
|
||||
: null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = _json ?? <String, dynamic>{};
|
||||
data['user_id'] = userId;
|
||||
data['usage'] = usage;
|
||||
data['keys'] = keys;
|
||||
|
||||
if (signatures != null) {
|
||||
data['signatures'] = signatures;
|
||||
}
|
||||
if (unsigned != null) {
|
||||
data['unsigned'] = unsigned;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -41,11 +41,6 @@ typedef RoomSorter = int Function(Room a, Room b);
|
|||
|
||||
enum LoginState { logged, loggedOut }
|
||||
|
||||
class GenericException implements Exception {
|
||||
final dynamic content;
|
||||
GenericException(this.content);
|
||||
}
|
||||
|
||||
/// Represents a Matrix client to communicate with a
|
||||
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
|
||||
/// SDK.
|
||||
|
@ -61,6 +56,8 @@ class Client {
|
|||
|
||||
Encryption encryption;
|
||||
|
||||
Set<KeyVerificationMethod> verificationMethods;
|
||||
|
||||
/// Create a client
|
||||
/// clientName = unique identifier of this client
|
||||
/// debug: Print debug output?
|
||||
|
@ -73,7 +70,9 @@ class Client {
|
|||
{this.debug = false,
|
||||
this.database,
|
||||
this.enableE2eeRecovery = false,
|
||||
this.verificationMethods,
|
||||
http.Client httpClient}) {
|
||||
verificationMethods ??= <KeyVerificationMethod>{};
|
||||
api = MatrixApi(debug: debug, httpClient: httpClient);
|
||||
onLoginStateChanged.stream.listen((loginState) {
|
||||
if (debug) {
|
||||
|
@ -642,7 +641,7 @@ class Client {
|
|||
encryption?.pickledOlmAccount,
|
||||
);
|
||||
}
|
||||
_userDeviceKeys = await database.getUserDeviceKeys(id);
|
||||
_userDeviceKeys = await database.getUserDeviceKeys(this);
|
||||
_rooms = await database.getRoomList(this, onlyLeft: false);
|
||||
_sortRooms();
|
||||
accountData = await database.getAccountData(id);
|
||||
|
@ -813,9 +812,6 @@ class Client {
|
|||
if (encryptionEnabled) {
|
||||
await encryption.handleToDeviceEvent(toDeviceEvent);
|
||||
}
|
||||
if (toDeviceEvent.type.startsWith('m.secret.')) {
|
||||
ssss.handleToDeviceEvent(toDeviceEvent);
|
||||
}
|
||||
onToDeviceEvent.add(toDeviceEvent);
|
||||
}
|
||||
}
|
||||
|
@ -969,11 +965,8 @@ class Client {
|
|||
await database.storeEventUpdate(id, update);
|
||||
}
|
||||
_updateRoomsByEventUpdate(update);
|
||||
if (event['type'].startsWith('m.key.verification.') ||
|
||||
(event['type'] == 'm.room.message' &&
|
||||
(event['content']['msgtype'] is String) &&
|
||||
event['content']['msgtype'].startsWith('m.key.verification.'))) {
|
||||
_handleRoomKeyVerificationRequest(update);
|
||||
if (encryptionEnabled) {
|
||||
await encryption.handleEventUpdate(update);
|
||||
}
|
||||
onEvent.add(update);
|
||||
|
||||
|
@ -1167,14 +1160,21 @@ class Client {
|
|||
final deviceId = rawDeviceKeyEntry.key;
|
||||
|
||||
// Set the new device key for this device
|
||||
|
||||
if (!oldKeys.containsKey(deviceId)) {
|
||||
final entry =
|
||||
DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value);
|
||||
if (entry.isValid) {
|
||||
final entry = DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value, this);
|
||||
if (entry.isValid) {
|
||||
// is this a new key or the same one as an old one?
|
||||
// better store an update - the signatures might have changed!
|
||||
if (!oldKeys.containsKey(deviceId) ||
|
||||
oldKeys[deviceId].ed25519Key == entry.ed25519Key) {
|
||||
if (oldKeys.containsKey(deviceId)) {
|
||||
// be sure to save the verified status
|
||||
entry.setDirectVerified(oldKeys[deviceId].directVerified);
|
||||
entry.blocked = oldKeys[deviceId].blocked;
|
||||
entry.validSignatures = oldKeys[deviceId].validSignatures;
|
||||
}
|
||||
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
|
||||
if (deviceId == deviceID &&
|
||||
entry.ed25519Key == encryption?.fingerprintKey) {
|
||||
entry.ed25519Key == fingerprintKey) {
|
||||
// Always trust the own device
|
||||
entry.setDirectVerified(true);
|
||||
}
|
||||
|
@ -1185,16 +1185,16 @@ class Client {
|
|||
_userDeviceKeys[userId].deviceKeys[deviceId] =
|
||||
oldKeys[deviceId];
|
||||
}
|
||||
if (database != null) {
|
||||
dbActions.add(() => database.storeUserDeviceKey(
|
||||
id,
|
||||
userId,
|
||||
deviceId,
|
||||
json.encode(entry.toJson()),
|
||||
entry.directVerified,
|
||||
entry.blocked,
|
||||
));
|
||||
}
|
||||
}
|
||||
if (database != null) {
|
||||
dbActions.add(() => database.storeUserDeviceKey(
|
||||
id,
|
||||
userId,
|
||||
deviceId,
|
||||
json.encode(entry.toJson()),
|
||||
entry.directVerified,
|
||||
entry.blocked,
|
||||
));
|
||||
}
|
||||
}
|
||||
// delete old/unused entries
|
||||
|
@ -1215,29 +1215,33 @@ class Client {
|
|||
}
|
||||
}
|
||||
// next we parse and persist the cross signing keys
|
||||
for (final keyType in [
|
||||
'master_keys',
|
||||
'self_signing_keys',
|
||||
'user_signing_keys'
|
||||
]) {
|
||||
if (!(response[keyType] is Map)) {
|
||||
final crossSigningTypes = {
|
||||
'master': response.masterKeys,
|
||||
'self_signing': response.selfSigningKeys,
|
||||
'user_signing': response.userSigningKeys,
|
||||
};
|
||||
for (final crossSigningKeysEntry in crossSigningTypes.entries) {
|
||||
final keyType = crossSigningKeysEntry.key;
|
||||
final keys = crossSigningKeysEntry.value;
|
||||
if (keys == null) {
|
||||
continue;
|
||||
}
|
||||
for (final rawDeviceKeyListEntry in response[keyType].entries) {
|
||||
final String userId = rawDeviceKeyListEntry.key;
|
||||
final oldKeys = Map<String, CrossSigningKey>.from(
|
||||
_userDeviceKeys[userId].crossSigningKeys);
|
||||
for (final crossSigningKeyListEntry in keys.entries) {
|
||||
final userId = crossSigningKeyListEntry.key;
|
||||
if (!userDeviceKeys.containsKey(userId)) {
|
||||
_userDeviceKeys[userId] = DeviceKeysList(userId);
|
||||
}
|
||||
final oldKeys = Map<String, CrossSigningKey>.from(_userDeviceKeys[userId].crossSigningKeys);
|
||||
_userDeviceKeys[userId].crossSigningKeys = {};
|
||||
// add the types we arne't handling atm back
|
||||
// add the types we aren't handling atm back
|
||||
for (final oldEntry in oldKeys.entries) {
|
||||
if (!oldEntry.value.usage.contains(
|
||||
keyType.substring(0, keyType.length - '_keys'.length))) {
|
||||
if (!oldEntry.value.usage.contains(keyType)) {
|
||||
_userDeviceKeys[userId].crossSigningKeys[oldEntry.key] =
|
||||
oldEntry.value;
|
||||
}
|
||||
}
|
||||
final entry =
|
||||
CrossSigningKey.fromJson(rawDeviceKeyListEntry.value, this);
|
||||
CrossSigningKey.fromMatrixCrossSigningKey(crossSigningKeyListEntry.value, this);
|
||||
if (entry.isValid) {
|
||||
final publicKey = entry.publicKey;
|
||||
if (!oldKeys.containsKey(publicKey) ||
|
||||
|
@ -1267,19 +1271,6 @@ class Client {
|
|||
));
|
||||
}
|
||||
}
|
||||
// delete old/unused entries
|
||||
if (database != null) {
|
||||
for (final oldCrossSigningKeyEntry in oldKeys.entries) {
|
||||
final publicKey = oldCrossSigningKeyEntry.key;
|
||||
if (!_userDeviceKeys[userId]
|
||||
.crossSigningKeys
|
||||
.containsKey(publicKey)) {
|
||||
// we need to remove an old key
|
||||
dbActions.add(() => database.removeUserCrossSigningKey(
|
||||
id, userId, publicKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
_userDeviceKeys[userId].outdated = false;
|
||||
if (database != null) {
|
||||
dbActions.add(
|
||||
|
|
|
@ -1,349 +0,0 @@
|
|||
import 'dart:core';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import 'client.dart';
|
||||
import 'room.dart';
|
||||
import 'utils/to_device_event.dart';
|
||||
import 'utils/device_keys_list.dart';
|
||||
|
||||
const MEGOLM_KEY = 'm.megolm_backup.v1';
|
||||
|
||||
class KeyManager {
|
||||
final Client client;
|
||||
final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
||||
final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
||||
|
||||
KeyManager(this.client) {
|
||||
client.ssss.setValidator(MEGOLM_KEY, (String secret) async {
|
||||
final keyObj = olm.PkDecryption();
|
||||
try {
|
||||
final info = await getRoomKeysInfo();
|
||||
return keyObj.init_with_private_key(base64.decode(secret)) ==
|
||||
info['auth_data']['public_key'];
|
||||
} catch (_) {
|
||||
return false;
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool get enabled => client.accountData[MEGOLM_KEY] != null;
|
||||
|
||||
Future<Map<String, dynamic>> getRoomKeysInfo() async {
|
||||
return await client.jsonRequest(
|
||||
type: HTTPType.GET,
|
||||
action: '/client/r0/room_keys/version',
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> isCached() async {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
return (await client.ssss.getCached(MEGOLM_KEY)) != null;
|
||||
}
|
||||
|
||||
Future<void> loadFromResponse(Map<String, dynamic> payload) async {
|
||||
if (!(await isCached())) {
|
||||
return;
|
||||
}
|
||||
if (!(payload['rooms'] is Map)) {
|
||||
return;
|
||||
}
|
||||
final privateKey = base64.decode(await client.ssss.getCached(MEGOLM_KEY));
|
||||
final decryption = olm.PkDecryption();
|
||||
final info = await getRoomKeysInfo();
|
||||
String backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privateKey);
|
||||
|
||||
if (backupPubKey == null ||
|
||||
!info.containsKey('auth_data') ||
|
||||
!(info['auth_data'] is Map) ||
|
||||
info['auth_data']['public_key'] != backupPubKey) {
|
||||
return;
|
||||
}
|
||||
for (final roomEntries in payload['rooms'].entries) {
|
||||
final roomId = roomEntries.key;
|
||||
if (!(roomEntries.value is Map) ||
|
||||
!(roomEntries.value['sessions'] is Map)) {
|
||||
continue;
|
||||
}
|
||||
for (final sessionEntries in roomEntries.value['sessions'].entries) {
|
||||
final sessionId = sessionEntries.key;
|
||||
final rawEncryptedSession = sessionEntries.value;
|
||||
if (!(rawEncryptedSession is Map)) {
|
||||
continue;
|
||||
}
|
||||
final firstMessageIndex =
|
||||
rawEncryptedSession['first_message_index'] is int
|
||||
? rawEncryptedSession['first_message_index']
|
||||
: null;
|
||||
final forwardedCount = rawEncryptedSession['forwarded_count'] is int
|
||||
? rawEncryptedSession['forwarded_count']
|
||||
: null;
|
||||
final isVerified = rawEncryptedSession['is_verified'] is bool
|
||||
? rawEncryptedSession['is_verified']
|
||||
: null;
|
||||
final sessionData = rawEncryptedSession['session_data'];
|
||||
if (firstMessageIndex == null ||
|
||||
forwardedCount == null ||
|
||||
isVerified == null ||
|
||||
!(sessionData is Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, dynamic> decrypted;
|
||||
try {
|
||||
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
|
||||
sessionData['mac'], sessionData['ciphertext']));
|
||||
} catch (err) {
|
||||
print('[LibOlm] Error decrypting room key: ' + err.toString());
|
||||
}
|
||||
if (decrypted != null) {
|
||||
decrypted['session_id'] = sessionId;
|
||||
decrypted['room_id'] = roomId;
|
||||
final room =
|
||||
client.getRoomById(roomId) ?? Room(id: roomId, client: client);
|
||||
room.setInboundGroupSession(sessionId, decrypted, forwarded: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadSingleKey(String roomId, String sessionId) async {
|
||||
final info = await getRoomKeysInfo();
|
||||
final ret = await client.jsonRequest(
|
||||
type: HTTPType.GET,
|
||||
action:
|
||||
'/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}',
|
||||
);
|
||||
await loadFromResponse({
|
||||
'rooms': {
|
||||
roomId: {
|
||||
'sessions': {
|
||||
sessionId: ret,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// Request a certain key from another device
|
||||
Future<void> request(Room room, String sessionId, String senderKey) async {
|
||||
// let's first check our online key backup store thingy...
|
||||
var hadPreviously = room.inboundGroupSessions.containsKey(sessionId);
|
||||
try {
|
||||
await loadSingleKey(room.id, sessionId);
|
||||
} catch (err, stacktrace) {
|
||||
print('++++++++++++++++++');
|
||||
print(err.toString());
|
||||
print(stacktrace);
|
||||
}
|
||||
if (!hadPreviously && room.inboundGroupSessions.containsKey(sessionId)) {
|
||||
return; // we managed to load the session from online backup, no need to care about it now
|
||||
}
|
||||
// while we just send the to-device event to '*', we still need to save the
|
||||
// devices themself to know where to send the cancel to after receiving a reply
|
||||
final devices = await room.getUserDeviceKeys();
|
||||
final requestId = client.generateUniqueTransactionId();
|
||||
final request = KeyManagerKeyShareRequest(
|
||||
requestId: requestId,
|
||||
devices: devices,
|
||||
room: room,
|
||||
sessionId: sessionId,
|
||||
senderKey: senderKey,
|
||||
);
|
||||
await client.sendToDevice(
|
||||
[],
|
||||
'm.room_key_request',
|
||||
{
|
||||
'action': 'request',
|
||||
'body': {
|
||||
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||
'room_id': room.id,
|
||||
'sender_key': senderKey,
|
||||
'session_id': sessionId,
|
||||
},
|
||||
'request_id': requestId,
|
||||
'requesting_device_id': client.deviceID,
|
||||
},
|
||||
encrypted: false,
|
||||
toUsers: await room.requestParticipants());
|
||||
outgoingShareRequests[request.requestId] = request;
|
||||
}
|
||||
|
||||
/// Handle an incoming to_device event that is related to key sharing
|
||||
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||
if (event.type == 'm.room_key_request') {
|
||||
if (!event.content.containsKey('request_id')) {
|
||||
return; // invalid event
|
||||
}
|
||||
if (event.content['action'] == 'request') {
|
||||
// we are *receiving* a request
|
||||
if (!event.content.containsKey('body')) {
|
||||
return; // no body
|
||||
}
|
||||
if (!client.userDeviceKeys.containsKey(event.sender) ||
|
||||
!client.userDeviceKeys[event.sender].deviceKeys
|
||||
.containsKey(event.content['requesting_device_id'])) {
|
||||
return; // device not found
|
||||
}
|
||||
final device = client.userDeviceKeys[event.sender]
|
||||
.deviceKeys[event.content['requesting_device_id']];
|
||||
if (device.userId == client.userID &&
|
||||
device.deviceId == client.deviceID) {
|
||||
return; // ignore requests by ourself
|
||||
}
|
||||
final room = client.getRoomById(event.content['body']['room_id']);
|
||||
if (room == null) {
|
||||
return; // unknown room
|
||||
}
|
||||
final sessionId = event.content['body']['session_id'];
|
||||
// okay, let's see if we have this session at all
|
||||
await room.loadInboundGroupSessionKey(sessionId);
|
||||
if (!room.inboundGroupSessions.containsKey(sessionId)) {
|
||||
return; // we don't have this session anyways
|
||||
}
|
||||
final request = KeyManagerKeyShareRequest(
|
||||
requestId: event.content['request_id'],
|
||||
devices: [device],
|
||||
room: room,
|
||||
sessionId: event.content['body']['session_id'],
|
||||
senderKey: event.content['body']['sender_key'],
|
||||
);
|
||||
if (incomingShareRequests.containsKey(request.requestId)) {
|
||||
return; // we don't want to process one and the same request multiple times
|
||||
}
|
||||
incomingShareRequests[request.requestId] = request;
|
||||
final roomKeyRequest =
|
||||
RoomKeyRequest.fromToDeviceEvent(event, this, request);
|
||||
if (device.userId == client.userID &&
|
||||
device.verified &&
|
||||
!device.blocked) {
|
||||
// alright, we can forward the key
|
||||
await roomKeyRequest.forwardKey();
|
||||
} else {
|
||||
client.onRoomKeyRequest
|
||||
.add(roomKeyRequest); // let the client handle this
|
||||
}
|
||||
} else if (event.content['action'] == 'request_cancellation') {
|
||||
// we got told to cancel an incoming request
|
||||
if (!incomingShareRequests.containsKey(event.content['request_id'])) {
|
||||
return; // we don't know this request anyways
|
||||
}
|
||||
// alright, let's just cancel this request
|
||||
final request = incomingShareRequests[event.content['request_id']];
|
||||
request.canceled = true;
|
||||
incomingShareRequests.remove(request.requestId);
|
||||
}
|
||||
} else if (event.type == 'm.forwarded_room_key') {
|
||||
// we *received* an incoming key request
|
||||
if (event.encryptedContent == null) {
|
||||
return; // event wasn't encrypted, this is a security risk
|
||||
}
|
||||
final request = outgoingShareRequests.values.firstWhere(
|
||||
(r) =>
|
||||
r.room.id == event.content['room_id'] &&
|
||||
r.sessionId == event.content['session_id'] &&
|
||||
r.senderKey == event.content['sender_key'],
|
||||
orElse: () => null);
|
||||
if (request == null || request.canceled) {
|
||||
return; // no associated request found or it got canceled
|
||||
}
|
||||
final device = request.devices.firstWhere(
|
||||
(d) =>
|
||||
d.userId == event.sender &&
|
||||
d.curve25519Key == event.encryptedContent['sender_key'],
|
||||
orElse: () => null);
|
||||
if (device == null) {
|
||||
return; // someone we didn't send our request to replied....better ignore this
|
||||
}
|
||||
// TODO: verify that the keys work to decrypt a message
|
||||
// alright, all checks out, let's go ahead and store this session
|
||||
request.room.setInboundGroupSession(request.sessionId, event.content,
|
||||
forwarded: true);
|
||||
request.devices.removeWhere(
|
||||
(k) => k.userId == device.userId && k.deviceId == device.deviceId);
|
||||
outgoingShareRequests.remove(request.requestId);
|
||||
// send cancel to all other devices
|
||||
if (request.devices.isEmpty) {
|
||||
return; // no need to send any cancellation
|
||||
}
|
||||
await client.sendToDevice(
|
||||
request.devices,
|
||||
'm.room_key_request',
|
||||
{
|
||||
'action': 'request_cancellation',
|
||||
'request_id': request.requestId,
|
||||
'requesting_device_id': client.deviceID,
|
||||
},
|
||||
encrypted: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KeyManagerKeyShareRequest {
|
||||
final String requestId;
|
||||
final List<DeviceKeys> devices;
|
||||
final Room room;
|
||||
final String sessionId;
|
||||
final String senderKey;
|
||||
bool canceled;
|
||||
|
||||
KeyManagerKeyShareRequest(
|
||||
{this.requestId,
|
||||
this.devices,
|
||||
this.room,
|
||||
this.sessionId,
|
||||
this.senderKey,
|
||||
this.canceled = false});
|
||||
}
|
||||
|
||||
class RoomKeyRequest extends ToDeviceEvent {
|
||||
KeyManager keyManager;
|
||||
KeyManagerKeyShareRequest request;
|
||||
RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent,
|
||||
KeyManager keyManager, KeyManagerKeyShareRequest request) {
|
||||
this.keyManager = keyManager;
|
||||
this.request = request;
|
||||
sender = toDeviceEvent.sender;
|
||||
content = toDeviceEvent.content;
|
||||
type = toDeviceEvent.type;
|
||||
}
|
||||
|
||||
Room get room => request.room;
|
||||
|
||||
DeviceKeys get requestingDevice => request.devices.first;
|
||||
|
||||
Future<void> forwardKey() async {
|
||||
if (request.canceled) {
|
||||
keyManager.incomingShareRequests.remove(request.requestId);
|
||||
return; // request is canceled, don't send anything
|
||||
}
|
||||
var room = this.room;
|
||||
await room.loadInboundGroupSessionKey(request.sessionId);
|
||||
final session = room.inboundGroupSessions[request.sessionId];
|
||||
var forwardedKeys = <dynamic>[keyManager.client.identityKey];
|
||||
for (final key in session.forwardingCurve25519KeyChain) {
|
||||
forwardedKeys.add(key);
|
||||
}
|
||||
var message = session.content;
|
||||
message['forwarding_curve25519_key_chain'] = forwardedKeys;
|
||||
|
||||
message['session_key'] = session.inboundGroupSession
|
||||
.export_session(session.inboundGroupSession.first_known_index());
|
||||
// send the actual reply of the key back to the requester
|
||||
await keyManager.client.sendToDevice(
|
||||
[requestingDevice],
|
||||
'm.forwarded_room_key',
|
||||
message,
|
||||
);
|
||||
keyManager.incomingShareRequests.remove(request.requestId);
|
||||
}
|
||||
}
|
|
@ -60,7 +60,7 @@ class DeviceKeysList {
|
|||
throw 'Unable to start new room';
|
||||
}
|
||||
final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client);
|
||||
final request = KeyVerification(client: client, room: room, userId: userId);
|
||||
final request = KeyVerification(encryption: client.encryption, room: room, userId: userId);
|
||||
await request.start();
|
||||
// no need to add to the request client object. As we are doing a room
|
||||
// verification request that'll happen automatically once we know the transaction id
|
||||
|
@ -94,34 +94,6 @@ class DeviceKeysList {
|
|||
}
|
||||
}
|
||||
|
||||
DeviceKeysList.fromJson(Map<String, dynamic> json, Client cl) {
|
||||
client = cl;
|
||||
userId = json['user_id'];
|
||||
outdated = json['outdated'];
|
||||
deviceKeys = {};
|
||||
for (final rawDeviceKeyEntry in json['device_keys'].entries) {
|
||||
deviceKeys[rawDeviceKeyEntry.key] =
|
||||
DeviceKeys.fromJson(rawDeviceKeyEntry.value, client);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
var map = <String, dynamic>{};
|
||||
final data = map;
|
||||
data['user_id'] = userId;
|
||||
data['outdated'] = outdated ?? true;
|
||||
|
||||
var rawDeviceKeys = <String, dynamic>{};
|
||||
for (final deviceKeyEntry in deviceKeys.entries) {
|
||||
rawDeviceKeys[deviceKeyEntry.key] = deviceKeyEntry.value.toJson();
|
||||
}
|
||||
data['device_keys'] = rawDeviceKeys;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => json.encode(toJson());
|
||||
|
||||
DeviceKeysList(this.userId);
|
||||
}
|
||||
|
||||
|
@ -137,7 +109,6 @@ abstract class SignedKey {
|
|||
bool blocked;
|
||||
|
||||
String get ed25519Key => keys['ed25519:$identifier'];
|
||||
|
||||
bool get verified => (directVerified || crossVerified) && !blocked;
|
||||
|
||||
void setDirectVerified(bool v) {
|
||||
|
@ -145,9 +116,7 @@ abstract class SignedKey {
|
|||
}
|
||||
|
||||
bool get directVerified => _verified;
|
||||
|
||||
bool get crossVerified => hasValidSignatureChain();
|
||||
|
||||
bool get signed => hasValidSignatureChain(verifiedOnly: false);
|
||||
|
||||
String get signingContent {
|
||||
|
@ -255,9 +224,9 @@ abstract class SignedKey {
|
|||
|
||||
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
||||
_verified = newVerified;
|
||||
if (sign && client.crossSigning.signable([this])) {
|
||||
if (sign && client.encryptionEnabled && client.encryption.crossSigning.signable([this])) {
|
||||
// sign the key!
|
||||
client.crossSigning.sign([this]);
|
||||
client.encryption.crossSigning.sign([this]);
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
|
@ -297,6 +266,20 @@ class CrossSigningKey extends SignedKey {
|
|||
newBlocked, client.id, userId, publicKey);
|
||||
}
|
||||
|
||||
CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl) {
|
||||
client = cl;
|
||||
content = Map<String, dynamic>.from(k.toJson());
|
||||
userId = k.userId;
|
||||
identifier = k.publicKey;
|
||||
usage = content['usage'].cast<String>();
|
||||
keys = content['keys'] != null ? Map<String, String>.from(content['keys']) : null;
|
||||
signatures = content['signatures'] != null
|
||||
? Map<String, dynamic>.from(content['signatures'])
|
||||
: null;
|
||||
_verified = false;
|
||||
blocked = false;
|
||||
}
|
||||
|
||||
CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl) {
|
||||
client = cl;
|
||||
final json = Event.getMapFromPayload(dbEntry.content);
|
||||
|
@ -346,17 +329,36 @@ class DeviceKeys extends SignedKey {
|
|||
@override
|
||||
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
||||
super.setVerified(newVerified, sign);
|
||||
return client.database
|
||||
return client?.database
|
||||
?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBlocked(bool newBlocked) {
|
||||
blocked = newBlocked;
|
||||
return client.database
|
||||
return client?.database
|
||||
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
|
||||
}
|
||||
|
||||
DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl) {
|
||||
client = cl;
|
||||
content = Map<String, dynamic>.from(k.toJson());
|
||||
userId = k.userId;
|
||||
identifier = k.deviceId;
|
||||
algorithms = content['algorithms'].cast<String>();
|
||||
keys = content['keys'] != null
|
||||
? Map<String, String>.from(content['keys'])
|
||||
: null;
|
||||
signatures = content['signatures'] != null
|
||||
? Map<String, dynamic>.from(content['signatures'])
|
||||
: null;
|
||||
unsigned = content['unsigned'] != null
|
||||
? Map<String, dynamic>.from(content['unsigned'])
|
||||
: null;
|
||||
_verified = false;
|
||||
blocked = false;
|
||||
}
|
||||
|
||||
DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl) {
|
||||
client = cl;
|
||||
final json = Event.getMapFromPayload(dbEntry.content);
|
||||
|
@ -365,45 +367,10 @@ class DeviceKeys extends SignedKey {
|
|||
identifier = dbEntry.deviceId;
|
||||
algorithms = content['algorithms'].cast<String>();
|
||||
keys = content['keys'] != null
|
||||
}) : super(userId, deviceId, algorithms, keys, signatures,
|
||||
unsigned: unsigned);
|
||||
|
||||
DeviceKeys({
|
||||
String userId,
|
||||
String deviceId,
|
||||
List<String> algorithms,
|
||||
Map<String, String> keys,
|
||||
Map<String, Map<String, String>> signatures,
|
||||
Map<String, dynamic> unsigned,
|
||||
this.verified,
|
||||
this.blocked,
|
||||
}) : super(userId, deviceId, algorithms, keys, signatures,
|
||||
unsigned: unsigned);
|
||||
|
||||
factory DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys matrixDeviceKeys) =>
|
||||
DeviceKeys(
|
||||
userId: matrixDeviceKeys.userId,
|
||||
deviceId: matrixDeviceKeys.deviceId,
|
||||
algorithms: matrixDeviceKeys.algorithms,
|
||||
keys: matrixDeviceKeys.keys,
|
||||
signatures: matrixDeviceKeys.signatures,
|
||||
unsigned: matrixDeviceKeys.unsigned,
|
||||
verified: false,
|
||||
blocked: false,
|
||||
);
|
||||
|
||||
static DeviceKeys fromDb(DbUserDeviceKeysKey dbEntry) {
|
||||
var deviceKeys = DeviceKeys();
|
||||
final content = Event.getMapFromPayload(dbEntry.content);
|
||||
deviceKeys.userId = dbEntry.userId;
|
||||
deviceKeys.deviceId = dbEntry.deviceId;
|
||||
deviceKeys.algorithms = content['algorithms'].cast<String>();
|
||||
deviceKeys.keys = content['keys'] != null
|
||||
? Map<String, String>.from(content['keys'])
|
||||
: null;
|
||||
deviceKeys.signatures = content['signatures'] != null
|
||||
? Map<String, Map<String, String>>.from((content['signatures'] as Map)
|
||||
.map((k, v) => MapEntry(k, Map<String, String>.from(v))))
|
||||
signatures = content['signatures'] != null
|
||||
? Map<String, dynamic>.from(content['signatures'])
|
||||
: null;
|
||||
unsigned = json['unsigned'] != null
|
||||
? Map<String, dynamic>.from(json['unsigned'])
|
||||
|
@ -431,7 +398,7 @@ class DeviceKeys extends SignedKey {
|
|||
|
||||
KeyVerification startVerification() {
|
||||
final request =
|
||||
KeyVerification(client: client, userId: userId, deviceId: deviceId);
|
||||
KeyVerification(encryption: client.encryption, userId: userId, deviceId: deviceId);
|
||||
|
||||
request.start();
|
||||
client.encryption.keyVerificationManager.addRequest(request);
|
||||
|
|
|
@ -388,7 +388,7 @@ void main() {
|
|||
'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA'
|
||||
}
|
||||
}
|
||||
});
|
||||
}, matrix);
|
||||
test('sendToDevice', () async {
|
||||
await matrix.sendToDevice(
|
||||
[deviceKeys],
|
||||
|
|
|
@ -41,8 +41,6 @@ void main() {
|
|||
}
|
||||
},
|
||||
'unsigned': {'device_display_name': "Alice's mobile phone"},
|
||||
'verified': false,
|
||||
'blocked': true,
|
||||
};
|
||||
var rawListJson = <String, dynamic>{
|
||||
'user_id': '@alice:example.com',
|
||||
|
@ -50,28 +48,12 @@ void main() {
|
|||
'device_keys': {'JLAFKJWSCS': rawJson},
|
||||
};
|
||||
|
||||
var userDeviceKeys = <String, DeviceKeysList>{
|
||||
'@alice:example.com': DeviceKeysList.fromJson(rawListJson, null),
|
||||
};
|
||||
var userDeviceKeyRaw = <String, dynamic>{
|
||||
'@alice:example.com': rawListJson,
|
||||
};
|
||||
|
||||
final key = DeviceKeys.fromJson(rawJson, null);
|
||||
rawJson.remove('verified');
|
||||
rawJson.remove('blocked');
|
||||
key.setVerified(false, false);
|
||||
key.setBlocked(true);
|
||||
expect(json.encode(key.toJson()), json.encode(rawJson));
|
||||
expect(key.verified, false);
|
||||
expect(key.directVerified, false);
|
||||
expect(key.blocked, true);
|
||||
expect(json.encode(DeviceKeysList.fromJson(rawListJson, null).toJson()),
|
||||
json.encode(rawListJson));
|
||||
|
||||
var mapFromRaw = <String, DeviceKeysList>{};
|
||||
for (final rawListEntry in userDeviceKeyRaw.entries) {
|
||||
mapFromRaw[rawListEntry.key] =
|
||||
DeviceKeysList.fromJson(rawListEntry.value, null);
|
||||
}
|
||||
expect(mapFromRaw.toString(), userDeviceKeys.toString());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -61,17 +61,15 @@ void main() {
|
|||
);
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
device = DeviceKeys(
|
||||
userId: client.userID,
|
||||
deviceId: client.deviceID,
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
keys: {
|
||||
device = DeviceKeys.fromJson({
|
||||
'user_id': client.userID,
|
||||
'device_id': client.deviceID,
|
||||
'algorithms': ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
'keys': {
|
||||
'curve25519:${client.deviceID}': client.identityKey,
|
||||
'ed25519:${client.deviceID}': client.fingerprintKey,
|
||||
},
|
||||
verified: true,
|
||||
blocked: false,
|
||||
);
|
||||
}, client);
|
||||
});
|
||||
|
||||
test('encryptToDeviceMessage', () async {
|
||||
|
|
|
@ -85,10 +85,10 @@ void main() {
|
|||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await matrix
|
||||
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
|
||||
.setBlocked(false, matrix);
|
||||
.setBlocked(false);
|
||||
await matrix
|
||||
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
|
||||
.setVerified(true, matrix);
|
||||
.setVerified(true);
|
||||
// test a successful share
|
||||
var event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
|
|
Loading…
Reference in a new issue