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:convert';
import 'package:olm/olm.dart' as olm;
import 'package:famedlysdk/famedlysdk.dart';
import 'client.dart';
import 'utils/device_keys_list.dart';
import 'encryption.dart';
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
const USER_SIGNING_KEY = 'm.cross_signing.user_signing';
const MASTER_KEY = 'm.cross_signing.master';
class CrossSigning {
final Client client;
CrossSigning(this.client) {
client.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async {
final Encryption encryption;
Client get client => encryption.client;
CrossSigning(this.encryption) {
encryption.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async {
final keyObj = olm.PkSigning();
try {
return keyObj.init_with_seed(base64.decode(secret)) ==
@ -24,7 +43,7 @@ class CrossSigning {
keyObj.free();
}
});
client.ssss.setValidator(USER_SIGNING_KEY, (String secret) async {
encryption.ssss.setValidator(USER_SIGNING_KEY, (String secret) async {
final keyObj = olm.PkSigning();
try {
return keyObj.init_with_seed(base64.decode(secret)) ==
@ -46,12 +65,12 @@ class CrossSigning {
if (!enabled) {
return false;
}
return (await client.ssss.getCached(SELF_SIGNING_KEY)) != null &&
(await client.ssss.getCached(USER_SIGNING_KEY)) != null;
return (await encryption.ssss.getCached(SELF_SIGNING_KEY)) != null &&
(await encryption.ssss.getCached(USER_SIGNING_KEY)) != null;
}
Future<void> selfSign({String password, String recoveryKey}) async {
final handle = client.ssss.open(MASTER_KEY);
final handle = encryption.ssss.open(MASTER_KEY);
await handle.unlock(password: password, recoveryKey: recoveryKey);
await handle.maybeCacheAll();
final masterPrivateKey = base64.decode(await handle.getStored(MASTER_KEY));
@ -133,7 +152,7 @@ class CrossSigning {
if (key is CrossSigningKey) {
if (key.usage.contains('master')) {
// okay, we'll sign our own master key
final signature = client.signString(key.signingContent);
final signature = encryption.olmManager.signString(key.signingContent);
addSignature(
key,
client
@ -144,8 +163,8 @@ class CrossSigning {
} else {
// okay, we'll sign a device key with our self signing key
selfSigningKey ??= base64
.decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? '');
if (selfSigningKey != null) {
.decode(await encryption.ssss.getCached(SELF_SIGNING_KEY) ?? '');
if (selfSigningKey.isNotEmpty) {
final signature = _sign(key.signingContent, selfSigningKey);
addSignature(key,
client.userDeviceKeys[client.userID].selfSigningKey, signature);
@ -154,8 +173,8 @@ class CrossSigning {
} else if (key is CrossSigningKey && key.usage.contains('master')) {
// we are signing someone elses master key
userSigningKey ??=
base64.decode(await client.ssss.getCached(USER_SIGNING_KEY) ?? '');
if (userSigningKey != null) {
base64.decode(await encryption.ssss.getCached(USER_SIGNING_KEY) ?? '');
if (userSigningKey.isNotEmpty) {
final signature = _sign(key.signingContent, userSigningKey);
addSignature(key, client.userDeviceKeys[client.userID].userSigningKey,
signature);
@ -165,7 +184,7 @@ class CrossSigning {
if (signedKey) {
// post our new keys!
await client.jsonRequest(
type: HTTPType.POST,
type: RequestType.POST,
action: '/client/r0/keys/signatures/upload',
data: signatures,
);

View File

@ -24,6 +24,8 @@ import 'package:pedantic/pedantic.dart';
import 'key_manager.dart';
import 'olm_manager.dart';
import 'key_verification_manager.dart';
import 'cross_signing.dart';
import 'ssss.dart';
class Encryption {
final Client client;
@ -42,15 +44,19 @@ class Encryption {
KeyManager keyManager;
OlmManager olmManager;
KeyVerificationManager keyVerificationManager;
CrossSigning crossSigning;
SSSS ssss;
Encryption({
this.client,
this.debug,
this.enableE2eeRecovery,
}) {
ssss = SSSS(this);
keyManager = KeyManager(this);
olmManager = OlmManager(this);
keyVerificationManager = KeyVerificationManager(this);
crossSigning = CrossSigning(this);
}
Future<void> init(String olmAccount) async {
@ -79,6 +85,16 @@ class Encryption {
}
}
Future<void> handleEventUpdate(EventUpdate update) async {
if (update.type == 'ephemeral') {
return;
}
if (update.eventType.startsWith('m.key.verification.') || (update.eventType == 'm.room.message' && (update.content['content']['msgtype'] is String) && update.content['content']['msgtype'].startsWith('m.key.verification.'))) {
// "just" key verification, no need to do this in sync
unawaited(keyVerificationManager.handleEventUpdate(update));
}
}
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
return await olmManager.decryptToDeviceEvent(event);
}

View File

@ -27,6 +27,8 @@ import './encryption.dart';
import './utils/session_key.dart';
import './utils/outbound_group_session.dart';
const MEGOLM_KEY = 'm.megolm_backup.v1';
class KeyManager {
final Encryption encryption;
Client get client => encryption.client;
@ -37,7 +39,22 @@ class KeyManager {
final Set<String> _loadedOutboundGroupSessions = <String>{};
final Set<String> _requestedSessionIds = <String>{};
KeyManager(this.encryption);
KeyManager(this.encryption) {
encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async {
final keyObj = olm.PkDecryption();
try {
final info = await getRoomKeysInfo();
return keyObj.init_with_private_key(base64.decode(secret)) ==
info['auth_data']['public_key'];
} catch (_) {
return false;
} finally {
keyObj.free();
}
});
}
bool get enabled => client.accountData[MEGOLM_KEY] != null;
/// clear all cached inbound group sessions. useful for testing
void clearInboundGroupSessions() {
@ -283,8 +300,120 @@ class KeyManager {
_outboundGroupSessions[roomId] = sess;
}
Future<Map<String, dynamic>> getRoomKeysInfo() async {
return await client.jsonRequest(
type: RequestType.GET,
action: '/client/r0/room_keys/version',
);
}
Future<bool> isCached() async {
if (!enabled) {
return false;
}
return (await encryption.ssss.getCached(MEGOLM_KEY)) != null;
}
Future<void> loadFromResponse(Map<String, dynamic> payload) async {
if (!(await isCached())) {
return;
}
if (!(payload['rooms'] is Map)) {
return;
}
final privateKey = base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
final decryption = olm.PkDecryption();
final info = await getRoomKeysInfo();
String backupPubKey;
try {
backupPubKey = decryption.init_with_private_key(privateKey);
if (backupPubKey == null ||
!info.containsKey('auth_data') ||
!(info['auth_data'] is Map) ||
info['auth_data']['public_key'] != backupPubKey) {
return;
}
for (final roomEntries in payload['rooms'].entries) {
final roomId = roomEntries.key;
if (!(roomEntries.value is Map) ||
!(roomEntries.value['sessions'] is Map)) {
continue;
}
for (final sessionEntries in roomEntries.value['sessions'].entries) {
final sessionId = sessionEntries.key;
final rawEncryptedSession = sessionEntries.value;
if (!(rawEncryptedSession is Map)) {
continue;
}
final firstMessageIndex =
rawEncryptedSession['first_message_index'] is int
? rawEncryptedSession['first_message_index']
: null;
final forwardedCount = rawEncryptedSession['forwarded_count'] is int
? rawEncryptedSession['forwarded_count']
: null;
final isVerified = rawEncryptedSession['is_verified'] is bool
? rawEncryptedSession['is_verified']
: null;
final sessionData = rawEncryptedSession['session_data'];
if (firstMessageIndex == null ||
forwardedCount == null ||
isVerified == null ||
!(sessionData is Map)) {
continue;
}
Map<String, dynamic> decrypted;
try {
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
sessionData['mac'], sessionData['ciphertext']));
} catch (err) {
print('[LibOlm] Error decrypting room key: ' + err.toString());
}
if (decrypted != null) {
decrypted['session_id'] = sessionId;
decrypted['room_id'] = roomId;
setInboundGroupSession(roomId, sessionId, decrypted['sender_key'], decrypted, forwarded: true);
}
}
}
} finally {
decryption.free();
}
}
Future<void> loadSingleKey(String roomId, String sessionId) async {
final info = await getRoomKeysInfo();
final ret = await client.jsonRequest(
type: RequestType.GET,
action:
'/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}',
);
await loadFromResponse({
'rooms': {
roomId: {
'sessions': {
sessionId: ret,
},
},
},
});
}
/// Request a certain key from another device
Future<void> request(Room room, String sessionId, String senderKey) async {
// let's first check our online key backup store thingy...
var hadPreviously = getInboundGroupSession(room.id, sessionId, senderKey) != null;
try {
await loadSingleKey(room.id, sessionId);
} catch (err, stacktrace) {
print('++++++++++++++++++');
print(err.toString());
print(stacktrace);
}
if (!hadPreviously && getInboundGroupSession(room.id, sessionId, senderKey) != null) {
return; // we managed to load the session from online backup, no need to care about it now
}
// while we just send the to-device event to '*', we still need to save the
// devices themself to know where to send the cancel to after receiving a reply
final devices = await room.getUserDeviceKeys();
@ -500,7 +629,6 @@ class RoomKeyRequest extends ToDeviceEvent {
for (final key in session.forwardingCurve25519KeyChain) {
forwardedKeys.add(key);
}
await requestingDevice.setVerified(true, keyManager.client);
var message = session.content;
message['forwarding_curve25519_key_chain'] = forwardedKeys;

View File

@ -51,7 +51,7 @@ class KeyVerificationManager {
}
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
if (!event.type.startsWith('m.key.verification')) {
if (!event.type.startsWith('m.key.verification') || client.verificationMethods.isEmpty) {
return;
}
// we have key verification going on!
@ -75,6 +75,52 @@ class KeyVerificationManager {
}
}
Future<void> handleEventUpdate(EventUpdate update) async {
final event = update.content;
final type = event['type'].startsWith('m.key.verification.')
? event['type']
: event['content']['msgtype'];
if (type == null || !type.startsWith('m.key.verification.') || client.verificationMethods.isEmpty) {
return;
}
if (type == 'm.key.verification.request') {
event['content']['timestamp'] = event['origin_server_ts'];
}
final transactionId =
KeyVerification.getTransactionId(event['content']) ?? event['event_id'];
if (_requests.containsKey(transactionId)) {
final req = _requests[transactionId];
if (event['sender'] != client.userID) {
req.handlePayload(type, event['content'], event['event_id']);
} else if (req.userId == client.userID && req.deviceId == null) {
// okay, maybe another of our devices answered
await req.handlePayload(type, event['content'], event['event_id']);
if (req.deviceId != client.deviceID) {
req.otherDeviceAccepted();
req.dispose();
_requests.remove(transactionId);
}
}
} else if (event['sender'] != client.userID) {
final room =
client.getRoomById(update.roomID) ?? Room(id: update.roomID, client: client);
final newKeyRequest =
KeyVerification(encryption: encryption, userId: event['sender'], room: room);
await newKeyRequest
.handlePayload(type, event['content'], event['event_id']);
if (newKeyRequest.state != KeyVerificationState.askAccept) {
// something went wrong, let's just dispose the request
newKeyRequest.dispose();
} else {
// new request! Let's notify it and stuff
_requests[transactionId] = newKeyRequest;
client.onKeyVerificationRequest.add(newKeyRequest);
}
}
}
void dispose() {
for (final req in _requests.values) {
req.dispose();

View File

@ -96,6 +96,10 @@ class OlmManager {
return payload;
}
String signString(String s) {
return _olmAccount.sign(s);
}
/// Checks the signature of a signed json object.
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
String userId, String deviceId) {

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:convert';
@ -5,11 +23,10 @@ import 'package:encrypt/encrypt.dart';
import 'package:crypto/crypto.dart';
import 'package:base58check/base58.dart';
import 'package:password_hash/password_hash.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart';
import 'client.dart';
import 'account_data.dart';
import 'utils/device_keys_list.dart';
import 'utils/to_device_event.dart';
import 'encryption.dart';
const CACHE_TYPES = <String>[
'm.cross_signing.self_signing',
@ -25,10 +42,11 @@ const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm
class SSSS {
final Client client;
final Encryption encryption;
Client get client => encryption.client;
final pendingShareRequests = <String, _ShareRequest>{};
final _validators = <String, Future<bool> Function(String)>{};
SSSS(this.client);
SSSS(this.encryption);
static _DerivedKeys deriveKeys(Uint8List key, String name) {
final zerosalt = Uint8List(8);
@ -129,11 +147,11 @@ class SSSS {
return keyData.content['key'];
}
AccountData getKey(String keyId) {
BasicEvent getKey(String keyId) {
return client.accountData['m.secret_storage.key.${keyId}'];
}
bool checkKey(Uint8List key, AccountData keyData) {
bool checkKey(Uint8List key, BasicEvent keyData) {
final info = keyData.content;
if (info['algorithm'] == 'm.secret_storage.v1.aes-hmac-sha2') {
if ((info['mac'] is String) && (info['iv'] is String)) {
@ -200,7 +218,7 @@ class SSSS {
};
// store the thing in your account data
await client.jsonRequest(
type: HTTPType.PUT,
type: RequestType.PUT,
action: '/client/r0/user/${client.userID}/account_data/${type}',
data: content,
);
@ -421,7 +439,7 @@ class _PasswordInfo {
class OpenSSSS {
final SSSS ssss;
final String keyId;
final AccountData keyData;
final BasicEvent keyData;
OpenSSSS({this.ssss, this.keyId, this.keyData});
Uint8List privateKey;

View File

@ -184,8 +184,8 @@ class KeyVerification {
if (room == null) {
transactionId = client.generateUniqueTransactionId();
}
if (client.crossSigning.enabled &&
!(await client.crossSigning.isCached()) &&
if (encryption.crossSigning.enabled &&
!(await encryption.crossSigning.isCached()) &&
!client.isUnknownSession) {
setState(KeyVerificationState.askSSSS);
_nextAction = 'request';
@ -241,7 +241,6 @@ class KeyVerification {
print('Setting device id start: ' + _deviceId.toString());
transactionId ??= eventId ?? payload['transaction_id'];
if (method != null) {
print('DUPLICATE START');
// the other side sent us a start, even though we already sent one
if (payload['method'] == method.type) {
// same method. Determine priority
@ -250,10 +249,8 @@ class KeyVerification {
entries.sort();
if (entries.first == ourEntry) {
// our start won, nothing to do
print('we won, nothing to do');
return;
} else {
print('They won, handing off');
// the other start won, let's hand off
startedVerification = false; // it is now as if they started
lastStep =
@ -324,7 +321,7 @@ class KeyVerification {
} else if (_nextAction == 'done') {
if (_verifiedDevices != null) {
// and now let's sign them all in the background
client.crossSigning.sign(_verifiedDevices);
encryption.crossSigning.sign(_verifiedDevices);
}
setState(KeyVerificationState.done);
}
@ -333,7 +330,7 @@ class KeyVerification {
next();
return;
}
final handle = client.ssss.open('m.cross_signing.user_signing');
final handle = encryption.ssss.open('m.cross_signing.user_signing');
await handle.unlock(password: password, recoveryKey: recoveryKey);
await handle.maybeCacheAll();
next();
@ -437,18 +434,18 @@ class KeyVerification {
if (verifiedMasterKey && userId == client.userID) {
// it was our own master key, let's request the cross signing keys
// we do it in the background, thus no await needed here
unawaited(client.ssss
unawaited(encryption.ssss
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()));
}
await send('m.key.verification.done', {});
var askingSSSS = false;
if (client.crossSigning.enabled &&
client.crossSigning.signable(_verifiedDevices)) {
if (encryption.crossSigning.enabled &&
encryption.crossSigning.signable(_verifiedDevices)) {
// these keys can be signed! Let's do so
if (await client.crossSigning.isCached()) {
if (await encryption.crossSigning.isCached()) {
// and now let's sign them all in the background
unawaited(client.crossSigning.sign(_verifiedDevices));
unawaited(encryption.crossSigning.sign(_verifiedDevices));
} else if (!wasUnknownSession) {
askingSSSS = true;
}

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/login_response.dart';
export 'package:famedlysdk/matrix_api/model/login_types.dart';
export 'package:famedlysdk/matrix_api/model/matrix_cross_signing_key.dart';
export 'package:famedlysdk/matrix_api/model/matrix_device_keys.dart';
export 'package:famedlysdk/matrix_api/model/matrix_exception.dart';
export 'package:famedlysdk/matrix_api/model/message_types.dart';

View File

@ -17,10 +17,14 @@
*/
import 'matrix_device_keys.dart';
import 'matrix_cross_signing_key.dart';
class KeysQueryResponse {
Map<String, dynamic> failures;
Map<String, Map<String, MatrixDeviceKeys>> deviceKeys;
Map<String, MatrixCrossSigningKey> masterKeys;
Map<String, MatrixCrossSigningKey> selfSigningKeys;
Map<String, MatrixCrossSigningKey> userSigningKeys;
KeysQueryResponse.fromJson(Map<String, dynamic> json) {
failures = Map<String, dynamic>.from(json['failures']);
@ -37,6 +41,32 @@ class KeysQueryResponse {
),
)
: null;
masterKeys = json['master_keys'] != null ?
(json['master_keys'] as Map).map(
(k, v) => MapEntry(
k,
MatrixCrossSigningKey.fromJson(v),
),
)
: null;
selfSigningKeys = json['self_signing_keys'] != null ?
(json['self_signing_keys'] as Map).map(
(k, v) => MapEntry(
k,
MatrixCrossSigningKey.fromJson(v),
),
)
: null;
userSigningKeys = json['user_signing_keys'] != null ?
(json['user_signing_keys'] as Map).map(
(k, v) => MapEntry(
k,
MatrixCrossSigningKey.fromJson(v),
),
)
: null;
}
Map<String, dynamic> toJson() {
@ -57,6 +87,30 @@ class KeysQueryResponse {
),
);
}
if (masterKeys != null) {
data['master_keys'] = masterKeys.map(
(k, v) => MapEntry(
k,
v.toJson(),
),
);
}
if (selfSigningKeys != null) {
data['self_signing_keys'] = selfSigningKeys.map(
(k, v) => MapEntry(
k,
v.toJson(),
),
);
}
if (userSigningKeys != null) {
data['user_signing_keys'] = userSigningKeys.map(
(k, v) => MapEntry(
k,
v.toJson(),
),
);
}
return data;
}
}

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 }
class GenericException implements Exception {
final dynamic content;
GenericException(this.content);
}
/// Represents a Matrix client to communicate with a
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
/// SDK.
@ -61,6 +56,8 @@ class Client {
Encryption encryption;
Set<KeyVerificationMethod> verificationMethods;
/// Create a client
/// clientName = unique identifier of this client
/// debug: Print debug output?
@ -73,7 +70,9 @@ class Client {
{this.debug = false,
this.database,
this.enableE2eeRecovery = false,
this.verificationMethods,
http.Client httpClient}) {
verificationMethods ??= <KeyVerificationMethod>{};
api = MatrixApi(debug: debug, httpClient: httpClient);
onLoginStateChanged.stream.listen((loginState) {
if (debug) {
@ -642,7 +641,7 @@ class Client {
encryption?.pickledOlmAccount,
);
}
_userDeviceKeys = await database.getUserDeviceKeys(id);
_userDeviceKeys = await database.getUserDeviceKeys(this);
_rooms = await database.getRoomList(this, onlyLeft: false);
_sortRooms();
accountData = await database.getAccountData(id);
@ -813,9 +812,6 @@ class Client {
if (encryptionEnabled) {
await encryption.handleToDeviceEvent(toDeviceEvent);
}
if (toDeviceEvent.type.startsWith('m.secret.')) {
ssss.handleToDeviceEvent(toDeviceEvent);
}
onToDeviceEvent.add(toDeviceEvent);
}
}
@ -969,11 +965,8 @@ class Client {
await database.storeEventUpdate(id, update);
}
_updateRoomsByEventUpdate(update);
if (event['type'].startsWith('m.key.verification.') ||
(event['type'] == 'm.room.message' &&
(event['content']['msgtype'] is String) &&
event['content']['msgtype'].startsWith('m.key.verification.'))) {
_handleRoomKeyVerificationRequest(update);
if (encryptionEnabled) {
await encryption.handleEventUpdate(update);
}
onEvent.add(update);
@ -1167,14 +1160,21 @@ class Client {
final deviceId = rawDeviceKeyEntry.key;
// Set the new device key for this device
if (!oldKeys.containsKey(deviceId)) {
final entry =
DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value);
if (entry.isValid) {
final entry = DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value, this);
if (entry.isValid) {
// is this a new key or the same one as an old one?
// better store an update - the signatures might have changed!
if (!oldKeys.containsKey(deviceId) ||
oldKeys[deviceId].ed25519Key == entry.ed25519Key) {
if (oldKeys.containsKey(deviceId)) {
// be sure to save the verified status
entry.setDirectVerified(oldKeys[deviceId].directVerified);
entry.blocked = oldKeys[deviceId].blocked;
entry.validSignatures = oldKeys[deviceId].validSignatures;
}
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
if (deviceId == deviceID &&
entry.ed25519Key == encryption?.fingerprintKey) {
entry.ed25519Key == fingerprintKey) {
// Always trust the own device
entry.setDirectVerified(true);
}
@ -1185,16 +1185,16 @@ class Client {
_userDeviceKeys[userId].deviceKeys[deviceId] =
oldKeys[deviceId];
}
if (database != null) {
dbActions.add(() => database.storeUserDeviceKey(
id,
userId,
deviceId,
json.encode(entry.toJson()),
entry.directVerified,
entry.blocked,
));
}
}
if (database != null) {
dbActions.add(() => database.storeUserDeviceKey(
id,
userId,
deviceId,
json.encode(entry.toJson()),
entry.directVerified,
entry.blocked,
));
}
}
// delete old/unused entries
@ -1215,29 +1215,33 @@ class Client {
}
}
// next we parse and persist the cross signing keys
for (final keyType in [
'master_keys',
'self_signing_keys',
'user_signing_keys'
]) {
if (!(response[keyType] is Map)) {
final crossSigningTypes = {
'master': response.masterKeys,
'self_signing': response.selfSigningKeys,
'user_signing': response.userSigningKeys,
};
for (final crossSigningKeysEntry in crossSigningTypes.entries) {
final keyType = crossSigningKeysEntry.key;
final keys = crossSigningKeysEntry.value;
if (keys == null) {
continue;
}
for (final rawDeviceKeyListEntry in response[keyType].entries) {
final String userId = rawDeviceKeyListEntry.key;
final oldKeys = Map<String, CrossSigningKey>.from(
_userDeviceKeys[userId].crossSigningKeys);
for (final crossSigningKeyListEntry in keys.entries) {
final userId = crossSigningKeyListEntry.key;
if (!userDeviceKeys.containsKey(userId)) {
_userDeviceKeys[userId] = DeviceKeysList(userId);
}
final oldKeys = Map<String, CrossSigningKey>.from(_userDeviceKeys[userId].crossSigningKeys);
_userDeviceKeys[userId].crossSigningKeys = {};
// add the types we arne't handling atm back
// add the types we aren't handling atm back
for (final oldEntry in oldKeys.entries) {
if (!oldEntry.value.usage.contains(
keyType.substring(0, keyType.length - '_keys'.length))) {
if (!oldEntry.value.usage.contains(keyType)) {
_userDeviceKeys[userId].crossSigningKeys[oldEntry.key] =
oldEntry.value;
}
}
final entry =
CrossSigningKey.fromJson(rawDeviceKeyListEntry.value, this);
CrossSigningKey.fromMatrixCrossSigningKey(crossSigningKeyListEntry.value, this);
if (entry.isValid) {
final publicKey = entry.publicKey;
if (!oldKeys.containsKey(publicKey) ||
@ -1267,19 +1271,6 @@ class Client {
));
}
}
// delete old/unused entries
if (database != null) {
for (final oldCrossSigningKeyEntry in oldKeys.entries) {
final publicKey = oldCrossSigningKeyEntry.key;
if (!_userDeviceKeys[userId]
.crossSigningKeys
.containsKey(publicKey)) {
// we need to remove an old key
dbActions.add(() => database.removeUserCrossSigningKey(
id, userId, publicKey));
}
}
}
_userDeviceKeys[userId].outdated = false;
if (database != null) {
dbActions.add(

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';
}
final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client);
final request = KeyVerification(client: client, room: room, userId: userId);
final request = KeyVerification(encryption: client.encryption, room: room, userId: userId);
await request.start();
// no need to add to the request client object. As we are doing a room
// verification request that'll happen automatically once we know the transaction id
@ -94,34 +94,6 @@ class DeviceKeysList {
}
}
DeviceKeysList.fromJson(Map<String, dynamic> json, Client cl) {
client = cl;
userId = json['user_id'];
outdated = json['outdated'];
deviceKeys = {};
for (final rawDeviceKeyEntry in json['device_keys'].entries) {
deviceKeys[rawDeviceKeyEntry.key] =
DeviceKeys.fromJson(rawDeviceKeyEntry.value, client);
}
}
Map<String, dynamic> toJson() {
var map = <String, dynamic>{};
final data = map;
data['user_id'] = userId;
data['outdated'] = outdated ?? true;
var rawDeviceKeys = <String, dynamic>{};
for (final deviceKeyEntry in deviceKeys.entries) {
rawDeviceKeys[deviceKeyEntry.key] = deviceKeyEntry.value.toJson();
}
data['device_keys'] = rawDeviceKeys;
return data;
}
@override
String toString() => json.encode(toJson());
DeviceKeysList(this.userId);
}
@ -137,7 +109,6 @@ abstract class SignedKey {
bool blocked;
String get ed25519Key => keys['ed25519:$identifier'];
bool get verified => (directVerified || crossVerified) && !blocked;
void setDirectVerified(bool v) {
@ -145,9 +116,7 @@ abstract class SignedKey {
}
bool get directVerified => _verified;
bool get crossVerified => hasValidSignatureChain();
bool get signed => hasValidSignatureChain(verifiedOnly: false);
String get signingContent {
@ -255,9 +224,9 @@ abstract class SignedKey {
Future<void> setVerified(bool newVerified, [bool sign = true]) {
_verified = newVerified;
if (sign && client.crossSigning.signable([this])) {
if (sign && client.encryptionEnabled && client.encryption.crossSigning.signable([this])) {
// sign the key!
client.crossSigning.sign([this]);
client.encryption.crossSigning.sign([this]);
}
return Future.value();
}
@ -297,6 +266,20 @@ class CrossSigningKey extends SignedKey {
newBlocked, client.id, userId, publicKey);
}
CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl) {
client = cl;
content = Map<String, dynamic>.from(k.toJson());
userId = k.userId;
identifier = k.publicKey;
usage = content['usage'].cast<String>();
keys = content['keys'] != null ? Map<String, String>.from(content['keys']) : null;
signatures = content['signatures'] != null
? Map<String, dynamic>.from(content['signatures'])
: null;
_verified = false;
blocked = false;
}
CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl) {
client = cl;
final json = Event.getMapFromPayload(dbEntry.content);
@ -346,17 +329,36 @@ class DeviceKeys extends SignedKey {
@override
Future<void> setVerified(bool newVerified, [bool sign = true]) {
super.setVerified(newVerified, sign);
return client.database
return client?.database
?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
}
@override
Future<void> setBlocked(bool newBlocked) {
blocked = newBlocked;
return client.database
return client?.database
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
}
DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl) {
client = cl;
content = Map<String, dynamic>.from(k.toJson());
userId = k.userId;
identifier = k.deviceId;
algorithms = content['algorithms'].cast<String>();
keys = content['keys'] != null
? Map<String, String>.from(content['keys'])
: null;
signatures = content['signatures'] != null
? Map<String, dynamic>.from(content['signatures'])
: null;
unsigned = content['unsigned'] != null
? Map<String, dynamic>.from(content['unsigned'])
: null;
_verified = false;
blocked = false;
}
DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl) {
client = cl;
final json = Event.getMapFromPayload(dbEntry.content);
@ -365,45 +367,10 @@ class DeviceKeys extends SignedKey {
identifier = dbEntry.deviceId;
algorithms = content['algorithms'].cast<String>();
keys = content['keys'] != null
}) : super(userId, deviceId, algorithms, keys, signatures,
unsigned: unsigned);
DeviceKeys({
String userId,
String deviceId,
List<String> algorithms,
Map<String, String> keys,
Map<String, Map<String, String>> signatures,
Map<String, dynamic> unsigned,
this.verified,
this.blocked,
}) : super(userId, deviceId, algorithms, keys, signatures,
unsigned: unsigned);
factory DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys matrixDeviceKeys) =>
DeviceKeys(
userId: matrixDeviceKeys.userId,
deviceId: matrixDeviceKeys.deviceId,
algorithms: matrixDeviceKeys.algorithms,
keys: matrixDeviceKeys.keys,
signatures: matrixDeviceKeys.signatures,
unsigned: matrixDeviceKeys.unsigned,
verified: false,
blocked: false,
);
static DeviceKeys fromDb(DbUserDeviceKeysKey dbEntry) {
var deviceKeys = DeviceKeys();
final content = Event.getMapFromPayload(dbEntry.content);
deviceKeys.userId = dbEntry.userId;
deviceKeys.deviceId = dbEntry.deviceId;
deviceKeys.algorithms = content['algorithms'].cast<String>();
deviceKeys.keys = content['keys'] != null
? Map<String, String>.from(content['keys'])
: null;
deviceKeys.signatures = content['signatures'] != null
? Map<String, Map<String, String>>.from((content['signatures'] as Map)
.map((k, v) => MapEntry(k, Map<String, String>.from(v))))
signatures = content['signatures'] != null
? Map<String, dynamic>.from(content['signatures'])
: null;
unsigned = json['unsigned'] != null
? Map<String, dynamic>.from(json['unsigned'])
@ -431,7 +398,7 @@ class DeviceKeys extends SignedKey {
KeyVerification startVerification() {
final request =
KeyVerification(client: client, userId: userId, deviceId: deviceId);
KeyVerification(encryption: client.encryption, userId: userId, deviceId: deviceId);
request.start();
client.encryption.keyVerificationManager.addRequest(request);

View File

@ -388,7 +388,7 @@ void main() {
'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA'
}
}
});
}, matrix);
test('sendToDevice', () async {
await matrix.sendToDevice(
[deviceKeys],

View File

@ -41,8 +41,6 @@ void main() {
}
},
'unsigned': {'device_display_name': "Alice's mobile phone"},
'verified': false,
'blocked': true,
};
var rawListJson = <String, dynamic>{
'user_id': '@alice:example.com',
@ -50,28 +48,12 @@ void main() {
'device_keys': {'JLAFKJWSCS': rawJson},
};
var userDeviceKeys = <String, DeviceKeysList>{
'@alice:example.com': DeviceKeysList.fromJson(rawListJson, null),
};
var userDeviceKeyRaw = <String, dynamic>{
'@alice:example.com': rawListJson,
};
final key = DeviceKeys.fromJson(rawJson, null);
rawJson.remove('verified');
rawJson.remove('blocked');
key.setVerified(false, false);
key.setBlocked(true);
expect(json.encode(key.toJson()), json.encode(rawJson));
expect(key.verified, false);
expect(key.directVerified, false);
expect(key.blocked, true);
expect(json.encode(DeviceKeysList.fromJson(rawListJson, null).toJson()),
json.encode(rawListJson));
var mapFromRaw = <String, DeviceKeysList>{};
for (final rawListEntry in userDeviceKeyRaw.entries) {
mapFromRaw[rawListEntry.key] =
DeviceKeysList.fromJson(rawListEntry.value, null);
}
expect(mapFromRaw.toString(), userDeviceKeys.toString());
});
});
}

View File

@ -61,17 +61,15 @@ void main() {
);
await Future.delayed(Duration(milliseconds: 10));
device = DeviceKeys(
userId: client.userID,
deviceId: client.deviceID,
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
keys: {
device = DeviceKeys.fromJson({
'user_id': client.userID,
'device_id': client.deviceID,
'algorithms': ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
'keys': {
'curve25519:${client.deviceID}': client.identityKey,
'ed25519:${client.deviceID}': client.fingerprintKey,
},
verified: true,
blocked: false,
);
}, client);
});
test('encryptToDeviceMessage', () async {

View File

@ -85,10 +85,10 @@ void main() {
FakeMatrixApi.calledEndpoints.clear();
await matrix
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
.setBlocked(false, matrix);
.setBlocked(false);
await matrix
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
.setVerified(true, matrix);
.setVerified(true);
// test a successful share
var event = ToDeviceEvent(
sender: '@alice:example.com',