Merge commit 'a46942a14051cb02e70e9223fb3e2648a71c0891' into yiffed
This commit is contained in:
commit
d82179d62b
54 changed files with 5731 additions and 1076 deletions
.gitlab
build.yamllib
encryption.dart
pubspec.lockpubspec.yamltest.shencryption
cross_signing.dartencryption.dartkey_manager.dartkey_verification_manager.dartolm_manager.dartssss.dart
matrix_api.dartutils
matrix_api
matrix_api.dart
model
src
test
client_test.dartdevice_keys_list_test.dart
encryption
cross_signing_test.dartencrypt_decrypt_room_message_test.dartencrypt_decrypt_to_device_test.dartkey_request_test.dartkey_verification_test.dartolm_manager_test.dartonline_key_backup_test.dartssss_test.dart
fake_client.dartfake_matrix_api.dartmatrix_api_test.dartmatrix_file_test.dartroom_test.darttimeline_test.dartuser_test.darttest_driver
|
@ -1 +1 @@
|
|||
* @christianpauly
|
||||
* @christianpauly @sorunome
|
6
build.yaml
Normal file
6
build.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
targets:
|
||||
$default:
|
||||
builders:
|
||||
moor_generator:
|
||||
options:
|
||||
generate_connect_constructor: true
|
|
@ -20,4 +20,5 @@ library encryption;
|
|||
|
||||
export './encryption/encryption.dart';
|
||||
export './encryption/key_manager.dart';
|
||||
export './encryption/ssss.dart';
|
||||
export './encryption/utils/key_verification.dart';
|
||||
|
|
183
lib/encryption/cross_signing.dart
Normal file
183
lib/encryption/cross_signing.dart
Normal file
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* 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 '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 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)) ==
|
||||
client.userDeviceKeys[client.userID].selfSigningKey.ed25519Key;
|
||||
} catch (_) {
|
||||
return false;
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
});
|
||||
encryption.ssss.setValidator(USER_SIGNING_KEY, (String secret) async {
|
||||
final keyObj = olm.PkSigning();
|
||||
try {
|
||||
return keyObj.init_with_seed(base64.decode(secret)) ==
|
||||
client.userDeviceKeys[client.userID].userSigningKey.ed25519Key;
|
||||
} catch (_) {
|
||||
return false;
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool get enabled =>
|
||||
client.accountData[SELF_SIGNING_KEY] != null &&
|
||||
client.accountData[USER_SIGNING_KEY] != null &&
|
||||
client.accountData[MASTER_KEY] != null;
|
||||
|
||||
Future<bool> isCached() async {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
return (await encryption.ssss.getCached(SELF_SIGNING_KEY)) != null &&
|
||||
(await encryption.ssss.getCached(USER_SIGNING_KEY)) != null;
|
||||
}
|
||||
|
||||
Future<void> selfSign({String passphrase, String recoveryKey}) async {
|
||||
final handle = encryption.ssss.open(MASTER_KEY);
|
||||
await handle.unlock(passphrase: passphrase, recoveryKey: recoveryKey);
|
||||
await handle.maybeCacheAll();
|
||||
final masterPrivateKey = base64.decode(await handle.getStored(MASTER_KEY));
|
||||
final keyObj = olm.PkSigning();
|
||||
String masterPubkey;
|
||||
try {
|
||||
masterPubkey = keyObj.init_with_seed(masterPrivateKey);
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
if (masterPubkey == null ||
|
||||
!client.userDeviceKeys.containsKey(client.userID) ||
|
||||
!client.userDeviceKeys[client.userID].deviceKeys
|
||||
.containsKey(client.deviceID)) {
|
||||
throw 'Master or user keys not found';
|
||||
}
|
||||
final masterKey = client.userDeviceKeys[client.userID].masterKey;
|
||||
if (masterKey == null || masterKey.ed25519Key != masterPubkey) {
|
||||
throw 'Master pubkey key doesn\'t match';
|
||||
}
|
||||
// master key is valid, set it to verified
|
||||
await masterKey.setVerified(true, false);
|
||||
// and now sign both our own key and our master key
|
||||
await sign([
|
||||
masterKey,
|
||||
client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]
|
||||
]);
|
||||
}
|
||||
|
||||
bool signable(List<SignableKey> keys) {
|
||||
for (final key in keys) {
|
||||
if (key is CrossSigningKey && key.usage.contains('master')) {
|
||||
return true;
|
||||
}
|
||||
if (key.userId == client.userID &&
|
||||
(key is DeviceKeys) &&
|
||||
key.identifier != client.deviceID) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> sign(List<SignableKey> keys) async {
|
||||
Uint8List selfSigningKey;
|
||||
Uint8List userSigningKey;
|
||||
final signedKeys = <MatrixSignableKey>[];
|
||||
final addSignature =
|
||||
(SignableKey key, SignableKey signedWith, String signature) {
|
||||
if (key == null || signedWith == null || signature == null) {
|
||||
return;
|
||||
}
|
||||
final signedKey = key.cloneForSigning();
|
||||
signedKey.signatures[signedWith.userId] = <String, String>{};
|
||||
signedKey.signatures[signedWith.userId]
|
||||
['ed25519:${signedWith.identifier}'] = signature;
|
||||
signedKeys.add(signedKey);
|
||||
};
|
||||
for (final key in keys) {
|
||||
if (key.userId == client.userID) {
|
||||
// we are singing a key of ourself
|
||||
if (key is CrossSigningKey) {
|
||||
if (key.usage.contains('master')) {
|
||||
// okay, we'll sign our own master key
|
||||
final signature =
|
||||
encryption.olmManager.signString(key.signingContent);
|
||||
addSignature(
|
||||
key,
|
||||
client
|
||||
.userDeviceKeys[client.userID].deviceKeys[client.deviceID],
|
||||
signature);
|
||||
}
|
||||
// we don't care about signing other cross-signing keys
|
||||
} else {
|
||||
// okay, we'll sign a device key with our self signing key
|
||||
selfSigningKey ??= base64
|
||||
.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);
|
||||
}
|
||||
}
|
||||
} else if (key is CrossSigningKey && key.usage.contains('master')) {
|
||||
// we are signing someone elses master key
|
||||
userSigningKey ??= 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (signedKeys.isNotEmpty) {
|
||||
// post our new keys!
|
||||
await client.api.uploadKeySignatures(signedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
String _sign(String canonicalJson, Uint8List key) {
|
||||
final keyObj = olm.PkSigning();
|
||||
try {
|
||||
keyObj.init_with_seed(key);
|
||||
return keyObj.sign(canonicalJson);
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
@ -77,6 +83,24 @@ class Encryption {
|
|||
// do this in the background
|
||||
unawaited(keyVerificationManager.handleToDeviceEvent(event));
|
||||
}
|
||||
if (event.type.startsWith('m.secret.')) {
|
||||
// some ssss thing. We can do this in the background
|
||||
unawaited(ssss.handleToDeviceEvent(event));
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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,29 @@ 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 client.api.getRoomKeysBackup();
|
||||
if (info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2) {
|
||||
return false;
|
||||
}
|
||||
if (keyObj.init_with_private_key(base64.decode(secret)) ==
|
||||
info.authData['public_key']) {
|
||||
_requestedSessionIds.clear();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} 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() {
|
||||
|
@ -296,8 +320,101 @@ class KeyManager {
|
|||
_outboundGroupSessions[roomId] = sess;
|
||||
}
|
||||
|
||||
Future<bool> isCached() async {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
return (await encryption.ssss.getCached(MEGOLM_KEY)) != null;
|
||||
}
|
||||
|
||||
Future<void> loadFromResponse(RoomKeys keys) async {
|
||||
if (!(await isCached())) {
|
||||
return;
|
||||
}
|
||||
final privateKey =
|
||||
base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
|
||||
final decryption = olm.PkDecryption();
|
||||
final info = await client.api.getRoomKeysBackup();
|
||||
String backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privateKey);
|
||||
|
||||
if (backupPubKey == null ||
|
||||
info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2 ||
|
||||
info.authData['public_key'] != backupPubKey) {
|
||||
return;
|
||||
}
|
||||
for (final roomEntry in keys.rooms.entries) {
|
||||
final roomId = roomEntry.key;
|
||||
for (final sessionEntry in roomEntry.value.sessions.entries) {
|
||||
final sessionId = sessionEntry.key;
|
||||
final session = sessionEntry.value;
|
||||
final firstMessageIndex = session.firstMessageIndex;
|
||||
final forwardedCount = session.forwardedCount;
|
||||
final isVerified = session.isVerified;
|
||||
final sessionData = session.sessionData;
|
||||
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 client.api.getRoomKeysBackup();
|
||||
final ret =
|
||||
await client.api.getRoomKeysSingleKey(roomId, sessionId, info.version);
|
||||
final keys = RoomKeys.fromJson({
|
||||
'rooms': {
|
||||
roomId: {
|
||||
'sessions': {
|
||||
sessionId: ret.toJson(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await loadFromResponse(keys);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
{bool tryOnlineBackup = true}) async {
|
||||
if (tryOnlineBackup) {
|
||||
// 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('[KeyManager] Failed to access online key backup: ' +
|
||||
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();
|
||||
|
@ -331,27 +448,32 @@ class KeyManager {
|
|||
/// 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')) {
|
||||
if (!(event.content['request_id'] is String)) {
|
||||
return; // invalid event
|
||||
}
|
||||
if (event.content['action'] == 'request') {
|
||||
// we are *receiving* a request
|
||||
print('[KeyManager] Received key sharing request...');
|
||||
if (!event.content.containsKey('body')) {
|
||||
print('[KeyManager] No body, doing nothing');
|
||||
return; // no body
|
||||
}
|
||||
if (!client.userDeviceKeys.containsKey(event.sender) ||
|
||||
!client.userDeviceKeys[event.sender].deviceKeys
|
||||
.containsKey(event.content['requesting_device_id'])) {
|
||||
print('[KeyManager] Device not found, doing nothing');
|
||||
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) {
|
||||
print('[KeyManager] Request is by ourself, ignoring');
|
||||
return; // ignore requests by ourself
|
||||
}
|
||||
final room = client.getRoomById(event.content['body']['room_id']);
|
||||
if (room == null) {
|
||||
print('[KeyManager] Unknown room, ignoring');
|
||||
return; // unknown room
|
||||
}
|
||||
final sessionId = event.content['body']['session_id'];
|
||||
|
@ -359,6 +481,7 @@ class KeyManager {
|
|||
// okay, let's see if we have this session at all
|
||||
if ((await loadInboundGroupSession(room.id, sessionId, senderKey)) ==
|
||||
null) {
|
||||
print('[KeyManager] Unknown session, ignoring');
|
||||
return; // we don't have this session anyways
|
||||
}
|
||||
final request = KeyManagerKeyShareRequest(
|
||||
|
@ -369,6 +492,7 @@ class KeyManager {
|
|||
senderKey: senderKey,
|
||||
);
|
||||
if (incomingShareRequests.containsKey(request.requestId)) {
|
||||
print('[KeyManager] Already processed this request, ignoring');
|
||||
return; // we don't want to process one and the same request multiple times
|
||||
}
|
||||
incomingShareRequests[request.requestId] = request;
|
||||
|
@ -377,9 +501,11 @@ class KeyManager {
|
|||
if (device.userId == client.userID &&
|
||||
device.verified &&
|
||||
!device.blocked) {
|
||||
print('[KeyManager] All checks out, forwarding key...');
|
||||
// alright, we can forward the key
|
||||
await roomKeyRequest.forwardKey();
|
||||
} else {
|
||||
print('[KeyManager] Asking client, if the key should be forwarded');
|
||||
client.onRoomKeyRequest
|
||||
.add(roomKeyRequest); // let the client handle this
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ class KeyVerificationManager {
|
|||
final Map<String, KeyVerification> _requests = {};
|
||||
|
||||
Future<void> cleanup() async {
|
||||
final Set entriesToDispose = <String>{};
|
||||
for (final entry in _requests.entries) {
|
||||
var dispose = entry.value.canceled ||
|
||||
entry.value.state == KeyVerificationState.done ||
|
||||
|
@ -38,9 +39,12 @@ class KeyVerificationManager {
|
|||
}
|
||||
if (dispose) {
|
||||
entry.value.dispose();
|
||||
_requests.remove(entry.key);
|
||||
entriesToDispose.add(entry.key);
|
||||
}
|
||||
}
|
||||
for (final k in entriesToDispose) {
|
||||
_requests.remove(k);
|
||||
}
|
||||
}
|
||||
|
||||
void addRequest(KeyVerification request) {
|
||||
|
@ -51,7 +55,8 @@ 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 +80,54 @@ 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];
|
||||
final otherDeviceId = event['content']['from_device'];
|
||||
if (event['sender'] != client.userID) {
|
||||
await req.handlePayload(type, event['content'], event['event_id']);
|
||||
} else if (event['sender'] == client.userID &&
|
||||
otherDeviceId != null &&
|
||||
otherDeviceId != client.deviceID) {
|
||||
// okay, another of our devices answered
|
||||
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();
|
||||
|
|
|
@ -18,11 +18,13 @@
|
|||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import './encryption.dart';
|
||||
import './utils/olm_session.dart';
|
||||
|
||||
class OlmManager {
|
||||
final Encryption encryption;
|
||||
|
@ -43,8 +45,8 @@ class OlmManager {
|
|||
OlmManager(this.encryption);
|
||||
|
||||
/// A map from Curve25519 identity keys to existing olm sessions.
|
||||
Map<String, List<olm.Session>> get olmSessions => _olmSessions;
|
||||
final Map<String, List<olm.Session>> _olmSessions = {};
|
||||
Map<String, List<OlmSession>> get olmSessions => _olmSessions;
|
||||
final Map<String, List<OlmSession>> _olmSessions = {};
|
||||
|
||||
Future<void> init(String olmAccount) async {
|
||||
if (olmAccount == null) {
|
||||
|
@ -96,6 +98,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) {
|
||||
|
@ -200,25 +206,24 @@ class OlmManager {
|
|||
}
|
||||
}
|
||||
|
||||
void storeOlmSession(String curve25519IdentityKey, olm.Session session) {
|
||||
void storeOlmSession(OlmSession session) {
|
||||
if (client.database == null) {
|
||||
return;
|
||||
}
|
||||
if (!_olmSessions.containsKey(curve25519IdentityKey)) {
|
||||
_olmSessions[curve25519IdentityKey] = [];
|
||||
if (!_olmSessions.containsKey(session.identityKey)) {
|
||||
_olmSessions[session.identityKey] = [];
|
||||
}
|
||||
final ix = _olmSessions[curve25519IdentityKey]
|
||||
.indexWhere((s) => s.session_id() == session.session_id());
|
||||
final ix = _olmSessions[session.identityKey]
|
||||
.indexWhere((s) => s.sessionId == session.sessionId);
|
||||
if (ix == -1) {
|
||||
// add a new session
|
||||
_olmSessions[curve25519IdentityKey].add(session);
|
||||
_olmSessions[session.identityKey].add(session);
|
||||
} else {
|
||||
// update an existing session
|
||||
_olmSessions[curve25519IdentityKey][ix] = session;
|
||||
_olmSessions[session.identityKey][ix] = session;
|
||||
}
|
||||
final pickle = session.pickle(client.userID);
|
||||
client.database.storeOlmSession(
|
||||
client.id, curve25519IdentityKey, session.session_id(), pickle);
|
||||
client.database.storeOlmSession(client.id, session.identityKey,
|
||||
session.sessionId, session.pickledSession, session.lastReceived);
|
||||
}
|
||||
|
||||
ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) {
|
||||
|
@ -241,14 +246,16 @@ class OlmManager {
|
|||
var existingSessions = olmSessions[senderKey];
|
||||
if (existingSessions != null) {
|
||||
for (var session in existingSessions) {
|
||||
if (type == 0 && session.matches_inbound(body) == true) {
|
||||
plaintext = session.decrypt(type, body);
|
||||
storeOlmSession(senderKey, session);
|
||||
if (type == 0 && session.session.matches_inbound(body) == true) {
|
||||
plaintext = session.session.decrypt(type, body);
|
||||
session.lastReceived = DateTime.now();
|
||||
storeOlmSession(session);
|
||||
break;
|
||||
} else if (type == 1) {
|
||||
try {
|
||||
plaintext = session.decrypt(type, body);
|
||||
storeOlmSession(senderKey, session);
|
||||
plaintext = session.session.decrypt(type, body);
|
||||
session.lastReceived = DateTime.now();
|
||||
storeOlmSession(session);
|
||||
break;
|
||||
} catch (_) {
|
||||
plaintext = null;
|
||||
|
@ -267,7 +274,13 @@ class OlmManager {
|
|||
_olmAccount.remove_one_time_keys(newSession);
|
||||
client.database?.updateClientKeys(pickledOlmAccount, client.id);
|
||||
plaintext = newSession.decrypt(type, body);
|
||||
storeOlmSession(senderKey, newSession);
|
||||
storeOlmSession(OlmSession(
|
||||
key: client.userID,
|
||||
identityKey: senderKey,
|
||||
sessionId: newSession.session_id(),
|
||||
session: newSession,
|
||||
lastReceived: DateTime.now(),
|
||||
));
|
||||
} catch (_) {
|
||||
newSession?.free();
|
||||
rethrow;
|
||||
|
@ -295,6 +308,35 @@ class OlmManager {
|
|||
);
|
||||
}
|
||||
|
||||
Future<List<OlmSession>> getOlmSessionsFromDatabase(String senderKey) async {
|
||||
if (client.database == null) {
|
||||
return [];
|
||||
}
|
||||
final rows =
|
||||
await client.database.dbGetOlmSessions(client.id, senderKey).get();
|
||||
final res = <OlmSession>[];
|
||||
for (final row in rows) {
|
||||
final sess = OlmSession.fromDb(row, client.userID);
|
||||
if (sess.isValid) {
|
||||
res.add(sess);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<void> restoreOlmSession(String userId, String senderKey) async {
|
||||
if (!client.userDeviceKeys.containsKey(userId)) {
|
||||
return;
|
||||
}
|
||||
final device = client.userDeviceKeys[userId].deviceKeys.values
|
||||
.firstWhere((d) => d.curve25519Key == senderKey, orElse: () => null);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
await startOutgoingOlmSessions([device]);
|
||||
await client.sendToDevice([device], 'm.dummy', {});
|
||||
}
|
||||
|
||||
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
|
||||
if (event.type != EventTypes.Encrypted) {
|
||||
return event;
|
||||
|
@ -304,8 +346,7 @@ class OlmManager {
|
|||
if (client.database == null) {
|
||||
return false;
|
||||
}
|
||||
final sessions = await client.database
|
||||
.getSingleOlmSessions(client.id, senderKey, client.userID);
|
||||
final sessions = await getOlmSessionsFromDatabase(senderKey);
|
||||
if (sessions.isEmpty) {
|
||||
return false; // okay, can't do anything
|
||||
}
|
||||
|
@ -315,12 +356,20 @@ class OlmManager {
|
|||
if (!_olmSessions.containsKey(senderKey)) {
|
||||
await loadFromDb();
|
||||
}
|
||||
event = _decryptToDeviceEvent(event);
|
||||
if (event.type != EventTypes.Encrypted || !(await loadFromDb())) {
|
||||
return event;
|
||||
try {
|
||||
event = _decryptToDeviceEvent(event);
|
||||
if (event.type != EventTypes.Encrypted || !(await loadFromDb())) {
|
||||
return event;
|
||||
}
|
||||
// retry to decrypt!
|
||||
return _decryptToDeviceEvent(event);
|
||||
} catch (_) {
|
||||
// okay, the thing errored while decrypting. It is safe to assume that the olm session is corrupt and we should generate a new one
|
||||
if (client.enableE2eeRecovery) {
|
||||
unawaited(restoreOlmSession(event.senderId, senderKey));
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
// retry to decrypt!
|
||||
return _decryptToDeviceEvent(event);
|
||||
}
|
||||
|
||||
Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys) async {
|
||||
|
@ -348,11 +397,19 @@ class OlmManager {
|
|||
fingerprintKey, deviceKey, userId, deviceId)) {
|
||||
continue;
|
||||
}
|
||||
var session = olm.Session();
|
||||
try {
|
||||
var session = olm.Session();
|
||||
session.create_outbound(_olmAccount, identityKey, deviceKey['key']);
|
||||
await storeOlmSession(identityKey, session);
|
||||
await storeOlmSession(OlmSession(
|
||||
key: client.userID,
|
||||
identityKey: identityKey,
|
||||
sessionId: session.session_id(),
|
||||
session: session,
|
||||
lastReceived:
|
||||
DateTime.now(), // we want to use a newly created session
|
||||
));
|
||||
} catch (e) {
|
||||
session.free();
|
||||
print('[LibOlm] Could not create new outbound olm session: ' +
|
||||
e.toString());
|
||||
}
|
||||
|
@ -365,14 +422,15 @@ class OlmManager {
|
|||
DeviceKeys device, String type, Map<String, dynamic> payload) async {
|
||||
var sess = olmSessions[device.curve25519Key];
|
||||
if (sess == null || sess.isEmpty) {
|
||||
final sessions = await client.database
|
||||
.getSingleOlmSessions(client.id, device.curve25519Key, client.userID);
|
||||
final sessions = await getOlmSessionsFromDatabase(device.curve25519Key);
|
||||
if (sessions.isEmpty) {
|
||||
throw ('No olm session found');
|
||||
}
|
||||
sess = _olmSessions[device.curve25519Key] = sessions;
|
||||
}
|
||||
sess.sort((a, b) => a.session_id().compareTo(b.session_id()));
|
||||
sess.sort((a, b) => a.lastReceived == b.lastReceived
|
||||
? a.sessionId.compareTo(b.sessionId)
|
||||
: b.lastReceived.compareTo(a.lastReceived));
|
||||
final fullPayload = {
|
||||
'type': type,
|
||||
'content': payload,
|
||||
|
@ -381,8 +439,8 @@ class OlmManager {
|
|||
'recipient': device.userId,
|
||||
'recipient_keys': {'ed25519': device.ed25519Key},
|
||||
};
|
||||
final encryptResult = sess.first.encrypt(json.encode(fullPayload));
|
||||
storeOlmSession(device.curve25519Key, sess.first);
|
||||
final encryptResult = sess.first.session.encrypt(json.encode(fullPayload));
|
||||
storeOlmSession(sess.first);
|
||||
final encryptedBody = <String, dynamic>{
|
||||
'algorithm': 'm.olm.v1.curve25519-aes-sha2',
|
||||
'sender_key': identityKey,
|
||||
|
@ -400,6 +458,18 @@ class OlmManager {
|
|||
String type,
|
||||
Map<String, dynamic> payload) async {
|
||||
var data = <String, Map<String, Map<String, dynamic>>>{};
|
||||
// first check if any of our sessions we want to encrypt for are in the database
|
||||
if (client.database != null) {
|
||||
for (final device in deviceKeys) {
|
||||
if (!olmSessions.containsKey(device.curve25519Key)) {
|
||||
final sessions =
|
||||
await getOlmSessionsFromDatabase(device.curve25519Key);
|
||||
if (sessions.isNotEmpty) {
|
||||
_olmSessions[device.curve25519Key] = sessions;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
|
||||
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
|
||||
olmSessions.containsKey(deviceKeys.curve25519Key));
|
||||
|
@ -424,7 +494,7 @@ class OlmManager {
|
|||
void dispose() {
|
||||
for (final sessions in olmSessions.values) {
|
||||
for (final sess in sessions) {
|
||||
sess.free();
|
||||
sess.dispose();
|
||||
}
|
||||
}
|
||||
_olmAccount?.free();
|
||||
|
|
483
lib/encryption/ssss.dart
Normal file
483
lib/encryption/ssss.dart
Normal file
|
@ -0,0 +1,483 @@
|
|||
/*
|
||||
* 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: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 'encryption.dart';
|
||||
|
||||
const CACHE_TYPES = <String>[
|
||||
'm.cross_signing.self_signing',
|
||||
'm.cross_signing.user_signing',
|
||||
'm.megolm_backup.v1'
|
||||
];
|
||||
const ZERO_STR =
|
||||
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00';
|
||||
const BASE58_ALPHABET =
|
||||
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
const base58 = Base58Codec(BASE58_ALPHABET);
|
||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||
const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm
|
||||
|
||||
class SSSS {
|
||||
final Encryption encryption;
|
||||
Client get client => encryption.client;
|
||||
final pendingShareRequests = <String, _ShareRequest>{};
|
||||
final _validators = <String, Future<bool> Function(String)>{};
|
||||
SSSS(this.encryption);
|
||||
|
||||
static _DerivedKeys deriveKeys(Uint8List key, String name) {
|
||||
final zerosalt = Uint8List(8);
|
||||
final prk = Hmac(sha256, zerosalt).convert(key);
|
||||
final b = Uint8List(1);
|
||||
b[0] = 1;
|
||||
final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b);
|
||||
b[0] = 2;
|
||||
final hmacKey =
|
||||
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
|
||||
return _DerivedKeys(aesKey: aesKey.bytes, hmacKey: hmacKey.bytes);
|
||||
}
|
||||
|
||||
static _Encrypted encryptAes(String data, Uint8List key, String name,
|
||||
[String ivStr]) {
|
||||
Uint8List iv;
|
||||
if (ivStr != null) {
|
||||
iv = base64.decode(ivStr);
|
||||
} else {
|
||||
iv = Uint8List.fromList(SecureRandom(16).bytes);
|
||||
}
|
||||
// we need to clear bit 63 of the IV
|
||||
iv[8] &= 0x7f;
|
||||
|
||||
final keys = deriveKeys(key, name);
|
||||
|
||||
final plain = Uint8List.fromList(utf8.encode(data));
|
||||
final ciphertext = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null)
|
||||
.encrypt(plain, iv: IV(iv))
|
||||
.bytes;
|
||||
|
||||
final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext);
|
||||
|
||||
return _Encrypted(
|
||||
iv: base64.encode(iv),
|
||||
ciphertext: base64.encode(ciphertext),
|
||||
mac: base64.encode(hmac.bytes));
|
||||
}
|
||||
|
||||
static String decryptAes(_Encrypted data, Uint8List key, String name) {
|
||||
final keys = deriveKeys(key, name);
|
||||
final cipher = base64.decode(data.ciphertext);
|
||||
final hmac = base64
|
||||
.encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes)
|
||||
.replaceAll(RegExp(r'=+$'), '');
|
||||
if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
|
||||
throw 'Bad MAC';
|
||||
}
|
||||
final decipher = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null)
|
||||
.decrypt(Encrypted(cipher), iv: IV(base64.decode(data.iv)));
|
||||
return String.fromCharCodes(decipher);
|
||||
}
|
||||
|
||||
static Uint8List decodeRecoveryKey(String recoveryKey) {
|
||||
final result = base58.decode(recoveryKey.replaceAll(' ', ''));
|
||||
|
||||
var parity = 0;
|
||||
for (final b in result) {
|
||||
parity ^= b;
|
||||
}
|
||||
if (parity != 0) {
|
||||
throw 'Incorrect parity';
|
||||
}
|
||||
|
||||
for (var i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; i++) {
|
||||
if (result[i] != OLM_RECOVERY_KEY_PREFIX[i]) {
|
||||
throw 'Incorrect prefix';
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length !=
|
||||
OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH + 1) {
|
||||
throw 'Incorrect length';
|
||||
}
|
||||
|
||||
return Uint8List.fromList(result.sublist(OLM_RECOVERY_KEY_PREFIX.length,
|
||||
OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH));
|
||||
}
|
||||
|
||||
static Uint8List keyFromPassphrase(String passphrase, _PassphraseInfo info) {
|
||||
if (info.algorithm != 'm.pbkdf2') {
|
||||
throw 'Unknown algorithm';
|
||||
}
|
||||
final generator = PBKDF2(hashAlgorithm: sha512);
|
||||
return Uint8List.fromList(generator.generateKey(passphrase, info.salt,
|
||||
info.iterations, info.bits != null ? info.bits / 8 : 32));
|
||||
}
|
||||
|
||||
void setValidator(String type, Future<bool> Function(String) validator) {
|
||||
_validators[type] = validator;
|
||||
}
|
||||
|
||||
String get defaultKeyId {
|
||||
final keyData = client.accountData['m.secret_storage.default_key'];
|
||||
if (keyData == null || !(keyData.content['key'] is String)) {
|
||||
return null;
|
||||
}
|
||||
return keyData.content['key'];
|
||||
}
|
||||
|
||||
BasicEvent getKey(String keyId) {
|
||||
return client.accountData['m.secret_storage.key.${keyId}'];
|
||||
}
|
||||
|
||||
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)) {
|
||||
final encrypted = encryptAes(ZERO_STR, key, '', info['iv']);
|
||||
return info['mac'].replaceAll(RegExp(r'=+$'), '') ==
|
||||
encrypted.mac.replaceAll(RegExp(r'=+$'), '');
|
||||
} else {
|
||||
// no real information about the key, assume it is valid
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
throw 'Unknown Algorithm';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getCached(String type) async {
|
||||
if (client.database == null) {
|
||||
return null;
|
||||
}
|
||||
final ret = await client.database.getSSSSCache(client.id, type);
|
||||
if (ret == null) {
|
||||
return null;
|
||||
}
|
||||
// check if it is still valid
|
||||
final keys = keyIdsFromType(type);
|
||||
if (keys.contains(ret.keyId) &&
|
||||
client.accountData[type].content['encrypted'][ret.keyId]
|
||||
['ciphertext'] ==
|
||||
ret.ciphertext) {
|
||||
return ret.content;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> getStored(String type, String keyId, Uint8List key) async {
|
||||
final secretInfo = client.accountData[type];
|
||||
if (secretInfo == null) {
|
||||
throw 'Not found';
|
||||
}
|
||||
if (!(secretInfo.content['encrypted'] is Map)) {
|
||||
throw 'Content is not encrypted';
|
||||
}
|
||||
if (!(secretInfo.content['encrypted'][keyId] is Map)) {
|
||||
throw 'Wrong / unknown key';
|
||||
}
|
||||
final enc = secretInfo.content['encrypted'][keyId];
|
||||
final encryptInfo = _Encrypted(
|
||||
iv: enc['iv'], ciphertext: enc['ciphertext'], mac: enc['mac']);
|
||||
final decrypted = decryptAes(encryptInfo, key, type);
|
||||
if (CACHE_TYPES.contains(type) && client.database != null) {
|
||||
// cache the thing
|
||||
await client.database
|
||||
.storeSSSSCache(client.id, type, keyId, enc['ciphertext'], decrypted);
|
||||
}
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
Future<void> store(
|
||||
String type, String secret, String keyId, Uint8List key) async {
|
||||
final encrypted = encryptAes(secret, key, type);
|
||||
final content = <String, dynamic>{
|
||||
'encrypted': <String, dynamic>{},
|
||||
};
|
||||
content['encrypted'][keyId] = <String, dynamic>{
|
||||
'iv': encrypted.iv,
|
||||
'ciphertext': encrypted.ciphertext,
|
||||
'mac': encrypted.mac,
|
||||
};
|
||||
// store the thing in your account data
|
||||
await client.api.setAccountData(client.userID, type, content);
|
||||
if (CACHE_TYPES.contains(type) && client.database != null) {
|
||||
// cache the thing
|
||||
await client.database
|
||||
.storeSSSSCache(client.id, type, keyId, encrypted.ciphertext, secret);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> maybeCacheAll(String keyId, Uint8List key) async {
|
||||
for (final type in CACHE_TYPES) {
|
||||
final secret = await getCached(type);
|
||||
if (secret == null) {
|
||||
try {
|
||||
await getStored(type, keyId, key);
|
||||
} catch (_) {
|
||||
// the entry wasn't stored, just ignore it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
|
||||
for (final type in CACHE_TYPES) {
|
||||
final secret = await getCached(type);
|
||||
if (secret == null) {
|
||||
await request(type, devices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> request(String type, List<DeviceKeys> devices) async {
|
||||
// only send to own, verified devices
|
||||
print('[SSSS] Requesting type ${type}...');
|
||||
devices.removeWhere((DeviceKeys d) =>
|
||||
d.userId != client.userID ||
|
||||
!d.verified ||
|
||||
d.blocked ||
|
||||
d.deviceId == client.deviceID);
|
||||
if (devices.isEmpty) {
|
||||
print('[SSSS] Warn: No devices');
|
||||
return;
|
||||
}
|
||||
final requestId = client.generateUniqueTransactionId();
|
||||
final request = _ShareRequest(
|
||||
requestId: requestId,
|
||||
type: type,
|
||||
devices: devices,
|
||||
);
|
||||
pendingShareRequests[requestId] = request;
|
||||
await client.sendToDevice(devices, 'm.secret.request', {
|
||||
'action': 'request',
|
||||
'requesting_device_id': client.deviceID,
|
||||
'request_id': requestId,
|
||||
'name': type,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||
if (event.type == 'm.secret.request') {
|
||||
// got a request to share a secret
|
||||
print('[SSSS] Received sharing request...');
|
||||
if (event.sender != client.userID ||
|
||||
!client.userDeviceKeys.containsKey(client.userID)) {
|
||||
print('[SSSS] Not sent by us');
|
||||
return; // we aren't asking for it ourselves, so ignore
|
||||
}
|
||||
if (event.content['action'] != 'request') {
|
||||
print('[SSSS] it is actually a cancelation');
|
||||
return; // not actually requesting, so ignore
|
||||
}
|
||||
final device = client.userDeviceKeys[client.userID]
|
||||
.deviceKeys[event.content['requesting_device_id']];
|
||||
if (device == null || !device.verified || device.blocked) {
|
||||
print('[SSSS] Unknown / unverified devices, ignoring');
|
||||
return; // nope....unknown or untrusted device
|
||||
}
|
||||
// alright, all seems fine...let's check if we actually have the secret they are asking for
|
||||
final type = event.content['name'];
|
||||
final secret = await getCached(type);
|
||||
if (secret == null) {
|
||||
print('[SSSS] We don\'t have the secret for ${type} ourself, ignoring');
|
||||
return; // seems like we don't have this, either
|
||||
}
|
||||
// okay, all checks out...time to share this secret!
|
||||
print('[SSSS] Replying with secret for ${type}');
|
||||
await client.sendToDevice(
|
||||
[device],
|
||||
'm.secret.send',
|
||||
{
|
||||
'request_id': event.content['request_id'],
|
||||
'secret': secret,
|
||||
});
|
||||
} else if (event.type == 'm.secret.send') {
|
||||
// receiving a secret we asked for
|
||||
print('[SSSS] Received shared secret...');
|
||||
if (event.sender != client.userID ||
|
||||
!pendingShareRequests.containsKey(event.content['request_id']) ||
|
||||
event.encryptedContent == null) {
|
||||
print('[SSSS] Not by us or unknown request');
|
||||
return; // we have no idea what we just received
|
||||
}
|
||||
final request = pendingShareRequests[event.content['request_id']];
|
||||
// alright, as we received a known request id, let's check if the sender is valid
|
||||
final device = request.devices.firstWhere(
|
||||
(d) =>
|
||||
d.userId == event.sender &&
|
||||
d.curve25519Key == event.encryptedContent['sender_key'],
|
||||
orElse: () => null);
|
||||
if (device == null) {
|
||||
print('[SSSS] Someone else replied?');
|
||||
return; // someone replied whom we didn't send the share request to
|
||||
}
|
||||
final secret = event.content['secret'];
|
||||
if (!(event.content['secret'] is String)) {
|
||||
print('[SSSS] Secret wasn\'t a string');
|
||||
return; // the secret wasn't a string....wut?
|
||||
}
|
||||
// let's validate if the secret is, well, valid
|
||||
if (_validators.containsKey(request.type) &&
|
||||
!(await _validators[request.type](secret))) {
|
||||
print('[SSSS] The received secret was invalid');
|
||||
return; // didn't pass the validator
|
||||
}
|
||||
pendingShareRequests.remove(request.requestId);
|
||||
if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) {
|
||||
print('[SSSS] Request is too far in the past');
|
||||
return; // our request is more than 15min in the past...better not trust it anymore
|
||||
}
|
||||
print('[SSSS] Secret for type ${request.type} is ok, storing it');
|
||||
if (client.database != null) {
|
||||
final keyId = keyIdFromType(request.type);
|
||||
if (keyId != null) {
|
||||
final ciphertext = client.accountData[request.type]
|
||||
.content['encrypted'][keyId]['ciphertext'];
|
||||
await client.database.storeSSSSCache(
|
||||
client.id, request.type, keyId, ciphertext, secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> keyIdsFromType(String type) {
|
||||
final data = client.accountData[type];
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
if (data.content['encrypted'] is Map) {
|
||||
final Set keys = <String>{};
|
||||
for (final key in data.content['encrypted'].keys) {
|
||||
keys.add(key);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String keyIdFromType(String type) {
|
||||
final keys = keyIdsFromType(type);
|
||||
if (keys == null || keys.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (keys.contains(defaultKeyId)) {
|
||||
return defaultKeyId;
|
||||
}
|
||||
return keys.first;
|
||||
}
|
||||
|
||||
OpenSSSS open([String identifier]) {
|
||||
identifier ??= defaultKeyId;
|
||||
if (identifier == null) {
|
||||
throw 'Dont know what to open';
|
||||
}
|
||||
final keyToOpen = keyIdFromType(identifier) ?? identifier;
|
||||
if (keyToOpen == null) {
|
||||
throw 'No key found to open';
|
||||
}
|
||||
final key = getKey(keyToOpen);
|
||||
if (key == null) {
|
||||
throw 'Unknown key to open';
|
||||
}
|
||||
return OpenSSSS(ssss: this, keyId: keyToOpen, keyData: key);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareRequest {
|
||||
final String requestId;
|
||||
final String type;
|
||||
final List<DeviceKeys> devices;
|
||||
final DateTime start;
|
||||
|
||||
_ShareRequest({this.requestId, this.type, this.devices})
|
||||
: start = DateTime.now();
|
||||
}
|
||||
|
||||
class _Encrypted {
|
||||
final String iv;
|
||||
final String ciphertext;
|
||||
final String mac;
|
||||
|
||||
_Encrypted({this.iv, this.ciphertext, this.mac});
|
||||
}
|
||||
|
||||
class _DerivedKeys {
|
||||
final Uint8List aesKey;
|
||||
final Uint8List hmacKey;
|
||||
|
||||
_DerivedKeys({this.aesKey, this.hmacKey});
|
||||
}
|
||||
|
||||
class _PassphraseInfo {
|
||||
final String algorithm;
|
||||
final String salt;
|
||||
final int iterations;
|
||||
final int bits;
|
||||
|
||||
_PassphraseInfo({this.algorithm, this.salt, this.iterations, this.bits});
|
||||
}
|
||||
|
||||
class OpenSSSS {
|
||||
final SSSS ssss;
|
||||
final String keyId;
|
||||
final BasicEvent keyData;
|
||||
OpenSSSS({this.ssss, this.keyId, this.keyData});
|
||||
Uint8List privateKey;
|
||||
|
||||
bool get isUnlocked => privateKey != null;
|
||||
|
||||
void unlock({String passphrase, String recoveryKey}) {
|
||||
if (passphrase != null) {
|
||||
privateKey = SSSS.keyFromPassphrase(
|
||||
passphrase,
|
||||
_PassphraseInfo(
|
||||
algorithm: keyData.content['passphrase']['algorithm'],
|
||||
salt: keyData.content['passphrase']['salt'],
|
||||
iterations: keyData.content['passphrase']['iterations'],
|
||||
bits: keyData.content['passphrase']['bits']));
|
||||
} else if (recoveryKey != null) {
|
||||
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
||||
} else {
|
||||
throw 'Nothing specified';
|
||||
}
|
||||
// verify the validity of the key
|
||||
if (!ssss.checkKey(privateKey, keyData)) {
|
||||
privateKey = null;
|
||||
throw 'Inalid key';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getStored(String type) async {
|
||||
return await ssss.getStored(type, keyId, privateKey);
|
||||
}
|
||||
|
||||
Future<void> store(String type, String secret) async {
|
||||
await ssss.store(type, secret, keyId, privateKey);
|
||||
}
|
||||
|
||||
Future<void> maybeCacheAll() async {
|
||||
await ssss.maybeCacheAll(keyId, privateKey);
|
||||
}
|
||||
}
|
|
@ -16,9 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:random_string/random_string.dart';
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
|
@ -63,6 +64,7 @@ import '../encryption.dart';
|
|||
|
||||
enum KeyVerificationState {
|
||||
askAccept,
|
||||
askSSSS,
|
||||
waitingAccept,
|
||||
askSas,
|
||||
waitingSas,
|
||||
|
@ -70,6 +72,8 @@ enum KeyVerificationState {
|
|||
error
|
||||
}
|
||||
|
||||
enum KeyVerificationMethod { emoji, numbers }
|
||||
|
||||
List<String> _intersect(List<String> a, List<dynamic> b) {
|
||||
if (b == null || a == null) {
|
||||
return [];
|
||||
|
@ -103,11 +107,9 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
|
|||
return ret;
|
||||
}
|
||||
|
||||
final VERIFICATION_METHODS = [_KeyVerificationMethodSas.type];
|
||||
|
||||
_KeyVerificationMethod _makeVerificationMethod(
|
||||
String type, KeyVerification request) {
|
||||
if (type == _KeyVerificationMethodSas.type) {
|
||||
if (type == 'm.sas.v1') {
|
||||
return _KeyVerificationMethodSas(request: request);
|
||||
}
|
||||
throw 'Unkown method type';
|
||||
|
@ -126,6 +128,8 @@ class KeyVerification {
|
|||
_KeyVerificationMethod method;
|
||||
List<String> possibleMethods;
|
||||
Map<String, dynamic> startPaylaod;
|
||||
String _nextAction;
|
||||
List<SignableKey> _verifiedDevices;
|
||||
|
||||
DateTime lastActivity;
|
||||
String lastStep;
|
||||
|
@ -157,22 +161,50 @@ class KeyVerification {
|
|||
: null);
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
if (room == null) {
|
||||
transactionId = randomString(512);
|
||||
List<String> get knownVerificationMethods {
|
||||
final methods = <String>[];
|
||||
if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
|
||||
client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
|
||||
methods.add('m.sas.v1');
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
Future<void> sendStart() async {
|
||||
await send('m.key.verification.request', {
|
||||
'methods': VERIFICATION_METHODS,
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
'methods': knownVerificationMethods,
|
||||
if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
});
|
||||
startedVerification = true;
|
||||
setState(KeyVerificationState.waitingAccept);
|
||||
lastActivity = DateTime.now();
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
if (room == null) {
|
||||
transactionId = client.generateUniqueTransactionId();
|
||||
}
|
||||
if (encryption.crossSigning.enabled &&
|
||||
!(await encryption.crossSigning.isCached()) &&
|
||||
!client.isUnknownSession) {
|
||||
setState(KeyVerificationState.askSSSS);
|
||||
_nextAction = 'request';
|
||||
} else {
|
||||
await sendStart();
|
||||
}
|
||||
}
|
||||
|
||||
bool _handlePayloadLock = false;
|
||||
|
||||
Future<void> handlePayload(String type, Map<String, dynamic> payload,
|
||||
[String eventId]) async {
|
||||
while (_handlePayloadLock) {
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
}
|
||||
_handlePayloadLock = true;
|
||||
print('[Key Verification] Received type ${type}: ' + payload.toString());
|
||||
try {
|
||||
var thisLastStep = lastStep;
|
||||
switch (type) {
|
||||
case 'm.key.verification.request':
|
||||
_deviceId ??= payload['from_device'];
|
||||
|
@ -188,7 +220,7 @@ class KeyVerification {
|
|||
}
|
||||
// verify it has a method we can use
|
||||
possibleMethods =
|
||||
_intersect(VERIFICATION_METHODS, payload['methods']);
|
||||
_intersect(knownVerificationMethods, payload['methods']);
|
||||
if (possibleMethods.isEmpty) {
|
||||
// reject it outright
|
||||
await cancel('m.unknown_method');
|
||||
|
@ -197,13 +229,17 @@ class KeyVerification {
|
|||
setState(KeyVerificationState.askAccept);
|
||||
break;
|
||||
case 'm.key.verification.ready':
|
||||
_deviceId ??= payload['from_device'];
|
||||
possibleMethods =
|
||||
_intersect(VERIFICATION_METHODS, payload['methods']);
|
||||
_intersect(knownVerificationMethods, payload['methods']);
|
||||
if (possibleMethods.isEmpty) {
|
||||
// reject it outright
|
||||
await cancel('m.unknown_method');
|
||||
return;
|
||||
}
|
||||
// as both parties can send a start, the last step being "ready" is race-condition prone
|
||||
// as such, we better set it *before* we send our start
|
||||
lastStep = type;
|
||||
// TODO: Pick method?
|
||||
method = _makeVerificationMethod(possibleMethods.first, this);
|
||||
await method.sendStart();
|
||||
|
@ -212,10 +248,33 @@ class KeyVerification {
|
|||
case 'm.key.verification.start':
|
||||
_deviceId ??= payload['from_device'];
|
||||
transactionId ??= eventId ?? payload['transaction_id'];
|
||||
if (method != null) {
|
||||
// the other side sent us a start, even though we already sent one
|
||||
if (payload['method'] == method.type) {
|
||||
// same method. Determine priority
|
||||
final ourEntry = '${client.userID}|${client.deviceID}';
|
||||
final entries = [ourEntry, '${userId}|${deviceId}'];
|
||||
entries.sort();
|
||||
if (entries.first == ourEntry) {
|
||||
// our start won, nothing to do
|
||||
return;
|
||||
} else {
|
||||
// the other start won, let's hand off
|
||||
startedVerification = false; // it is now as if they started
|
||||
thisLastStep = lastStep =
|
||||
'm.key.verification.request'; // we fake the last step
|
||||
method.dispose(); // in case anything got created already
|
||||
}
|
||||
} else {
|
||||
// methods don't match up, let's cancel this
|
||||
await cancel('m.unexpected_message');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!(await verifyLastStep(['m.key.verification.request', null]))) {
|
||||
return; // abort
|
||||
}
|
||||
if (!VERIFICATION_METHODS.contains(payload['method'])) {
|
||||
if (!knownVerificationMethods.contains(payload['method'])) {
|
||||
await cancel('m.unknown_method');
|
||||
return;
|
||||
}
|
||||
|
@ -228,6 +287,7 @@ class KeyVerification {
|
|||
startPaylaod = payload;
|
||||
setState(KeyVerificationState.askAccept);
|
||||
} else {
|
||||
print('handling start in method.....');
|
||||
await method.handlePayload(type, payload);
|
||||
}
|
||||
break;
|
||||
|
@ -244,16 +304,50 @@ class KeyVerification {
|
|||
await method.handlePayload(type, payload);
|
||||
break;
|
||||
}
|
||||
lastStep = type;
|
||||
if (lastStep == thisLastStep) {
|
||||
lastStep = type;
|
||||
}
|
||||
} catch (err, stacktrace) {
|
||||
print('[Key Verification] An error occured: ' + err.toString());
|
||||
print(stacktrace);
|
||||
if (deviceId != null) {
|
||||
await cancel('m.invalid_message');
|
||||
}
|
||||
} finally {
|
||||
_handlePayloadLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
void otherDeviceAccepted() {
|
||||
canceled = true;
|
||||
canceledCode = 'm.accepted';
|
||||
canceledReason = 'm.accepted';
|
||||
setState(KeyVerificationState.error);
|
||||
}
|
||||
|
||||
Future<void> openSSSS(
|
||||
{String passphrase, String recoveryKey, bool skip = false}) async {
|
||||
final next = () {
|
||||
if (_nextAction == 'request') {
|
||||
sendStart();
|
||||
} else if (_nextAction == 'done') {
|
||||
if (_verifiedDevices != null) {
|
||||
// and now let's sign them all in the background
|
||||
encryption.crossSigning.sign(_verifiedDevices);
|
||||
}
|
||||
setState(KeyVerificationState.done);
|
||||
}
|
||||
};
|
||||
if (skip) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
final handle = encryption.ssss.open('m.cross_signing.user_signing');
|
||||
await handle.unlock(passphrase: passphrase, recoveryKey: recoveryKey);
|
||||
await handle.maybeCacheAll();
|
||||
next();
|
||||
}
|
||||
|
||||
/// called when the user accepts an incoming verification
|
||||
Future<void> acceptVerification() async {
|
||||
if (!(await verifyLastStep(
|
||||
|
@ -318,9 +412,29 @@ class KeyVerification {
|
|||
return [];
|
||||
}
|
||||
|
||||
Future<void> maybeRequestSSSSSecrets([int i = 0]) async {
|
||||
final requestInterval = <int>[10, 60];
|
||||
if ((!encryption.crossSigning.enabled ||
|
||||
(encryption.crossSigning.enabled &&
|
||||
(await encryption.crossSigning.isCached()))) &&
|
||||
(!encryption.keyManager.enabled ||
|
||||
(encryption.keyManager.enabled &&
|
||||
(await encryption.keyManager.isCached())))) {
|
||||
// no need to request cache, we already have it
|
||||
return;
|
||||
}
|
||||
unawaited(encryption.ssss
|
||||
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()));
|
||||
if (requestInterval.length <= i) {
|
||||
return;
|
||||
}
|
||||
Timer(Duration(seconds: requestInterval[i]),
|
||||
() => maybeRequestSSSSSecrets(i + 1));
|
||||
}
|
||||
|
||||
Future<void> verifyKeys(Map<String, String> keys,
|
||||
Future<bool> Function(String, DeviceKeys) verifier) async {
|
||||
final verifiedDevices = <String>[];
|
||||
Future<bool> Function(String, SignableKey) verifier) async {
|
||||
_verifiedDevices = <SignableKey>[];
|
||||
|
||||
if (!client.userDeviceKeys.containsKey(userId)) {
|
||||
await cancel('m.key_mismatch');
|
||||
|
@ -330,23 +444,48 @@ class KeyVerification {
|
|||
final keyId = entry.key;
|
||||
final verifyDeviceId = keyId.substring('ed25519:'.length);
|
||||
final keyInfo = entry.value;
|
||||
if (client.userDeviceKeys[userId].deviceKeys
|
||||
.containsKey(verifyDeviceId)) {
|
||||
if (!(await verifier(keyInfo,
|
||||
client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]))) {
|
||||
final key = client.userDeviceKeys[userId].getKey(verifyDeviceId);
|
||||
if (key != null) {
|
||||
if (!(await verifier(keyInfo, key))) {
|
||||
await cancel('m.key_mismatch');
|
||||
return;
|
||||
}
|
||||
verifiedDevices.add(verifyDeviceId);
|
||||
} else {
|
||||
// TODO: we would check here if what we are verifying is actually a
|
||||
// cross-signing key and not a "normal" device key
|
||||
_verifiedDevices.add(key);
|
||||
}
|
||||
}
|
||||
// okay, we reached this far, so all the devices are verified!
|
||||
for (final verifyDeviceId in verifiedDevices) {
|
||||
await client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]
|
||||
.setVerified(true, client);
|
||||
var verifiedMasterKey = false;
|
||||
final wasUnknownSession = client.isUnknownSession;
|
||||
for (final key in _verifiedDevices) {
|
||||
await key.setVerified(
|
||||
true, false); // we don't want to sign the keys juuuust yet
|
||||
if (key is CrossSigningKey && key.usage.contains('master')) {
|
||||
verifiedMasterKey = true;
|
||||
}
|
||||
}
|
||||
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(maybeRequestSSSSSecrets());
|
||||
}
|
||||
await send('m.key.verification.done', {});
|
||||
|
||||
var askingSSSS = false;
|
||||
if (encryption.crossSigning.enabled &&
|
||||
encryption.crossSigning.signable(_verifiedDevices)) {
|
||||
// these keys can be signed! Let's do so
|
||||
if (await encryption.crossSigning.isCached()) {
|
||||
// and now let's sign them all in the background
|
||||
unawaited(encryption.crossSigning.sign(_verifiedDevices));
|
||||
} else if (!wasUnknownSession) {
|
||||
askingSSSS = true;
|
||||
}
|
||||
}
|
||||
if (askingSSSS) {
|
||||
setState(KeyVerificationState.askSSSS);
|
||||
_nextAction = 'done';
|
||||
} else {
|
||||
setState(KeyVerificationState.done);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,8 +537,8 @@ class KeyVerification {
|
|||
Future<void> send(String type, Map<String, dynamic> payload) async {
|
||||
makePayload(payload);
|
||||
print('[Key Verification] Sending type ${type}: ' + payload.toString());
|
||||
print('[Key Verification] Sending to ${userId} device ${deviceId}');
|
||||
if (room != null) {
|
||||
print('[Key Verification] Sending to ${userId} in room ${room.id}');
|
||||
if (['m.key.verification.request'].contains(type)) {
|
||||
payload['msgtype'] = type;
|
||||
payload['to'] = userId;
|
||||
|
@ -413,6 +552,7 @@ class KeyVerification {
|
|||
encryption.keyVerificationManager.addRequest(this);
|
||||
}
|
||||
} else {
|
||||
print('[Key Verification] Sending to ${userId} device ${deviceId}');
|
||||
await client.sendToDevice(
|
||||
[client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
|
||||
}
|
||||
|
@ -439,6 +579,9 @@ abstract class _KeyVerificationMethod {
|
|||
return false;
|
||||
}
|
||||
|
||||
String _type;
|
||||
String get type => _type;
|
||||
|
||||
Future<void> sendStart();
|
||||
void dispose() {}
|
||||
}
|
||||
|
@ -446,13 +589,13 @@ abstract class _KeyVerificationMethod {
|
|||
const KNOWN_KEY_AGREEMENT_PROTOCOLS = ['curve25519-hkdf-sha256', 'curve25519'];
|
||||
const KNOWN_HASHES = ['sha256'];
|
||||
const KNOWN_MESSAGE_AUTHENTIFICATION_CODES = ['hkdf-hmac-sha256'];
|
||||
const KNOWN_AUTHENTICATION_TYPES = ['emoji', 'decimal'];
|
||||
|
||||
class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||
_KeyVerificationMethodSas({KeyVerification request})
|
||||
: super(request: request);
|
||||
|
||||
static String type = 'm.sas.v1';
|
||||
@override
|
||||
final _type = 'm.sas.v1';
|
||||
|
||||
String keyAgreementProtocol;
|
||||
String hash;
|
||||
|
@ -469,6 +612,19 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
sas?.free();
|
||||
}
|
||||
|
||||
List<String> get knownAuthentificationTypes {
|
||||
final types = <String>[];
|
||||
if (request.client.verificationMethods
|
||||
.contains(KeyVerificationMethod.emoji)) {
|
||||
types.add('emoji');
|
||||
}
|
||||
if (request.client.verificationMethods
|
||||
.contains(KeyVerificationMethod.numbers)) {
|
||||
types.add('decimal');
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
|
||||
try {
|
||||
|
@ -550,7 +706,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
'key_agreement_protocols': KNOWN_KEY_AGREEMENT_PROTOCOLS,
|
||||
'hashes': KNOWN_HASHES,
|
||||
'message_authentication_codes': KNOWN_MESSAGE_AUTHENTIFICATION_CODES,
|
||||
'short_authentication_string': KNOWN_AUTHENTICATION_TYPES,
|
||||
'short_authentication_string': knownAuthentificationTypes,
|
||||
};
|
||||
request.makePayload(payload);
|
||||
// We just store the canonical json in here for later verification
|
||||
|
@ -582,7 +738,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
}
|
||||
messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
|
||||
final possibleAuthenticationTypes = _intersect(
|
||||
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
|
||||
knownAuthentificationTypes, payload['short_authentication_string']);
|
||||
if (possibleAuthenticationTypes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
@ -620,7 +776,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
}
|
||||
messageAuthenticationCode = payload['message_authentication_code'];
|
||||
final possibleAuthenticationTypes = _intersect(
|
||||
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
|
||||
knownAuthentificationTypes, payload['short_authentication_string']);
|
||||
if (possibleAuthenticationTypes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
@ -690,6 +846,17 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
_calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId);
|
||||
keyList.add(deviceKeyId);
|
||||
|
||||
final masterKey = client.userDeviceKeys.containsKey(client.userID)
|
||||
? client.userDeviceKeys[client.userID].masterKey
|
||||
: null;
|
||||
if (masterKey != null && masterKey.verified) {
|
||||
// we have our own master key verified, let's send it!
|
||||
final masterKeyId = 'ed25519:${masterKey.publicKey}';
|
||||
mac[masterKeyId] =
|
||||
_calculateMac(masterKey.publicKey, baseInfo + masterKeyId);
|
||||
keyList.add(masterKeyId);
|
||||
}
|
||||
|
||||
keyList.sort();
|
||||
final keys = _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS');
|
||||
await request.send('m.key.verification.mac', {
|
||||
|
@ -725,15 +892,10 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
mac[entry.key] = entry.value;
|
||||
}
|
||||
}
|
||||
await request.verifyKeys(mac, (String mac, DeviceKeys device) async {
|
||||
await request.verifyKeys(mac, (String mac, SignableKey key) async {
|
||||
return mac ==
|
||||
_calculateMac(
|
||||
device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId);
|
||||
_calculateMac(key.ed25519Key, baseInfo + 'ed25519:' + key.identifier);
|
||||
});
|
||||
await request.send('m.key.verification.done', {});
|
||||
if (request.state != KeyVerificationState.error) {
|
||||
request.setState(KeyVerificationState.done);
|
||||
}
|
||||
}
|
||||
|
||||
String _makeCommitment(String pubKey, String canonicalJson) {
|
||||
|
|
59
lib/encryption/utils/olm_session.dart
Normal file
59
lib/encryption/utils/olm_session.dart
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 'package:olm/olm.dart' as olm;
|
||||
import '../../src/database/database.dart' show DbOlmSessions;
|
||||
|
||||
class OlmSession {
|
||||
String identityKey;
|
||||
String sessionId;
|
||||
olm.Session session;
|
||||
DateTime lastReceived;
|
||||
final String key;
|
||||
String get pickledSession => session.pickle(key);
|
||||
|
||||
bool get isValid => session != null;
|
||||
|
||||
OlmSession({
|
||||
this.key,
|
||||
this.identityKey,
|
||||
this.sessionId,
|
||||
this.session,
|
||||
this.lastReceived,
|
||||
});
|
||||
|
||||
OlmSession.fromDb(DbOlmSessions dbEntry, String key) : key = key {
|
||||
session = olm.Session();
|
||||
try {
|
||||
session.unpickle(key, dbEntry.pickle);
|
||||
identityKey = dbEntry.identityKey;
|
||||
sessionId = dbEntry.sessionId;
|
||||
lastReceived =
|
||||
dbEntry.lastReceived ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
assert(sessionId == session.session_id());
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
session?.free();
|
||||
session = null;
|
||||
}
|
||||
}
|
|
@ -31,8 +31,8 @@ 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_device_keys.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/matrix_exception.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/matrix_keys.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/message_types.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/presence_content.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/notifications_query_response.dart';
|
||||
|
@ -46,6 +46,8 @@ export 'package:famedlysdk/matrix_api/model/push_rule_set.dart';
|
|||
export 'package:famedlysdk/matrix_api/model/pusher.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/request_token_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/room_alias_informations.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/room_keys_info.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/room_keys_keys.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/room_summary.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/server_capabilities.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/stripped_state_event.dart';
|
||||
|
@ -58,6 +60,7 @@ export 'package:famedlysdk/matrix_api/model/third_party_location.dart';
|
|||
export 'package:famedlysdk/matrix_api/model/third_party_user.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/timeline_history_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/turn_server_credentials.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/upload_key_signatures_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/user_search_result.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/well_known_informations.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/who_is_info.dart';
|
||||
|
|
|
@ -18,13 +18,14 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/matrix_api/model/filter.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/keys_query_response.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/login_types.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/notifications_query_response.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/open_graph_data.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/request_token_response.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/profile.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/request_token_response.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/server_capabilities.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/supported_versions.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/sync_update.dart';
|
||||
|
@ -32,16 +33,16 @@ import 'package:famedlysdk/matrix_api/model/third_party_location.dart';
|
|||
import 'package:famedlysdk/matrix_api/model/timeline_history_response.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/user_search_result.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mime_type/mime_type.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
import 'model/device.dart';
|
||||
import 'model/matrix_device_keys.dart';
|
||||
import 'model/matrix_event.dart';
|
||||
import 'model/event_context.dart';
|
||||
import 'model/events_sync_update.dart';
|
||||
import 'model/login_response.dart';
|
||||
import 'model/matrix_event.dart';
|
||||
import 'model/matrix_exception.dart';
|
||||
import 'model/matrix_keys.dart';
|
||||
import 'model/one_time_keys_claim_response.dart';
|
||||
import 'model/open_id_credentials.dart';
|
||||
import 'model/presence_content.dart';
|
||||
|
@ -49,11 +50,14 @@ import 'model/public_rooms_response.dart';
|
|||
import 'model/push_rule_set.dart';
|
||||
import 'model/pusher.dart';
|
||||
import 'model/room_alias_informations.dart';
|
||||
import 'model/room_keys_info.dart';
|
||||
import 'model/room_keys_keys.dart';
|
||||
import 'model/supported_protocol.dart';
|
||||
import 'model/tag.dart';
|
||||
import 'model/third_party_identifier.dart';
|
||||
import 'model/third_party_user.dart';
|
||||
import 'model/turn_server_credentials.dart';
|
||||
import 'model/upload_key_signatures_response.dart';
|
||||
import 'model/well_known_informations.dart';
|
||||
import 'model/who_is_info.dart';
|
||||
|
||||
|
@ -65,6 +69,13 @@ enum Direction { b, f }
|
|||
enum Visibility { public, private }
|
||||
enum CreateRoomPreset { private_chat, public_chat, trusted_private_chat }
|
||||
|
||||
String describeEnum(Object enumEntry) {
|
||||
final description = enumEntry.toString();
|
||||
final indexOfDot = description.indexOf('.');
|
||||
assert(indexOfDot != -1 && indexOfDot < description.length - 1);
|
||||
return description.substring(indexOfDot + 1);
|
||||
}
|
||||
|
||||
class MatrixApi {
|
||||
/// The homeserver this client is communicating with.
|
||||
Uri homeserver;
|
||||
|
@ -117,10 +128,14 @@ class MatrixApi {
|
|||
/// );
|
||||
/// ```
|
||||
///
|
||||
Future<Map<String, dynamic>> request(RequestType type, String action,
|
||||
{dynamic data = '',
|
||||
int timeout,
|
||||
String contentType = 'application/json'}) async {
|
||||
Future<Map<String, dynamic>> request(
|
||||
RequestType type,
|
||||
String action, {
|
||||
dynamic data = '',
|
||||
int timeout,
|
||||
String contentType = 'application/json',
|
||||
Map<String, String> query,
|
||||
}) async {
|
||||
if (homeserver == null) {
|
||||
throw ('No homeserver specified.');
|
||||
}
|
||||
|
@ -130,7 +145,13 @@ class MatrixApi {
|
|||
(!(data is String)) ? json = jsonEncode(data) : json = data;
|
||||
if (data is List<int> || action.startsWith('/media/r0/upload')) json = data;
|
||||
|
||||
final url = '${homeserver.toString()}/_matrix${action}';
|
||||
final queryPart = query?.entries
|
||||
?.where((x) => x.value != null)
|
||||
?.map((x) => [x.key, x.value].map(Uri.encodeQueryComponent).join('='))
|
||||
?.join('&');
|
||||
final url = ['${homeserver.toString()}/_matrix${action}', queryPart]
|
||||
.where((x) => x != null && x != '')
|
||||
.join('?');
|
||||
|
||||
var headers = <String, String>{};
|
||||
if (type == RequestType.PUT || type == RequestType.POST) {
|
||||
|
@ -142,13 +163,13 @@ class MatrixApi {
|
|||
|
||||
if (debug) {
|
||||
print(
|
||||
"[REQUEST ${type.toString().split('.').last}] $action, Data: ${jsonEncode(data)}");
|
||||
'[REQUEST ${describeEnum(type)}] $action, Data: ${jsonEncode(data)}');
|
||||
}
|
||||
|
||||
http.Response resp;
|
||||
var jsonResp = <String, dynamic>{};
|
||||
try {
|
||||
switch (type.toString().split('.').last) {
|
||||
switch (describeEnum(type)) {
|
||||
case 'GET':
|
||||
resp = await httpClient.get(url, headers: headers).timeout(
|
||||
Duration(seconds: timeout),
|
||||
|
@ -172,7 +193,13 @@ class MatrixApi {
|
|||
);
|
||||
break;
|
||||
}
|
||||
var jsonString = String.fromCharCodes(resp.body.runes);
|
||||
var respBody = resp.body;
|
||||
try {
|
||||
respBody = utf8.decode(resp.bodyBytes);
|
||||
} catch (_) {
|
||||
// No-OP
|
||||
}
|
||||
var jsonString = String.fromCharCodes(respBody.runes);
|
||||
if (jsonString.startsWith('[') && jsonString.endsWith(']')) {
|
||||
jsonString = '\{"chunk":$jsonString\}';
|
||||
}
|
||||
|
@ -211,8 +238,11 @@ class MatrixApi {
|
|||
/// Gets discovery information about the domain. The file may include additional keys.
|
||||
/// https://matrix.org/docs/spec/client_server/r0.6.0#get-well-known-matrix-client
|
||||
Future<WellKnownInformations> requestWellKnownInformations() async {
|
||||
final response = await httpClient
|
||||
.get('${homeserver.toString()}/.well-known/matrix/client');
|
||||
var baseUrl = homeserver.toString();
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
|
||||
}
|
||||
final response = await httpClient.get('$baseUrl/.well-known/matrix/client');
|
||||
final rawJson = json.decode(response.body);
|
||||
return WellKnownInformations.fromJson(rawJson);
|
||||
}
|
||||
|
@ -433,8 +463,7 @@ class MatrixApi {
|
|||
});
|
||||
|
||||
return IdServerUnbindResult.values.firstWhere(
|
||||
(i) =>
|
||||
i.toString().split('.').last == response['id_server_unbind_result'],
|
||||
(i) => describeEnum(i) == response['id_server_unbind_result'],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -504,12 +533,11 @@ class MatrixApi {
|
|||
RequestType.POST, '/client/r0/account/3pid/delete',
|
||||
data: {
|
||||
'address': address,
|
||||
'medium': medium.toString().split('.').last,
|
||||
'medium': describeEnum(medium),
|
||||
'id_server': idServer,
|
||||
});
|
||||
return IdServerUnbindResult.values.firstWhere(
|
||||
(i) =>
|
||||
i.toString().split('.').last == response['id_server_unbind_result'],
|
||||
(i) => describeEnum(i) == response['id_server_unbind_result'],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -524,12 +552,11 @@ class MatrixApi {
|
|||
RequestType.POST, '/client/r0/account/3pid/unbind',
|
||||
data: {
|
||||
'address': address,
|
||||
'medium': medium.toString().split('.').last,
|
||||
'medium': describeEnum(medium),
|
||||
'id_server': idServer,
|
||||
});
|
||||
return IdServerUnbindResult.values.firstWhere(
|
||||
(i) =>
|
||||
i.toString().split('.').last == response['id_server_unbind_result'],
|
||||
(i) => describeEnum(i) == response['id_server_unbind_result'],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -637,37 +664,16 @@ class MatrixApi {
|
|||
PresenceType setPresence,
|
||||
int timeout,
|
||||
}) async {
|
||||
var atLeastOneParameter = false;
|
||||
var action = '/client/r0/sync';
|
||||
if (filter != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'filter=${Uri.encodeQueryComponent(filter)}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (since != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'since=${Uri.encodeQueryComponent(since)}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (fullState != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'full_state=${Uri.encodeQueryComponent(fullState.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (setPresence != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action +=
|
||||
'set_presence=${Uri.encodeQueryComponent(setPresence.toString().split('.').last)}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (timeout != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'timeout=${Uri.encodeQueryComponent(timeout.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
action,
|
||||
'/client/r0/sync',
|
||||
query: {
|
||||
if (filter != null) 'filter': filter,
|
||||
if (since != null) 'since': since,
|
||||
if (fullState != null) 'full_state': fullState.toString(),
|
||||
if (setPresence != null) 'set_presence': describeEnum(setPresence),
|
||||
if (timeout != null) 'timeout': timeout.toString(),
|
||||
},
|
||||
);
|
||||
return SyncUpdate.fromJson(response);
|
||||
}
|
||||
|
@ -722,28 +728,15 @@ class MatrixApi {
|
|||
Membership membership,
|
||||
Membership notMembership,
|
||||
}) async {
|
||||
var action = '/client/r0/rooms/${Uri.encodeComponent(roomId)}/members';
|
||||
var atLeastOneParameter = false;
|
||||
if (at != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'at=${Uri.encodeQueryComponent(at)}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (membership != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action +=
|
||||
'membership=${Uri.encodeQueryComponent(membership.toString().split('.').last)}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (notMembership != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action +=
|
||||
'not_membership=${Uri.encodeQueryComponent(notMembership.toString().split('.').last)}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
action,
|
||||
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/members',
|
||||
query: {
|
||||
if (at != null) 'at': at,
|
||||
if (membership != null) 'membership': describeEnum(membership),
|
||||
if (notMembership != null)
|
||||
'not_membership': describeEnum(notMembership),
|
||||
},
|
||||
);
|
||||
return (response['chunk'] as List)
|
||||
.map((i) => MatrixEvent.fromJson(i))
|
||||
|
@ -773,23 +766,15 @@ class MatrixApi {
|
|||
int limit,
|
||||
String filter,
|
||||
}) async {
|
||||
var action = '/client/r0/rooms/${Uri.encodeComponent(roomId)}/messages';
|
||||
action += '?from=${Uri.encodeQueryComponent(from)}';
|
||||
action +=
|
||||
'&dir=${Uri.encodeQueryComponent(dir.toString().split('.').last)}';
|
||||
if (to != null) {
|
||||
action += '&to=${Uri.encodeQueryComponent(to)}';
|
||||
}
|
||||
if (limit != null) {
|
||||
action += '&limit=${Uri.encodeQueryComponent(limit.toString())}';
|
||||
}
|
||||
if (filter != null) {
|
||||
action += '&filter=${Uri.encodeQueryComponent(filter)}';
|
||||
}
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
action,
|
||||
);
|
||||
final response = await request(RequestType.GET,
|
||||
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/messages',
|
||||
query: {
|
||||
'from': from,
|
||||
'dir': describeEnum(dir),
|
||||
if (to != null) 'to': to,
|
||||
if (limit != null) 'limit': limit.toString(),
|
||||
if (filter != null) 'filter': filter,
|
||||
});
|
||||
return TimelineHistoryResponse.fromJson(response);
|
||||
}
|
||||
|
||||
|
@ -856,8 +841,7 @@ class MatrixApi {
|
|||
}) async {
|
||||
final response =
|
||||
await request(RequestType.POST, '/client/r0/createRoom', data: {
|
||||
if (visibility != null)
|
||||
'visibility': visibility.toString().split('.').last,
|
||||
if (visibility != null) 'visibility': describeEnum(visibility),
|
||||
if (roomAliasName != null) 'room_alias_name': roomAliasName,
|
||||
if (name != null) 'name': name,
|
||||
if (topic != null) 'topic': topic,
|
||||
|
@ -866,7 +850,7 @@ class MatrixApi {
|
|||
if (roomVersion != null) 'room_version': roomVersion,
|
||||
if (creationContent != null) 'creation_content': creationContent,
|
||||
if (initialState != null) 'initial_state': initialState,
|
||||
if (preset != null) 'preset': preset.toString().split('.').last,
|
||||
if (preset != null) 'preset': describeEnum(preset),
|
||||
if (isDirect != null) 'is_direct': isDirect,
|
||||
if (powerLevelContentOverride != null)
|
||||
'power_level_content_override': powerLevelContentOverride,
|
||||
|
@ -975,15 +959,11 @@ class MatrixApi {
|
|||
Map<String, dynamic> thirdPidSignedSiganture,
|
||||
}) async {
|
||||
var action = '/client/r0/join/${Uri.encodeComponent(roomIdOrAlias)}';
|
||||
if (servers != null) {
|
||||
for (var i = 0; i < servers.length; i++) {
|
||||
if (i == 0) {
|
||||
action += '?';
|
||||
} else {
|
||||
action += '&';
|
||||
}
|
||||
action += 'server_name=${Uri.encodeQueryComponent(servers[i])}';
|
||||
}
|
||||
final queryPart = servers
|
||||
?.map((x) => 'server_name=${Uri.encodeQueryComponent(x)}')
|
||||
?.join('&');
|
||||
if (queryPart != null && queryPart != '') {
|
||||
action += '?' + queryPart;
|
||||
}
|
||||
final response = await request(
|
||||
RequestType.POST,
|
||||
|
@ -1067,8 +1047,8 @@ class MatrixApi {
|
|||
RequestType.GET,
|
||||
'/client/r0/directory/list/room/${Uri.encodeComponent(roomId)}',
|
||||
);
|
||||
return Visibility.values.firstWhere(
|
||||
(v) => v.toString().split('.').last == response['visibility']);
|
||||
return Visibility.values
|
||||
.firstWhere((v) => describeEnum(v) == response['visibility']);
|
||||
}
|
||||
|
||||
/// Sets the visibility of a given room in the server's public room directory.
|
||||
|
@ -1078,7 +1058,7 @@ class MatrixApi {
|
|||
RequestType.PUT,
|
||||
'/client/r0/directory/list/room/${Uri.encodeComponent(roomId)}',
|
||||
data: {
|
||||
'visibility': visibility.toString().split('.').last,
|
||||
'visibility': describeEnum(visibility),
|
||||
},
|
||||
);
|
||||
return;
|
||||
|
@ -1091,26 +1071,14 @@ class MatrixApi {
|
|||
String since,
|
||||
String server,
|
||||
}) async {
|
||||
var action = '/client/r0/publicRooms';
|
||||
var atLeastOneParameter = false;
|
||||
if (limit != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'limit=${Uri.encodeQueryComponent(limit.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (since != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'since=${Uri.encodeQueryComponent(since.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (server != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'server=${Uri.encodeQueryComponent(server.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
action,
|
||||
'/client/r0/publicRooms',
|
||||
query: {
|
||||
if (limit != null) 'limit': limit.toString(),
|
||||
if (since != null) 'since': since,
|
||||
if (server != null) 'server': server,
|
||||
},
|
||||
);
|
||||
return PublicRoomsResponse.fromJson(response);
|
||||
}
|
||||
|
@ -1125,13 +1093,12 @@ class MatrixApi {
|
|||
bool includeAllNetworks,
|
||||
String thirdPartyInstanceId,
|
||||
}) async {
|
||||
var action = '/client/r0/publicRooms';
|
||||
if (server != null) {
|
||||
action += '?server=${Uri.encodeQueryComponent(server.toString())}';
|
||||
}
|
||||
final response = await request(
|
||||
RequestType.POST,
|
||||
action,
|
||||
'/client/r0/publicRooms',
|
||||
query: {
|
||||
if (server != null) 'server': server,
|
||||
},
|
||||
data: {
|
||||
if (limit != null) 'limit': limit,
|
||||
if (since != null) 'since': since,
|
||||
|
@ -1298,7 +1265,7 @@ class MatrixApi {
|
|||
RequestType.PUT,
|
||||
'/client/r0/presence/${Uri.encodeComponent(userId)}/status',
|
||||
data: {
|
||||
'presence': presenceType.toString().split('.').last,
|
||||
'presence': describeEnum(presenceType),
|
||||
if (statusMsg != null) 'status_msg': statusMsg,
|
||||
},
|
||||
);
|
||||
|
@ -1323,7 +1290,8 @@ class MatrixApi {
|
|||
fileName = fileName.split('/').last;
|
||||
var headers = <String, String>{};
|
||||
headers['Authorization'] = 'Bearer $accessToken';
|
||||
headers['Content-Type'] = contentType ?? mime(fileName);
|
||||
headers['Content-Type'] =
|
||||
contentType ?? lookupMimeType(fileName, headerBytes: file);
|
||||
fileName = Uri.encodeQueryComponent(fileName);
|
||||
final url =
|
||||
'${homeserver.toString()}/_matrix/media/r0/upload?filename=$fileName';
|
||||
|
@ -1342,7 +1310,7 @@ class MatrixApi {
|
|||
.codeUnits
|
||||
: await streamedResponse.stream.first),
|
||||
);
|
||||
if (!jsonResponse.containsKey('content_uri')) {
|
||||
if (!(jsonResponse['content_uri'] is String)) {
|
||||
throw MatrixException.fromJson(jsonResponse);
|
||||
}
|
||||
return jsonResponse['content_uri'];
|
||||
|
@ -1503,6 +1471,55 @@ class MatrixApi {
|
|||
return DeviceListsUpdate.fromJson(response);
|
||||
}
|
||||
|
||||
/// Uploads your own cross-signing keys.
|
||||
/// https://github.com/matrix-org/matrix-doc/pull/2536
|
||||
Future<void> uploadDeviceSigningKeys({
|
||||
MatrixCrossSigningKey masterKey,
|
||||
MatrixCrossSigningKey selfSigningKey,
|
||||
MatrixCrossSigningKey userSigningKey,
|
||||
}) async {
|
||||
await request(
|
||||
RequestType.POST,
|
||||
'/client/r0/keys/device_signing/upload',
|
||||
data: {
|
||||
'master_key': masterKey.toJson(),
|
||||
'self_signing_key': selfSigningKey.toJson(),
|
||||
'user_signing_key': userSigningKey.toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Uploads new signatures of keys
|
||||
/// https://github.com/matrix-org/matrix-doc/pull/2536
|
||||
Future<UploadKeySignaturesResponse> uploadKeySignatures(
|
||||
List<MatrixSignableKey> keys) async {
|
||||
final payload = <String, dynamic>{};
|
||||
for (final key in keys) {
|
||||
if (key.identifier == null ||
|
||||
key.signatures == null ||
|
||||
key.signatures.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (!payload.containsKey(key.userId)) {
|
||||
payload[key.userId] = <String, dynamic>{};
|
||||
}
|
||||
if (payload[key.userId].containsKey(key.identifier)) {
|
||||
// we need to merge signature objects
|
||||
payload[key.userId][key.identifier]['signatures']
|
||||
.addAll(key.signatures);
|
||||
} else {
|
||||
// we can just add signatures
|
||||
payload[key.userId][key.identifier] = key.toJson();
|
||||
}
|
||||
}
|
||||
final response = await request(
|
||||
RequestType.POST,
|
||||
'/client/r0/keys/signatures/upload',
|
||||
data: payload,
|
||||
);
|
||||
return UploadKeySignaturesResponse.fromJson(response);
|
||||
}
|
||||
|
||||
/// Gets all currently active pushers for the authenticated user.
|
||||
/// https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-pushers
|
||||
Future<List<Pusher>> requestPushers() async {
|
||||
|
@ -1540,26 +1557,14 @@ class MatrixApi {
|
|||
int limit,
|
||||
String only,
|
||||
}) async {
|
||||
var action = '/client/r0/notifications';
|
||||
var atLeastOneParameter = false;
|
||||
if (from != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'from=${Uri.encodeQueryComponent(from.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (limit != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'limit=${Uri.encodeQueryComponent(limit.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (only != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'only=${Uri.encodeQueryComponent(only.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
action,
|
||||
'/client/r0/notifications',
|
||||
query: {
|
||||
if (from != null) 'from': from,
|
||||
if (limit != null) 'limit': limit.toString(),
|
||||
if (only != null) 'only': only,
|
||||
},
|
||||
);
|
||||
return NotificationsQueryResponse.fromJson(response);
|
||||
}
|
||||
|
@ -1585,7 +1590,7 @@ class MatrixApi {
|
|||
) async {
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(kind.toString().split('.').last)}/${Uri.encodeComponent(ruleId)}',
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(describeEnum(kind))}/${Uri.encodeComponent(ruleId)}',
|
||||
);
|
||||
return PushRule.fromJson(response);
|
||||
}
|
||||
|
@ -1599,7 +1604,7 @@ class MatrixApi {
|
|||
) async {
|
||||
await request(
|
||||
RequestType.DELETE,
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(kind.toString().split('.').last)}/${Uri.encodeComponent(ruleId)}',
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(describeEnum(kind))}/${Uri.encodeComponent(ruleId)}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -1617,26 +1622,18 @@ class MatrixApi {
|
|||
List<PushConditions> conditions,
|
||||
String pattern,
|
||||
}) async {
|
||||
var action =
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(kind.toString().split('.').last)}/${Uri.encodeComponent(ruleId)}';
|
||||
var atLeastOneParameter = false;
|
||||
if (before != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'before=${Uri.encodeQueryComponent(before.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (after != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'after=${Uri.encodeQueryComponent(after.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
await request(RequestType.PUT, action, data: {
|
||||
'actions': actions.map((i) => i.toString().split('.').last).toList(),
|
||||
if (conditions != null)
|
||||
'conditions':
|
||||
conditions.map((i) => i.toString().split('.').last).toList(),
|
||||
if (pattern != null) 'pattern': pattern,
|
||||
});
|
||||
await request(RequestType.PUT,
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(describeEnum(kind))}/${Uri.encodeComponent(ruleId)}',
|
||||
query: {
|
||||
if (before != null) 'before': before,
|
||||
if (after != null) 'after': after,
|
||||
},
|
||||
data: {
|
||||
'actions': actions.map(describeEnum).toList(),
|
||||
if (conditions != null)
|
||||
'conditions': conditions.map((c) => c.toJson()).toList(),
|
||||
if (pattern != null) 'pattern': pattern,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1649,7 +1646,7 @@ class MatrixApi {
|
|||
) async {
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(kind.toString().split('.').last)}/${Uri.encodeComponent(ruleId)}/enabled',
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(describeEnum(kind))}/${Uri.encodeComponent(ruleId)}/enabled',
|
||||
);
|
||||
return response['enabled'];
|
||||
}
|
||||
|
@ -1664,7 +1661,7 @@ class MatrixApi {
|
|||
) async {
|
||||
await request(
|
||||
RequestType.PUT,
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(kind.toString().split('.').last)}/${Uri.encodeComponent(ruleId)}/enabled',
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(describeEnum(kind))}/${Uri.encodeComponent(ruleId)}/enabled',
|
||||
data: {'enabled': enabled},
|
||||
);
|
||||
return;
|
||||
|
@ -1679,11 +1676,11 @@ class MatrixApi {
|
|||
) async {
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(kind.toString().split('.').last)}/${Uri.encodeComponent(ruleId)}/actions',
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(describeEnum(kind))}/${Uri.encodeComponent(ruleId)}/actions',
|
||||
);
|
||||
return (response['actions'] as List)
|
||||
.map((i) => PushRuleAction.values
|
||||
.firstWhere((a) => a.toString().split('.').last == i))
|
||||
.map((i) =>
|
||||
PushRuleAction.values.firstWhere((a) => describeEnum(a) == i))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
@ -1697,10 +1694,8 @@ class MatrixApi {
|
|||
) async {
|
||||
await request(
|
||||
RequestType.PUT,
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(kind.toString().split('.').last)}/${Uri.encodeComponent(ruleId)}/actions',
|
||||
data: {
|
||||
'actions': actions.map((a) => a.toString().split('.').last).toList()
|
||||
},
|
||||
'/client/r0/pushrules/${Uri.encodeComponent(scope)}/${Uri.encodeComponent(describeEnum(kind))}/${Uri.encodeComponent(ruleId)}/actions',
|
||||
data: {'actions': actions.map((a) => describeEnum(a)).toList()},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -1725,24 +1720,12 @@ class MatrixApi {
|
|||
int timeout,
|
||||
String roomId,
|
||||
}) async {
|
||||
var action = '/client/r0/events';
|
||||
var atLeastOneParameter = false;
|
||||
if (from != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'from=${Uri.encodeQueryComponent(from)}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (timeout != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'timeout=${Uri.encodeQueryComponent(timeout.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (roomId != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'roomId=${Uri.encodeQueryComponent(roomId)}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
final response = await request(RequestType.GET, action);
|
||||
final response =
|
||||
await request(RequestType.GET, '/client/r0/events', query: {
|
||||
if (from != null) 'from': from,
|
||||
if (timeout != null) 'timeout': timeout.toString(),
|
||||
if (roomId != null) 'roomId': roomId,
|
||||
});
|
||||
return EventsSyncUpdate.fromJson(response);
|
||||
}
|
||||
|
||||
|
@ -1861,20 +1844,12 @@ class MatrixApi {
|
|||
int limit,
|
||||
String filter,
|
||||
}) async {
|
||||
var action =
|
||||
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/context/${Uri.encodeQueryComponent(eventId)}';
|
||||
var atLeastOneParameter = false;
|
||||
if (filter != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'filter=${Uri.encodeQueryComponent(filter)}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
if (limit != null) {
|
||||
action += atLeastOneParameter ? '&' : '?';
|
||||
action += 'limit=${Uri.encodeQueryComponent(limit.toString())}';
|
||||
atLeastOneParameter = true;
|
||||
}
|
||||
final response = await request(RequestType.GET, action);
|
||||
final response = await request(RequestType.GET,
|
||||
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/context/${Uri.encodeQueryComponent(eventId)}',
|
||||
query: {
|
||||
if (filter != null) 'filter': filter,
|
||||
if (limit != null) 'limit': limit.toString(),
|
||||
});
|
||||
return EventContext.fromJson(response);
|
||||
}
|
||||
|
||||
|
@ -1986,4 +1961,156 @@ class MatrixApi {
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Create room keys backup
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#post-matrix-client-r0-room-keys-version
|
||||
Future<String> createRoomKeysBackup(
|
||||
RoomKeysAlgorithmType algorithm, Map<String, dynamic> authData) async {
|
||||
final ret = await request(
|
||||
RequestType.POST,
|
||||
'/client/unstable/room_keys/version',
|
||||
data: {
|
||||
'algorithm': algorithm.algorithmString,
|
||||
'auth_data': authData,
|
||||
},
|
||||
);
|
||||
return ret['version'];
|
||||
}
|
||||
|
||||
/// Gets a room key backup
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-version
|
||||
Future<RoomKeysVersionResponse> getRoomKeysBackup([String version]) async {
|
||||
var url = '/client/unstable/room_keys/version';
|
||||
if (version != null) {
|
||||
url += '/${Uri.encodeComponent(version)}';
|
||||
}
|
||||
final ret = await request(
|
||||
RequestType.GET,
|
||||
url,
|
||||
);
|
||||
return RoomKeysVersionResponse.fromJson(ret);
|
||||
}
|
||||
|
||||
/// Updates a room key backup
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-version-version
|
||||
Future<void> updateRoomKeysBackup(String version,
|
||||
RoomKeysAlgorithmType algorithm, Map<String, dynamic> authData) async {
|
||||
await request(
|
||||
RequestType.PUT,
|
||||
'/client/unstable/room_keys/version/${Uri.encodeComponent(version)}',
|
||||
data: {
|
||||
'algorithm': algorithm.algorithmString,
|
||||
'auth_data': authData,
|
||||
'version': version,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a room key backup
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-version-version
|
||||
Future<void> deleteRoomKeysBackup(String version) async {
|
||||
await request(
|
||||
RequestType.DELETE,
|
||||
'/client/unstable/room_keys/version/${Uri.encodeComponent(version)}',
|
||||
);
|
||||
}
|
||||
|
||||
/// Stores a single room key
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys-roomid-sessionid
|
||||
Future<RoomKeysUpdateResponse> storeRoomKeysSingleKey(String roomId,
|
||||
String sessionId, String version, RoomKeysSingleKey session) async {
|
||||
final ret = await request(
|
||||
RequestType.PUT,
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}',
|
||||
data: session.toJson(),
|
||||
);
|
||||
return RoomKeysUpdateResponse.fromJson(ret);
|
||||
}
|
||||
|
||||
/// Gets a single room key
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys-roomid-sessionid
|
||||
Future<RoomKeysSingleKey> getRoomKeysSingleKey(
|
||||
String roomId, String sessionId, String version) async {
|
||||
final ret = await request(
|
||||
RequestType.GET,
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}',
|
||||
);
|
||||
return RoomKeysSingleKey.fromJson(ret);
|
||||
}
|
||||
|
||||
/// Deletes a single room key
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid-sessionid
|
||||
Future<RoomKeysUpdateResponse> deleteRoomKeysSingleKey(
|
||||
String roomId, String sessionId, String version) async {
|
||||
final ret = await request(
|
||||
RequestType.DELETE,
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${Uri.encodeComponent(version)}',
|
||||
);
|
||||
return RoomKeysUpdateResponse.fromJson(ret);
|
||||
}
|
||||
|
||||
/// Stores room keys for a room
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys-roomid
|
||||
Future<RoomKeysUpdateResponse> storeRoomKeysRoom(
|
||||
String roomId, String version, RoomKeysRoom keys) async {
|
||||
final ret = await request(
|
||||
RequestType.PUT,
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}',
|
||||
data: keys.toJson(),
|
||||
);
|
||||
return RoomKeysUpdateResponse.fromJson(ret);
|
||||
}
|
||||
|
||||
/// Gets room keys for a room
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys-roomid
|
||||
Future<RoomKeysRoom> getRoomKeysRoom(String roomId, String version) async {
|
||||
final ret = await request(
|
||||
RequestType.GET,
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}',
|
||||
);
|
||||
return RoomKeysRoom.fromJson(ret);
|
||||
}
|
||||
|
||||
/// Deletes room ekys for a room
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid
|
||||
Future<RoomKeysUpdateResponse> deleteRoomKeysRoom(
|
||||
String roomId, String version) async {
|
||||
final ret = await request(
|
||||
RequestType.DELETE,
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent(roomId)}?version=${Uri.encodeComponent(version)}',
|
||||
);
|
||||
return RoomKeysUpdateResponse.fromJson(ret);
|
||||
}
|
||||
|
||||
/// Store multiple room keys
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#put-matrix-client-r0-room-keys-keys
|
||||
Future<RoomKeysUpdateResponse> storeRoomKeys(
|
||||
String version, RoomKeys keys) async {
|
||||
final ret = await request(
|
||||
RequestType.PUT,
|
||||
'/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}',
|
||||
data: keys.toJson(),
|
||||
);
|
||||
return RoomKeysUpdateResponse.fromJson(ret);
|
||||
}
|
||||
|
||||
/// get all room keys
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#get-matrix-client-r0-room-keys-keys
|
||||
Future<RoomKeys> getRoomKeys(String version) async {
|
||||
final ret = await request(
|
||||
RequestType.GET,
|
||||
'/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}',
|
||||
);
|
||||
return RoomKeys.fromJson(ret);
|
||||
}
|
||||
|
||||
/// delete all room keys
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys
|
||||
Future<RoomKeysUpdateResponse> deleteRoomKeys(String version) async {
|
||||
final ret = await request(
|
||||
RequestType.DELETE,
|
||||
'/client/unstable/room_keys/keys?version=${Uri.encodeComponent(version)}',
|
||||
);
|
||||
return RoomKeysUpdateResponse.fromJson(ret);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ abstract class EventTypes {
|
|||
static const String RoomMember = 'm.room.member';
|
||||
static const String RoomPowerLevels = 'm.room.power_levels';
|
||||
static const String RoomName = 'm.room.name';
|
||||
static const String RoomPinnedEvents = 'm.room.pinned_events';
|
||||
static const String RoomTopic = 'm.room.topic';
|
||||
static const String RoomAvatar = 'm.room.avatar';
|
||||
static const String RoomTombstone = 'm.room.tombsone';
|
||||
|
|
|
@ -16,11 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'matrix_device_keys.dart';
|
||||
import 'matrix_keys.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 +40,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 +86,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class LoginResponse {
|
|||
userId = json['user_id'];
|
||||
accessToken = json['access_token'];
|
||||
deviceId = json['device_id'];
|
||||
if (json.containsKey('well_known')) {
|
||||
if (json['well_known'] is Map) {
|
||||
wellKnownInformations =
|
||||
WellKnownInformations.fromJson(json['well_known']);
|
||||
}
|
||||
|
|
|
@ -16,38 +16,28 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
class MatrixDeviceKeys {
|
||||
class MatrixSignableKey {
|
||||
String userId;
|
||||
String deviceId;
|
||||
List<String> algorithms;
|
||||
String identifier;
|
||||
Map<String, String> keys;
|
||||
Map<String, Map<String, String>> signatures;
|
||||
Map<String, dynamic> unsigned;
|
||||
String get deviceDisplayName =>
|
||||
unsigned != null ? unsigned['device_display_name'] : null;
|
||||
|
||||
MatrixSignableKey(this.userId, this.identifier, this.keys, this.signatures,
|
||||
{this.unsigned});
|
||||
|
||||
// This object is used for signing so we need the raw json too
|
||||
Map<String, dynamic> _json;
|
||||
|
||||
MatrixDeviceKeys(
|
||||
this.userId,
|
||||
this.deviceId,
|
||||
this.algorithms,
|
||||
this.keys,
|
||||
this.signatures, {
|
||||
this.unsigned,
|
||||
});
|
||||
|
||||
MatrixDeviceKeys.fromJson(Map<String, dynamic> json) {
|
||||
MatrixSignableKey.fromJson(Map<String, dynamic> json) {
|
||||
_json = json;
|
||||
userId = json['user_id'];
|
||||
deviceId = json['device_id'];
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
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
|
||||
signatures = json['signatures'] is Map
|
||||
? Map<String, Map<String, String>>.from((json['signatures'] as Map)
|
||||
.map((k, v) => MapEntry(k, Map<String, String>.from(v))))
|
||||
: null;
|
||||
unsigned = json['unsigned'] is Map
|
||||
? Map<String, dynamic>.from(json['unsigned'])
|
||||
: null;
|
||||
}
|
||||
|
@ -55,8 +45,6 @@ class MatrixDeviceKeys {
|
|||
Map<String, dynamic> toJson() {
|
||||
final data = _json ?? <String, dynamic>{};
|
||||
data['user_id'] = userId;
|
||||
data['device_id'] = deviceId;
|
||||
data['algorithms'] = algorithms;
|
||||
data['keys'] = keys;
|
||||
|
||||
if (signatures != null) {
|
||||
|
@ -68,3 +56,60 @@ class MatrixDeviceKeys {
|
|||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixCrossSigningKey extends MatrixSignableKey {
|
||||
List<String> usage;
|
||||
String get publicKey => identifier;
|
||||
|
||||
MatrixCrossSigningKey(
|
||||
String userId,
|
||||
this.usage,
|
||||
Map<String, String> keys,
|
||||
Map<String, Map<String, String>> signatures, {
|
||||
Map<String, dynamic> unsigned,
|
||||
}) : super(userId, keys?.values?.first, keys, signatures, unsigned: unsigned);
|
||||
|
||||
@override
|
||||
MatrixCrossSigningKey.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json) {
|
||||
usage = List<String>.from(json['usage']);
|
||||
identifier = keys?.values?.first;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = super.toJson();
|
||||
data['usage'] = usage;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixDeviceKeys extends MatrixSignableKey {
|
||||
String get deviceId => identifier;
|
||||
List<String> algorithms;
|
||||
String get deviceDisplayName =>
|
||||
unsigned != null ? unsigned['device_display_name'] : null;
|
||||
|
||||
MatrixDeviceKeys(
|
||||
String userId,
|
||||
String deviceId,
|
||||
this.algorithms,
|
||||
Map<String, String> keys,
|
||||
Map<String, Map<String, String>> signatures, {
|
||||
Map<String, dynamic> unsigned,
|
||||
}) : super(userId, deviceId, keys, signatures, unsigned: unsigned);
|
||||
|
||||
@override
|
||||
MatrixDeviceKeys.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
||||
identifier = json['device_id'];
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = super.toJson();
|
||||
data['device_id'] = deviceId;
|
||||
data['algorithms'] = algorithms;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -74,7 +74,9 @@ class PusherData {
|
|||
});
|
||||
|
||||
PusherData.fromJson(Map<String, dynamic> json) {
|
||||
url = Uri.parse(json['url']);
|
||||
if (json.containsKey('url')) {
|
||||
url = Uri.parse(json['url']);
|
||||
}
|
||||
format = json['format'];
|
||||
}
|
||||
|
||||
|
|
67
lib/matrix_api/model/room_keys_info.dart
Normal file
67
lib/matrix_api/model/room_keys_info.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
enum RoomKeysAlgorithmType { v1Curve25519AesSha2 }
|
||||
|
||||
extension RoomKeysAlgorithmTypeExtension on RoomKeysAlgorithmType {
|
||||
String get algorithmString {
|
||||
switch (this) {
|
||||
case RoomKeysAlgorithmType.v1Curve25519AesSha2:
|
||||
return 'm.megolm_backup.v1.curve25519-aes-sha2';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static RoomKeysAlgorithmType fromAlgorithmString(String s) {
|
||||
switch (s) {
|
||||
case 'm.megolm_backup.v1.curve25519-aes-sha2':
|
||||
return RoomKeysAlgorithmType.v1Curve25519AesSha2;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RoomKeysVersionResponse {
|
||||
RoomKeysAlgorithmType algorithm;
|
||||
Map<String, dynamic> authData;
|
||||
int count;
|
||||
String etag;
|
||||
String version;
|
||||
|
||||
RoomKeysVersionResponse.fromJson(Map<String, dynamic> json) {
|
||||
algorithm =
|
||||
RoomKeysAlgorithmTypeExtension.fromAlgorithmString(json['algorithm']);
|
||||
authData = json['auth_data'];
|
||||
count = json['count'];
|
||||
etag =
|
||||
json['etag'].toString(); // synapse replies an int but docs say string?
|
||||
version = json['version'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['algorithm'] = algorithm?.algorithmString;
|
||||
data['auth_data'] = authData;
|
||||
data['count'] = count;
|
||||
data['etag'] = etag;
|
||||
data['version'] = version;
|
||||
return data;
|
||||
}
|
||||
}
|
87
lib/matrix_api/model/room_keys_keys.dart
Normal file
87
lib/matrix_api/model/room_keys_keys.dart
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 RoomKeysSingleKey {
|
||||
int firstMessageIndex;
|
||||
int forwardedCount;
|
||||
bool isVerified;
|
||||
Map<String, dynamic> sessionData;
|
||||
|
||||
RoomKeysSingleKey.fromJson(Map<String, dynamic> json) {
|
||||
firstMessageIndex = json['first_message_index'];
|
||||
forwardedCount = json['forwarded_count'];
|
||||
isVerified = json['is_verified'];
|
||||
sessionData = json['session_data'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['first_message_index'] = firstMessageIndex;
|
||||
data['forwarded_count'] = forwardedCount;
|
||||
data['is_verified'] = isVerified;
|
||||
data['session_data'] = sessionData;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomKeysRoom {
|
||||
Map<String, RoomKeysSingleKey> sessions;
|
||||
|
||||
RoomKeysRoom.fromJson(Map<String, dynamic> json) {
|
||||
sessions = (json['sessions'] as Map)
|
||||
.map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v)));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['sessions'] = sessions.map((k, v) => MapEntry(k, v.toJson()));
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomKeys {
|
||||
Map<String, RoomKeysRoom> rooms;
|
||||
|
||||
RoomKeys.fromJson(Map<String, dynamic> json) {
|
||||
rooms = (json['rooms'] as Map)
|
||||
.map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v)));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['rooms'] = rooms.map((k, v) => MapEntry(k, v.toJson()));
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomKeysUpdateResponse {
|
||||
String etag;
|
||||
int count;
|
||||
|
||||
RoomKeysUpdateResponse.fromJson(Map<String, dynamic> json) {
|
||||
etag = json['etag']; // synapse replies an int but docs say string?
|
||||
count = json['count'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['etag'] = etag;
|
||||
data['count'] = count;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -33,6 +33,8 @@ class SyncUpdate {
|
|||
DeviceListsUpdate deviceLists;
|
||||
Map<String, int> deviceOneTimeKeysCount;
|
||||
|
||||
SyncUpdate();
|
||||
|
||||
SyncUpdate.fromJson(Map<String, dynamic> json) {
|
||||
nextBatch = json['next_batch'];
|
||||
rooms = json['rooms'] != null ? RoomsUpdate.fromJson(json['rooms']) : null;
|
||||
|
@ -97,6 +99,8 @@ class RoomsUpdate {
|
|||
Map<String, InvitedRoomUpdate> invite;
|
||||
Map<String, LeftRoomUpdate> leave;
|
||||
|
||||
RoomsUpdate();
|
||||
|
||||
RoomsUpdate.fromJson(Map<String, dynamic> json) {
|
||||
join = json['join'] != null
|
||||
? (json['join'] as Map)
|
||||
|
@ -136,6 +140,8 @@ class JoinedRoomUpdate extends SyncRoomUpdate {
|
|||
List<BasicRoomEvent> accountData;
|
||||
UnreadNotificationCounts unreadNotifications;
|
||||
|
||||
JoinedRoomUpdate();
|
||||
|
||||
JoinedRoomUpdate.fromJson(Map<String, dynamic> json) {
|
||||
summary =
|
||||
json['summary'] != null ? RoomSummary.fromJson(json['summary']) : null;
|
||||
|
@ -260,6 +266,9 @@ class TimelineUpdate {
|
|||
List<MatrixEvent> events;
|
||||
bool limited;
|
||||
String prevBatch;
|
||||
|
||||
TimelineUpdate();
|
||||
|
||||
TimelineUpdate.fromJson(Map<String, dynamic> json) {
|
||||
events = json['events'] != null
|
||||
? (json['events'] as List).map((i) => MatrixEvent.fromJson(i)).toList()
|
||||
|
@ -267,6 +276,7 @@ class TimelineUpdate {
|
|||
limited = json['limited'];
|
||||
prevBatch = json['prev_batch'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
if (events != null) {
|
||||
|
|
|
@ -20,7 +20,7 @@ class Tag {
|
|||
double order;
|
||||
|
||||
Tag.fromJson(Map<String, dynamic> json) {
|
||||
order = json['order'];
|
||||
order = double.tryParse(json['order'].toString());
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
@ -31,3 +31,12 @@ class Tag {
|
|||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TagType {
|
||||
static const String Favourite = 'm.favourite';
|
||||
static const String LowPriority = 'm.lowpriority';
|
||||
static const String ServerNotice = 'm.server_notice';
|
||||
static bool isValid(String tag) => tag.startsWith('m.')
|
||||
? [Favourite, LowPriority, ServerNotice].contains(tag)
|
||||
: true;
|
||||
}
|
||||
|
|
55
lib/matrix_api/model/upload_key_signatures_response.dart
Normal file
55
lib/matrix_api/model/upload_key_signatures_response.dart
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 'matrix_exception.dart';
|
||||
|
||||
class UploadKeySignaturesResponse {
|
||||
Map<String, Map<String, MatrixException>> failures;
|
||||
|
||||
UploadKeySignaturesResponse.fromJson(Map<String, dynamic> json) {
|
||||
failures = json['failures'] != null
|
||||
? (json['failures'] as Map).map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
(v as Map).map((k, v) => MapEntry(
|
||||
k,
|
||||
MatrixException.fromJson(v),
|
||||
)),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
if (failures != null) {
|
||||
data['failures'] = failures.map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
v.map(
|
||||
(k, v) => MapEntry(
|
||||
k,
|
||||
v.raw,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -20,9 +20,9 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/src/room.dart';
|
||||
import 'package:famedlysdk/src/utils/device_keys_list.dart';
|
||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
|
@ -30,12 +30,12 @@ import 'package:famedlysdk/src/utils/to_device_event.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
import 'database/database.dart' show Database;
|
||||
import 'event.dart';
|
||||
import 'room.dart';
|
||||
import 'user.dart';
|
||||
import 'utils/event_update.dart';
|
||||
import 'utils/room_update.dart';
|
||||
import 'user.dart';
|
||||
import 'database/database.dart' show Database;
|
||||
|
||||
typedef RoomSorter = int Function(Room a, Room b);
|
||||
|
||||
|
@ -56,16 +56,50 @@ class Client {
|
|||
|
||||
Encryption encryption;
|
||||
|
||||
Set<KeyVerificationMethod> verificationMethods;
|
||||
|
||||
Set<String> importantStateEvents;
|
||||
|
||||
/// Create a client
|
||||
/// clientName = unique identifier of this client
|
||||
/// debug: Print debug output?
|
||||
/// database: The database instance to use
|
||||
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
|
||||
/// verificationMethods: A set of all the verification methods this client can handle. Includes:
|
||||
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
|
||||
/// KeyVerificationMethod.emoji: Compare emojis
|
||||
/// importantStateEvents: A set of all the important state events to load when the client connects.
|
||||
/// To speed up performance only a set of state events is loaded on startup, those that are
|
||||
/// needed to display a room list. All the remaining state events are automatically post-loaded
|
||||
/// when opening the timeline of a room or manually by calling `room.postLoad()`.
|
||||
/// This set will always include the following state events:
|
||||
/// - m.room.name
|
||||
/// - m.room.avatar
|
||||
/// - m.room.message
|
||||
/// - m.room.encrypted
|
||||
/// - m.room.encryption
|
||||
/// - m.room.canonical_alias
|
||||
/// - m.room.tombstone
|
||||
/// - *some* m.room.member events, where needed
|
||||
Client(this.clientName,
|
||||
{this.debug = false,
|
||||
this.database,
|
||||
this.enableE2eeRecovery = false,
|
||||
http.Client httpClient}) {
|
||||
this.verificationMethods,
|
||||
http.Client httpClient,
|
||||
this.importantStateEvents,
|
||||
this.pinUnreadRooms = false}) {
|
||||
verificationMethods ??= <KeyVerificationMethod>{};
|
||||
importantStateEvents ??= <String>{};
|
||||
importantStateEvents.addAll([
|
||||
EventTypes.RoomName,
|
||||
EventTypes.RoomAvatar,
|
||||
EventTypes.Message,
|
||||
EventTypes.Encrypted,
|
||||
EventTypes.Encryption,
|
||||
EventTypes.RoomCanonicalAlias,
|
||||
EventTypes.RoomTombstone,
|
||||
]);
|
||||
api = MatrixApi(debug: debug, httpClient: httpClient);
|
||||
onLoginStateChanged.stream.listen((loginState) {
|
||||
if (debug) {
|
||||
|
@ -111,6 +145,12 @@ class Client {
|
|||
String get identityKey => encryption?.identityKey ?? '';
|
||||
String get fingerprintKey => encryption?.fingerprintKey ?? '';
|
||||
|
||||
/// Wheather this session is unknown to others
|
||||
bool get isUnknownSession =>
|
||||
!userDeviceKeys.containsKey(userID) ||
|
||||
!userDeviceKeys[userID].deviceKeys.containsKey(deviceID) ||
|
||||
!userDeviceKeys[userID].deviceKeys[deviceID].signed;
|
||||
|
||||
/// Warning! This endpoint is for testing only!
|
||||
set rooms(List<Room> newList) {
|
||||
print('Warning! This endpoint is for testing only!');
|
||||
|
@ -168,13 +208,12 @@ class Client {
|
|||
if (accountData['m.direct'] != null &&
|
||||
accountData['m.direct'].content[userId] is List<dynamic> &&
|
||||
accountData['m.direct'].content[userId].length > 0) {
|
||||
if (getRoomById(accountData['m.direct'].content[userId][0]) != null) {
|
||||
return accountData['m.direct'].content[userId][0];
|
||||
for (final roomId in accountData['m.direct'].content[userId]) {
|
||||
final room = getRoomById(roomId);
|
||||
if (room != null && room.membership == Membership.join) {
|
||||
return roomId;
|
||||
}
|
||||
}
|
||||
(accountData['m.direct'].content[userId] as List<dynamic>)
|
||||
.remove(accountData['m.direct'].content[userId][0]);
|
||||
api.setAccountData(userId, 'm.direct', directChats);
|
||||
return getDirectChatFromUserId(userId);
|
||||
}
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
if (rooms[i].membership == Membership.invite &&
|
||||
|
@ -427,36 +466,13 @@ class Client {
|
|||
return archiveList;
|
||||
}
|
||||
|
||||
/// Loads the contact list for this user excluding the user itself.
|
||||
/// Currently the contacts are found by discovering the contacts of
|
||||
/// the famedlyContactDiscovery room, which is
|
||||
/// defined by the autojoin room feature in Synapse.
|
||||
Future<List<User>> loadFamedlyContacts() async {
|
||||
var contacts = <User>[];
|
||||
var contactDiscoveryRoom =
|
||||
getRoomByAlias('#famedlyContactDiscovery:${userID.domain}');
|
||||
if (contactDiscoveryRoom != null) {
|
||||
contacts = await contactDiscoveryRoom.requestParticipants();
|
||||
} else {
|
||||
var userMap = <String, bool>{};
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
var roomUsers = rooms[i].getParticipants();
|
||||
for (var j = 0; j < roomUsers.length; j++) {
|
||||
if (userMap[roomUsers[j].id] != true) contacts.add(roomUsers[j]);
|
||||
userMap[roomUsers[j].id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
|
||||
/// Changes the user's displayname.
|
||||
Future<void> setDisplayname(String displayname) =>
|
||||
api.setDisplayname(userID, displayname);
|
||||
|
||||
/// Uploads a new user avatar for this user.
|
||||
Future<void> setAvatar(MatrixFile file) async {
|
||||
final uploadResp = await api.upload(file.bytes, file.path);
|
||||
final uploadResp = await api.upload(file.bytes, file.name);
|
||||
await api.setAvatarUrl(userID, Uri.parse(uploadResp));
|
||||
return;
|
||||
}
|
||||
|
@ -647,11 +663,11 @@ 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);
|
||||
presences = await database.getPresences(id);
|
||||
presences.clear();
|
||||
}
|
||||
|
||||
onLoginStateChanged.add(LoginState.logged);
|
||||
|
@ -676,18 +692,25 @@ class Client {
|
|||
}
|
||||
|
||||
Future<SyncUpdate> _syncRequest;
|
||||
Exception _lastSyncError;
|
||||
|
||||
Future<void> _sync() async {
|
||||
if (isLogged() == false || _disposed) return;
|
||||
try {
|
||||
_syncRequest = api.sync(
|
||||
_syncRequest = api
|
||||
.sync(
|
||||
filter: syncFilters,
|
||||
since: prevBatch,
|
||||
timeout: prevBatch != null ? 30000 : null,
|
||||
);
|
||||
)
|
||||
.catchError((e) {
|
||||
_lastSyncError = e;
|
||||
return null;
|
||||
});
|
||||
if (_disposed) return;
|
||||
final hash = _syncRequest.hashCode;
|
||||
final syncResp = await _syncRequest;
|
||||
if (syncResp == null) throw _lastSyncError;
|
||||
if (hash != _syncRequest.hashCode) return;
|
||||
if (database != null) {
|
||||
await database.transaction(() async {
|
||||
|
@ -715,6 +738,9 @@ class Client {
|
|||
onError.add(exception);
|
||||
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
|
||||
} catch (e, s) {
|
||||
if (isLogged() == false || _disposed) {
|
||||
return;
|
||||
}
|
||||
print('Error during processing events: ' + e.toString());
|
||||
print(s);
|
||||
onSyncError.add(SyncError(
|
||||
|
@ -724,31 +750,26 @@ class Client {
|
|||
}
|
||||
|
||||
/// Use this method only for testing utilities!
|
||||
Future<void> handleSync(SyncUpdate sync) async {
|
||||
Future<void> handleSync(SyncUpdate sync, {bool sortAtTheEnd = false}) async {
|
||||
if (sync.toDevice != null) {
|
||||
await _handleToDeviceEvents(sync.toDevice);
|
||||
}
|
||||
if (sync.rooms != null) {
|
||||
if (sync.rooms.join != null) {
|
||||
await _handleRooms(sync.rooms.join, Membership.join);
|
||||
await _handleRooms(sync.rooms.join, Membership.join,
|
||||
sortAtTheEnd: sortAtTheEnd);
|
||||
}
|
||||
if (sync.rooms.invite != null) {
|
||||
await _handleRooms(sync.rooms.invite, Membership.invite);
|
||||
await _handleRooms(sync.rooms.invite, Membership.invite,
|
||||
sortAtTheEnd: sortAtTheEnd);
|
||||
}
|
||||
if (sync.rooms.leave != null) {
|
||||
await _handleRooms(sync.rooms.leave, Membership.leave);
|
||||
await _handleRooms(sync.rooms.leave, Membership.leave,
|
||||
sortAtTheEnd: sortAtTheEnd);
|
||||
}
|
||||
}
|
||||
if (sync.presence != null) {
|
||||
for (final newPresence in sync.presence) {
|
||||
if (database != null) {
|
||||
await database.storeUserEventUpdate(
|
||||
id,
|
||||
'presence',
|
||||
newPresence.type,
|
||||
newPresence.toJson(),
|
||||
);
|
||||
}
|
||||
presences[newPresence.senderId] = newPresence;
|
||||
onPresence.add(newPresence);
|
||||
}
|
||||
|
@ -756,11 +777,10 @@ class Client {
|
|||
if (sync.accountData != null) {
|
||||
for (final newAccountData in sync.accountData) {
|
||||
if (database != null) {
|
||||
await database.storeUserEventUpdate(
|
||||
await database.storeAccountData(
|
||||
id,
|
||||
'account_data',
|
||||
newAccountData.type,
|
||||
newAccountData.toJson(),
|
||||
jsonEncode(newAccountData.content),
|
||||
);
|
||||
}
|
||||
accountData[newAccountData.type] = newAccountData;
|
||||
|
@ -823,7 +843,8 @@ class Client {
|
|||
}
|
||||
|
||||
Future<void> _handleRooms(
|
||||
Map<String, SyncRoomUpdate> rooms, Membership membership) async {
|
||||
Map<String, SyncRoomUpdate> rooms, Membership membership,
|
||||
{bool sortAtTheEnd = false}) async {
|
||||
for (final entry in rooms.entries) {
|
||||
final id = entry.key;
|
||||
final room = entry.value;
|
||||
|
@ -849,8 +870,11 @@ class Client {
|
|||
handledEvents = true;
|
||||
}
|
||||
if (room.timeline?.events?.isNotEmpty ?? false) {
|
||||
await _handleRoomEvents(id,
|
||||
room.timeline.events.map((i) => i.toJson()).toList(), 'timeline');
|
||||
await _handleRoomEvents(
|
||||
id,
|
||||
room.timeline.events.map((i) => i.toJson()).toList(),
|
||||
sortAtTheEnd ? 'history' : 'timeline',
|
||||
sortAtTheEnd: sortAtTheEnd);
|
||||
handledEvents = true;
|
||||
}
|
||||
if (room.ephemeral?.isNotEmpty ?? false) {
|
||||
|
@ -934,14 +958,16 @@ class Client {
|
|||
}
|
||||
|
||||
Future<void> _handleRoomEvents(
|
||||
String chat_id, List<dynamic> events, String type) async {
|
||||
String chat_id, List<dynamic> events, String type,
|
||||
{bool sortAtTheEnd = false}) async {
|
||||
for (num i = 0; i < events.length; i++) {
|
||||
await _handleEvent(events[i], chat_id, type);
|
||||
await _handleEvent(events[i], chat_id, type, sortAtTheEnd: sortAtTheEnd);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleEvent(
|
||||
Map<String, dynamic> event, String roomID, String type) async {
|
||||
Map<String, dynamic> event, String roomID, String type,
|
||||
{bool sortAtTheEnd = false}) async {
|
||||
if (event['type'] is String && event['content'] is Map<String, dynamic>) {
|
||||
// The client must ignore any new m.room.encryption event to prevent
|
||||
// man-in-the-middle attacks!
|
||||
|
@ -956,7 +982,9 @@ class Client {
|
|||
|
||||
// ephemeral events aren't persisted and don't need a sort order - they are
|
||||
// expected to be processed as soon as they come in
|
||||
final sortOrder = type != 'ephemeral' ? room.newSortOrder : 0.0;
|
||||
final sortOrder = type != 'ephemeral'
|
||||
? (sortAtTheEnd ? room.oldSortOrder : room.newSortOrder)
|
||||
: 0.0;
|
||||
var update = EventUpdate(
|
||||
eventType: event['type'],
|
||||
roomID: roomID,
|
||||
|
@ -967,10 +995,23 @@ class Client {
|
|||
if (event['type'] == EventTypes.Encrypted && encryptionEnabled) {
|
||||
update = await update.decrypt(room);
|
||||
}
|
||||
if (event['type'] == EventTypes.Message &&
|
||||
!room.isDirectChat &&
|
||||
database != null &&
|
||||
room.getState(EventTypes.RoomMember, event['sender']) == null) {
|
||||
// In order to correctly render room list previews we need to fetch the member from the database
|
||||
final user = await database.getUser(id, event['sender'], room);
|
||||
if (user != null) {
|
||||
room.setState(user);
|
||||
}
|
||||
}
|
||||
if (type != 'ephemeral' && database != null) {
|
||||
await database.storeEventUpdate(id, update);
|
||||
}
|
||||
_updateRoomsByEventUpdate(update);
|
||||
if (encryptionEnabled) {
|
||||
await encryption.handleEventUpdate(update);
|
||||
}
|
||||
onEvent.add(update);
|
||||
|
||||
if (event['type'] == 'm.call.invite') {
|
||||
|
@ -1086,16 +1127,23 @@ class Client {
|
|||
BasicRoomEvent.fromJson(eventUpdate.content);
|
||||
}
|
||||
if (rooms[j].onUpdate != null) rooms[j].onUpdate.add(rooms[j].id);
|
||||
if (eventUpdate.type == 'timeline') _sortRooms();
|
||||
if (['timeline', 'account_data'].contains(eventUpdate.type)) _sortRooms();
|
||||
}
|
||||
|
||||
bool _sortLock = false;
|
||||
|
||||
/// If [true] then unread rooms are pinned at the top of the room list.
|
||||
bool pinUnreadRooms;
|
||||
|
||||
/// The compare function how the rooms should be sorted internally. By default
|
||||
/// rooms are sorted by timestamp of the last m.room.message event or the last
|
||||
/// event if there is no known message.
|
||||
RoomSorter sortRoomsBy = (a, b) => b.timeCreated.millisecondsSinceEpoch
|
||||
.compareTo(a.timeCreated.millisecondsSinceEpoch);
|
||||
RoomSorter get sortRoomsBy => (a, b) => (a.isFavourite != b.isFavourite)
|
||||
? (a.isFavourite ? -1 : 1)
|
||||
: (pinUnreadRooms && a.notificationCount != b.notificationCount)
|
||||
? b.notificationCount.compareTo(a.notificationCount)
|
||||
: b.timeCreated.millisecondsSinceEpoch
|
||||
.compareTo(a.timeCreated.millisecondsSinceEpoch);
|
||||
|
||||
void _sortRooms() {
|
||||
if (prevBatch == null || _sortLock || rooms.length < 2) return;
|
||||
|
@ -1138,7 +1186,7 @@ class Client {
|
|||
var outdatedLists = <String, dynamic>{};
|
||||
for (var userId in trackedUserIds) {
|
||||
if (!userDeviceKeys.containsKey(userId)) {
|
||||
_userDeviceKeys[userId] = DeviceKeysList(userId);
|
||||
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
|
||||
}
|
||||
var deviceKeysList = userDeviceKeys[userId];
|
||||
if (deviceKeysList.outdated) {
|
||||
|
@ -1154,7 +1202,7 @@ class Client {
|
|||
for (final rawDeviceKeyListEntry in response.deviceKeys.entries) {
|
||||
final userId = rawDeviceKeyListEntry.key;
|
||||
if (!userDeviceKeys.containsKey(userId)) {
|
||||
_userDeviceKeys[userId] = DeviceKeysList(userId);
|
||||
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
|
||||
}
|
||||
final oldKeys =
|
||||
Map<String, DeviceKeys>.from(_userDeviceKeys[userId].deviceKeys);
|
||||
|
@ -1163,34 +1211,45 @@ 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.verified = true;
|
||||
entry.setDirectVerified(true);
|
||||
}
|
||||
} else {
|
||||
// This shouldn't ever happen. The same device ID has gotten
|
||||
// a new public key. So we ignore the update. TODO: ask krille
|
||||
// if we should instead use the new key with unknown verified / blocked status
|
||||
_userDeviceKeys[userId].deviceKeys[deviceId] =
|
||||
oldKeys[deviceId];
|
||||
}
|
||||
if (database != null) {
|
||||
dbActions.add(() => database.storeUserDeviceKey(
|
||||
id,
|
||||
userId,
|
||||
deviceId,
|
||||
json.encode(_userDeviceKeys[userId]
|
||||
.deviceKeys[deviceId]
|
||||
.toJson()),
|
||||
_userDeviceKeys[userId].deviceKeys[deviceId].verified,
|
||||
_userDeviceKeys[userId].deviceKeys[deviceId].blocked,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
_userDeviceKeys[userId].deviceKeys[deviceId] = oldKeys[deviceId];
|
||||
}
|
||||
if (database != null) {
|
||||
dbActions.add(() => database.storeUserDeviceKey(
|
||||
id,
|
||||
userId,
|
||||
deviceId,
|
||||
json.encode(entry.toJson()),
|
||||
entry.directVerified,
|
||||
entry.blocked,
|
||||
));
|
||||
}
|
||||
}
|
||||
// delete old/unused entries
|
||||
if (database != null) {
|
||||
for (final oldDeviceKeyEntry in oldKeys.entries) {
|
||||
final deviceId = oldDeviceKeyEntry.key;
|
||||
|
@ -1207,6 +1266,71 @@ class Client {
|
|||
.add(() => database.storeUserDeviceKeysInfo(id, userId, false));
|
||||
}
|
||||
}
|
||||
// next we parse and persist the cross signing keys
|
||||
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 crossSigningKeyListEntry in keys.entries) {
|
||||
final userId = crossSigningKeyListEntry.key;
|
||||
if (!userDeviceKeys.containsKey(userId)) {
|
||||
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
|
||||
}
|
||||
final oldKeys = Map<String, CrossSigningKey>.from(
|
||||
_userDeviceKeys[userId].crossSigningKeys);
|
||||
_userDeviceKeys[userId].crossSigningKeys = {};
|
||||
// add the types we aren't handling atm back
|
||||
for (final oldEntry in oldKeys.entries) {
|
||||
if (!oldEntry.value.usage.contains(keyType)) {
|
||||
_userDeviceKeys[userId].crossSigningKeys[oldEntry.key] =
|
||||
oldEntry.value;
|
||||
}
|
||||
}
|
||||
final entry = CrossSigningKey.fromMatrixCrossSigningKey(
|
||||
crossSigningKeyListEntry.value, this);
|
||||
if (entry.isValid) {
|
||||
final publicKey = entry.publicKey;
|
||||
if (!oldKeys.containsKey(publicKey) ||
|
||||
oldKeys[publicKey].ed25519Key == entry.ed25519Key) {
|
||||
if (oldKeys.containsKey(publicKey)) {
|
||||
// be sure to save the verification status
|
||||
entry.setDirectVerified(oldKeys[publicKey].directVerified);
|
||||
entry.blocked = oldKeys[publicKey].blocked;
|
||||
entry.validSignatures = oldKeys[publicKey].validSignatures;
|
||||
}
|
||||
_userDeviceKeys[userId].crossSigningKeys[publicKey] = entry;
|
||||
} else {
|
||||
// This shouldn't ever happen. The same device ID has gotten
|
||||
// a new public key. So we ignore the update. TODO: ask krille
|
||||
// if we should instead use the new key with unknown verified / blocked status
|
||||
_userDeviceKeys[userId].crossSigningKeys[publicKey] =
|
||||
oldKeys[publicKey];
|
||||
}
|
||||
if (database != null) {
|
||||
dbActions.add(() => database.storeUserCrossSigningKey(
|
||||
id,
|
||||
userId,
|
||||
publicKey,
|
||||
json.encode(entry.toJson()),
|
||||
entry.directVerified,
|
||||
entry.blocked,
|
||||
));
|
||||
}
|
||||
}
|
||||
_userDeviceKeys[userId].outdated = false;
|
||||
if (database != null) {
|
||||
dbActions.add(
|
||||
() => database.storeUserDeviceKeysInfo(id, userId, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await database?.transaction(() async {
|
||||
for (final f in dbActions) {
|
||||
|
@ -1226,12 +1350,16 @@ class Client {
|
|||
Map<String, dynamic> message, {
|
||||
bool encrypted = true,
|
||||
List<User> toUsers,
|
||||
bool onlyVerified = false,
|
||||
}) async {
|
||||
if (encrypted && !encryptionEnabled) return;
|
||||
// Don't send this message to blocked devices.
|
||||
// Don't send this message to blocked devices, and if specified onlyVerified
|
||||
// then only send it to verified devices
|
||||
if (deviceKeys.isNotEmpty) {
|
||||
deviceKeys.removeWhere((DeviceKeys deviceKeys) =>
|
||||
deviceKeys.blocked || deviceKeys.deviceId == deviceID);
|
||||
deviceKeys.blocked ||
|
||||
deviceKeys.deviceId == deviceID ||
|
||||
(onlyVerified && !deviceKeys.verified));
|
||||
if (deviceKeys.isEmpty) return;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,10 @@ part 'database.g.dart';
|
|||
class Database extends _$Database {
|
||||
Database(QueryExecutor e) : super(e);
|
||||
|
||||
Database.connect(DatabaseConnection connection) : super.connect(connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
int get schemaVersion => 5;
|
||||
|
||||
int get maxFileSize => 1 * 1024 * 1024;
|
||||
|
||||
|
@ -44,6 +46,28 @@ class Database extends _$Database {
|
|||
if (from == 2) {
|
||||
await m.deleteTable('outbound_group_sessions');
|
||||
await m.createTable(outboundGroupSessions);
|
||||
from++;
|
||||
}
|
||||
if (from == 3) {
|
||||
await m.createTable(userCrossSigningKeys);
|
||||
await m.createTable(ssssCache);
|
||||
// mark all keys as outdated so that the cross signing keys will be fetched
|
||||
await m.issueCustomQuery(
|
||||
'UPDATE user_device_keys SET outdated = true');
|
||||
from++;
|
||||
}
|
||||
if (from == 4) {
|
||||
await m.addColumn(olmSessions, olmSessions.lastReceived);
|
||||
from++;
|
||||
}
|
||||
},
|
||||
beforeOpen: (_) async {
|
||||
if (executor.dialect == SqlDialect.sqlite) {
|
||||
final ret = await customSelect('PRAGMA journal_mode=WAL').get();
|
||||
if (ret.isNotEmpty) {
|
||||
print('[Moor] Switched database to mode ' +
|
||||
ret.first.data['journal_mode'].toString());
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -55,16 +79,20 @@ class Database extends _$Database {
|
|||
}
|
||||
|
||||
Future<Map<String, sdk.DeviceKeysList>> getUserDeviceKeys(
|
||||
int clientId) async {
|
||||
final deviceKeys = await getAllUserDeviceKeys(clientId).get();
|
||||
sdk.Client client) async {
|
||||
final deviceKeys = await getAllUserDeviceKeys(client.id).get();
|
||||
if (deviceKeys.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
final deviceKeysKeys = await getAllUserDeviceKeysKeys(clientId).get();
|
||||
final deviceKeysKeys = await getAllUserDeviceKeysKeys(client.id).get();
|
||||
final crossSigningKeys = await getAllUserCrossSigningKeys(client.id).get();
|
||||
final res = <String, sdk.DeviceKeysList>{};
|
||||
for (final entry in deviceKeys) {
|
||||
res[entry.userId] = sdk.DeviceKeysList.fromDb(entry,
|
||||
deviceKeysKeys.where((k) => k.userId == entry.userId).toList());
|
||||
res[entry.userId] = sdk.DeviceKeysList.fromDb(
|
||||
entry,
|
||||
deviceKeysKeys.where((k) => k.userId == entry.userId).toList(),
|
||||
crossSigningKeys.where((k) => k.userId == entry.userId).toList(),
|
||||
client);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
@ -91,22 +119,6 @@ class Database extends _$Database {
|
|||
return res;
|
||||
}
|
||||
|
||||
Future<List<olm.Session>> getSingleOlmSessions(
|
||||
int clientId, String identityKey, String userId) async {
|
||||
final rows = await dbGetOlmSessions(clientId, identityKey).get();
|
||||
final res = <olm.Session>[];
|
||||
for (final row in rows) {
|
||||
try {
|
||||
var session = olm.Session();
|
||||
session.unpickle(userId, row.pickle);
|
||||
res.add(session);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<DbOutboundGroupSession> getDbOutboundGroupSession(
|
||||
int clientId, String roomId) async {
|
||||
final res = await dbGetOutboundGroupSession(clientId, roomId).get();
|
||||
|
@ -131,6 +143,14 @@ class Database extends _$Database {
|
|||
return res.first;
|
||||
}
|
||||
|
||||
Future<DbSSSSCache> getSSSSCache(int clientId, String type) async {
|
||||
final res = await dbGetSSSSCache(clientId, type).get();
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return res.first;
|
||||
}
|
||||
|
||||
Future<List<sdk.Room>> getRoomList(sdk.Client client,
|
||||
{bool onlyLeft = false}) async {
|
||||
final res = await (select(rooms)
|
||||
|
@ -138,9 +158,12 @@ class Database extends _$Database {
|
|||
? t.membership.equals('leave')
|
||||
: t.membership.equals('leave').not()))
|
||||
.get();
|
||||
final resStates = await getAllRoomStates(client.id).get();
|
||||
final resStates = await getImportantRoomStates(
|
||||
client.id, client.importantStateEvents.toList())
|
||||
.get();
|
||||
final resAccountData = await getAllRoomAccountData(client.id).get();
|
||||
final roomList = <sdk.Room>[];
|
||||
final allMembersToPostload = <String, Set<String>>{};
|
||||
for (final r in res) {
|
||||
final room = await sdk.Room.getRoomFromTableRow(
|
||||
r,
|
||||
|
@ -149,6 +172,81 @@ class Database extends _$Database {
|
|||
roomAccountData: resAccountData.where((rs) => rs.roomId == r.roomId),
|
||||
);
|
||||
roomList.add(room);
|
||||
// let's see if we need any m.room.member events
|
||||
final membersToPostload = <String>{};
|
||||
// the lastEvent message preview might have an author we need to fetch, if it is a group chat
|
||||
if (room.getState(EventTypes.Message) != null && !room.isDirectChat) {
|
||||
membersToPostload.add(room.getState(EventTypes.Message).senderId);
|
||||
}
|
||||
// if the room has no name and no canonical alias, its name is calculated
|
||||
// based on the heroes of the room
|
||||
if (room.getState(EventTypes.RoomName) == null &&
|
||||
room.getState(EventTypes.RoomCanonicalAlias) == null &&
|
||||
room.mHeroes != null) {
|
||||
// we don't have a name and no canonical alias, so we'll need to
|
||||
// post-load the heroes
|
||||
membersToPostload.addAll(room.mHeroes.where((h) => h.isNotEmpty));
|
||||
}
|
||||
// okay, only load from the database if we actually have stuff to load
|
||||
if (membersToPostload.isNotEmpty) {
|
||||
// save it for loading later
|
||||
allMembersToPostload[room.id] = membersToPostload;
|
||||
}
|
||||
}
|
||||
// now we postload all members, if thre are any
|
||||
if (allMembersToPostload.isNotEmpty) {
|
||||
// we will generate a query to fetch as many events as possible at once, as that
|
||||
// significantly improves performance. However, to prevent too large queries from being constructed,
|
||||
// we limit to only fetching 500 rooms at once.
|
||||
// This value might be fine-tune-able to be larger (and thus increase performance more for very large accounts),
|
||||
// however this very conservative value should be on the safe side.
|
||||
const MAX_ROOMS_PER_QUERY = 500;
|
||||
// as we iterate over our entries in separate chunks one-by-one we use an iterator
|
||||
// which persists accross the chunks, and thus we just re-sume iteration at the place
|
||||
// we prreviously left off.
|
||||
final entriesIterator = allMembersToPostload.entries.iterator;
|
||||
// now we iterate over all our 500-room-chunks...
|
||||
for (var i = 0;
|
||||
i < allMembersToPostload.keys.length;
|
||||
i += MAX_ROOMS_PER_QUERY) {
|
||||
// query the current chunk and build the query
|
||||
final membersRes = await (select(roomStates)
|
||||
..where((s) {
|
||||
// all chunks have to have the reight client id and must be of type `m.room.member`
|
||||
final basequery = s.clientId.equals(client.id) &
|
||||
s.type.equals('m.room.member');
|
||||
// this is where the magic happens. Here we build a query with the form
|
||||
// OR room_id = '!roomId1' AND state_key IN ('@member') OR room_id = '!roomId2' AND state_key IN ('@member')
|
||||
// subqueries holds our query fragment
|
||||
Expression<bool> subqueries;
|
||||
// here we iterate over our chunk....we musn't forget to progress our iterator!
|
||||
// we must check for if our chunk is done *before* progressing the
|
||||
// iterator, else we might progress it twice around chunk edges, missing on rooms
|
||||
for (var j = 0;
|
||||
j < MAX_ROOMS_PER_QUERY && entriesIterator.moveNext();
|
||||
j++) {
|
||||
final entry = entriesIterator.current;
|
||||
// builds room_id = '!roomId1' AND state_key IN ('@member')
|
||||
final q =
|
||||
s.roomId.equals(entry.key) & s.stateKey.isIn(entry.value);
|
||||
// adds it either as the start of subqueries or as a new OR condition to it
|
||||
if (subqueries == null) {
|
||||
subqueries = q;
|
||||
} else {
|
||||
subqueries = subqueries | q;
|
||||
}
|
||||
}
|
||||
// combinde the basequery with the subquery together, giving our final query
|
||||
return basequery & subqueries;
|
||||
}))
|
||||
.get();
|
||||
// now that we got all the entries from the database, set them as room states
|
||||
for (final dbMember in membersRes) {
|
||||
final room = roomList.firstWhere((r) => r.id == dbMember.roomId);
|
||||
final event = sdk.Event.fromDb(dbMember, room);
|
||||
room.setState(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
return roomList;
|
||||
}
|
||||
|
@ -157,7 +255,14 @@ class Database extends _$Database {
|
|||
final newAccountData = <String, api.BasicEvent>{};
|
||||
final rawAccountData = await getAllAccountData(clientId).get();
|
||||
for (final d in rawAccountData) {
|
||||
final content = sdk.Event.getMapFromPayload(d.content);
|
||||
var content = sdk.Event.getMapFromPayload(d.content);
|
||||
// there was a bug where it stored the entire event, not just the content
|
||||
// in the databse. This is the temporary fix for those affected by the bug
|
||||
if (content['content'] is Map && content['type'] is String) {
|
||||
content = content['content'];
|
||||
// and save
|
||||
await storeAccountData(clientId, d.type, jsonEncode(content));
|
||||
}
|
||||
newAccountData[d.type] = api.BasicEvent(
|
||||
content: content,
|
||||
type: d.type,
|
||||
|
@ -166,22 +271,6 @@ class Database extends _$Database {
|
|||
return newAccountData;
|
||||
}
|
||||
|
||||
Future<Map<String, api.Presence>> getPresences(int clientId) async {
|
||||
final newPresences = <String, api.Presence>{};
|
||||
final rawPresences = await getAllPresences(clientId).get();
|
||||
for (final d in rawPresences) {
|
||||
// TODO: Why is this not working?
|
||||
try {
|
||||
final content = sdk.Event.getMapFromPayload(d.content);
|
||||
var presence = api.Presence.fromJson(content);
|
||||
presence.senderId = d.sender;
|
||||
presence.type = d.type;
|
||||
newPresences[d.sender] = api.Presence.fromJson(content);
|
||||
} catch (_) {}
|
||||
}
|
||||
return newPresences;
|
||||
}
|
||||
|
||||
/// Stores a RoomUpdate object in the database. Must be called inside of
|
||||
/// [transaction].
|
||||
final Set<String> _ensuredRooms = {};
|
||||
|
@ -246,23 +335,6 @@ class Database extends _$Database {
|
|||
}
|
||||
}
|
||||
|
||||
/// Stores an UserUpdate object in the database. Must be called inside of
|
||||
/// [transaction].
|
||||
Future<void> storeUserEventUpdate(
|
||||
int clientId,
|
||||
String type,
|
||||
String eventType,
|
||||
Map<String, dynamic> content,
|
||||
) async {
|
||||
if (type == 'account_data') {
|
||||
await storeAccountData(
|
||||
clientId, eventType, json.encode(content['content']));
|
||||
} else if (type == 'presence') {
|
||||
await storePresence(clientId, eventType, content['sender'],
|
||||
json.encode(content['content']));
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores an EventUpdate object in the database. Must be called inside of
|
||||
/// [transaction].
|
||||
Future<void> storeEventUpdate(
|
||||
|
@ -322,7 +394,7 @@ class Database extends _$Database {
|
|||
|
||||
// is there a transaction id? Then delete the event with this id.
|
||||
if (status != -1 &&
|
||||
eventUpdate.content.containsKey('unsigned') &&
|
||||
eventUpdate.content['unsigned'] is Map &&
|
||||
eventUpdate.content['unsigned']['transaction_id'] is String) {
|
||||
await removeEvent(clientId,
|
||||
eventUpdate.content['unsigned']['transaction_id'], chatId);
|
||||
|
@ -436,11 +508,16 @@ class Database extends _$Database {
|
|||
await (delete(inboundGroupSessions)
|
||||
..where((r) => r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(ssssCache)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(olmSessions)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(userCrossSigningKeys)
|
||||
..where((r) => r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(ssssCache)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(clients)..where((r) => r.clientId.equals(clientId))).go();
|
||||
}
|
||||
|
||||
|
@ -452,6 +529,15 @@ class Database extends _$Database {
|
|||
return sdk.Event.fromDb(res.first, room).asUser;
|
||||
}
|
||||
|
||||
Future<List<sdk.User>> getUsers(int clientId, sdk.Room room) async {
|
||||
final res = await dbGetUsers(clientId, room.id).get();
|
||||
final ret = <sdk.User>[];
|
||||
for (final r in res) {
|
||||
ret.add(sdk.Event.fromDb(r, room).asUser);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Future<List<sdk.Event>> getEventList(int clientId, sdk.Room room) async {
|
||||
final res = await dbGetEventList(clientId, room.id).get();
|
||||
final eventList = <sdk.Event>[];
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -32,11 +32,23 @@ CREATE TABLE user_device_keys_key (
|
|||
) as DbUserDeviceKeysKey;
|
||||
CREATE INDEX user_device_keys_key_index ON user_device_keys_key(client_id);
|
||||
|
||||
CREATE TABLE user_cross_signing_keys (
|
||||
client_id INTEGER NOT NULL REFERENCES clients(client_id),
|
||||
user_id TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
verified BOOLEAN DEFAULT false,
|
||||
blocked BOOLEAN DEFAULT false,
|
||||
UNIQUE(client_id, user_id, public_key)
|
||||
) as DbUserCrossSigningKey;
|
||||
CREATE INDEX user_cross_signing_keys_index ON user_cross_signing_keys(client_id);
|
||||
|
||||
CREATE TABLE olm_sessions (
|
||||
client_id INTEGER NOT NULL REFERENCES clients(client_id),
|
||||
identity_key TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
pickle TEXT NOT NULL,
|
||||
last_received DATETIME,
|
||||
UNIQUE(client_id, identity_key, session_id)
|
||||
) AS DbOlmSessions;
|
||||
CREATE INDEX olm_sessions_index ON olm_sessions(client_id);
|
||||
|
@ -63,6 +75,15 @@ CREATE TABLE inbound_group_sessions (
|
|||
) AS DbInboundGroupSession;
|
||||
CREATE INDEX inbound_group_sessions_index ON inbound_group_sessions(client_id);
|
||||
|
||||
CREATE TABLE ssss_cache (
|
||||
client_id INTEGER NOT NULL REFERENCES clients(client_id),
|
||||
type TEXT NOT NULL,
|
||||
key_id TEXT NOT NULL,
|
||||
ciphertext TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
UNIQUE(client_id, type)
|
||||
) AS DbSSSSCache;
|
||||
|
||||
CREATE TABLE rooms (
|
||||
client_id INTEGER NOT NULL REFERENCES clients(client_id),
|
||||
room_id TEXT NOT NULL,
|
||||
|
@ -154,9 +175,10 @@ updateClientKeys: UPDATE clients SET olm_account = :olm_account WHERE client_id
|
|||
storePrevBatch: UPDATE clients SET prev_batch = :prev_batch WHERE client_id = :client_id;
|
||||
getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id;
|
||||
getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key WHERE client_id = :client_id;
|
||||
getAllUserCrossSigningKeys: SELECT * FROM user_cross_signing_keys WHERE client_id = :client_id;
|
||||
getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id;
|
||||
dbGetOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key;
|
||||
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle);
|
||||
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle, last_received) VALUES (:client_id, :identitiy_key, :session_id, :pickle, :last_received);
|
||||
getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id;
|
||||
dbGetOutboundGroupSession: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
|
||||
storeOutboundGroupSession: INSERT OR REPLACE INTO outbound_group_sessions (client_id, room_id, pickle, device_ids, creation_time, sent_messages) VALUES (:client_id, :room_id, :pickle, :device_ids, :creation_time, :sent_messages);
|
||||
|
@ -171,25 +193,34 @@ setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified W
|
|||
setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
||||
storeUserDeviceKey: INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, verified, blocked) VALUES (:client_id, :user_id, :device_id, :content, :verified, :blocked);
|
||||
removeUserDeviceKey: DELETE FROM user_device_keys_key WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
||||
setVerifiedUserCrossSigningKey: UPDATE user_cross_signing_keys SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key;
|
||||
setBlockedUserCrossSigningKey: UPDATE user_cross_signing_keys SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key;
|
||||
storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :verified, :blocked);
|
||||
removeUserCrossSigningKey: DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key;
|
||||
storeSSSSCache: INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, ciphertext, content) VALUES (:client_id, :type, :key_id, :ciphertext, :content);
|
||||
dbGetSSSSCache: SELECT * FROM ssss_cache WHERE client_id = :client_id AND type = :type;
|
||||
clearSSSSCache: DELETE FROM ssss_cache WHERE client_id = :client_id;
|
||||
insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account);
|
||||
ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership);
|
||||
setRoomPrevBatch: UPDATE rooms SET prev_batch = :prev_batch WHERE client_id = :client_id AND room_id = :room_id;
|
||||
updateRoomSortOrder: UPDATE rooms SET oldest_sort_order = :oldest_sort_order, newest_sort_order = :newest_sort_order WHERE client_id = :client_id AND room_id = :room_id;
|
||||
getAllAccountData: SELECT * FROM account_data WHERE client_id = :client_id;
|
||||
storeAccountData: INSERT OR REPLACE INTO account_data (client_id, type, content) VALUES (:client_id, :type, :content);
|
||||
getAllPresences: SELECT * FROM presences WHERE client_id = :client_id;
|
||||
storePresence: INSERT OR REPLACE INTO presences (client_id, type, sender, content) VALUES (:client_id, :type, :sender, :content);
|
||||
updateEvent: UPDATE events SET unsigned = :unsigned, content = :content, prev_content = :prev_content WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
||||
updateEventStatus: UPDATE events SET status = :status, event_id = :new_event_id WHERE client_id = :client_id AND event_id = :old_event_id AND room_id = :room_id;
|
||||
getImportantRoomStates: SELECT * FROM room_states WHERE client_id = :client_id AND type IN :events;
|
||||
getAllRoomStates: SELECT * FROM room_states WHERE client_id = :client_id;
|
||||
getUnimportantRoomStatesForRoom: SELECT * FROM room_states WHERE client_id = :client_id AND room_id = :room_id AND type NOT IN :events;
|
||||
storeEvent: INSERT OR REPLACE INTO events (client_id, event_id, room_id, sort_order, origin_server_ts, sender, type, unsigned, content, prev_content, state_key, status) VALUES (:client_id, :event_id, :room_id, :sort_order, :origin_server_ts, :sender, :type, :unsigned, :content, :prev_content, :state_key, :status);
|
||||
storeRoomState: INSERT OR REPLACE INTO room_states (client_id, event_id, room_id, sort_order, origin_server_ts, sender, type, unsigned, content, prev_content, state_key) VALUES (:client_id, :event_id, :room_id, :sort_order, :origin_server_ts, :sender, :type, :unsigned, :content, :prev_content, :state_key);
|
||||
getAllRoomAccountData: SELECT * FROM room_account_data WHERE client_id = :client_id;
|
||||
storeRoomAccountData: INSERT OR REPLACE INTO room_account_data (client_id, type, room_id, content) VALUES (:client_id, :type, :room_id, :content);
|
||||
dbGetUser: SELECT * FROM room_states WHERE client_id = :client_id AND type = 'm.room.member' AND state_key = :state_key AND room_id = :room_id;
|
||||
dbGetUsers: SELECT * FROM room_states WHERE client_id = :client_id AND type = 'm.room.member' AND room_id = :room_id;
|
||||
dbGetEventList: SELECT * FROM events WHERE client_id = :client_id AND room_id = :room_id GROUP BY event_id ORDER BY sort_order DESC;
|
||||
getStates: SELECT * FROM room_states WHERE client_id = :client_id AND room_id = :room_id;
|
||||
resetNotificationCount: UPDATE rooms SET notification_count = 0, highlight_count = 0 WHERE client_id = :client_id AND room_id = :room_id;
|
||||
getRoom: SELECT * FROM rooms WHERE client_id = :client_id AND room_id = :room_id;
|
||||
getEvent: SELECT * FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
||||
removeEvent: DELETE FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
||||
removeRoom: DELETE FROM rooms WHERE client_id = :client_id AND room_id = :room_id;
|
||||
|
|
|
@ -60,7 +60,7 @@ class Event extends MatrixEvent {
|
|||
|
||||
/// Optional. The event that redacted this event, if any. Otherwise null.
|
||||
Event get redactedBecause =>
|
||||
unsigned != null && unsigned.containsKey('redacted_because')
|
||||
unsigned != null && unsigned['redacted_because'] is Map
|
||||
? Event.fromJson(unsigned['redacted_because'], room)
|
||||
: null;
|
||||
|
||||
|
@ -89,7 +89,13 @@ class Event extends MatrixEvent {
|
|||
this.roomId = roomId ?? room?.id;
|
||||
this.senderId = senderId;
|
||||
this.unsigned = unsigned;
|
||||
this.prevContent = prevContent;
|
||||
// synapse unfortunatley isn't following the spec and tosses the prev_content
|
||||
// into the unsigned block
|
||||
this.prevContent = prevContent != null && prevContent.isNotEmpty
|
||||
? prevContent
|
||||
: (unsigned != null && unsigned['prev_content'] is Map
|
||||
? unsigned['prev_content']
|
||||
: null);
|
||||
this.stateKey = stateKey;
|
||||
this.originServerTs = originServerTs;
|
||||
}
|
||||
|
@ -206,7 +212,7 @@ class Event extends MatrixEvent {
|
|||
unsigned: unsigned,
|
||||
room: room);
|
||||
|
||||
String get messageType => (content.containsKey('m.relates_to') &&
|
||||
String get messageType => (content['m.relates_to'] is Map &&
|
||||
content['m.relates_to']['m.in_reply_to'] != null)
|
||||
? MessageTypes.Reply
|
||||
: content['msgtype'] ?? MessageTypes.Text;
|
||||
|
@ -307,7 +313,10 @@ class Event extends MatrixEvent {
|
|||
Future<String> sendAgain({String txid}) async {
|
||||
if (status != -1) return null;
|
||||
await remove();
|
||||
final eventID = await room.sendTextEvent(text, txid: txid);
|
||||
final eventID = await room.sendEvent(
|
||||
content,
|
||||
txid: txid ?? unsigned['transaction_id'],
|
||||
);
|
||||
return eventID;
|
||||
}
|
||||
|
||||
|
@ -350,8 +359,8 @@ class Event extends MatrixEvent {
|
|||
|
||||
bool get hasThumbnail =>
|
||||
content['info'] is Map<String, dynamic> &&
|
||||
(content['info'].containsKey('thumbnail_url') ||
|
||||
content['info'].containsKey('thumbnail_file'));
|
||||
(content['info']['thumbnail_url'] is String ||
|
||||
content['info']['thumbnail_file'] is Map);
|
||||
|
||||
/// Downloads (and decryptes if necessary) the attachment of this
|
||||
/// event and returns it as a [MatrixFile]. If this event doesn't
|
||||
|
@ -363,16 +372,16 @@ class Event extends MatrixEvent {
|
|||
throw ("This event has the type '$type' and so it can't contain an attachment.");
|
||||
}
|
||||
if (!getThumbnail &&
|
||||
!content.containsKey('url') &&
|
||||
!content.containsKey('file')) {
|
||||
!(content['url'] is String) &&
|
||||
!(content['file'] is Map)) {
|
||||
throw ("This event hasn't any attachment.");
|
||||
}
|
||||
if (getThumbnail && !hasThumbnail) {
|
||||
throw ("This event hasn't any thumbnail.");
|
||||
}
|
||||
final isEncrypted = getThumbnail
|
||||
? !content['info'].containsKey('thumbnail_url')
|
||||
: !content.containsKey('url');
|
||||
? !(content['info']['thumbnail_url'] is String)
|
||||
: !(content['url'] is String);
|
||||
|
||||
if (isEncrypted && !room.client.encryptionEnabled) {
|
||||
throw ('Encryption is not enabled in your Client.');
|
||||
|
@ -421,7 +430,7 @@ class Event extends MatrixEvent {
|
|||
encryptedFile.sha256 = fileMap['hashes']['sha256'];
|
||||
uint8list = await decryptFile(encryptedFile);
|
||||
}
|
||||
return MatrixFile(bytes: uint8list, path: '/$body');
|
||||
return MatrixFile(bytes: uint8list, name: body);
|
||||
}
|
||||
|
||||
/// Returns a localized String representation of this event. For a
|
||||
|
|
|
@ -25,9 +25,7 @@ import 'package:famedlysdk/src/event.dart';
|
|||
import 'package:famedlysdk/src/utils/event_update.dart';
|
||||
import 'package:famedlysdk/src/utils/room_update.dart';
|
||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
import 'package:image/image.dart';
|
||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||
import 'package:mime_type/mime_type.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
|
||||
import './user.dart';
|
||||
|
@ -81,7 +79,11 @@ class Room {
|
|||
double _oldestSortOrder;
|
||||
|
||||
double get newSortOrder {
|
||||
_newestSortOrder++;
|
||||
var now = DateTime.now().millisecondsSinceEpoch.toDouble();
|
||||
if (_newestSortOrder >= now) {
|
||||
now = _newestSortOrder + 1;
|
||||
}
|
||||
_newestSortOrder = now;
|
||||
return _newestSortOrder;
|
||||
}
|
||||
|
||||
|
@ -99,6 +101,25 @@ class Room {
|
|||
_oldestSortOrder, _newestSortOrder, client.id, id);
|
||||
}
|
||||
|
||||
/// Flag if the room is partial, meaning not all state events have been loaded yet
|
||||
bool partial = true;
|
||||
|
||||
/// Load all the missing state events for the room from the database. If the room has already been loaded, this does nothing.
|
||||
Future<void> postLoad() async {
|
||||
if (!partial || client.database == null) {
|
||||
return;
|
||||
}
|
||||
final allStates = await client.database
|
||||
.getUnimportantRoomStatesForRoom(
|
||||
client.id, id, client.importantStateEvents.toList())
|
||||
.get();
|
||||
for (final state in allStates) {
|
||||
final newState = Event.fromDb(state, this);
|
||||
setState(newState);
|
||||
}
|
||||
partial = false;
|
||||
}
|
||||
|
||||
/// Returns the [Event] for the given [typeKey] and optional [stateKey].
|
||||
/// If no [stateKey] is provided, it defaults to an empty string.
|
||||
Event getState(String typeKey, [String stateKey = '']) =>
|
||||
|
@ -152,6 +173,14 @@ class Room {
|
|||
? states[EventTypes.RoomName].content['name']
|
||||
: '';
|
||||
|
||||
/// The pinned events for this room. If there are no this returns an empty
|
||||
/// list.
|
||||
List<String> get pinnedEventIds => states[EventTypes.RoomPinnedEvents] != null
|
||||
? (states[EventTypes.RoomPinnedEvents].content['pinned'] is List<String>
|
||||
? states[EventTypes.RoomPinnedEvents].content['pinned']
|
||||
: <String>[])
|
||||
: <String>[];
|
||||
|
||||
/// Returns a localized displayname for this server. If the room is a groupchat
|
||||
/// without a name, then it will return the localized version of 'Group with Alice' instead
|
||||
/// of just 'Alice' to make it different to a direct chat.
|
||||
|
@ -353,6 +382,48 @@ class Room {
|
|||
{'topic': newName},
|
||||
);
|
||||
|
||||
/// Add a tag to the room.
|
||||
Future<void> addTag(String tag, {double order}) => client.api.addRoomTag(
|
||||
client.userID,
|
||||
id,
|
||||
tag,
|
||||
order: order,
|
||||
);
|
||||
|
||||
/// Removes a tag from the room.
|
||||
Future<void> removeTag(String tag) => client.api.removeRoomTag(
|
||||
client.userID,
|
||||
id,
|
||||
tag,
|
||||
);
|
||||
|
||||
/// Returns all tags for this room.
|
||||
Map<String, Tag> get tags {
|
||||
if (roomAccountData['m.tag'] == null ||
|
||||
!(roomAccountData['m.tag'].content['tags'] is Map)) {
|
||||
return {};
|
||||
}
|
||||
final tags = (roomAccountData['m.tag'].content['tags'] as Map)
|
||||
.map((k, v) => MapEntry<String, Tag>(k, Tag.fromJson(v)));
|
||||
tags.removeWhere((k, v) => !TagType.isValid(k));
|
||||
return tags;
|
||||
}
|
||||
|
||||
/// Returns true if this room has a m.favourite tag.
|
||||
bool get isFavourite => tags[TagType.Favourite] != null;
|
||||
|
||||
/// Sets the m.favourite tag for this room.
|
||||
Future<void> setFavourite(bool favourite) =>
|
||||
favourite ? addTag(TagType.Favourite) : removeTag(TagType.Favourite);
|
||||
|
||||
/// Call the Matrix API to change the pinned events of this room.
|
||||
Future<String> setPinnedEvents(List<String> pinnedEventIds) =>
|
||||
client.api.sendState(
|
||||
id,
|
||||
EventTypes.RoomPinnedEvents,
|
||||
{'pinned': pinnedEventIds},
|
||||
);
|
||||
|
||||
/// return all current emote packs for this room
|
||||
Map<String, Map<String, String>> get emotePacks {
|
||||
final packs = <String, Map<String, String>>{};
|
||||
|
@ -411,7 +482,9 @@ class Room {
|
|||
final event = room.getState('im.ponies.room_emotes', stateKey);
|
||||
if (event != null && stateKeyEntry.value is Map) {
|
||||
addEmotePack(
|
||||
room.canonicalAlias.isEmpty ? room.id : canonicalAlias,
|
||||
(room.canonicalAlias?.isEmpty ?? true)
|
||||
? room.id
|
||||
: canonicalAlias,
|
||||
event.content,
|
||||
stateKeyEntry.value['name']);
|
||||
}
|
||||
|
@ -448,79 +521,53 @@ class Room {
|
|||
return sendEvent(event, txid: txid, inReplyTo: inReplyTo);
|
||||
}
|
||||
|
||||
/// Sends a [file] to this room after uploading it. The [msgType] is optional
|
||||
/// and will be detected by the mimetype of the file. Returns the mxc uri of
|
||||
/// Sends a [file] to this room after uploading it. Returns the mxc uri of
|
||||
/// the uploaded file. If [waitUntilSent] is true, the future will wait until
|
||||
/// the message event has received the server. Otherwise the future will only
|
||||
/// wait until the file has been uploaded.
|
||||
Future<String> sendFileEvent(
|
||||
MatrixFile file, {
|
||||
String msgType,
|
||||
String txid,
|
||||
Event inReplyTo,
|
||||
Map<String, dynamic> info,
|
||||
bool waitUntilSent = false,
|
||||
MatrixFile thumbnail,
|
||||
MatrixImageFile thumbnail,
|
||||
}) async {
|
||||
Image fileImage;
|
||||
Image thumbnailImage;
|
||||
EncryptedFile encryptedThumbnail;
|
||||
String thumbnailUploadResp;
|
||||
|
||||
var fileName = file.path.split('/').last;
|
||||
final mimeType = mime(file.path) ?? '';
|
||||
if (msgType == null) {
|
||||
final metaType = (mimeType).split('/')[0];
|
||||
switch (metaType) {
|
||||
case 'image':
|
||||
case 'audio':
|
||||
case 'video':
|
||||
msgType = 'm.$metaType';
|
||||
break;
|
||||
default:
|
||||
msgType = 'm.file';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (msgType == 'm.image') {
|
||||
fileImage = decodeImage(file.bytes.toList());
|
||||
if (thumbnail != null) {
|
||||
thumbnailImage = decodeImage(thumbnail.bytes.toList());
|
||||
}
|
||||
}
|
||||
|
||||
final sendEncrypted = encrypted && client.fileEncryptionEnabled;
|
||||
MatrixFile uploadFile = file; // ignore: omit_local_variable_types
|
||||
MatrixFile uploadThumbnail = thumbnail; // ignore: omit_local_variable_types
|
||||
EncryptedFile encryptedFile;
|
||||
if (sendEncrypted) {
|
||||
EncryptedFile encryptedThumbnail;
|
||||
if (encrypted && client.fileEncryptionEnabled) {
|
||||
encryptedFile = await file.encrypt();
|
||||
uploadFile = encryptedFile.toMatrixFile();
|
||||
|
||||
if (thumbnail != null) {
|
||||
encryptedThumbnail = await thumbnail.encrypt();
|
||||
uploadThumbnail = encryptedThumbnail.toMatrixFile();
|
||||
}
|
||||
}
|
||||
final uploadResp = await client.api.upload(
|
||||
file.bytes,
|
||||
file.path,
|
||||
contentType: sendEncrypted ? 'application/octet-stream' : null,
|
||||
uploadFile.bytes,
|
||||
uploadFile.name,
|
||||
contentType: uploadFile.mimeType,
|
||||
);
|
||||
if (thumbnail != null) {
|
||||
thumbnailUploadResp = await client.api.upload(
|
||||
thumbnail.bytes,
|
||||
thumbnail.path,
|
||||
contentType: sendEncrypted ? 'application/octet-stream' : null,
|
||||
);
|
||||
}
|
||||
final thumbnailUploadResp = uploadThumbnail != null
|
||||
? await client.api.upload(
|
||||
uploadThumbnail.bytes,
|
||||
uploadThumbnail.name,
|
||||
contentType: uploadThumbnail.mimeType,
|
||||
)
|
||||
: null;
|
||||
|
||||
// Send event
|
||||
var content = <String, dynamic>{
|
||||
'msgtype': msgType,
|
||||
'body': fileName,
|
||||
'filename': fileName,
|
||||
if (!sendEncrypted) 'url': uploadResp,
|
||||
if (sendEncrypted)
|
||||
'msgtype': file.msgType,
|
||||
'body': file.name,
|
||||
'filename': file.name,
|
||||
if (encryptedFile == null) 'url': uploadResp,
|
||||
if (encryptedFile != null)
|
||||
'file': {
|
||||
'url': uploadResp,
|
||||
'mimetype': mimeType,
|
||||
'mimetype': file.mimeType,
|
||||
'v': 'v2',
|
||||
'key': {
|
||||
'alg': 'A256CTR',
|
||||
|
@ -532,37 +579,27 @@ class Room {
|
|||
'iv': encryptedFile.iv,
|
||||
'hashes': {'sha256': encryptedFile.sha256}
|
||||
},
|
||||
'info': info ??
|
||||
{
|
||||
'mimetype': mimeType,
|
||||
'size': file.size,
|
||||
if (fileImage != null) 'h': fileImage.height,
|
||||
if (fileImage != null) 'w': fileImage.width,
|
||||
if (thumbnailUploadResp != null && !sendEncrypted)
|
||||
'thumbnail_url': thumbnailUploadResp,
|
||||
if (thumbnailUploadResp != null && sendEncrypted)
|
||||
'thumbnail_file': {
|
||||
'url': thumbnailUploadResp,
|
||||
'mimetype': mimeType,
|
||||
'v': 'v2',
|
||||
'key': {
|
||||
'alg': 'A256CTR',
|
||||
'ext': true,
|
||||
'k': encryptedThumbnail.k,
|
||||
'key_ops': ['encrypt', 'decrypt'],
|
||||
'kty': 'oct'
|
||||
},
|
||||
'iv': encryptedThumbnail.iv,
|
||||
'hashes': {'sha256': encryptedThumbnail.sha256}
|
||||
},
|
||||
if (thumbnailImage != null)
|
||||
'thumbnail_info': {
|
||||
'h': thumbnailImage.height,
|
||||
'mimetype': mimeType,
|
||||
'size': thumbnail.size,
|
||||
'w': thumbnailImage.width,
|
||||
}
|
||||
}
|
||||
'info': {
|
||||
...file.info,
|
||||
if (thumbnail != null && encryptedThumbnail == null)
|
||||
'thumbnail_url': thumbnailUploadResp,
|
||||
if (thumbnail != null && encryptedThumbnail != null)
|
||||
'thumbnail_file': {
|
||||
'url': thumbnailUploadResp,
|
||||
'mimetype': thumbnail.mimeType,
|
||||
'v': 'v2',
|
||||
'key': {
|
||||
'alg': 'A256CTR',
|
||||
'ext': true,
|
||||
'k': encryptedThumbnail.k,
|
||||
'key_ops': ['encrypt', 'decrypt'],
|
||||
'kty': 'oct'
|
||||
},
|
||||
'iv': encryptedThumbnail.iv,
|
||||
'hashes': {'sha256': encryptedThumbnail.sha256}
|
||||
},
|
||||
if (thumbnail != null) 'thumbnail_info': thumbnail.info,
|
||||
}
|
||||
};
|
||||
final sendResponse = sendEvent(
|
||||
content,
|
||||
|
@ -575,80 +612,6 @@ class Room {
|
|||
return uploadResp;
|
||||
}
|
||||
|
||||
/// Sends an audio file to this room and returns the mxc uri.
|
||||
Future<String> sendAudioEvent(MatrixFile file,
|
||||
{String txid, Event inReplyTo}) async {
|
||||
return await sendFileEvent(file,
|
||||
msgType: 'm.audio', txid: txid, inReplyTo: inReplyTo);
|
||||
}
|
||||
|
||||
/// Sends an image to this room and returns the mxc uri.
|
||||
Future<String> sendImageEvent(MatrixFile file,
|
||||
{String txid, int width, int height, Event inReplyTo}) async {
|
||||
return await sendFileEvent(file,
|
||||
msgType: 'm.image',
|
||||
txid: txid,
|
||||
inReplyTo: inReplyTo,
|
||||
info: {
|
||||
'size': file.size,
|
||||
'mimetype': mime(file.path.split('/').last),
|
||||
'w': width,
|
||||
'h': height,
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends an video to this room and returns the mxc uri.
|
||||
Future<String> sendVideoEvent(MatrixFile file,
|
||||
{String txid,
|
||||
int videoWidth,
|
||||
int videoHeight,
|
||||
int duration,
|
||||
MatrixFile thumbnail,
|
||||
int thumbnailWidth,
|
||||
int thumbnailHeight,
|
||||
Event inReplyTo}) async {
|
||||
var fileName = file.path.split('/').last;
|
||||
var info = <String, dynamic>{
|
||||
'size': file.size,
|
||||
'mimetype': mime(fileName),
|
||||
};
|
||||
if (videoWidth != null) {
|
||||
info['w'] = videoWidth;
|
||||
}
|
||||
if (thumbnailHeight != null) {
|
||||
info['h'] = thumbnailHeight;
|
||||
}
|
||||
if (duration != null) {
|
||||
info['duration'] = duration;
|
||||
}
|
||||
if (thumbnail != null && !(encrypted && client.encryptionEnabled)) {
|
||||
var thumbnailName = file.path.split('/').last;
|
||||
final thumbnailUploadResp = await client.api.upload(
|
||||
thumbnail.bytes,
|
||||
thumbnail.path,
|
||||
);
|
||||
info['thumbnail_url'] = thumbnailUploadResp;
|
||||
info['thumbnail_info'] = {
|
||||
'size': thumbnail.size,
|
||||
'mimetype': mime(thumbnailName),
|
||||
};
|
||||
if (thumbnailWidth != null) {
|
||||
info['thumbnail_info']['w'] = thumbnailWidth;
|
||||
}
|
||||
if (thumbnailHeight != null) {
|
||||
info['thumbnail_info']['h'] = thumbnailHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return await sendFileEvent(
|
||||
file,
|
||||
msgType: 'm.video',
|
||||
txid: txid,
|
||||
inReplyTo: inReplyTo,
|
||||
info: info,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends an event to this room with this json as a content. Returns the
|
||||
/// event ID generated from the server.
|
||||
Future<String> sendEvent(Map<String, dynamic> content,
|
||||
|
@ -828,28 +791,17 @@ class Room {
|
|||
final loadFn = () async {
|
||||
if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return;
|
||||
|
||||
if (resp.state != null) {
|
||||
for (final state in resp.state) {
|
||||
await EventUpdate(
|
||||
type: 'state',
|
||||
roomID: id,
|
||||
eventType: state.type,
|
||||
content: state.toJson(),
|
||||
sortOrder: oldSortOrder,
|
||||
).decrypt(this, store: true);
|
||||
}
|
||||
}
|
||||
|
||||
for (final hist in resp.chunk) {
|
||||
final eventUpdate = await EventUpdate(
|
||||
type: 'history',
|
||||
roomID: id,
|
||||
eventType: hist.type,
|
||||
content: hist.toJson(),
|
||||
sortOrder: oldSortOrder,
|
||||
).decrypt(this, store: true);
|
||||
client.onEvent.add(eventUpdate);
|
||||
}
|
||||
await client.handleSync(
|
||||
SyncUpdate()
|
||||
..rooms = (RoomsUpdate()
|
||||
..join = {
|
||||
'$id': (JoinedRoomUpdate()
|
||||
..state = resp.state
|
||||
..timeline = (TimelineUpdate()
|
||||
..events = resp.chunk
|
||||
..prevBatch = resp.end)),
|
||||
}),
|
||||
sortAtTheEnd: true);
|
||||
};
|
||||
|
||||
if (client.database != null) {
|
||||
|
@ -861,21 +813,12 @@ class Room {
|
|||
} else {
|
||||
await loadFn();
|
||||
}
|
||||
client.onRoomUpdate.add(
|
||||
RoomUpdate(
|
||||
id: id,
|
||||
membership: membership,
|
||||
prev_batch: resp.end,
|
||||
notification_count: notificationCount,
|
||||
highlight_count: highlightCount,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sets this room as a direct chat for this user if not already.
|
||||
Future<void> addToDirectChat(String userID) async {
|
||||
var directChats = client.directChats;
|
||||
if (directChats.containsKey(userID)) {
|
||||
if (directChats[userID] is List) {
|
||||
if (!directChats[userID].contains(id)) {
|
||||
directChats[userID].add(id);
|
||||
} else {
|
||||
|
@ -885,9 +828,8 @@ class Room {
|
|||
directChats[userID] = [id];
|
||||
}
|
||||
|
||||
await client.api.setRoomAccountData(
|
||||
await client.api.setAccountData(
|
||||
client.userID,
|
||||
id,
|
||||
'm.direct',
|
||||
directChats,
|
||||
);
|
||||
|
@ -897,7 +839,7 @@ class Room {
|
|||
/// Removes this room from all direct chat tags.
|
||||
Future<void> removeFromDirectChat() async {
|
||||
var directChats = client.directChats;
|
||||
if (directChats.containsKey(directChatMatrixID) &&
|
||||
if (directChats[directChatMatrixID] is List &&
|
||||
directChats[directChatMatrixID].contains(id)) {
|
||||
directChats[directChatMatrixID].remove(id);
|
||||
} else {
|
||||
|
@ -991,6 +933,7 @@ class Room {
|
|||
Future<Timeline> getTimeline(
|
||||
{onTimelineUpdateCallback onUpdate,
|
||||
onTimelineInsertCallback onInsert}) async {
|
||||
await postLoad();
|
||||
var events;
|
||||
if (client.database != null) {
|
||||
events = await client.database.getEventList(client.id, this);
|
||||
|
@ -1041,10 +984,20 @@ class Room {
|
|||
/// Request the full list of participants from the server. The local list
|
||||
/// from the store is not complete if the client uses lazy loading.
|
||||
Future<List<User>> requestParticipants() async {
|
||||
if (!participantListComplete && partial && client.database != null) {
|
||||
// we aren't fully loaded, maybe the users are in the database
|
||||
final users = await client.database.getUsers(client.id, this);
|
||||
for (final user in users) {
|
||||
setState(user);
|
||||
}
|
||||
}
|
||||
if (participantListComplete) return getParticipants();
|
||||
final matrixEvents = await client.api.requestMembers(id);
|
||||
final users =
|
||||
matrixEvents.map((e) => Event.fromMatrixEvent(e, this).asUser).toList();
|
||||
for (final user in users) {
|
||||
setState(user); // at *least* cache this in-memory
|
||||
}
|
||||
users.removeWhere(
|
||||
(u) => [Membership.leave, Membership.ban].contains(u.membership));
|
||||
return users;
|
||||
|
@ -1080,11 +1033,25 @@ class Room {
|
|||
final Set<String> _requestingMatrixIds = {};
|
||||
|
||||
/// Requests a missing [User] for this room. Important for clients using
|
||||
/// lazy loading.
|
||||
Future<User> requestUser(String mxID, {bool ignoreErrors = false}) async {
|
||||
/// lazy loading. If the user can't be found this method tries to fetch
|
||||
/// the displayname and avatar from the profile if [requestProfile] is true.
|
||||
Future<User> requestUser(
|
||||
String mxID, {
|
||||
bool ignoreErrors = false,
|
||||
bool requestProfile = true,
|
||||
}) async {
|
||||
if (getState(EventTypes.RoomMember, mxID) != null) {
|
||||
return getState(EventTypes.RoomMember, mxID).asUser;
|
||||
}
|
||||
if (client.database != null) {
|
||||
// it may be in the database
|
||||
final user = await client.database.getUser(client.id, mxID, this);
|
||||
if (user != null) {
|
||||
setState(user);
|
||||
if (onUpdate != null) onUpdate.add(id);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
|
||||
Map<String, dynamic> resp;
|
||||
try {
|
||||
|
@ -1094,8 +1061,22 @@ class Room {
|
|||
mxID,
|
||||
);
|
||||
} catch (exception) {
|
||||
_requestingMatrixIds.remove(mxID);
|
||||
if (!ignoreErrors) rethrow;
|
||||
if (!ignoreErrors) {
|
||||
_requestingMatrixIds.remove(mxID);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
if (resp == null && requestProfile) {
|
||||
try {
|
||||
final profile = await client.api.requestProfile(mxID);
|
||||
resp = {
|
||||
'displayname': profile.displayname,
|
||||
'avatar_url': profile.avatarUrl,
|
||||
};
|
||||
} catch (exception) {
|
||||
_requestingMatrixIds.remove(mxID);
|
||||
if (!ignoreErrors) rethrow;
|
||||
}
|
||||
}
|
||||
if (resp == null) {
|
||||
return null;
|
||||
|
@ -1163,7 +1144,7 @@ class Room {
|
|||
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
||||
/// m.room.avatar event.
|
||||
Future<String> setAvatar(MatrixFile file) async {
|
||||
final uploadResp = await client.api.upload(file.bytes, file.path);
|
||||
final uploadResp = await client.api.upload(file.bytes, file.name);
|
||||
return await client.api.sendState(
|
||||
id,
|
||||
EventTypes.RoomAvatar,
|
||||
|
|
|
@ -147,17 +147,19 @@ class Timeline {
|
|||
if (i < events.length) events.removeAt(i);
|
||||
}
|
||||
// Is this event already in the timeline?
|
||||
else if (eventUpdate.content.containsKey('unsigned') &&
|
||||
else if (eventUpdate.content['unsigned'] is Map &&
|
||||
eventUpdate.content['unsigned']['transaction_id'] is String) {
|
||||
var i = _findEvent(
|
||||
event_id: eventUpdate.content['event_id'],
|
||||
unsigned_txid: eventUpdate.content.containsKey('unsigned')
|
||||
unsigned_txid: eventUpdate.content['unsigned'] is Map
|
||||
? eventUpdate.content['unsigned']['transaction_id']
|
||||
: null);
|
||||
|
||||
if (i < events.length) {
|
||||
final tempSortOrder = events[i].sortOrder;
|
||||
events[i] = Event.fromJson(
|
||||
eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
events[i].sortOrder = tempSortOrder;
|
||||
}
|
||||
} else {
|
||||
Event newEvent;
|
||||
|
|
|
@ -71,7 +71,10 @@ class User extends Event {
|
|||
String get id => stateKey;
|
||||
|
||||
/// The displayname of the user if the user has set one.
|
||||
String get displayName => content != null ? content['displayname'] : null;
|
||||
String get displayName =>
|
||||
content != null && content.containsKey('displayname')
|
||||
? content['displayname']
|
||||
: (prevContent != null ? prevContent['displayname'] : null);
|
||||
|
||||
/// Returns the power level of this user.
|
||||
int get powerLevel => room?.getPowerLevelByUserId(id);
|
||||
|
@ -89,19 +92,26 @@ class User extends Event {
|
|||
}, orElse: () => Membership.join);
|
||||
|
||||
/// The avatar if the user has one.
|
||||
Uri get avatarUrl => content != null && content['avatar_url'] is String
|
||||
? Uri.parse(content['avatar_url'])
|
||||
: null;
|
||||
Uri get avatarUrl => content != null && content.containsKey('avatar_url')
|
||||
? (content['avatar_url'] is String
|
||||
? Uri.parse(content['avatar_url'])
|
||||
: null)
|
||||
: (prevContent != null && prevContent['avatar_url'] is String
|
||||
? Uri.parse(prevContent['avatar_url'])
|
||||
: null);
|
||||
|
||||
/// Returns the displayname or the local part of the Matrix ID if the user
|
||||
/// has no displayname. If [formatLocalpart] is true, then the localpart will
|
||||
/// be formatted in the way, that all "_" characters are becomming white spaces and
|
||||
/// the first character of each word becomes uppercase.
|
||||
String calcDisplayname({bool formatLocalpart = true}) {
|
||||
/// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
|
||||
/// if there is no other displayname available. If not then this will return "Unknown user".
|
||||
String calcDisplayname(
|
||||
{bool formatLocalpart = true, bool mxidLocalPartFallback = true}) {
|
||||
if (displayName?.isNotEmpty ?? false) {
|
||||
return displayName;
|
||||
}
|
||||
if (stateKey != null) {
|
||||
if (stateKey != null && mxidLocalPartFallback) {
|
||||
if (!formatLocalpart) {
|
||||
return stateKey.localpart;
|
||||
}
|
||||
|
@ -113,7 +123,7 @@ class User extends Event {
|
|||
}
|
||||
return words.join(' ');
|
||||
}
|
||||
return 'Unknown User';
|
||||
return 'Unknown user';
|
||||
}
|
||||
|
||||
/// Call the Matrix API to kick this user from this room.
|
||||
|
|
|
@ -1,158 +1,371 @@
|
|||
import 'dart:convert';
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
|
||||
import '../client.dart';
|
||||
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
|
||||
import '../user.dart';
|
||||
import '../room.dart';
|
||||
import '../database/database.dart'
|
||||
show DbUserDeviceKey, DbUserDeviceKeysKey, DbUserCrossSigningKey;
|
||||
import '../event.dart';
|
||||
|
||||
enum UserVerifiedStatus { verified, unknown, unknownDevice }
|
||||
|
||||
class DeviceKeysList {
|
||||
Client client;
|
||||
String userId;
|
||||
bool outdated = true;
|
||||
Map<String, DeviceKeys> deviceKeys = {};
|
||||
Map<String, CrossSigningKey> crossSigningKeys = {};
|
||||
|
||||
SignableKey getKey(String id) {
|
||||
if (deviceKeys.containsKey(id)) {
|
||||
return deviceKeys[id];
|
||||
}
|
||||
if (crossSigningKeys.containsKey(id)) {
|
||||
return crossSigningKeys[id];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
CrossSigningKey getCrossSigningKey(String type) => crossSigningKeys.values
|
||||
.firstWhere((k) => k.usage.contains(type), orElse: () => null);
|
||||
|
||||
CrossSigningKey get masterKey => getCrossSigningKey('master');
|
||||
CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing');
|
||||
CrossSigningKey get userSigningKey => getCrossSigningKey('user_signing');
|
||||
|
||||
UserVerifiedStatus get verified {
|
||||
if (masterKey == null) {
|
||||
return UserVerifiedStatus.unknown;
|
||||
}
|
||||
if (masterKey.verified) {
|
||||
for (final key in deviceKeys.values) {
|
||||
if (!key.verified) {
|
||||
return UserVerifiedStatus.unknownDevice;
|
||||
}
|
||||
}
|
||||
return UserVerifiedStatus.verified;
|
||||
}
|
||||
return UserVerifiedStatus.unknown;
|
||||
}
|
||||
|
||||
Future<KeyVerification> startVerification() async {
|
||||
final roomId =
|
||||
await User(userId, room: Room(client: client)).startDirectChat();
|
||||
if (roomId == null) {
|
||||
throw 'Unable to start new room';
|
||||
}
|
||||
final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client);
|
||||
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
|
||||
return request;
|
||||
}
|
||||
|
||||
DeviceKeysList.fromDb(
|
||||
DbUserDeviceKey dbEntry, List<DbUserDeviceKeysKey> childEntries) {
|
||||
DbUserDeviceKey dbEntry,
|
||||
List<DbUserDeviceKeysKey> childEntries,
|
||||
List<DbUserCrossSigningKey> crossSigningEntries,
|
||||
Client cl) {
|
||||
client = cl;
|
||||
userId = dbEntry.userId;
|
||||
outdated = dbEntry.outdated;
|
||||
deviceKeys = {};
|
||||
for (final childEntry in childEntries) {
|
||||
final entry = DeviceKeys.fromDb(childEntry);
|
||||
final entry = DeviceKeys.fromDb(childEntry, client);
|
||||
if (entry.isValid) {
|
||||
deviceKeys[childEntry.deviceId] = entry;
|
||||
} else {
|
||||
outdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeviceKeysList.fromJson(Map<String, dynamic> json) {
|
||||
userId = json['user_id'];
|
||||
outdated = json['outdated'];
|
||||
deviceKeys = {};
|
||||
for (final rawDeviceKeyEntry in json['device_keys'].entries) {
|
||||
deviceKeys[rawDeviceKeyEntry.key] =
|
||||
DeviceKeys.fromJson(rawDeviceKeyEntry.value);
|
||||
for (final crossSigningEntry in crossSigningEntries) {
|
||||
final entry = CrossSigningKey.fromDb(crossSigningEntry, client);
|
||||
if (entry.isValid) {
|
||||
crossSigningKeys[crossSigningEntry.publicKey] = entry;
|
||||
} else {
|
||||
outdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeviceKeysList(this.userId, this.client);
|
||||
}
|
||||
|
||||
abstract class SignableKey extends MatrixSignableKey {
|
||||
Client client;
|
||||
Map<String, dynamic> validSignatures;
|
||||
bool _verified;
|
||||
bool blocked;
|
||||
|
||||
String get ed25519Key => keys['ed25519:$identifier'];
|
||||
bool get verified => (directVerified || crossVerified) && !blocked;
|
||||
|
||||
void setDirectVerified(bool v) {
|
||||
_verified = v;
|
||||
}
|
||||
|
||||
bool get directVerified => _verified;
|
||||
bool get crossVerified => hasValidSignatureChain();
|
||||
bool get signed => hasValidSignatureChain(verifiedOnly: false);
|
||||
|
||||
SignableKey.fromJson(Map<String, dynamic> json, Client cl)
|
||||
: client = cl,
|
||||
super.fromJson(json) {
|
||||
_verified = false;
|
||||
blocked = false;
|
||||
}
|
||||
|
||||
MatrixSignableKey cloneForSigning() {
|
||||
final newKey =
|
||||
MatrixSignableKey.fromJson(Map<String, dynamic>.from(toJson()));
|
||||
newKey.identifier = identifier;
|
||||
newKey.signatures ??= <String, Map<String, String>>{};
|
||||
newKey.signatures.clear();
|
||||
return newKey;
|
||||
}
|
||||
|
||||
String get signingContent {
|
||||
final data = Map<String, dynamic>.from(super.toJson());
|
||||
// some old data might have the custom verified and blocked keys
|
||||
data.remove('verified');
|
||||
data.remove('blocked');
|
||||
// remove the keys not needed for signing
|
||||
data.remove('unsigned');
|
||||
data.remove('signatures');
|
||||
return String.fromCharCodes(canonicalJson.encode(data));
|
||||
}
|
||||
|
||||
bool _verifySignature(String pubKey, String signature) {
|
||||
final olmutil = olm.Utility();
|
||||
var valid = false;
|
||||
try {
|
||||
olmutil.ed25519_verify(pubKey, signingContent, signature);
|
||||
valid = true;
|
||||
} catch (_) {
|
||||
// bad signature
|
||||
valid = false;
|
||||
} finally {
|
||||
olmutil.free();
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
bool hasValidSignatureChain({bool verifiedOnly = true, Set<String> visited}) {
|
||||
if (!client.encryptionEnabled) {
|
||||
return false;
|
||||
}
|
||||
visited ??= <String>{};
|
||||
final setKey = '${userId};${identifier}';
|
||||
if (visited.contains(setKey)) {
|
||||
return false; // prevent recursion
|
||||
}
|
||||
visited.add(setKey);
|
||||
for (final signatureEntries in signatures.entries) {
|
||||
final otherUserId = signatureEntries.key;
|
||||
if (!(signatureEntries.value is Map) ||
|
||||
!client.userDeviceKeys.containsKey(otherUserId)) {
|
||||
continue;
|
||||
}
|
||||
for (final signatureEntry in signatureEntries.value.entries) {
|
||||
final fullKeyId = signatureEntry.key;
|
||||
final signature = signatureEntry.value;
|
||||
if (!(fullKeyId is String) || !(signature is String)) {
|
||||
continue;
|
||||
}
|
||||
final keyId = fullKeyId.substring('ed25519:'.length);
|
||||
SignableKey key;
|
||||
if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) {
|
||||
key = client.userDeviceKeys[otherUserId].deviceKeys[keyId];
|
||||
} else if (client.userDeviceKeys[otherUserId].crossSigningKeys
|
||||
.containsKey(keyId)) {
|
||||
key = client.userDeviceKeys[otherUserId].crossSigningKeys[keyId];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (key.blocked) {
|
||||
continue; // we can't be bothered about this keys signatures
|
||||
}
|
||||
var haveValidSignature = false;
|
||||
var gotSignatureFromCache = false;
|
||||
if (validSignatures != null &&
|
||||
validSignatures.containsKey(otherUserId) &&
|
||||
validSignatures[otherUserId].containsKey(fullKeyId)) {
|
||||
if (validSignatures[otherUserId][fullKeyId] == true) {
|
||||
haveValidSignature = true;
|
||||
gotSignatureFromCache = true;
|
||||
} else if (validSignatures[otherUserId][fullKeyId] == false) {
|
||||
haveValidSignature = false;
|
||||
gotSignatureFromCache = true;
|
||||
}
|
||||
}
|
||||
if (!gotSignatureFromCache) {
|
||||
// validate the signature manually
|
||||
haveValidSignature = _verifySignature(key.ed25519Key, signature);
|
||||
validSignatures ??= <String, dynamic>{};
|
||||
if (!validSignatures.containsKey(otherUserId)) {
|
||||
validSignatures[otherUserId] = <String, dynamic>{};
|
||||
}
|
||||
validSignatures[otherUserId][fullKeyId] = haveValidSignature;
|
||||
}
|
||||
if (!haveValidSignature) {
|
||||
// no valid signature, this key is useless
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((verifiedOnly && key.directVerified) ||
|
||||
(key is CrossSigningKey &&
|
||||
key.usage.contains('master') &&
|
||||
key.directVerified &&
|
||||
key.userId == client.userID)) {
|
||||
return true; // we verified this key and it is valid...all checks out!
|
||||
}
|
||||
// or else we just recurse into that key and chack if it works out
|
||||
final haveChain = key.hasValidSignatureChain(
|
||||
verifiedOnly: verifiedOnly, visited: visited);
|
||||
if (haveChain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void setVerified(bool newVerified, [bool sign = true]) {
|
||||
_verified = newVerified;
|
||||
if (newVerified &&
|
||||
sign &&
|
||||
client.encryptionEnabled &&
|
||||
client.encryption.crossSigning.signable([this])) {
|
||||
// sign the key!
|
||||
client.encryption.crossSigning.sign([this]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setBlocked(bool newBlocked);
|
||||
|
||||
@override
|
||||
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;
|
||||
final data = Map<String, dynamic>.from(super.toJson());
|
||||
// some old data may have the verified and blocked keys which are unneeded now
|
||||
data.remove('verified');
|
||||
data.remove('blocked');
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => json.encode(toJson());
|
||||
|
||||
DeviceKeysList(this.userId);
|
||||
}
|
||||
|
||||
class DeviceKeys extends MatrixDeviceKeys {
|
||||
bool verified;
|
||||
bool blocked;
|
||||
class CrossSigningKey extends SignableKey {
|
||||
String get publicKey => identifier;
|
||||
List<String> usage;
|
||||
|
||||
bool get isValid =>
|
||||
userId != null && publicKey != null && keys != null && ed25519Key != null;
|
||||
|
||||
@override
|
||||
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
||||
super.setVerified(newVerified, sign);
|
||||
return client.database?.setVerifiedUserCrossSigningKey(
|
||||
newVerified, client.id, userId, publicKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBlocked(bool newBlocked) {
|
||||
blocked = newBlocked;
|
||||
return client.database?.setBlockedUserCrossSigningKey(
|
||||
newBlocked, client.id, userId, publicKey);
|
||||
}
|
||||
|
||||
CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl)
|
||||
: super.fromJson(Map<String, dynamic>.from(k.toJson()), cl) {
|
||||
final json = toJson();
|
||||
identifier = k.publicKey;
|
||||
usage = json['usage'].cast<String>();
|
||||
}
|
||||
|
||||
CrossSigningKey.fromDb(DbUserCrossSigningKey dbEntry, Client cl)
|
||||
: super.fromJson(Event.getMapFromPayload(dbEntry.content), cl) {
|
||||
final json = toJson();
|
||||
identifier = dbEntry.publicKey;
|
||||
usage = json['usage'].cast<String>();
|
||||
_verified = dbEntry.verified;
|
||||
blocked = dbEntry.blocked;
|
||||
}
|
||||
|
||||
CrossSigningKey.fromJson(Map<String, dynamic> json, Client cl)
|
||||
: super.fromJson(Map<String, dynamic>.from(json), cl) {
|
||||
final json = toJson();
|
||||
usage = json['usage'].cast<String>();
|
||||
if (keys != null && keys.isNotEmpty) {
|
||||
identifier = keys.values.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceKeys extends SignableKey {
|
||||
String get deviceId => identifier;
|
||||
List<String> algorithms;
|
||||
|
||||
String get curve25519Key => keys['curve25519:$deviceId'];
|
||||
String get ed25519Key => keys['ed25519:$deviceId'];
|
||||
String get deviceDisplayName =>
|
||||
unsigned != null ? unsigned['device_display_name'] : null;
|
||||
|
||||
bool get isValid =>
|
||||
userId != null &&
|
||||
deviceId != null &&
|
||||
keys != null &&
|
||||
curve25519Key != null &&
|
||||
ed25519Key != null;
|
||||
|
||||
Future<void> setVerified(bool newVerified, Client client) {
|
||||
verified = newVerified;
|
||||
return client.database
|
||||
@override
|
||||
Future<void> setVerified(bool newVerified, [bool sign = true]) {
|
||||
super.setVerified(newVerified, sign);
|
||||
return client?.database
|
||||
?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
|
||||
}
|
||||
|
||||
Future<void> setBlocked(bool newBlocked, Client client) {
|
||||
@override
|
||||
Future<void> setBlocked(bool newBlocked) {
|
||||
blocked = newBlocked;
|
||||
return client.database
|
||||
return client?.database
|
||||
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
|
||||
}
|
||||
|
||||
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))))
|
||||
: null;
|
||||
deviceKeys.unsigned = content['unsigned'] != null
|
||||
? Map<String, dynamic>.from(content['unsigned'])
|
||||
: null;
|
||||
deviceKeys.verified = dbEntry.verified;
|
||||
deviceKeys.blocked = dbEntry.blocked;
|
||||
return deviceKeys;
|
||||
DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl)
|
||||
: super.fromJson(Map<String, dynamic>.from(k.toJson()), cl) {
|
||||
final json = toJson();
|
||||
identifier = k.deviceId;
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
}
|
||||
|
||||
static DeviceKeys fromJson(Map<String, dynamic> json) {
|
||||
var matrixDeviceKeys = MatrixDeviceKeys.fromJson(json);
|
||||
var deviceKeys = DeviceKeys(
|
||||
userId: matrixDeviceKeys.userId,
|
||||
deviceId: matrixDeviceKeys.deviceId,
|
||||
algorithms: matrixDeviceKeys.algorithms,
|
||||
keys: matrixDeviceKeys.keys,
|
||||
signatures: matrixDeviceKeys.signatures,
|
||||
unsigned: matrixDeviceKeys.unsigned,
|
||||
);
|
||||
deviceKeys.verified = json['verified'] ?? false;
|
||||
deviceKeys.blocked = json['blocked'] ?? false;
|
||||
return deviceKeys;
|
||||
DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl)
|
||||
: super.fromJson(Event.getMapFromPayload(dbEntry.content), cl) {
|
||||
final json = toJson();
|
||||
identifier = dbEntry.deviceId;
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
_verified = dbEntry.verified;
|
||||
blocked = dbEntry.blocked;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = super.toJson();
|
||||
data['verified'] = verified;
|
||||
data['blocked'] = blocked;
|
||||
return data;
|
||||
DeviceKeys.fromJson(Map<String, dynamic> json, Client cl)
|
||||
: super.fromJson(Map<String, dynamic>.from(json), cl) {
|
||||
final json = toJson();
|
||||
identifier = json['device_id'];
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
}
|
||||
|
||||
KeyVerification startVerification(Client client) {
|
||||
KeyVerification startVerification() {
|
||||
final request = KeyVerification(
|
||||
encryption: client.encryption, userId: userId, deviceId: deviceId);
|
||||
|
||||
request.start();
|
||||
client.encryption.keyVerificationManager.addRequest(request);
|
||||
return request;
|
||||
|
|
|
@ -2,20 +2,100 @@
|
|||
|
||||
import 'dart:typed_data';
|
||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
class MatrixFile {
|
||||
Uint8List bytes;
|
||||
String path;
|
||||
String name;
|
||||
String mimeType;
|
||||
|
||||
/// Encrypts this file, changes the [bytes] and returns the
|
||||
/// Encrypts this file and returns the
|
||||
/// encryption information as an [EncryptedFile].
|
||||
Future<EncryptedFile> encrypt() async {
|
||||
var encryptFile2 = encryptFile(bytes);
|
||||
final encryptedFile = await encryptFile2;
|
||||
bytes = encryptedFile.data;
|
||||
return encryptedFile;
|
||||
return await encryptFile(bytes);
|
||||
}
|
||||
|
||||
MatrixFile({this.bytes, this.name, this.mimeType}) {
|
||||
mimeType ??= lookupMimeType(name, headerBytes: bytes);
|
||||
name = name.split('/').last.toLowerCase();
|
||||
}
|
||||
|
||||
MatrixFile({this.bytes, String path}) : path = path.toLowerCase();
|
||||
int get size => bytes.length;
|
||||
|
||||
String get msgType => 'm.file';
|
||||
|
||||
Map<String, dynamic> get info => ({
|
||||
'mimetype': mimeType,
|
||||
'size': size,
|
||||
});
|
||||
}
|
||||
|
||||
class MatrixImageFile extends MatrixFile {
|
||||
int width;
|
||||
int height;
|
||||
String blurhash;
|
||||
|
||||
MatrixImageFile(
|
||||
{Uint8List bytes,
|
||||
String name,
|
||||
String mimeType,
|
||||
this.width,
|
||||
this.height,
|
||||
this.blurhash})
|
||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
@override
|
||||
String get msgType => 'm.image';
|
||||
@override
|
||||
Map<String, dynamic> get info => ({
|
||||
...super.info,
|
||||
if (width != null) 'w': width,
|
||||
if (height != null) 'h': height,
|
||||
if (blurhash != null) 'xyz.amorgan.blurhash': blurhash,
|
||||
});
|
||||
}
|
||||
|
||||
class MatrixVideoFile extends MatrixFile {
|
||||
int width;
|
||||
int height;
|
||||
int duration;
|
||||
|
||||
MatrixVideoFile(
|
||||
{Uint8List bytes,
|
||||
String name,
|
||||
String mimeType,
|
||||
this.width,
|
||||
this.height,
|
||||
this.duration})
|
||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
@override
|
||||
String get msgType => 'm.video';
|
||||
@override
|
||||
Map<String, dynamic> get info => ({
|
||||
...super.info,
|
||||
if (width != null) 'w': width,
|
||||
if (height != null) 'h': height,
|
||||
if (duration != null) 'duration': duration,
|
||||
});
|
||||
}
|
||||
|
||||
class MatrixAudioFile extends MatrixFile {
|
||||
int duration;
|
||||
|
||||
MatrixAudioFile(
|
||||
{Uint8List bytes, String name, String mimeType, this.duration})
|
||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
@override
|
||||
String get msgType => 'm.audio';
|
||||
@override
|
||||
Map<String, dynamic> get info => ({
|
||||
...super.info,
|
||||
if (duration != null) 'duration': duration,
|
||||
});
|
||||
}
|
||||
|
||||
extension ToMatrixFile on EncryptedFile {
|
||||
MatrixFile toMatrixFile() {
|
||||
return MatrixFile(
|
||||
bytes: data, name: 'crypt', mimeType: 'application/octet-stream');
|
||||
}
|
||||
}
|
||||
|
|
94
pubspec.lock
94
pubspec.lock
|
@ -22,13 +22,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -36,6 +29,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.4"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -43,6 +43,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
base58check:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: base58check
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -134,6 +141,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -163,7 +177,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.13.9"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
|
@ -183,6 +197,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.6"
|
||||
encrypt:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: encrypt
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -246,13 +267,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.12"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -305,12 +319,10 @@ packages:
|
|||
matrix_file_e2ee:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "1.x.y"
|
||||
resolved-ref: "32edeff765369a7a77a0822f4b19302ca24a017b"
|
||||
url: "https://gitlab.com/famedly/libraries/matrix_file_e2ee.git"
|
||||
source: git
|
||||
version: "1.0.3"
|
||||
name: matrix_file_e2ee
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -319,19 +331,12 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.8"
|
||||
mime:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.6+3"
|
||||
mime_type:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mime_type
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
moor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -384,12 +389,10 @@ packages:
|
|||
olm:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "1.x.y"
|
||||
resolved-ref: f66975bd1b5cb1865eba5efe6e3a392aa5e396a5
|
||||
url: "https://gitlab.com/famedly/libraries/dart-olm.git"
|
||||
source: git
|
||||
version: "1.1.1"
|
||||
name: olm
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -397,6 +400,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.3"
|
||||
password_hash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: password_hash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -411,13 +421,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -649,13 +652,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.4"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
21
pubspec.yaml
21
pubspec.yaml
|
@ -9,23 +9,18 @@ environment:
|
|||
|
||||
dependencies:
|
||||
http: ^0.12.1
|
||||
mime_type: ^0.3.0
|
||||
mime: ^0.9.6
|
||||
canonical_json: ^1.0.0
|
||||
image: ^2.1.4
|
||||
markdown: ^2.1.3
|
||||
html_unescape: ^1.0.1+3
|
||||
moor: ^3.0.2
|
||||
random_string: ^2.0.1
|
||||
|
||||
olm:
|
||||
git:
|
||||
url: https://gitlab.com/famedly/libraries/dart-olm.git
|
||||
ref: 1.x.y
|
||||
|
||||
matrix_file_e2ee:
|
||||
git:
|
||||
url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git
|
||||
ref: 1.x.y
|
||||
encrypt: ^4.0.2
|
||||
crypto: ^2.1.4
|
||||
base58check: ^1.0.1
|
||||
password_hash: ^2.0.0
|
||||
olm: ^1.2.1
|
||||
matrix_file_e2ee: ^1.0.4
|
||||
|
||||
dev_dependencies:
|
||||
test: ^1.0.0
|
||||
|
@ -33,4 +28,4 @@ dev_dependencies:
|
|||
moor_generator: ^3.0.0
|
||||
build_runner: ^1.5.2
|
||||
pedantic: ^1.9.0
|
||||
moor_ffi: ^0.5.0
|
||||
moor_ffi: ^0.5.0
|
||||
|
|
2
test.sh
2
test.sh
|
@ -2,5 +2,5 @@
|
|||
pub run test -p vm
|
||||
pub run test_coverage
|
||||
pub global activate remove_from_coverage
|
||||
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '.g.dart$'
|
||||
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$'
|
||||
genhtml -o coverage coverage/lcov.info || true
|
||||
|
|
|
@ -127,7 +127,7 @@ void main() {
|
|||
}
|
||||
expect(sync.nextBatch == matrix.prevBatch, true);
|
||||
|
||||
expect(matrix.accountData.length, 3);
|
||||
expect(matrix.accountData.length, 9);
|
||||
expect(matrix.getDirectChatFromUserId('@bob:example.com'),
|
||||
'!726s6s6q:example.com');
|
||||
expect(matrix.rooms[1].directChatMatrixID, '@bob:example.com');
|
||||
|
@ -151,13 +151,10 @@ void main() {
|
|||
expect(matrix.rooms.length, 2);
|
||||
expect(matrix.rooms[1].canonicalAlias,
|
||||
"#famedlyContactDiscovery:${matrix.userID.split(":")[1]}");
|
||||
final contacts = await matrix.loadFamedlyContacts();
|
||||
expect(contacts.length, 2);
|
||||
expect(contacts[0].senderId, '@alice:example.com');
|
||||
expect(matrix.presences['@alice:example.com'].presence.presence,
|
||||
PresenceType.online);
|
||||
expect(presenceCounter, 1);
|
||||
expect(accountDataCounter, 3);
|
||||
expect(accountDataCounter, 9);
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(matrix.userDeviceKeys.length, 4);
|
||||
expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false);
|
||||
|
@ -207,10 +204,6 @@ void main() {
|
|||
matrix.getRoomByAlias(
|
||||
"#famedlyContactDiscovery:${matrix.userID.split(":")[1]}"),
|
||||
null);
|
||||
final altContacts = await matrix.loadFamedlyContacts();
|
||||
altContacts.forEach((u) => print(u.id));
|
||||
expect(altContacts.length, 2);
|
||||
expect(altContacts[0].senderId, '@alice:example.com');
|
||||
});
|
||||
|
||||
test('Logout', () async {
|
||||
|
@ -258,7 +251,7 @@ void main() {
|
|||
|
||||
var eventUpdateList = await eventUpdateListFuture;
|
||||
|
||||
expect(eventUpdateList.length, 13);
|
||||
expect(eventUpdateList.length, 14);
|
||||
|
||||
expect(eventUpdateList[0].eventType, 'm.room.member');
|
||||
expect(eventUpdateList[0].roomID, '!726s6s6q:example.com');
|
||||
|
@ -272,41 +265,45 @@ void main() {
|
|||
expect(eventUpdateList[2].roomID, '!726s6s6q:example.com');
|
||||
expect(eventUpdateList[2].type, 'state');
|
||||
|
||||
expect(eventUpdateList[3].eventType, 'm.room.member');
|
||||
expect(eventUpdateList[3].eventType, 'm.room.pinned_events');
|
||||
expect(eventUpdateList[3].roomID, '!726s6s6q:example.com');
|
||||
expect(eventUpdateList[3].type, 'timeline');
|
||||
expect(eventUpdateList[3].type, 'state');
|
||||
|
||||
expect(eventUpdateList[4].eventType, 'm.room.message');
|
||||
expect(eventUpdateList[4].eventType, 'm.room.member');
|
||||
expect(eventUpdateList[4].roomID, '!726s6s6q:example.com');
|
||||
expect(eventUpdateList[4].type, 'timeline');
|
||||
|
||||
expect(eventUpdateList[5].eventType, 'm.typing');
|
||||
expect(eventUpdateList[5].eventType, 'm.room.message');
|
||||
expect(eventUpdateList[5].roomID, '!726s6s6q:example.com');
|
||||
expect(eventUpdateList[5].type, 'ephemeral');
|
||||
expect(eventUpdateList[5].type, 'timeline');
|
||||
|
||||
expect(eventUpdateList[6].eventType, 'm.receipt');
|
||||
expect(eventUpdateList[6].eventType, 'm.typing');
|
||||
expect(eventUpdateList[6].roomID, '!726s6s6q:example.com');
|
||||
expect(eventUpdateList[6].type, 'ephemeral');
|
||||
|
||||
expect(eventUpdateList[7].eventType, 'm.receipt');
|
||||
expect(eventUpdateList[7].roomID, '!726s6s6q:example.com');
|
||||
expect(eventUpdateList[7].type, 'account_data');
|
||||
expect(eventUpdateList[7].type, 'ephemeral');
|
||||
|
||||
expect(eventUpdateList[8].eventType, 'm.tag');
|
||||
expect(eventUpdateList[8].eventType, 'm.receipt');
|
||||
expect(eventUpdateList[8].roomID, '!726s6s6q:example.com');
|
||||
expect(eventUpdateList[8].type, 'account_data');
|
||||
|
||||
expect(eventUpdateList[9].eventType, 'org.example.custom.room.config');
|
||||
expect(eventUpdateList[9].eventType, 'm.tag');
|
||||
expect(eventUpdateList[9].roomID, '!726s6s6q:example.com');
|
||||
expect(eventUpdateList[9].type, 'account_data');
|
||||
|
||||
expect(eventUpdateList[10].eventType, 'm.room.name');
|
||||
expect(eventUpdateList[10].roomID, '!696r7674:example.com');
|
||||
expect(eventUpdateList[10].type, 'invite_state');
|
||||
expect(eventUpdateList[10].eventType, 'org.example.custom.room.config');
|
||||
expect(eventUpdateList[10].roomID, '!726s6s6q:example.com');
|
||||
expect(eventUpdateList[10].type, 'account_data');
|
||||
|
||||
expect(eventUpdateList[11].eventType, 'm.room.member');
|
||||
expect(eventUpdateList[11].eventType, 'm.room.name');
|
||||
expect(eventUpdateList[11].roomID, '!696r7674:example.com');
|
||||
expect(eventUpdateList[11].type, 'invite_state');
|
||||
|
||||
expect(eventUpdateList[12].eventType, 'm.room.member');
|
||||
expect(eventUpdateList[12].roomID, '!696r7674:example.com');
|
||||
expect(eventUpdateList[12].type, 'invite_state');
|
||||
});
|
||||
|
||||
test('To Device Update Test', () async {
|
||||
|
@ -339,8 +336,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('setAvatar', () async {
|
||||
final testFile =
|
||||
MatrixFile(bytes: Uint8List(0), path: 'fake/path/file.jpeg');
|
||||
final testFile = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg');
|
||||
await matrix.setAvatar(testFile);
|
||||
});
|
||||
|
||||
|
@ -388,7 +384,7 @@ void main() {
|
|||
'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA'
|
||||
}
|
||||
}
|
||||
});
|
||||
}, matrix);
|
||||
test('sendToDevice', () async {
|
||||
await matrix.sendToDevice(
|
||||
[deviceKeys],
|
||||
|
|
|
@ -20,6 +20,10 @@ import 'dart:convert';
|
|||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import './fake_client.dart';
|
||||
import './fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
/// All Tests related to device keys
|
||||
|
@ -41,33 +45,133 @@ void main() {
|
|||
}
|
||||
},
|
||||
'unsigned': {'device_display_name': "Alice's mobile phone"},
|
||||
'verified': false,
|
||||
'blocked': true,
|
||||
};
|
||||
var rawListJson = <String, dynamic>{
|
||||
'user_id': '@alice:example.com',
|
||||
'outdated': true,
|
||||
'device_keys': {'JLAFKJWSCS': rawJson},
|
||||
};
|
||||
|
||||
var userDeviceKeys = <String, DeviceKeysList>{
|
||||
'@alice:example.com': DeviceKeysList.fromJson(rawListJson),
|
||||
};
|
||||
var userDeviceKeyRaw = <String, dynamic>{
|
||||
'@alice:example.com': rawListJson,
|
||||
};
|
||||
final key = DeviceKeys.fromJson(rawJson, null);
|
||||
await key.setVerified(false, false);
|
||||
await key.setBlocked(true);
|
||||
expect(json.encode(key.toJson()), json.encode(rawJson));
|
||||
expect(key.directVerified, false);
|
||||
expect(key.blocked, true);
|
||||
|
||||
expect(json.encode(DeviceKeys.fromJson(rawJson).toJson()),
|
||||
json.encode(rawJson));
|
||||
expect(json.encode(DeviceKeysList.fromJson(rawListJson).toJson()),
|
||||
json.encode(rawListJson));
|
||||
rawJson = <String, dynamic>{
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['master'],
|
||||
'keys': {
|
||||
'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
|
||||
'82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8',
|
||||
},
|
||||
'signatures': {},
|
||||
};
|
||||
final crossKey = CrossSigningKey.fromJson(rawJson, null);
|
||||
expect(json.encode(crossKey.toJson()), json.encode(rawJson));
|
||||
expect(crossKey.usage.first, 'master');
|
||||
});
|
||||
|
||||
var mapFromRaw = <String, DeviceKeysList>{};
|
||||
for (final rawListEntry in userDeviceKeyRaw.entries) {
|
||||
mapFromRaw[rawListEntry.key] =
|
||||
DeviceKeysList.fromJson(rawListEntry.value);
|
||||
}
|
||||
expect(mapFromRaw.toString(), userDeviceKeys.toString());
|
||||
var olmEnabled = true;
|
||||
try {
|
||||
olm.init();
|
||||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
Client client;
|
||||
|
||||
test('setupClient', () async {
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('set blocked / verified', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
||||
final masterKey = client.userDeviceKeys[client.userID].masterKey;
|
||||
masterKey.setDirectVerified(true);
|
||||
// we need to populate the ssss cache to be able to test signing easily
|
||||
final handle = client.encryption.ssss.open();
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.maybeCacheAll();
|
||||
|
||||
expect(key.verified, true);
|
||||
await key.setBlocked(true);
|
||||
expect(key.verified, false);
|
||||
await key.setBlocked(false);
|
||||
expect(key.directVerified, false);
|
||||
expect(key.verified, true); // still verified via cross-sgining
|
||||
|
||||
expect(masterKey.verified, true);
|
||||
await masterKey.setBlocked(true);
|
||||
expect(masterKey.verified, false);
|
||||
await masterKey.setBlocked(false);
|
||||
expect(masterKey.verified, true);
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await key.setVerified(true);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys
|
||||
.any((k) => k == '/client/r0/keys/signatures/upload'),
|
||||
true);
|
||||
expect(key.directVerified, true);
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await key.setVerified(false);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys
|
||||
.any((k) => k == '/client/r0/keys/signatures/upload'),
|
||||
false);
|
||||
expect(key.directVerified, false);
|
||||
});
|
||||
|
||||
test('verification based on signatures', () async {
|
||||
final user = client.userDeviceKeys[client.userID];
|
||||
user.masterKey.setDirectVerified(true);
|
||||
expect(user.deviceKeys['GHTYAJCE'].crossVerified, true);
|
||||
expect(user.deviceKeys['GHTYAJCE'].signed, true);
|
||||
expect(user.getKey('GHTYAJCE').crossVerified, true);
|
||||
expect(user.deviceKeys['OTHERDEVICE'].crossVerified, true);
|
||||
expect(user.selfSigningKey.crossVerified, true);
|
||||
expect(
|
||||
user
|
||||
.getKey('F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY')
|
||||
.crossVerified,
|
||||
true);
|
||||
expect(user.userSigningKey.crossVerified, true);
|
||||
expect(user.verified, UserVerifiedStatus.verified);
|
||||
user.masterKey.setDirectVerified(false);
|
||||
expect(user.deviceKeys['GHTYAJCE'].crossVerified, false);
|
||||
expect(user.deviceKeys['OTHERDEVICE'].crossVerified, false);
|
||||
expect(user.verified, UserVerifiedStatus.unknown);
|
||||
user.masterKey.setDirectVerified(true);
|
||||
user.deviceKeys['GHTYAJCE'].signatures.clear();
|
||||
expect(user.deviceKeys['GHTYAJCE'].verified,
|
||||
true); // it's our own device, should be direct verified
|
||||
expect(
|
||||
user.deviceKeys['GHTYAJCE'].signed, false); // not verified for others
|
||||
user.deviceKeys['OTHERDEVICE'].signatures.clear();
|
||||
expect(user.verified, UserVerifiedStatus.unknownDevice);
|
||||
});
|
||||
|
||||
test('start verification', () async {
|
||||
var req = client
|
||||
.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS']
|
||||
.startVerification();
|
||||
expect(req != null, true);
|
||||
expect(req.room != null, false);
|
||||
|
||||
req =
|
||||
await client.userDeviceKeys['@alice:example.com'].startVerification();
|
||||
expect(req != null, true);
|
||||
expect(req.room != null, true);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
113
test/encryption/cross_signing_test.dart
Normal file
113
test/encryption/cross_signing_test.dart
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||
* 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:convert';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
group('Cross Signing', () {
|
||||
var olmEnabled = true;
|
||||
try {
|
||||
olm.init();
|
||||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
Client client;
|
||||
|
||||
test('setupClient', () async {
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('basic things', () async {
|
||||
expect(client.encryption.crossSigning.enabled, true);
|
||||
});
|
||||
|
||||
test('selfSign', () async {
|
||||
final key = client.userDeviceKeys[client.userID].masterKey;
|
||||
key.setDirectVerified(false);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption.crossSigning.selfSign(recoveryKey: SSSS_KEY);
|
||||
expect(key.directVerified, true);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints
|
||||
.containsKey('/client/r0/keys/signatures/upload'),
|
||||
true);
|
||||
expect(await client.encryption.crossSigning.isCached(), true);
|
||||
});
|
||||
|
||||
test('signable', () async {
|
||||
expect(
|
||||
client.encryption.crossSigning
|
||||
.signable([client.userDeviceKeys[client.userID].masterKey]),
|
||||
true);
|
||||
expect(
|
||||
client.encryption.crossSigning.signable([
|
||||
client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]
|
||||
]),
|
||||
false);
|
||||
expect(
|
||||
client.encryption.crossSigning.signable(
|
||||
[client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE']]),
|
||||
true);
|
||||
expect(
|
||||
client.encryption.crossSigning.signable([
|
||||
client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS']
|
||||
]),
|
||||
false);
|
||||
});
|
||||
|
||||
test('sign', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption.crossSigning.sign([
|
||||
client.userDeviceKeys[client.userID].masterKey,
|
||||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'],
|
||||
client.userDeviceKeys['@othertest:fakeServer.notExisting'].masterKey
|
||||
]);
|
||||
var body = json.decode(FakeMatrixApi
|
||||
.calledEndpoints['/client/r0/keys/signatures/upload'].first);
|
||||
expect(body['@test:fakeServer.notExisting'].containsKey('OTHERDEVICE'),
|
||||
true);
|
||||
expect(
|
||||
body['@test:fakeServer.notExisting'].containsKey(
|
||||
client.userDeviceKeys[client.userID].masterKey.publicKey),
|
||||
true);
|
||||
expect(
|
||||
body['@othertest:fakeServer.notExisting'].containsKey(client
|
||||
.userDeviceKeys['@othertest:fakeServer.notExisting']
|
||||
.masterKey
|
||||
.publicKey),
|
||||
true);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -84,12 +84,15 @@ void main() {
|
|||
room: room,
|
||||
originServerTs: now,
|
||||
eventId: '\$event',
|
||||
senderId: '@alice:example.com',
|
||||
);
|
||||
final decryptedEvent =
|
||||
await client.encryption.decryptRoomEvent(roomId, encryptedEvent);
|
||||
expect(decryptedEvent.type, 'm.room.message');
|
||||
expect(decryptedEvent.content['msgtype'], 'm.text');
|
||||
expect(decryptedEvent.content['text'], 'Hello foxies!');
|
||||
await client.encryption
|
||||
.decryptRoomEvent(roomId, encryptedEvent, store: true);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -56,8 +56,9 @@ void main() {
|
|||
test('Create Request', () async {
|
||||
var matrix = await getClient();
|
||||
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
|
||||
await matrix.encryption.keyManager
|
||||
.request(requestRoom, 'sessionId', validSenderKey);
|
||||
await matrix.encryption.keyManager.request(
|
||||
requestRoom, 'sessionId', validSenderKey,
|
||||
tryOnlineBackup: false);
|
||||
var foundEvent = false;
|
||||
for (var entry in FakeMatrixApi.calledEndpoints.entries) {
|
||||
final payload = jsonDecode(entry.value.first);
|
||||
|
@ -85,10 +86,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',
|
||||
|
@ -223,8 +224,9 @@ void main() {
|
|||
test('Receive shared keys', () async {
|
||||
var matrix = await getClient();
|
||||
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
|
||||
await matrix.encryption.keyManager
|
||||
.request(requestRoom, validSessionId, validSenderKey);
|
||||
await matrix.encryption.keyManager.request(
|
||||
requestRoom, validSessionId, validSenderKey,
|
||||
tryOnlineBackup: false);
|
||||
|
||||
final session = await matrix.encryption.keyManager
|
||||
.loadInboundGroupSession(
|
||||
|
@ -279,8 +281,9 @@ void main() {
|
|||
false);
|
||||
|
||||
// unknown device
|
||||
await matrix.encryption.keyManager
|
||||
.request(requestRoom, validSessionId, validSenderKey);
|
||||
await matrix.encryption.keyManager.request(
|
||||
requestRoom, validSessionId, validSenderKey,
|
||||
tryOnlineBackup: false);
|
||||
matrix.encryption.keyManager.clearInboundGroupSessions();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
|
@ -304,8 +307,9 @@ void main() {
|
|||
false);
|
||||
|
||||
// no encrypted content
|
||||
await matrix.encryption.keyManager
|
||||
.request(requestRoom, validSessionId, validSenderKey);
|
||||
await matrix.encryption.keyManager.request(
|
||||
requestRoom, validSessionId, validSenderKey,
|
||||
tryOnlineBackup: false);
|
||||
matrix.encryption.keyManager.clearInboundGroupSessions();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
|
|
|
@ -16,12 +16,47 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
class MockSSSS extends SSSS {
|
||||
MockSSSS(Encryption encryption) : super(encryption);
|
||||
|
||||
bool requestedSecrets = false;
|
||||
@override
|
||||
Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
|
||||
requestedSecrets = true;
|
||||
final handle = open();
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.maybeCacheAll();
|
||||
}
|
||||
}
|
||||
|
||||
EventUpdate getLastSentEvent(KeyVerification req) {
|
||||
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||
.firstWhere((p) => p.key.contains('/send/'));
|
||||
final type = entry.key.split('/')[6];
|
||||
final content = json.decode(entry.value.first);
|
||||
return EventUpdate(
|
||||
content: {
|
||||
'event_id': req.transactionId,
|
||||
'type': type,
|
||||
'content': content,
|
||||
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
|
||||
'sender': req.client.userID,
|
||||
},
|
||||
eventType: type,
|
||||
type: 'timeline',
|
||||
roomID: req.room.id,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
/// All Tests related to the ChatTime
|
||||
|
@ -38,70 +73,392 @@ void main() {
|
|||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
Client client;
|
||||
Room room;
|
||||
var updateCounter = 0;
|
||||
KeyVerification keyVerification;
|
||||
// key @othertest:fakeServer.notExisting
|
||||
const otherPickledOlmAccount =
|
||||
'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA';
|
||||
|
||||
Client client1;
|
||||
Client client2;
|
||||
|
||||
test('setupClient', () async {
|
||||
client = await getClient();
|
||||
room = Room(id: '!localpart:server.abc', client: client);
|
||||
keyVerification = KeyVerification(
|
||||
encryption: client.encryption,
|
||||
room: room,
|
||||
userId: '@alice:example.com',
|
||||
deviceId: 'ABCD',
|
||||
onUpdate: () => updateCounter++,
|
||||
client1 = await getClient();
|
||||
client2 =
|
||||
Client('othertestclient', debug: true, httpClient: FakeMatrixApi());
|
||||
client2.database = client1.database;
|
||||
await client2.checkServer('https://fakeServer.notExisting');
|
||||
client2.connect(
|
||||
newToken: 'abc',
|
||||
newUserID: '@othertest:fakeServer.notExisting',
|
||||
newHomeserver: client2.api.homeserver,
|
||||
newDeviceName: 'Text Matrix Client',
|
||||
newDeviceID: 'FOXDEVICE',
|
||||
newOlmAccount: otherPickledOlmAccount,
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
client1.verificationMethods = {
|
||||
KeyVerificationMethod.emoji,
|
||||
KeyVerificationMethod.numbers
|
||||
};
|
||||
client2.verificationMethods = {
|
||||
KeyVerificationMethod.emoji,
|
||||
KeyVerificationMethod.numbers
|
||||
};
|
||||
});
|
||||
|
||||
test('acceptSas', () async {
|
||||
await keyVerification.acceptSas();
|
||||
});
|
||||
test('acceptVerification', () async {
|
||||
await keyVerification.acceptVerification();
|
||||
});
|
||||
test('cancel', () async {
|
||||
await keyVerification.cancel('m.cancelcode');
|
||||
expect(keyVerification.canceled, true);
|
||||
expect(keyVerification.canceledCode, 'm.cancelcode');
|
||||
expect(keyVerification.canceledReason, null);
|
||||
});
|
||||
test('handlePayload', () async {
|
||||
await keyVerification.handlePayload('m.key.verification.request', {
|
||||
'from_device': 'AliceDevice2',
|
||||
'methods': ['m.sas.v1'],
|
||||
'timestamp': 1559598944869,
|
||||
'transaction_id': 'S0meUniqueAndOpaqueString'
|
||||
test('Run emoji / number verification', () async {
|
||||
// for a full run we test in-room verification in a cleartext room
|
||||
// because then we can easily intercept the payloads and inject in the other client
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
// make sure our master key is *not* verified to not triger SSSS for now
|
||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
|
||||
final req1 =
|
||||
await client1.userDeviceKeys[client2.userID].startVerification();
|
||||
var evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
KeyVerification req2;
|
||||
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||
req2 = req;
|
||||
});
|
||||
await keyVerification.handlePayload('m.key.verification.start', {
|
||||
'from_device': 'BobDevice1',
|
||||
'method': 'm.sas.v1',
|
||||
'transaction_id': 'S0meUniqueAndOpaqueString'
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
await sub.cancel();
|
||||
expect(req2 != null, true);
|
||||
|
||||
// send ready
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await req2.acceptVerification();
|
||||
evt = getLastSentEvent(req2);
|
||||
expect(req2.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
// send start
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req1);
|
||||
|
||||
// send accept
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req2);
|
||||
|
||||
// send key
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req1);
|
||||
|
||||
// send key
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req2);
|
||||
|
||||
// receive last key
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
|
||||
// compare emoji
|
||||
expect(req1.state, KeyVerificationState.askSas);
|
||||
expect(req2.state, KeyVerificationState.askSas);
|
||||
expect(req1.sasTypes[0], 'emoji');
|
||||
expect(req1.sasTypes[1], 'decimal');
|
||||
expect(req2.sasTypes[0], 'emoji');
|
||||
expect(req2.sasTypes[1], 'decimal');
|
||||
// compare emoji
|
||||
final emoji1 = req1.sasEmojis;
|
||||
final emoji2 = req2.sasEmojis;
|
||||
for (var i = 0; i < 7; i++) {
|
||||
expect(emoji1[i].emoji, emoji2[i].emoji);
|
||||
expect(emoji1[i].name, emoji2[i].name);
|
||||
}
|
||||
// compare numbers
|
||||
final numbers1 = req1.sasNumbers;
|
||||
final numbers2 = req2.sasNumbers;
|
||||
for (var i = 0; i < 3; i++) {
|
||||
expect(numbers1[i], numbers2[i]);
|
||||
}
|
||||
|
||||
// alright, they match
|
||||
|
||||
// send mac
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await req1.acceptSas();
|
||||
evt = getLastSentEvent(req1);
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
expect(req1.state, KeyVerificationState.waitingSas);
|
||||
|
||||
// send mac
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await req2.acceptSas();
|
||||
evt = getLastSentEvent(req2);
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
|
||||
expect(req1.state, KeyVerificationState.done);
|
||||
expect(req2.state, KeyVerificationState.done);
|
||||
expect(
|
||||
client1.userDeviceKeys[client2.userID].deviceKeys[client2.deviceID]
|
||||
.directVerified,
|
||||
true);
|
||||
expect(
|
||||
client2.userDeviceKeys[client1.userID].deviceKeys[client1.deviceID]
|
||||
.directVerified,
|
||||
true);
|
||||
await client1.encryption.keyVerificationManager.cleanup();
|
||||
await client2.encryption.keyVerificationManager.cleanup();
|
||||
});
|
||||
|
||||
test('ask SSSS start', () async {
|
||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
|
||||
await client1.database.clearSSSSCache(client1.id);
|
||||
final req1 =
|
||||
await client1.userDeviceKeys[client2.userID].startVerification();
|
||||
expect(req1.state, KeyVerificationState.askSSSS);
|
||||
await req1.openSSSS(recoveryKey: SSSS_KEY);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
await req1.cancel();
|
||||
await client1.encryption.keyVerificationManager.cleanup();
|
||||
});
|
||||
|
||||
test('ask SSSS end', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
// make sure our master key is *not* verified to not triger SSSS for now
|
||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
|
||||
// the other one has to have their master key verified to trigger asking for ssss
|
||||
client2.userDeviceKeys[client2.userID].masterKey.setDirectVerified(true);
|
||||
final req1 =
|
||||
await client1.userDeviceKeys[client2.userID].startVerification();
|
||||
var evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
KeyVerification req2;
|
||||
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||
req2 = req;
|
||||
});
|
||||
await keyVerification.handlePayload('m.key.verification.cancel', {
|
||||
'code': 'm.user',
|
||||
'reason': 'User rejected the key verification request',
|
||||
'transaction_id': 'S0meUniqueAndOpaqueString'
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
await sub.cancel();
|
||||
expect(req2 != null, true);
|
||||
|
||||
// send ready
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await req2.acceptVerification();
|
||||
evt = getLastSentEvent(req2);
|
||||
expect(req2.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
// send start
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req1);
|
||||
|
||||
// send accept
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req2);
|
||||
|
||||
// send key
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req1);
|
||||
|
||||
// send key
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req2);
|
||||
|
||||
// receive last key
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
|
||||
// compare emoji
|
||||
expect(req1.state, KeyVerificationState.askSas);
|
||||
expect(req2.state, KeyVerificationState.askSas);
|
||||
// compare emoji
|
||||
final emoji1 = req1.sasEmojis;
|
||||
final emoji2 = req2.sasEmojis;
|
||||
for (var i = 0; i < 7; i++) {
|
||||
expect(emoji1[i].emoji, emoji2[i].emoji);
|
||||
expect(emoji1[i].name, emoji2[i].name);
|
||||
}
|
||||
// compare numbers
|
||||
final numbers1 = req1.sasNumbers;
|
||||
final numbers2 = req2.sasNumbers;
|
||||
for (var i = 0; i < 3; i++) {
|
||||
expect(numbers1[i], numbers2[i]);
|
||||
}
|
||||
|
||||
// alright, they match
|
||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
|
||||
await client1.database.clearSSSSCache(client1.id);
|
||||
|
||||
// send mac
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await req1.acceptSas();
|
||||
evt = getLastSentEvent(req1);
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
expect(req1.state, KeyVerificationState.waitingSas);
|
||||
|
||||
// send mac
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await req2.acceptSas();
|
||||
evt = getLastSentEvent(req2);
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
|
||||
expect(req1.state, KeyVerificationState.askSSSS);
|
||||
expect(req2.state, KeyVerificationState.done);
|
||||
|
||||
await req1.openSSSS(recoveryKey: SSSS_KEY);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
expect(req1.state, KeyVerificationState.done);
|
||||
|
||||
client1.encryption.ssss = MockSSSS(client1.encryption);
|
||||
(client1.encryption.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client1.database.clearSSSSCache(client1.id);
|
||||
await req1.maybeRequestSSSSSecrets();
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
expect((client1.encryption.ssss as MockSSSS).requestedSecrets, true);
|
||||
// delay for 12 seconds to be sure no other tests clear the ssss cache
|
||||
await Future.delayed(Duration(seconds: 12));
|
||||
|
||||
await client1.encryption.keyVerificationManager.cleanup();
|
||||
await client2.encryption.keyVerificationManager.cleanup();
|
||||
});
|
||||
|
||||
test('reject verification', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
// make sure our master key is *not* verified to not triger SSSS for now
|
||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
|
||||
final req1 =
|
||||
await client1.userDeviceKeys[client2.userID].startVerification();
|
||||
var evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
KeyVerification req2;
|
||||
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||
req2 = req;
|
||||
});
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
await sub.cancel();
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await req2.rejectVerification();
|
||||
evt = getLastSentEvent(req2);
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
expect(req1.state, KeyVerificationState.error);
|
||||
expect(req2.state, KeyVerificationState.error);
|
||||
|
||||
await client1.encryption.keyVerificationManager.cleanup();
|
||||
await client2.encryption.keyVerificationManager.cleanup();
|
||||
});
|
||||
test('rejectSas', () async {
|
||||
await keyVerification.rejectSas();
|
||||
|
||||
test('reject sas', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
// make sure our master key is *not* verified to not triger SSSS for now
|
||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
|
||||
final req1 =
|
||||
await client1.userDeviceKeys[client2.userID].startVerification();
|
||||
var evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
KeyVerification req2;
|
||||
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||
req2 = req;
|
||||
});
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
await sub.cancel();
|
||||
expect(req2 != null, true);
|
||||
|
||||
// send ready
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await req2.acceptVerification();
|
||||
evt = getLastSentEvent(req2);
|
||||
expect(req2.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
// send start
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req1);
|
||||
|
||||
// send accept
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req2);
|
||||
|
||||
// send key
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req1);
|
||||
|
||||
// send key
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
evt = getLastSentEvent(req2);
|
||||
|
||||
// receive last key
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
|
||||
await req1.acceptSas();
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await req2.rejectSas();
|
||||
evt = getLastSentEvent(req2);
|
||||
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
expect(req1.state, KeyVerificationState.error);
|
||||
expect(req2.state, KeyVerificationState.error);
|
||||
|
||||
await client1.encryption.keyVerificationManager.cleanup();
|
||||
await client2.encryption.keyVerificationManager.cleanup();
|
||||
});
|
||||
test('rejectVerification', () async {
|
||||
await keyVerification.rejectVerification();
|
||||
});
|
||||
test('start', () async {
|
||||
await keyVerification.start();
|
||||
});
|
||||
test('verifyActivity', () async {
|
||||
final verified = await keyVerification.verifyActivity();
|
||||
expect(verified, true);
|
||||
keyVerification?.dispose();
|
||||
|
||||
test('other device accepted', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
// make sure our master key is *not* verified to not triger SSSS for now
|
||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
|
||||
final req1 =
|
||||
await client1.userDeviceKeys[client2.userID].startVerification();
|
||||
var evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
KeyVerification req2;
|
||||
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||
req2 = req;
|
||||
});
|
||||
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
await sub.cancel();
|
||||
expect(req2 != null, true);
|
||||
|
||||
await client2.encryption.keyVerificationManager
|
||||
.handleEventUpdate(EventUpdate(
|
||||
content: {
|
||||
'event_id': req2.transactionId,
|
||||
'type': 'm.key.verification.ready',
|
||||
'content': {
|
||||
'methods': ['m.sas.v1'],
|
||||
'from_device': 'SOMEOTHERDEVICE',
|
||||
'm.relates_to': {
|
||||
'rel_type': 'm.reference',
|
||||
'event_id': req2.transactionId,
|
||||
},
|
||||
},
|
||||
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
|
||||
'sender': client2.userID,
|
||||
},
|
||||
eventType: 'm.key.verification.ready',
|
||||
type: 'timeline',
|
||||
roomID: req2.room.id,
|
||||
));
|
||||
expect(req2.state, KeyVerificationState.error);
|
||||
|
||||
await req2.cancel();
|
||||
await client1.encryption.keyVerificationManager.cleanup();
|
||||
await client2.encryption.keyVerificationManager.cleanup();
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
await client1.dispose(closeDatabase: true);
|
||||
await client2.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -99,8 +99,26 @@ void main() {
|
|||
false);
|
||||
});
|
||||
|
||||
test('restoreOlmSession', () async {
|
||||
client.encryption.olmManager.olmSessions.clear();
|
||||
await client.encryption.olmManager
|
||||
.restoreOlmSession(client.userID, client.identityKey);
|
||||
expect(client.encryption.olmManager.olmSessions.length, 1);
|
||||
|
||||
client.encryption.olmManager.olmSessions.clear();
|
||||
await client.encryption.olmManager
|
||||
.restoreOlmSession(client.userID, 'invalid');
|
||||
expect(client.encryption.olmManager.olmSessions.length, 0);
|
||||
|
||||
client.encryption.olmManager.olmSessions.clear();
|
||||
await client.encryption.olmManager
|
||||
.restoreOlmSession('invalid', client.identityKey);
|
||||
expect(client.encryption.olmManager.olmSessions.length, 0);
|
||||
});
|
||||
|
||||
test('startOutgoingOlmSessions', () async {
|
||||
// start an olm session.....with ourself!
|
||||
client.encryption.olmManager.olmSessions.clear();
|
||||
await client.encryption.olmManager.startOutgoingOlmSessions(
|
||||
[client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]]);
|
||||
expect(
|
||||
|
|
73
test/encryption/online_key_backup_test.dart
Normal file
73
test/encryption/online_key_backup_test.dart
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||
* 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 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
|
||||
void main() {
|
||||
group('Online Key Backup', () {
|
||||
var olmEnabled = true;
|
||||
try {
|
||||
olm.init();
|
||||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
Client client;
|
||||
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
|
||||
|
||||
test('setupClient', () async {
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('basic things', () async {
|
||||
expect(client.encryption.keyManager.enabled, true);
|
||||
expect(await client.encryption.keyManager.isCached(), false);
|
||||
final handle = client.encryption.ssss.open();
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.maybeCacheAll();
|
||||
expect(await client.encryption.keyManager.isCached(), true);
|
||||
});
|
||||
|
||||
test('load key', () async {
|
||||
client.encryption.keyManager.clearInboundGroupSessions();
|
||||
await client.encryption.keyManager
|
||||
.request(client.getRoomById(roomId), sessionId, senderKey);
|
||||
expect(
|
||||
client.encryption.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
true);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
398
test/encryption/ssss_test.dart
Normal file
398
test/encryption/ssss_test.dart
Normal file
|
@ -0,0 +1,398 @@
|
|||
/*
|
||||
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||
* 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:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
group('SSSS', () {
|
||||
var olmEnabled = true;
|
||||
try {
|
||||
olm.init();
|
||||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
Client client;
|
||||
|
||||
test('setupClient', () async {
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('basic things', () async {
|
||||
expect(client.encryption.ssss.defaultKeyId,
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3');
|
||||
});
|
||||
|
||||
test('encrypt / decrypt', () {
|
||||
final key = Uint8List.fromList(SecureRandom(32).bytes);
|
||||
|
||||
final enc = SSSS.encryptAes('secret foxies', key, 'name');
|
||||
final dec = SSSS.decryptAes(enc, key, 'name');
|
||||
expect(dec, 'secret foxies');
|
||||
});
|
||||
|
||||
test('store', () async {
|
||||
final handle = client.encryption.ssss.open();
|
||||
var failed = false;
|
||||
try {
|
||||
handle.unlock(passphrase: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed, true);
|
||||
expect(handle.isUnlocked, false);
|
||||
failed = false;
|
||||
try {
|
||||
handle.unlock(recoveryKey: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed, true);
|
||||
expect(handle.isUnlocked, false);
|
||||
handle.unlock(passphrase: SSSS_PASSPHRASE);
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
expect(handle.isUnlocked, true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await handle.store('best animal', 'foxies');
|
||||
// alright, since we don't properly sync we will manually have to update
|
||||
// account_data for this test
|
||||
final content = FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal']
|
||||
.first;
|
||||
client.accountData['best animal'] = BasicEvent.fromJson({
|
||||
'type': 'best animal',
|
||||
'content': json.decode(content),
|
||||
});
|
||||
expect(await handle.getStored('best animal'), 'foxies');
|
||||
});
|
||||
|
||||
test('cache', () async {
|
||||
final handle =
|
||||
client.encryption.ssss.open('m.cross_signing.self_signing');
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
expect(
|
||||
(await client.encryption.ssss
|
||||
.getCached('m.cross_signing.self_signing')) !=
|
||||
null,
|
||||
false);
|
||||
expect(
|
||||
(await client.encryption.ssss
|
||||
.getCached('m.cross_signing.user_signing')) !=
|
||||
null,
|
||||
false);
|
||||
await handle.getStored('m.cross_signing.self_signing');
|
||||
expect(
|
||||
(await client.encryption.ssss
|
||||
.getCached('m.cross_signing.self_signing')) !=
|
||||
null,
|
||||
true);
|
||||
await handle.maybeCacheAll();
|
||||
expect(
|
||||
(await client.encryption.ssss
|
||||
.getCached('m.cross_signing.user_signing')) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
(await client.encryption.ssss.getCached('m.megolm_backup.v1')) !=
|
||||
null,
|
||||
true);
|
||||
});
|
||||
|
||||
test('make share requests', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
||||
key.setDirectVerified(true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption.ssss.request('some.type', [key]);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
true);
|
||||
});
|
||||
|
||||
test('answer to share requests', () async {
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.cross_signing.self_signing',
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
true);
|
||||
|
||||
// now test some fail scenarios
|
||||
|
||||
// not by us
|
||||
event = ToDeviceEvent(
|
||||
sender: '@someotheruser:example.org',
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.cross_signing.self_signing',
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// secret not cached
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.unknown.secret',
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// is a cancelation
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request_cancellation',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.cross_signing.self_signing',
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// device not verified
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
||||
key.setDirectVerified(false);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.cross_signing.self_signing',
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
key.setDirectVerified(true);
|
||||
});
|
||||
|
||||
test('receive share requests', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
||||
key.setDirectVerified(true);
|
||||
final handle =
|
||||
client.encryption.ssss.open('m.cross_signing.self_signing');
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption.ssss.getCached('best animal'), 'foxies!');
|
||||
|
||||
// test the different validators
|
||||
for (final type in [
|
||||
'm.cross_signing.self_signing',
|
||||
'm.cross_signing.user_signing',
|
||||
'm.megolm_backup.v1'
|
||||
]) {
|
||||
final secret = await handle.getStored(type);
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request(type, [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption.ssss.pendingShareRequests.keys.first,
|
||||
'secret': secret,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption.ssss.getCached(type), secret);
|
||||
}
|
||||
|
||||
// test different fail scenarios
|
||||
|
||||
// not encrypted
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
);
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||
|
||||
// unknown request id
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': 'invalid',
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||
|
||||
// not from a device we sent the request to
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': 'invalid',
|
||||
},
|
||||
);
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||
|
||||
// secret not a string
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 42,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||
|
||||
// validator doesn't check out
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('m.megolm_backup.v1', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
await client.encryption.ssss.getCached('m.megolm_backup.v1'), null);
|
||||
});
|
||||
|
||||
test('request all', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
||||
key.setDirectVerified(true);
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.maybeRequestAll([key]);
|
||||
expect(client.encryption.ssss.pendingShareRequests.length, 3);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -21,6 +21,9 @@ import 'package:famedlysdk/famedlysdk.dart';
|
|||
import 'fake_matrix_api.dart';
|
||||
import 'fake_database.dart';
|
||||
|
||||
const SSSS_PASSPHRASE = 'nae7ahDiequ7ohniufah3ieS2je1thohX4xeeka7aixohsho9O';
|
||||
const SSSS_KEY = 'EsT9 RzbW VhPW yqNp cC7j ViiW 5TZB LuY4 ryyv 9guN Ysmr WDPH';
|
||||
|
||||
// key @test:fakeServer.notExisting
|
||||
const pickledOlmAccount =
|
||||
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
|
||||
|
|
|
@ -47,6 +47,7 @@ class FakeMatrixApi extends MockClient {
|
|||
final dynamic data =
|
||||
method == 'GET' ? request.url.queryParameters : request.body;
|
||||
dynamic res = {};
|
||||
var statusCode = 200;
|
||||
|
||||
//print('\$method request to $action with Data: $data');
|
||||
|
||||
|
@ -68,25 +69,28 @@ class FakeMatrixApi extends MockClient {
|
|||
if (api.containsKey(method) && api[method].containsKey(action)) {
|
||||
res = api[method][action](data);
|
||||
if (res is Map && res.containsKey('errcode')) {
|
||||
return Response(json.encode(res), 405);
|
||||
statusCode = 405;
|
||||
}
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains('/client/r0/sendToDevice/')) {
|
||||
return Response(json.encode({}), 200);
|
||||
res = {};
|
||||
} else if (method == 'GET' &&
|
||||
action.contains('/client/r0/rooms/') &&
|
||||
action.contains('/state/m.room.member/')) {
|
||||
res = {'displayname': ''};
|
||||
return Response(json.encode(res), 200);
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains(
|
||||
'/client/r0/rooms/%211234%3AfakeServer.notExisting/send/')) {
|
||||
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
|
||||
} else {
|
||||
res = {
|
||||
'errcode': 'M_UNRECOGNIZED',
|
||||
'error': 'Unrecognized request'
|
||||
};
|
||||
return Response(json.encode(res), 405);
|
||||
statusCode = 405;
|
||||
}
|
||||
|
||||
return Response(json.encode(res), 200);
|
||||
return Response.bytes(utf8.encode(json.encode(res)), statusCode);
|
||||
});
|
||||
|
||||
static Map<String, dynamic> messagesResponse = {
|
||||
|
@ -194,6 +198,18 @@ class FakeMatrixApi extends MockClient {
|
|||
'origin_server_ts': 1417731086795,
|
||||
'event_id': '666972737430353:example.com'
|
||||
},
|
||||
{
|
||||
'content': {
|
||||
'pinned': ['1234:bla']
|
||||
},
|
||||
'type': 'm.room.pinned_events',
|
||||
'event_id': '21432735824443PhrSn:example.org',
|
||||
'room_id': '!1234:example.com',
|
||||
'sender': '@example:example.org',
|
||||
'origin_server_ts': 1432735824653,
|
||||
'unsigned': {'age': 1234},
|
||||
'state_key': ''
|
||||
},
|
||||
]
|
||||
},
|
||||
'timeline': {
|
||||
|
@ -516,6 +532,75 @@ class FakeMatrixApi extends MockClient {
|
|||
},
|
||||
'type': 'm.direct'
|
||||
},
|
||||
{
|
||||
'type': 'm.secret_storage.default_key',
|
||||
'content': {'key': '0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3'}
|
||||
},
|
||||
{
|
||||
'type': 'm.secret_storage.key.0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3',
|
||||
'content': {
|
||||
'algorithm': 'm.secret_storage.v1.aes-hmac-sha2',
|
||||
'passphrase': {
|
||||
'algorithm': 'm.pbkdf2',
|
||||
'iterations': 500000,
|
||||
'salt': 'F4jJ80mr0Fc8mRwU9JgA3lQDyjPuZXQL'
|
||||
},
|
||||
'iv': 'HjbTgIoQH2pI7jQo19NUzA==',
|
||||
'mac': 'QbJjQzDnAggU0cM4RBnDxw2XyarRGjdahcKukP9xVlk='
|
||||
}
|
||||
},
|
||||
{
|
||||
'type': 'm.cross_signing.master',
|
||||
'content': {
|
||||
'encrypted': {
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||
'iv': 'eIb2IITxtmcq+1TrT8D5eQ==',
|
||||
'ciphertext':
|
||||
'lWRTPo5qxf4LAVwVPzGHOyMcP181n7bb9/B0lvkLDC2Oy4DvAL0eLx2x3bY=',
|
||||
'mac': 'Ynx89tIxPkx0o6ljMgxszww17JOgB4tg4etmNnMC9XI='
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'type': 'm.cross_signing.self_signing',
|
||||
'content': {
|
||||
'encrypted': {
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||
'iv': 'YqU2XIjYulYZl+bkZtGgVw==',
|
||||
'ciphertext':
|
||||
'kM2TSoy/jR/4d357ZoRPbpPypxQl6XRLo3FsEXz+f7vIOp82GeRp28RYb3k=',
|
||||
'mac': 'F+DZa5tAFmWsYSryw5EuEpzTmmABRab4GETkM85bGGo='
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'type': 'm.cross_signing.user_signing',
|
||||
'content': {
|
||||
'encrypted': {
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||
'iv': 'D7AM3LXFu7ZlyGOkR+OeqQ==',
|
||||
'ciphertext':
|
||||
'bYA2+OMgsO6QB1E31aY+ESAWrT0fUBTXqajy4qmL7bVDSZY4Uj64EXNbHuA=',
|
||||
'mac': 'j2UtyPo/UBSoiaQCWfzCiRZXp3IRt0ZZujuXgUMjnw4='
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'type': 'm.megolm_backup.v1',
|
||||
'content': {
|
||||
'encrypted': {
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3': {
|
||||
'iv': 'cL/0MJZaiEd3fNU+I9oJrw==',
|
||||
'ciphertext':
|
||||
'WL73Pzdk5wZdaaSpaeRH0uZYKcxkuV8IS6Qa2FEfA1+vMeRLuHcWlXbMX0w=',
|
||||
'mac': '+xozp909S6oDX8KRV8D8ZFVRyh7eEYQpPP76f+DOsnw='
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
'to_device': {
|
||||
|
@ -1461,6 +1546,65 @@ class FakeMatrixApi extends MockClient {
|
|||
'event_format': 'client',
|
||||
'event_fields': ['type', 'content', 'sender']
|
||||
},
|
||||
'/client/unstable/room_keys/version': (var req) => {
|
||||
'algorithm': 'm.megolm_backup.v1.curve25519-aes-sha2',
|
||||
'auth_data': {
|
||||
'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM',
|
||||
'signatures': {},
|
||||
},
|
||||
'count': 0,
|
||||
'etag': '0',
|
||||
'version': '5',
|
||||
},
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5':
|
||||
(var req) => {
|
||||
'first_message_index': 0,
|
||||
'forwarded_count': 0,
|
||||
'is_verified': true,
|
||||
'session_data': {
|
||||
'ephemeral': 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc',
|
||||
'ciphertext':
|
||||
'19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg',
|
||||
'mac': 'QzKV/fgAs4U',
|
||||
},
|
||||
},
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5':
|
||||
(var req) => {
|
||||
'sessions': {
|
||||
'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU': {
|
||||
'first_message_index': 0,
|
||||
'forwarded_count': 0,
|
||||
'is_verified': true,
|
||||
'session_data': {
|
||||
'ephemeral':
|
||||
'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc',
|
||||
'ciphertext':
|
||||
'19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg',
|
||||
'mac': 'QzKV/fgAs4U',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/client/unstable/room_keys/keys?version=5': (var req) => {
|
||||
'rooms': {
|
||||
'!726s6s6q:example.com': {
|
||||
'sessions': {
|
||||
'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU': {
|
||||
'first_message_index': 0,
|
||||
'forwarded_count': 0,
|
||||
'is_verified': true,
|
||||
'session_data': {
|
||||
'ephemeral':
|
||||
'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc',
|
||||
'ciphertext':
|
||||
'19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg',
|
||||
'mac': 'QzKV/fgAs4U',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'POST': {
|
||||
'/client/r0/delete_devices': (var req) => {},
|
||||
|
@ -1671,7 +1815,30 @@ class FakeMatrixApi extends MockClient {
|
|||
'ed25519:GHTYAJCE':
|
||||
'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo'
|
||||
},
|
||||
'signatures': {},
|
||||
'signatures': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY':
|
||||
'Q4/55vZjEJD7M2EC40bgZqd9Zuy/4C75UPVopJdXeioQVaKtFf6EF0nUUuql0yD+r3hinsZcock0wO6Q2xcoAQ',
|
||||
},
|
||||
},
|
||||
},
|
||||
'OTHERDEVICE': {
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'device_id': 'OTHERDEVICE',
|
||||
'algorithms': [
|
||||
'm.olm.v1.curve25519-aes-sha2',
|
||||
'm.megolm.v1.aes-sha2'
|
||||
],
|
||||
'keys': {
|
||||
'curve25519:OTHERDEVICE': 'blah',
|
||||
'ed25519:OTHERDEVICE': 'blah'
|
||||
},
|
||||
'signatures': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY':
|
||||
'o7ucKPWrF2VKx7wYqP1f+aw4QohLMz7kX+SIw6aWCYsLC3XyIlg8rX/7QQ9B8figCVnRK7IjtjWvQodBCfWCAA',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'@othertest:fakeServer.notExisting': {
|
||||
|
@ -1692,6 +1859,73 @@ class FakeMatrixApi extends MockClient {
|
|||
},
|
||||
},
|
||||
},
|
||||
'master_keys': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['master'],
|
||||
'keys': {
|
||||
'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
|
||||
'82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8',
|
||||
},
|
||||
'signatures': {},
|
||||
},
|
||||
'@othertest:fakeServer.notExisting': {
|
||||
'user_id': '@othertest:fakeServer.notExisting',
|
||||
'usage': ['master'],
|
||||
'keys': {
|
||||
'ed25519:master': 'master',
|
||||
},
|
||||
'signatures': {},
|
||||
},
|
||||
},
|
||||
'self_signing_keys': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['self_signing'],
|
||||
'keys': {
|
||||
'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY':
|
||||
'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY',
|
||||
},
|
||||
'signatures': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
|
||||
'afkrbGvPn5Zb5zc7Lk9cz2skI3QrzI/L0st1GS+/GATxNjMzc6vKmGu7r9cMb1GJxy4RdeUpfH3L7Fs/fNL1Dw',
|
||||
},
|
||||
},
|
||||
},
|
||||
'@othertest:fakeServer.notExisting': {
|
||||
'user_id': '@othertest:fakeServer.notExisting',
|
||||
'usage': ['self_signing'],
|
||||
'keys': {
|
||||
'ed25519:self_signing': 'self_signing',
|
||||
},
|
||||
'signatures': {},
|
||||
},
|
||||
},
|
||||
'user_signing_keys': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['user_signing'],
|
||||
'keys': {
|
||||
'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g':
|
||||
'0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g',
|
||||
},
|
||||
'signatures': {
|
||||
'@test:fakeServer.notExisting': {
|
||||
'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
|
||||
'pvgbZxEbllaElhpiRnb7/uOIUhrglvHCFnpoxr3/5ZrWa0EK/uaefhex9eEV4uBLrHjHg2ymwdNaM7ap9+sBBg',
|
||||
},
|
||||
},
|
||||
},
|
||||
'@othertest:fakeServer.notExisting': {
|
||||
'user_id': '@othertest:fakeServer.notExisting',
|
||||
'usage': ['user_signing'],
|
||||
'keys': {
|
||||
'ed25519:user_signing': 'user_signing',
|
||||
},
|
||||
'signatures': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/client/r0/register': (var req) => {
|
||||
'user_id': '@testuser:example.com',
|
||||
|
@ -1739,6 +1973,9 @@ class FakeMatrixApi extends MockClient {
|
|||
'/client/r0/rooms/!localpart%3Aserver.abc/ban': (var reqI) => {},
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/unban': (var reqI) => {},
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/invite': (var reqI) => {},
|
||||
'/client/r0/keys/device_signing/upload': (var reqI) => {},
|
||||
'/client/r0/keys/signatures/upload': (var reqI) => {'failures': {}},
|
||||
'/client/unstable/room_keys/version': (var reqI) => {'version': '5'},
|
||||
},
|
||||
'PUT': {
|
||||
'/client/r0/presence/${Uri.encodeComponent('@alice:example.com')}/status':
|
||||
|
@ -1779,12 +2016,20 @@ class FakeMatrixApi extends MockClient {
|
|||
(var reqI) => {
|
||||
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||
},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%21localpart%3Aserver.abc/tags/m.favourite':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40alice%3Aexample.com/account_data/test.account.data':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/m.direct':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40othertest%3AfakeServer.notExisting/account_data/m.direct':
|
||||
(var req) => {},
|
||||
'/client/r0/profile/%40alice%3Aexample.com/displayname': (var reqI) => {},
|
||||
'/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {},
|
||||
'/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url':
|
||||
|
@ -1805,16 +2050,31 @@ class FakeMatrixApi extends MockClient {
|
|||
(var reqI) => {
|
||||
'event_id': '42',
|
||||
},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.pinned_events':
|
||||
(var reqI) => {
|
||||
'event_id': '42',
|
||||
},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.power_levels':
|
||||
(var reqI) => {
|
||||
'event_id': '42',
|
||||
},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%211234%3AfakeServer.notExisting/account_data/m.direct':
|
||||
(var reqI) => {},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%21localpart%3Aserver.abc/account_data/m.direct':
|
||||
(var reqI) => {},
|
||||
'/client/r0/directory/list/room/!localpart%3Aexample.com': (var req) =>
|
||||
{},
|
||||
'/client/unstable/room_keys/version/5': (var req) => {},
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5':
|
||||
(var req) => {
|
||||
'etag': 'asdf',
|
||||
'count': 1,
|
||||
},
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5':
|
||||
(var req) => {
|
||||
'etag': 'asdf',
|
||||
'count': 1,
|
||||
},
|
||||
'/client/unstable/room_keys/keys?version=5': (var req) => {
|
||||
'etag': 'asdf',
|
||||
'count': 1,
|
||||
},
|
||||
},
|
||||
'DELETE': {
|
||||
'/unknown/token': (var req) => {'errcode': 'M_UNKNOWN_TOKEN'},
|
||||
|
@ -1823,8 +2083,25 @@ class FakeMatrixApi extends MockClient {
|
|||
'/client/r0/pushrules/global/content/nocake': (var req) => {},
|
||||
'/client/r0/pushrules/global/override/!localpart%3Aserver.abc':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%21localpart%3Aserver.abc/tags/m.favourite':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag':
|
||||
(var req) => {},
|
||||
'/client/unstable/room_keys/version/5': (var req) => {},
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5':
|
||||
(var req) => {
|
||||
'etag': 'asdf',
|
||||
'count': 1,
|
||||
},
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5':
|
||||
(var req) => {
|
||||
'etag': 'asdf',
|
||||
'count': 1,
|
||||
},
|
||||
'/client/unstable/room_keys/keys?version=5': (var req) => {
|
||||
'etag': 'asdf',
|
||||
'count': 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import 'dart:typed_data';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/matrix_device_keys.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/matrix_keys.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/filter.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/matrix_exception.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/presence_content.dart';
|
||||
|
@ -1116,6 +1116,83 @@ void main() {
|
|||
|
||||
matrixApi.homeserver = matrixApi.accessToken = null;
|
||||
});
|
||||
test('uploadDeviceSigningKeys', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final masterKey = MatrixCrossSigningKey.fromJson({
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['master'],
|
||||
'keys': {
|
||||
'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
|
||||
'82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8',
|
||||
},
|
||||
'signatures': {},
|
||||
});
|
||||
final selfSigningKey = MatrixCrossSigningKey.fromJson({
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['self_signing'],
|
||||
'keys': {
|
||||
'ed25519:F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY':
|
||||
'F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY',
|
||||
},
|
||||
'signatures': {},
|
||||
});
|
||||
final userSigningKey = MatrixCrossSigningKey.fromJson({
|
||||
'user_id': '@test:fakeServer.notExisting',
|
||||
'usage': ['user_signing'],
|
||||
'keys': {
|
||||
'ed25519:0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g':
|
||||
'0PiwulzJ/RU86LlzSSZ8St80HUMN3dqjKa/orIJoA0g',
|
||||
},
|
||||
'signatures': {},
|
||||
});
|
||||
await matrixApi.uploadDeviceSigningKeys(
|
||||
masterKey: masterKey,
|
||||
selfSigningKey: selfSigningKey,
|
||||
userSigningKey: userSigningKey);
|
||||
});
|
||||
test('uploadKeySignatures', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final key1 = MatrixDeviceKeys.fromJson({
|
||||
'user_id': '@alice:example.com',
|
||||
'device_id': 'JLAFKJWSCS',
|
||||
'algorithms': ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
'keys': {
|
||||
'curve25519:JLAFKJWSCS':
|
||||
'3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
|
||||
'ed25519:JLAFKJWSCS': 'lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI'
|
||||
},
|
||||
'signatures': {
|
||||
'@alice:example.com': {
|
||||
'ed25519:JLAFKJWSCS':
|
||||
'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA'
|
||||
}
|
||||
},
|
||||
'unsigned': {'device_display_name': 'Alices mobile phone'},
|
||||
});
|
||||
final key2 = MatrixDeviceKeys.fromJson({
|
||||
'user_id': '@alice:example.com',
|
||||
'device_id': 'JLAFKJWSCS',
|
||||
'algorithms': ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
'keys': {
|
||||
'curve25519:JLAFKJWSCS':
|
||||
'3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
|
||||
'ed25519:JLAFKJWSCS': 'lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI'
|
||||
},
|
||||
'signatures': {
|
||||
'@alice:example.com': {'ed25519:OTHERDEVICE': 'OTHERSIG'}
|
||||
},
|
||||
'unsigned': {'device_display_name': 'Alices mobile phone'},
|
||||
});
|
||||
final ret = await matrixApi.uploadKeySignatures([key1, key2]);
|
||||
expect(
|
||||
FakeMatrixApi.api['POST']['/client/r0/keys/signatures/upload']({}),
|
||||
ret.toJson(),
|
||||
);
|
||||
});
|
||||
test('requestPushers', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
@ -1514,5 +1591,194 @@ void main() {
|
|||
|
||||
matrixApi.homeserver = matrixApi.accessToken = null;
|
||||
});
|
||||
test('createRoomKeysBackup', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final algorithm = RoomKeysAlgorithmType.v1Curve25519AesSha2;
|
||||
final authData = <String, dynamic>{
|
||||
'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM',
|
||||
'signatures': {},
|
||||
};
|
||||
final ret = await matrixApi.createRoomKeysBackup(algorithm, authData);
|
||||
expect(
|
||||
FakeMatrixApi.api['POST']
|
||||
['/client/unstable/room_keys/version']({})['version'],
|
||||
ret);
|
||||
});
|
||||
test('getRoomKeysBackup', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final ret = await matrixApi.getRoomKeysBackup();
|
||||
expect(FakeMatrixApi.api['GET']['/client/unstable/room_keys/version']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
test('updateRoomKeysBackup', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final algorithm = RoomKeysAlgorithmType.v1Curve25519AesSha2;
|
||||
final authData = <String, dynamic>{
|
||||
'public_key': 'GXYaxqhNhUK28zUdxOmEsFRguz+PzBsDlTLlF0O0RkM',
|
||||
'signatures': {},
|
||||
};
|
||||
await matrixApi.updateRoomKeysBackup('5', algorithm, authData);
|
||||
});
|
||||
test('deleteRoomKeysBackup', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
await matrixApi.deleteRoomKeysBackup('5');
|
||||
});
|
||||
test('storeRoomKeysSingleKey', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final session = RoomKeysSingleKey.fromJson({
|
||||
'first_message_index': 0,
|
||||
'forwarded_count': 0,
|
||||
'is_verified': true,
|
||||
'session_data': {
|
||||
'ephemeral': 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc',
|
||||
'ciphertext':
|
||||
'19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg',
|
||||
'mac': 'QzKV/fgAs4U',
|
||||
},
|
||||
});
|
||||
final ret = await matrixApi.storeRoomKeysSingleKey(
|
||||
roomId, sessionId, '5', session);
|
||||
expect(
|
||||
FakeMatrixApi.api['PUT'][
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
test('getRoomKeysSingleKey', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final ret = await matrixApi.getRoomKeysSingleKey(roomId, sessionId, '5');
|
||||
expect(
|
||||
FakeMatrixApi.api['GET'][
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
test('deleteRoomKeysSingleKey', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final ret =
|
||||
await matrixApi.deleteRoomKeysSingleKey(roomId, sessionId, '5');
|
||||
expect(
|
||||
FakeMatrixApi.api['DELETE'][
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
test('storeRoomKeysRoom', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final session = RoomKeysRoom.fromJson({
|
||||
'sessions': {
|
||||
sessionId: {
|
||||
'first_message_index': 0,
|
||||
'forwarded_count': 0,
|
||||
'is_verified': true,
|
||||
'session_data': {
|
||||
'ephemeral': 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc',
|
||||
'ciphertext':
|
||||
'19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg',
|
||||
'mac': 'QzKV/fgAs4U',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
final ret = await matrixApi.storeRoomKeysRoom(roomId, '5', session);
|
||||
expect(
|
||||
FakeMatrixApi.api['PUT'][
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
test('getRoomKeysRoom', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final ret = await matrixApi.getRoomKeysRoom(roomId, '5');
|
||||
expect(
|
||||
FakeMatrixApi.api['GET'][
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
test('deleteRoomKeysRoom', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final ret = await matrixApi.deleteRoomKeysRoom(roomId, '5');
|
||||
expect(
|
||||
FakeMatrixApi.api['DELETE'][
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}?version=5']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
test('storeRoomKeys', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final session = RoomKeys.fromJson({
|
||||
'rooms': {
|
||||
roomId: {
|
||||
'sessions': {
|
||||
sessionId: {
|
||||
'first_message_index': 0,
|
||||
'forwarded_count': 0,
|
||||
'is_verified': true,
|
||||
'session_data': {
|
||||
'ephemeral': 'fwRxYh+seqLykz5mQCLypJ4/59URdcFJ2s69OU1dGRc',
|
||||
'ciphertext':
|
||||
'19jkQYlbgdP+VL9DH3qY/Dvpk6onJZgf+6frZFl1TinPCm9OMK9AZZLuM1haS9XLAUK1YsREgjBqfl6T+Tq8JlJ5ONZGg2Wttt24sGYc0iTMZJ8rXcNDeKMZhM96ETyjufJSeYoXLqifiVLDw9rrVBmNStF7PskYp040em+0OZ4pF85Cwsdf7l9V7MMynzh9BoXqVUCBiwT03PNYH9AEmNUxXX+6ZwCpe/saONv8MgGt5uGXMZIK29phA3D8jD6uV/WOHsB8NjHNq9FrfSEAsl+dAcS4uiYie4BKSSeQN+zGAQqu1MMW4OAdxGOuf8WpIINx7n+7cKQfxlmc/Cgg5+MmIm2H0oDwQ+Xu7aSxp1OCUzbxQRdjz6+tnbYmZBuH0Ov2RbEvC5tDb261LRqKXpub0llg5fqKHl01D0ahv4OAQgRs5oU+4mq+H2QGTwIFGFqP9tCRo0I+aICawpxYOfoLJpFW6KvEPnM2Lr3sl6Nq2fmkz6RL5F7nUtzxN8OKazLQpv8DOYzXbi7+ayEsqS0/EINetq7RfCqgjrEUgfNWYuFXWqvUT8lnxLdNu+8cyrJqh1UquFjXWTw1kWcJ0pkokVeBtK9YysCnF1UYh/Iv3rl2ZoYSSLNtuvMSYlYHggZ8xV8bz9S3X2/NwBycBiWIy5Ou/OuSX7trIKgkkmda0xjBWEM1a2acVuqu2OFbMn2zFxm2a3YwKP//OlIgMg',
|
||||
'mac': 'QzKV/fgAs4U',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
final ret = await matrixApi.storeRoomKeys('5', session);
|
||||
expect(
|
||||
FakeMatrixApi.api['PUT']
|
||||
['/client/unstable/room_keys/keys?version=5']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
test('getRoomKeys', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final ret = await matrixApi.getRoomKeys('5');
|
||||
expect(
|
||||
FakeMatrixApi.api['GET']
|
||||
['/client/unstable/room_keys/keys?version=5']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
test('deleteRoomKeys', () async {
|
||||
matrixApi.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
matrixApi.accessToken = '1234';
|
||||
|
||||
final ret = await matrixApi.deleteRoomKeys('5');
|
||||
expect(
|
||||
FakeMatrixApi.api['DELETE']
|
||||
['/client/unstable/room_keys/keys?version=5']({}),
|
||||
ret.toJson());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ void main() {
|
|||
test('Decrypt', () async {
|
||||
final text = 'hello world';
|
||||
final file = MatrixFile(
|
||||
path: '/path/to/file.txt',
|
||||
name: 'file.txt',
|
||||
bytes: Uint8List.fromList(text.codeUnits),
|
||||
);
|
||||
var olmEnabled = true;
|
||||
|
|
|
@ -150,6 +150,19 @@ void main() {
|
|||
content: {'url': 'mxc://testurl'},
|
||||
stateKey: '');
|
||||
expect(room.avatar.toString(), 'mxc://testurl');
|
||||
|
||||
expect(room.pinnedEventIds, <String>[]);
|
||||
room.states['m.room.pinned_events'] = Event(
|
||||
senderId: '@test:example.com',
|
||||
type: 'm.room.pinned_events',
|
||||
roomId: room.id,
|
||||
room: room,
|
||||
eventId: '123',
|
||||
content: {
|
||||
'pinned': ['1234']
|
||||
},
|
||||
stateKey: '');
|
||||
expect(room.pinnedEventIds.first, '1234');
|
||||
room.states['m.room.message'] = Event(
|
||||
senderId: '@test:example.com',
|
||||
type: 'm.room.message',
|
||||
|
@ -287,6 +300,9 @@ void main() {
|
|||
});
|
||||
|
||||
test('getParticipants', () async {
|
||||
var userList = room.getParticipants();
|
||||
expect(userList.length, 4);
|
||||
// add new user
|
||||
room.setState(Event(
|
||||
senderId: '@alice:test.abc',
|
||||
type: 'm.room.member',
|
||||
|
@ -296,9 +312,9 @@ void main() {
|
|||
originServerTs: DateTime.now(),
|
||||
content: {'displayname': 'alice'},
|
||||
stateKey: '@alice:test.abc'));
|
||||
final userList = room.getParticipants();
|
||||
expect(userList.length, 4);
|
||||
expect(userList[3].displayName, 'alice');
|
||||
userList = room.getParticipants();
|
||||
expect(userList.length, 5);
|
||||
expect(userList[4].displayName, 'alice');
|
||||
});
|
||||
|
||||
test('addToDirectChat', () async {
|
||||
|
@ -320,8 +336,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('setAvatar', () async {
|
||||
final testFile =
|
||||
MatrixFile(bytes: Uint8List(0), path: 'fake/path/file.jpeg');
|
||||
final testFile = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg');
|
||||
final dynamic resp = await room.setAvatar(testFile);
|
||||
expect(resp, 'YUwRidLecu:example.com');
|
||||
});
|
||||
|
@ -348,10 +363,8 @@ void main() {
|
|||
});*/
|
||||
|
||||
test('sendFileEvent', () async {
|
||||
final testFile =
|
||||
MatrixFile(bytes: Uint8List(0), path: 'fake/path/file.jpeg');
|
||||
final dynamic resp = await room.sendFileEvent(testFile,
|
||||
msgType: 'm.file', txid: 'testtxid');
|
||||
final testFile = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg');
|
||||
final dynamic resp = await room.sendFileEvent(testFile, txid: 'testtxid');
|
||||
expect(resp, 'mxc://example.com/AQwafuaFswefuhsfAFAgsw');
|
||||
});
|
||||
|
||||
|
@ -396,6 +409,25 @@ void main() {
|
|||
await room.sendCallCandidates('1234', [], txid: '1234');
|
||||
});
|
||||
|
||||
test('Test tag methods', () async {
|
||||
await room.addTag(TagType.Favourite, order: 0.1);
|
||||
await room.removeTag(TagType.Favourite);
|
||||
expect(room.isFavourite, false);
|
||||
room.roomAccountData['m.tag'] = BasicRoomEvent.fromJson({
|
||||
'content': {
|
||||
'tags': {
|
||||
'm.favourite': {'order': 0.1},
|
||||
'm.wrong': {'order': 0.2},
|
||||
}
|
||||
},
|
||||
'type': 'm.tag'
|
||||
});
|
||||
expect(room.tags.length, 1);
|
||||
expect(room.tags[TagType.Favourite].order, 0.1);
|
||||
expect(room.isFavourite, true);
|
||||
await room.setFavourite(false);
|
||||
});
|
||||
|
||||
test('joinRules', () async {
|
||||
expect(room.canChangeJoinRules, false);
|
||||
expect(room.joinRules, JoinRules.public);
|
||||
|
|
|
@ -49,19 +49,6 @@ void main() {
|
|||
|
||||
test('Create', () async {
|
||||
await client.checkServer('https://fakeServer.notExisting');
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 2,
|
||||
'event_id': '1',
|
||||
'origin_server_ts': testTimeStamp
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
|
@ -75,7 +62,20 @@ void main() {
|
|||
'event_id': '2',
|
||||
'origin_server_ts': testTimeStamp - 1000
|
||||
},
|
||||
sortOrder: room.oldSortOrder));
|
||||
sortOrder: room.newSortOrder));
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 2,
|
||||
'event_id': '1',
|
||||
'origin_server_ts': testTimeStamp
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
|
||||
expect(timeline.sub != null, true);
|
||||
|
||||
|
|
|
@ -81,6 +81,8 @@ void main() {
|
|||
expect(user2.calcDisplayname(), 'SuperAlice');
|
||||
expect(user3.calcDisplayname(), 'Alice Mep');
|
||||
expect(user3.calcDisplayname(formatLocalpart: false), 'alice_mep');
|
||||
expect(
|
||||
user3.calcDisplayname(mxidLocalPartFallback: false), 'Unknown user');
|
||||
});
|
||||
test('kick', () async {
|
||||
await client.checkServer('https://fakeserver.notexisting');
|
||||
|
|
|
@ -107,7 +107,7 @@ void test() async {
|
|||
assert(!testClientB
|
||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].blocked);
|
||||
await testClientA.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID]
|
||||
.setVerified(true, testClientA);
|
||||
.setVerified(true);
|
||||
|
||||
print('++++ Check if own olm device is verified by default ++++');
|
||||
assert(testClientA.userDeviceKeys.containsKey(testUserA));
|
||||
|
@ -139,12 +139,10 @@ void test() async {
|
|||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||
.session_id() ==
|
||||
testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(inviteRoom.client.encryption.keyManager
|
||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
|
@ -162,12 +160,10 @@ void test() async {
|
|||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||
.session_id() ==
|
||||
testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
|
@ -231,24 +227,20 @@ void test() async {
|
|||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||
.session_id() ==
|
||||
testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientC.identityKey].length ==
|
||||
1);
|
||||
assert(testClientC
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientC.identityKey].first
|
||||
.session_id() ==
|
||||
testClientC
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientC.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientC.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
|
@ -281,12 +273,10 @@ void test() async {
|
|||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||
.session_id() ==
|
||||
testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
|
|
Loading…
Add table
Reference in a new issue