Merge commit 'a46942a14051cb02e70e9223fb3e2648a71c0891' into yiffed

This commit is contained in:
Inex Code 2020-07-22 03:48:26 +03:00
commit d82179d62b
54 changed files with 5731 additions and 1076 deletions

View File

@ -1 +1 @@
* @christianpauly
* @christianpauly @sorunome

6
build.yaml Normal file
View File

@ -0,0 +1,6 @@
targets:
$default:
builders:
moor_generator:
options:
generate_connect_constructor: true

View File

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

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

View File

@ -24,6 +24,8 @@ import 'package:pedantic/pedantic.dart';
import 'key_manager.dart';
import 'olm_manager.dart';
import 'key_verification_manager.dart';
import 'cross_signing.dart';
import 'ssss.dart';
class Encryption {
final Client client;
@ -42,15 +44,19 @@ class Encryption {
KeyManager keyManager;
OlmManager olmManager;
KeyVerificationManager keyVerificationManager;
CrossSigning crossSigning;
SSSS ssss;
Encryption({
this.client,
this.debug,
this.enableE2eeRecovery,
}) {
ssss = SSSS(this);
keyManager = KeyManager(this);
olmManager = OlmManager(this);
keyVerificationManager = KeyVerificationManager(this);
crossSigning = CrossSigning(this);
}
Future<void> init(String olmAccount) async {
@ -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 {

View File

@ -27,6 +27,8 @@ import './encryption.dart';
import './utils/session_key.dart';
import './utils/outbound_group_session.dart';
const MEGOLM_KEY = 'm.megolm_backup.v1';
class KeyManager {
final Encryption encryption;
Client get client => encryption.client;
@ -37,7 +39,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
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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