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:typed_data';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
|
||||||
import 'client.dart';
|
import 'encryption.dart';
|
||||||
import 'utils/device_keys_list.dart';
|
|
||||||
|
|
||||||
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
|
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
|
||||||
const USER_SIGNING_KEY = 'm.cross_signing.user_signing';
|
const USER_SIGNING_KEY = 'm.cross_signing.user_signing';
|
||||||
const MASTER_KEY = 'm.cross_signing.master';
|
const MASTER_KEY = 'm.cross_signing.master';
|
||||||
|
|
||||||
class CrossSigning {
|
class CrossSigning {
|
||||||
final Client client;
|
final Encryption encryption;
|
||||||
CrossSigning(this.client) {
|
Client get client => encryption.client;
|
||||||
client.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async {
|
CrossSigning(this.encryption) {
|
||||||
|
encryption.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async {
|
||||||
final keyObj = olm.PkSigning();
|
final keyObj = olm.PkSigning();
|
||||||
try {
|
try {
|
||||||
return keyObj.init_with_seed(base64.decode(secret)) ==
|
return keyObj.init_with_seed(base64.decode(secret)) ==
|
||||||
|
@ -24,7 +43,7 @@ class CrossSigning {
|
||||||
keyObj.free();
|
keyObj.free();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
client.ssss.setValidator(USER_SIGNING_KEY, (String secret) async {
|
encryption.ssss.setValidator(USER_SIGNING_KEY, (String secret) async {
|
||||||
final keyObj = olm.PkSigning();
|
final keyObj = olm.PkSigning();
|
||||||
try {
|
try {
|
||||||
return keyObj.init_with_seed(base64.decode(secret)) ==
|
return keyObj.init_with_seed(base64.decode(secret)) ==
|
||||||
|
@ -46,12 +65,12 @@ class CrossSigning {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (await client.ssss.getCached(SELF_SIGNING_KEY)) != null &&
|
return (await encryption.ssss.getCached(SELF_SIGNING_KEY)) != null &&
|
||||||
(await client.ssss.getCached(USER_SIGNING_KEY)) != null;
|
(await encryption.ssss.getCached(USER_SIGNING_KEY)) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selfSign({String password, String recoveryKey}) async {
|
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.unlock(password: password, recoveryKey: recoveryKey);
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
final masterPrivateKey = base64.decode(await handle.getStored(MASTER_KEY));
|
final masterPrivateKey = base64.decode(await handle.getStored(MASTER_KEY));
|
||||||
|
@ -133,7 +152,7 @@ class CrossSigning {
|
||||||
if (key is CrossSigningKey) {
|
if (key is CrossSigningKey) {
|
||||||
if (key.usage.contains('master')) {
|
if (key.usage.contains('master')) {
|
||||||
// okay, we'll sign our own master key
|
// okay, we'll sign our own master key
|
||||||
final signature = client.signString(key.signingContent);
|
final signature = encryption.olmManager.signString(key.signingContent);
|
||||||
addSignature(
|
addSignature(
|
||||||
key,
|
key,
|
||||||
client
|
client
|
||||||
|
@ -144,8 +163,8 @@ class CrossSigning {
|
||||||
} else {
|
} else {
|
||||||
// okay, we'll sign a device key with our self signing key
|
// okay, we'll sign a device key with our self signing key
|
||||||
selfSigningKey ??= base64
|
selfSigningKey ??= base64
|
||||||
.decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? '');
|
.decode(await encryption.ssss.getCached(SELF_SIGNING_KEY) ?? '');
|
||||||
if (selfSigningKey != null) {
|
if (selfSigningKey.isNotEmpty) {
|
||||||
final signature = _sign(key.signingContent, selfSigningKey);
|
final signature = _sign(key.signingContent, selfSigningKey);
|
||||||
addSignature(key,
|
addSignature(key,
|
||||||
client.userDeviceKeys[client.userID].selfSigningKey, signature);
|
client.userDeviceKeys[client.userID].selfSigningKey, signature);
|
||||||
|
@ -154,8 +173,8 @@ class CrossSigning {
|
||||||
} else if (key is CrossSigningKey && key.usage.contains('master')) {
|
} else if (key is CrossSigningKey && key.usage.contains('master')) {
|
||||||
// we are signing someone elses master key
|
// we are signing someone elses master key
|
||||||
userSigningKey ??=
|
userSigningKey ??=
|
||||||
base64.decode(await client.ssss.getCached(USER_SIGNING_KEY) ?? '');
|
base64.decode(await encryption.ssss.getCached(USER_SIGNING_KEY) ?? '');
|
||||||
if (userSigningKey != null) {
|
if (userSigningKey.isNotEmpty) {
|
||||||
final signature = _sign(key.signingContent, userSigningKey);
|
final signature = _sign(key.signingContent, userSigningKey);
|
||||||
addSignature(key, client.userDeviceKeys[client.userID].userSigningKey,
|
addSignature(key, client.userDeviceKeys[client.userID].userSigningKey,
|
||||||
signature);
|
signature);
|
||||||
|
@ -165,7 +184,7 @@ class CrossSigning {
|
||||||
if (signedKey) {
|
if (signedKey) {
|
||||||
// post our new keys!
|
// post our new keys!
|
||||||
await client.jsonRequest(
|
await client.jsonRequest(
|
||||||
type: HTTPType.POST,
|
type: RequestType.POST,
|
||||||
action: '/client/r0/keys/signatures/upload',
|
action: '/client/r0/keys/signatures/upload',
|
||||||
data: signatures,
|
data: signatures,
|
||||||
);
|
);
|
|
@ -24,6 +24,8 @@ import 'package:pedantic/pedantic.dart';
|
||||||
import 'key_manager.dart';
|
import 'key_manager.dart';
|
||||||
import 'olm_manager.dart';
|
import 'olm_manager.dart';
|
||||||
import 'key_verification_manager.dart';
|
import 'key_verification_manager.dart';
|
||||||
|
import 'cross_signing.dart';
|
||||||
|
import 'ssss.dart';
|
||||||
|
|
||||||
class Encryption {
|
class Encryption {
|
||||||
final Client client;
|
final Client client;
|
||||||
|
@ -42,15 +44,19 @@ class Encryption {
|
||||||
KeyManager keyManager;
|
KeyManager keyManager;
|
||||||
OlmManager olmManager;
|
OlmManager olmManager;
|
||||||
KeyVerificationManager keyVerificationManager;
|
KeyVerificationManager keyVerificationManager;
|
||||||
|
CrossSigning crossSigning;
|
||||||
|
SSSS ssss;
|
||||||
|
|
||||||
Encryption({
|
Encryption({
|
||||||
this.client,
|
this.client,
|
||||||
this.debug,
|
this.debug,
|
||||||
this.enableE2eeRecovery,
|
this.enableE2eeRecovery,
|
||||||
}) {
|
}) {
|
||||||
|
ssss = SSSS(this);
|
||||||
keyManager = KeyManager(this);
|
keyManager = KeyManager(this);
|
||||||
olmManager = OlmManager(this);
|
olmManager = OlmManager(this);
|
||||||
keyVerificationManager = KeyVerificationManager(this);
|
keyVerificationManager = KeyVerificationManager(this);
|
||||||
|
crossSigning = CrossSigning(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> init(String olmAccount) async {
|
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 {
|
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
|
||||||
return await olmManager.decryptToDeviceEvent(event);
|
return await olmManager.decryptToDeviceEvent(event);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ import './encryption.dart';
|
||||||
import './utils/session_key.dart';
|
import './utils/session_key.dart';
|
||||||
import './utils/outbound_group_session.dart';
|
import './utils/outbound_group_session.dart';
|
||||||
|
|
||||||
|
const MEGOLM_KEY = 'm.megolm_backup.v1';
|
||||||
|
|
||||||
class KeyManager {
|
class KeyManager {
|
||||||
final Encryption encryption;
|
final Encryption encryption;
|
||||||
Client get client => encryption.client;
|
Client get client => encryption.client;
|
||||||
|
@ -37,7 +39,22 @@ class KeyManager {
|
||||||
final Set<String> _loadedOutboundGroupSessions = <String>{};
|
final Set<String> _loadedOutboundGroupSessions = <String>{};
|
||||||
final Set<String> _requestedSessionIds = <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
|
/// clear all cached inbound group sessions. useful for testing
|
||||||
void clearInboundGroupSessions() {
|
void clearInboundGroupSessions() {
|
||||||
|
@ -283,8 +300,120 @@ class KeyManager {
|
||||||
_outboundGroupSessions[roomId] = sess;
|
_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
|
/// Request a certain key from another device
|
||||||
Future<void> request(Room room, String sessionId, String senderKey) async {
|
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
|
// 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
|
// devices themself to know where to send the cancel to after receiving a reply
|
||||||
final devices = await room.getUserDeviceKeys();
|
final devices = await room.getUserDeviceKeys();
|
||||||
|
@ -500,7 +629,6 @@ class RoomKeyRequest extends ToDeviceEvent {
|
||||||
for (final key in session.forwardingCurve25519KeyChain) {
|
for (final key in session.forwardingCurve25519KeyChain) {
|
||||||
forwardedKeys.add(key);
|
forwardedKeys.add(key);
|
||||||
}
|
}
|
||||||
await requestingDevice.setVerified(true, keyManager.client);
|
|
||||||
var message = session.content;
|
var message = session.content;
|
||||||
message['forwarding_curve25519_key_chain'] = forwardedKeys;
|
message['forwarding_curve25519_key_chain'] = forwardedKeys;
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ class KeyVerificationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||||
if (!event.type.startsWith('m.key.verification')) {
|
if (!event.type.startsWith('m.key.verification') || client.verificationMethods.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// we have key verification going on!
|
// 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() {
|
void dispose() {
|
||||||
for (final req in _requests.values) {
|
for (final req in _requests.values) {
|
||||||
req.dispose();
|
req.dispose();
|
||||||
|
|
|
@ -96,6 +96,10 @@ class OlmManager {
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String signString(String s) {
|
||||||
|
return _olmAccount.sign(s);
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks the signature of a signed json object.
|
/// Checks the signature of a signed json object.
|
||||||
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
|
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
|
||||||
String userId, String deviceId) {
|
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:typed_data';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
@ -5,11 +23,10 @@ import 'package:encrypt/encrypt.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:base58check/base58.dart';
|
import 'package:base58check/base58.dart';
|
||||||
import 'package:password_hash/password_hash.dart';
|
import 'package:password_hash/password_hash.dart';
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
|
|
||||||
import 'client.dart';
|
import 'encryption.dart';
|
||||||
import 'account_data.dart';
|
|
||||||
import 'utils/device_keys_list.dart';
|
|
||||||
import 'utils/to_device_event.dart';
|
|
||||||
|
|
||||||
const CACHE_TYPES = <String>[
|
const CACHE_TYPES = <String>[
|
||||||
'm.cross_signing.self_signing',
|
'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
|
const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm
|
||||||
|
|
||||||
class SSSS {
|
class SSSS {
|
||||||
final Client client;
|
final Encryption encryption;
|
||||||
|
Client get client => encryption.client;
|
||||||
final pendingShareRequests = <String, _ShareRequest>{};
|
final pendingShareRequests = <String, _ShareRequest>{};
|
||||||
final _validators = <String, Future<bool> Function(String)>{};
|
final _validators = <String, Future<bool> Function(String)>{};
|
||||||
SSSS(this.client);
|
SSSS(this.encryption);
|
||||||
|
|
||||||
static _DerivedKeys deriveKeys(Uint8List key, String name) {
|
static _DerivedKeys deriveKeys(Uint8List key, String name) {
|
||||||
final zerosalt = Uint8List(8);
|
final zerosalt = Uint8List(8);
|
||||||
|
@ -129,11 +147,11 @@ class SSSS {
|
||||||
return keyData.content['key'];
|
return keyData.content['key'];
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountData getKey(String keyId) {
|
BasicEvent getKey(String keyId) {
|
||||||
return client.accountData['m.secret_storage.key.${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;
|
final info = keyData.content;
|
||||||
if (info['algorithm'] == 'm.secret_storage.v1.aes-hmac-sha2') {
|
if (info['algorithm'] == 'm.secret_storage.v1.aes-hmac-sha2') {
|
||||||
if ((info['mac'] is String) && (info['iv'] is String)) {
|
if ((info['mac'] is String) && (info['iv'] is String)) {
|
||||||
|
@ -200,7 +218,7 @@ class SSSS {
|
||||||
};
|
};
|
||||||
// store the thing in your account data
|
// store the thing in your account data
|
||||||
await client.jsonRequest(
|
await client.jsonRequest(
|
||||||
type: HTTPType.PUT,
|
type: RequestType.PUT,
|
||||||
action: '/client/r0/user/${client.userID}/account_data/${type}',
|
action: '/client/r0/user/${client.userID}/account_data/${type}',
|
||||||
data: content,
|
data: content,
|
||||||
);
|
);
|
||||||
|
@ -421,7 +439,7 @@ class _PasswordInfo {
|
||||||
class OpenSSSS {
|
class OpenSSSS {
|
||||||
final SSSS ssss;
|
final SSSS ssss;
|
||||||
final String keyId;
|
final String keyId;
|
||||||
final AccountData keyData;
|
final BasicEvent keyData;
|
||||||
OpenSSSS({this.ssss, this.keyId, this.keyData});
|
OpenSSSS({this.ssss, this.keyId, this.keyData});
|
||||||
Uint8List privateKey;
|
Uint8List privateKey;
|
||||||
|
|
|
@ -184,8 +184,8 @@ class KeyVerification {
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
transactionId = client.generateUniqueTransactionId();
|
transactionId = client.generateUniqueTransactionId();
|
||||||
}
|
}
|
||||||
if (client.crossSigning.enabled &&
|
if (encryption.crossSigning.enabled &&
|
||||||
!(await client.crossSigning.isCached()) &&
|
!(await encryption.crossSigning.isCached()) &&
|
||||||
!client.isUnknownSession) {
|
!client.isUnknownSession) {
|
||||||
setState(KeyVerificationState.askSSSS);
|
setState(KeyVerificationState.askSSSS);
|
||||||
_nextAction = 'request';
|
_nextAction = 'request';
|
||||||
|
@ -241,7 +241,6 @@ class KeyVerification {
|
||||||
print('Setting device id start: ' + _deviceId.toString());
|
print('Setting device id start: ' + _deviceId.toString());
|
||||||
transactionId ??= eventId ?? payload['transaction_id'];
|
transactionId ??= eventId ?? payload['transaction_id'];
|
||||||
if (method != null) {
|
if (method != null) {
|
||||||
print('DUPLICATE START');
|
|
||||||
// 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
|
||||||
|
@ -250,10 +249,8 @@ class KeyVerification {
|
||||||
entries.sort();
|
entries.sort();
|
||||||
if (entries.first == ourEntry) {
|
if (entries.first == ourEntry) {
|
||||||
// our start won, nothing to do
|
// our start won, nothing to do
|
||||||
print('we won, nothing to do');
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
print('They won, handing off');
|
|
||||||
// the other start won, let's hand off
|
// the other start won, let's hand off
|
||||||
startedVerification = false; // it is now as if they started
|
startedVerification = false; // it is now as if they started
|
||||||
lastStep =
|
lastStep =
|
||||||
|
@ -324,7 +321,7 @@ class KeyVerification {
|
||||||
} else if (_nextAction == 'done') {
|
} else if (_nextAction == 'done') {
|
||||||
if (_verifiedDevices != null) {
|
if (_verifiedDevices != null) {
|
||||||
// and now let's sign them all in the background
|
// and now let's sign them all in the background
|
||||||
client.crossSigning.sign(_verifiedDevices);
|
encryption.crossSigning.sign(_verifiedDevices);
|
||||||
}
|
}
|
||||||
setState(KeyVerificationState.done);
|
setState(KeyVerificationState.done);
|
||||||
}
|
}
|
||||||
|
@ -333,7 +330,7 @@ class KeyVerification {
|
||||||
next();
|
next();
|
||||||
return;
|
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.unlock(password: password, recoveryKey: recoveryKey);
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
next();
|
next();
|
||||||
|
@ -437,18 +434,18 @@ class KeyVerification {
|
||||||
if (verifiedMasterKey && userId == client.userID) {
|
if (verifiedMasterKey && userId == client.userID) {
|
||||||
// it was our own master key, let's request the cross signing keys
|
// it was our own master key, let's request the cross signing keys
|
||||||
// we do it in the background, thus no await needed here
|
// we do it in the background, thus no await needed here
|
||||||
unawaited(client.ssss
|
unawaited(encryption.ssss
|
||||||
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()));
|
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()));
|
||||||
}
|
}
|
||||||
await send('m.key.verification.done', {});
|
await send('m.key.verification.done', {});
|
||||||
|
|
||||||
var askingSSSS = false;
|
var askingSSSS = false;
|
||||||
if (client.crossSigning.enabled &&
|
if (encryption.crossSigning.enabled &&
|
||||||
client.crossSigning.signable(_verifiedDevices)) {
|
encryption.crossSigning.signable(_verifiedDevices)) {
|
||||||
// these keys can be signed! Let's do so
|
// 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
|
// and now let's sign them all in the background
|
||||||
unawaited(client.crossSigning.sign(_verifiedDevices));
|
unawaited(encryption.crossSigning.sign(_verifiedDevices));
|
||||||
} else if (!wasUnknownSession) {
|
} else if (!wasUnknownSession) {
|
||||||
askingSSSS = true;
|
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/keys_query_response.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/login_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/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_device_keys.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/matrix_exception.dart';
|
export 'package:famedlysdk/matrix_api/model/matrix_exception.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/message_types.dart';
|
export 'package:famedlysdk/matrix_api/model/message_types.dart';
|
||||||
|
|
|
@ -17,10 +17,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'matrix_device_keys.dart';
|
import 'matrix_device_keys.dart';
|
||||||
|
import 'matrix_cross_signing_key.dart';
|
||||||
|
|
||||||
class KeysQueryResponse {
|
class KeysQueryResponse {
|
||||||
Map<String, dynamic> failures;
|
Map<String, dynamic> failures;
|
||||||
Map<String, Map<String, MatrixDeviceKeys>> deviceKeys;
|
Map<String, Map<String, MatrixDeviceKeys>> deviceKeys;
|
||||||
|
Map<String, MatrixCrossSigningKey> masterKeys;
|
||||||
|
Map<String, MatrixCrossSigningKey> selfSigningKeys;
|
||||||
|
Map<String, MatrixCrossSigningKey> userSigningKeys;
|
||||||
|
|
||||||
KeysQueryResponse.fromJson(Map<String, dynamic> json) {
|
KeysQueryResponse.fromJson(Map<String, dynamic> json) {
|
||||||
failures = Map<String, dynamic>.from(json['failures']);
|
failures = Map<String, dynamic>.from(json['failures']);
|
||||||
|
@ -37,6 +41,32 @@ class KeysQueryResponse {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null;
|
: 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() {
|
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;
|
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 }
|
enum LoginState { logged, loggedOut }
|
||||||
|
|
||||||
class GenericException implements Exception {
|
|
||||||
final dynamic content;
|
|
||||||
GenericException(this.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a Matrix client to communicate with a
|
/// Represents a Matrix client to communicate with a
|
||||||
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
|
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
|
||||||
/// SDK.
|
/// SDK.
|
||||||
|
@ -61,6 +56,8 @@ class Client {
|
||||||
|
|
||||||
Encryption encryption;
|
Encryption encryption;
|
||||||
|
|
||||||
|
Set<KeyVerificationMethod> verificationMethods;
|
||||||
|
|
||||||
/// Create a client
|
/// Create a client
|
||||||
/// clientName = unique identifier of this client
|
/// clientName = unique identifier of this client
|
||||||
/// debug: Print debug output?
|
/// debug: Print debug output?
|
||||||
|
@ -73,7 +70,9 @@ class Client {
|
||||||
{this.debug = false,
|
{this.debug = false,
|
||||||
this.database,
|
this.database,
|
||||||
this.enableE2eeRecovery = false,
|
this.enableE2eeRecovery = false,
|
||||||
|
this.verificationMethods,
|
||||||
http.Client httpClient}) {
|
http.Client httpClient}) {
|
||||||
|
verificationMethods ??= <KeyVerificationMethod>{};
|
||||||
api = MatrixApi(debug: debug, httpClient: httpClient);
|
api = MatrixApi(debug: debug, httpClient: httpClient);
|
||||||
onLoginStateChanged.stream.listen((loginState) {
|
onLoginStateChanged.stream.listen((loginState) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
|
@ -642,7 +641,7 @@ class Client {
|
||||||
encryption?.pickledOlmAccount,
|
encryption?.pickledOlmAccount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_userDeviceKeys = await database.getUserDeviceKeys(id);
|
_userDeviceKeys = await database.getUserDeviceKeys(this);
|
||||||
_rooms = await database.getRoomList(this, onlyLeft: false);
|
_rooms = await database.getRoomList(this, onlyLeft: false);
|
||||||
_sortRooms();
|
_sortRooms();
|
||||||
accountData = await database.getAccountData(id);
|
accountData = await database.getAccountData(id);
|
||||||
|
@ -813,9 +812,6 @@ class Client {
|
||||||
if (encryptionEnabled) {
|
if (encryptionEnabled) {
|
||||||
await encryption.handleToDeviceEvent(toDeviceEvent);
|
await encryption.handleToDeviceEvent(toDeviceEvent);
|
||||||
}
|
}
|
||||||
if (toDeviceEvent.type.startsWith('m.secret.')) {
|
|
||||||
ssss.handleToDeviceEvent(toDeviceEvent);
|
|
||||||
}
|
|
||||||
onToDeviceEvent.add(toDeviceEvent);
|
onToDeviceEvent.add(toDeviceEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -969,11 +965,8 @@ class Client {
|
||||||
await database.storeEventUpdate(id, update);
|
await database.storeEventUpdate(id, update);
|
||||||
}
|
}
|
||||||
_updateRoomsByEventUpdate(update);
|
_updateRoomsByEventUpdate(update);
|
||||||
if (event['type'].startsWith('m.key.verification.') ||
|
if (encryptionEnabled) {
|
||||||
(event['type'] == 'm.room.message' &&
|
await encryption.handleEventUpdate(update);
|
||||||
(event['content']['msgtype'] is String) &&
|
|
||||||
event['content']['msgtype'].startsWith('m.key.verification.'))) {
|
|
||||||
_handleRoomKeyVerificationRequest(update);
|
|
||||||
}
|
}
|
||||||
onEvent.add(update);
|
onEvent.add(update);
|
||||||
|
|
||||||
|
@ -1167,14 +1160,21 @@ class Client {
|
||||||
final deviceId = rawDeviceKeyEntry.key;
|
final deviceId = rawDeviceKeyEntry.key;
|
||||||
|
|
||||||
// Set the new device key for this device
|
// Set the new device key for this device
|
||||||
|
final entry = DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value, this);
|
||||||
if (!oldKeys.containsKey(deviceId)) {
|
if (entry.isValid) {
|
||||||
final entry =
|
// is this a new key or the same one as an old one?
|
||||||
DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value);
|
// better store an update - the signatures might have changed!
|
||||||
if (entry.isValid) {
|
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;
|
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
|
||||||
if (deviceId == deviceID &&
|
if (deviceId == deviceID &&
|
||||||
entry.ed25519Key == encryption?.fingerprintKey) {
|
entry.ed25519Key == fingerprintKey) {
|
||||||
// Always trust the own device
|
// Always trust the own device
|
||||||
entry.setDirectVerified(true);
|
entry.setDirectVerified(true);
|
||||||
}
|
}
|
||||||
|
@ -1185,16 +1185,16 @@ class Client {
|
||||||
_userDeviceKeys[userId].deviceKeys[deviceId] =
|
_userDeviceKeys[userId].deviceKeys[deviceId] =
|
||||||
oldKeys[deviceId];
|
oldKeys[deviceId];
|
||||||
}
|
}
|
||||||
if (database != null) {
|
}
|
||||||
dbActions.add(() => database.storeUserDeviceKey(
|
if (database != null) {
|
||||||
id,
|
dbActions.add(() => database.storeUserDeviceKey(
|
||||||
userId,
|
id,
|
||||||
deviceId,
|
userId,
|
||||||
json.encode(entry.toJson()),
|
deviceId,
|
||||||
entry.directVerified,
|
json.encode(entry.toJson()),
|
||||||
entry.blocked,
|
entry.directVerified,
|
||||||
));
|
entry.blocked,
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// delete old/unused entries
|
// delete old/unused entries
|
||||||
|
@ -1215,29 +1215,33 @@ class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// next we parse and persist the cross signing keys
|
// next we parse and persist the cross signing keys
|
||||||
for (final keyType in [
|
final crossSigningTypes = {
|
||||||
'master_keys',
|
'master': response.masterKeys,
|
||||||
'self_signing_keys',
|
'self_signing': response.selfSigningKeys,
|
||||||
'user_signing_keys'
|
'user_signing': response.userSigningKeys,
|
||||||
]) {
|
};
|
||||||
if (!(response[keyType] is Map)) {
|
for (final crossSigningKeysEntry in crossSigningTypes.entries) {
|
||||||
|
final keyType = crossSigningKeysEntry.key;
|
||||||
|
final keys = crossSigningKeysEntry.value;
|
||||||
|
if (keys == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (final rawDeviceKeyListEntry in response[keyType].entries) {
|
for (final crossSigningKeyListEntry in keys.entries) {
|
||||||
final String userId = rawDeviceKeyListEntry.key;
|
final userId = crossSigningKeyListEntry.key;
|
||||||
final oldKeys = Map<String, CrossSigningKey>.from(
|
if (!userDeviceKeys.containsKey(userId)) {
|
||||||
_userDeviceKeys[userId].crossSigningKeys);
|
_userDeviceKeys[userId] = DeviceKeysList(userId);
|
||||||
|
}
|
||||||
|
final oldKeys = Map<String, CrossSigningKey>.from(_userDeviceKeys[userId].crossSigningKeys);
|
||||||
_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) {
|
for (final oldEntry in oldKeys.entries) {
|
||||||
if (!oldEntry.value.usage.contains(
|
if (!oldEntry.value.usage.contains(keyType)) {
|
||||||
keyType.substring(0, keyType.length - '_keys'.length))) {
|
|
||||||
_userDeviceKeys[userId].crossSigningKeys[oldEntry.key] =
|
_userDeviceKeys[userId].crossSigningKeys[oldEntry.key] =
|
||||||
oldEntry.value;
|
oldEntry.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final entry =
|
final entry =
|
||||||
CrossSigningKey.fromJson(rawDeviceKeyListEntry.value, this);
|
CrossSigningKey.fromMatrixCrossSigningKey(crossSigningKeyListEntry.value, this);
|
||||||
if (entry.isValid) {
|
if (entry.isValid) {
|
||||||
final publicKey = entry.publicKey;
|
final publicKey = entry.publicKey;
|
||||||
if (!oldKeys.containsKey(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;
|
_userDeviceKeys[userId].outdated = false;
|
||||||
if (database != null) {
|
if (database != null) {
|
||||||
dbActions.add(
|
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';
|
throw 'Unable to start new room';
|
||||||
}
|
}
|
||||||
final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client);
|
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();
|
await request.start();
|
||||||
// no need to add to the request client object. As we are doing a room
|
// 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
|
// 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);
|
DeviceKeysList(this.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +109,6 @@ abstract class SignedKey {
|
||||||
bool blocked;
|
bool blocked;
|
||||||
|
|
||||||
String get ed25519Key => keys['ed25519:$identifier'];
|
String get ed25519Key => keys['ed25519:$identifier'];
|
||||||
|
|
||||||
bool get verified => (directVerified || crossVerified) && !blocked;
|
bool get verified => (directVerified || crossVerified) && !blocked;
|
||||||
|
|
||||||
void setDirectVerified(bool v) {
|
void setDirectVerified(bool v) {
|
||||||
|
@ -145,9 +116,7 @@ abstract class SignedKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get directVerified => _verified;
|
bool get directVerified => _verified;
|
||||||
|
|
||||||
bool get crossVerified => hasValidSignatureChain();
|
bool get crossVerified => hasValidSignatureChain();
|
||||||
|
|
||||||
bool get signed => hasValidSignatureChain(verifiedOnly: false);
|
bool get signed => hasValidSignatureChain(verifiedOnly: false);
|
||||||
|
|
||||||
String get signingContent {
|
String get signingContent {
|
||||||
|
@ -255,9 +224,9 @@ abstract class SignedKey {
|
||||||
|
|
||||||
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
||||||
_verified = newVerified;
|
_verified = newVerified;
|
||||||
if (sign && client.crossSigning.signable([this])) {
|
if (sign && client.encryptionEnabled && client.encryption.crossSigning.signable([this])) {
|
||||||
// sign the key!
|
// sign the key!
|
||||||
client.crossSigning.sign([this]);
|
client.encryption.crossSigning.sign([this]);
|
||||||
}
|
}
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
|
@ -297,6 +266,20 @@ class CrossSigningKey extends SignedKey {
|
||||||
newBlocked, client.id, userId, publicKey);
|
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) {
|
CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl) {
|
||||||
client = cl;
|
client = cl;
|
||||||
final json = Event.getMapFromPayload(dbEntry.content);
|
final json = Event.getMapFromPayload(dbEntry.content);
|
||||||
|
@ -346,17 +329,36 @@ class DeviceKeys extends SignedKey {
|
||||||
@override
|
@override
|
||||||
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
||||||
super.setVerified(newVerified, sign);
|
super.setVerified(newVerified, sign);
|
||||||
return client.database
|
return client?.database
|
||||||
?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
|
?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setBlocked(bool newBlocked) {
|
Future<void> setBlocked(bool newBlocked) {
|
||||||
blocked = newBlocked;
|
blocked = newBlocked;
|
||||||
return client.database
|
return client?.database
|
||||||
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
|
?.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) {
|
DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl) {
|
||||||
client = cl;
|
client = cl;
|
||||||
final json = Event.getMapFromPayload(dbEntry.content);
|
final json = Event.getMapFromPayload(dbEntry.content);
|
||||||
|
@ -365,45 +367,10 @@ class DeviceKeys extends SignedKey {
|
||||||
identifier = dbEntry.deviceId;
|
identifier = dbEntry.deviceId;
|
||||||
algorithms = content['algorithms'].cast<String>();
|
algorithms = content['algorithms'].cast<String>();
|
||||||
keys = content['keys'] != null
|
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'])
|
? Map<String, String>.from(content['keys'])
|
||||||
: null;
|
: null;
|
||||||
deviceKeys.signatures = content['signatures'] != null
|
signatures = content['signatures'] != null
|
||||||
? Map<String, Map<String, String>>.from((content['signatures'] as Map)
|
? Map<String, dynamic>.from(content['signatures'])
|
||||||
.map((k, v) => MapEntry(k, Map<String, String>.from(v))))
|
|
||||||
: null;
|
: null;
|
||||||
unsigned = json['unsigned'] != null
|
unsigned = json['unsigned'] != null
|
||||||
? Map<String, dynamic>.from(json['unsigned'])
|
? Map<String, dynamic>.from(json['unsigned'])
|
||||||
|
@ -431,7 +398,7 @@ class DeviceKeys extends SignedKey {
|
||||||
|
|
||||||
KeyVerification startVerification() {
|
KeyVerification startVerification() {
|
||||||
final request =
|
final request =
|
||||||
KeyVerification(client: client, userId: userId, deviceId: deviceId);
|
KeyVerification(encryption: client.encryption, userId: userId, deviceId: deviceId);
|
||||||
|
|
||||||
request.start();
|
request.start();
|
||||||
client.encryption.keyVerificationManager.addRequest(request);
|
client.encryption.keyVerificationManager.addRequest(request);
|
||||||
|
|
|
@ -388,7 +388,7 @@ void main() {
|
||||||
'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA'
|
'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}, matrix);
|
||||||
test('sendToDevice', () async {
|
test('sendToDevice', () async {
|
||||||
await matrix.sendToDevice(
|
await matrix.sendToDevice(
|
||||||
[deviceKeys],
|
[deviceKeys],
|
||||||
|
|
|
@ -41,8 +41,6 @@ void main() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'unsigned': {'device_display_name': "Alice's mobile phone"},
|
'unsigned': {'device_display_name': "Alice's mobile phone"},
|
||||||
'verified': false,
|
|
||||||
'blocked': true,
|
|
||||||
};
|
};
|
||||||
var rawListJson = <String, dynamic>{
|
var rawListJson = <String, dynamic>{
|
||||||
'user_id': '@alice:example.com',
|
'user_id': '@alice:example.com',
|
||||||
|
@ -50,28 +48,12 @@ void main() {
|
||||||
'device_keys': {'JLAFKJWSCS': rawJson},
|
'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);
|
final key = DeviceKeys.fromJson(rawJson, null);
|
||||||
rawJson.remove('verified');
|
key.setVerified(false, false);
|
||||||
rawJson.remove('blocked');
|
key.setBlocked(true);
|
||||||
expect(json.encode(key.toJson()), json.encode(rawJson));
|
expect(json.encode(key.toJson()), json.encode(rawJson));
|
||||||
expect(key.verified, false);
|
expect(key.directVerified, false);
|
||||||
expect(key.blocked, true);
|
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));
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
device = DeviceKeys(
|
device = DeviceKeys.fromJson({
|
||||||
userId: client.userID,
|
'user_id': client.userID,
|
||||||
deviceId: client.deviceID,
|
'device_id': client.deviceID,
|
||||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
'algorithms': ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||||
keys: {
|
'keys': {
|
||||||
'curve25519:${client.deviceID}': client.identityKey,
|
'curve25519:${client.deviceID}': client.identityKey,
|
||||||
'ed25519:${client.deviceID}': client.fingerprintKey,
|
'ed25519:${client.deviceID}': client.fingerprintKey,
|
||||||
},
|
},
|
||||||
verified: true,
|
}, client);
|
||||||
blocked: false,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('encryptToDeviceMessage', () async {
|
test('encryptToDeviceMessage', () async {
|
||||||
|
|
|
@ -85,10 +85,10 @@ void main() {
|
||||||
FakeMatrixApi.calledEndpoints.clear();
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
await matrix
|
await matrix
|
||||||
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
|
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
|
||||||
.setBlocked(false, matrix);
|
.setBlocked(false);
|
||||||
await matrix
|
await matrix
|
||||||
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
|
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
|
||||||
.setVerified(true, matrix);
|
.setVerified(true);
|
||||||
// test a successful share
|
// test a successful share
|
||||||
var event = ToDeviceEvent(
|
var event = ToDeviceEvent(
|
||||||
sender: '@alice:example.com',
|
sender: '@alice:example.com',
|
||||||
|
|
Loading…
Reference in a new issue