migrate to new thingy!

This commit is contained in:
Sorunome 2020-06-05 22:03:28 +02:00
parent d29fb9abfe
commit 4c60369b8d
No known key found for this signature in database
GPG key ID: B19471D07FC9BE9C
17 changed files with 489 additions and 552 deletions

View file

@ -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,
); );

View file

@ -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);
} }

View file

@ -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;

View file

@ -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();

View file

@ -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) {

View file

@ -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;

View file

@ -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;
} }

View file

@ -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';

View file

@ -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;
} }
} }

View 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;
}
}

View file

@ -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(

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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],

View file

@ -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());
}); });
}); });
} }

View file

@ -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 {

View file

@ -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',