Merge branch 'soru/modularize-e2ee' into 'master'

split encryption stuff to other library

See merge request famedly/famedlysdk!333
This commit is contained in:
Sorunome 2020-06-09 16:13:23 +00:00
commit b8c58faaab
35 changed files with 2838 additions and 1968 deletions

23
lib/encryption.dart Normal file
View file

@ -0,0 +1,23 @@
/*
* 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/>.
*/
library encryption;
export './encryption/encryption.dart';
export './encryption/key_manager.dart';
export './encryption/utils/key_verification.dart';

View file

@ -0,0 +1,283 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 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:famedlysdk/matrix_api.dart';
import 'package:pedantic/pedantic.dart';
import 'key_manager.dart';
import 'olm_manager.dart';
import 'key_verification_manager.dart';
class Encryption {
final Client client;
final bool debug;
final bool enableE2eeRecovery;
bool get enabled => olmManager.enabled;
/// Returns the base64 encoded keys to store them in a store.
/// This String should **never** leave the device!
String get pickledOlmAccount => olmManager.pickledOlmAccount;
String get fingerprintKey => olmManager.fingerprintKey;
String get identityKey => olmManager.identityKey;
KeyManager keyManager;
OlmManager olmManager;
KeyVerificationManager keyVerificationManager;
Encryption({
this.client,
this.debug,
this.enableE2eeRecovery,
}) {
keyManager = KeyManager(this);
olmManager = OlmManager(this);
keyVerificationManager = KeyVerificationManager(this);
}
Future<void> init(String olmAccount) async {
await olmManager.init(olmAccount);
}
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
olmManager.handleDeviceOneTimeKeysCount(countJson);
}
void onSync() {
keyVerificationManager.cleanup();
}
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
if (['m.room_key', 'm.room_key_request', 'm.forwarded_room_key']
.contains(event.type)) {
// a new room key or thelike. We need to handle this asap, before other
// events in /sync are handled
await keyManager.handleToDeviceEvent(event);
}
if (event.type.startsWith('m.key.verification.')) {
// some key verification event. No need to handle it now, we can easily
// do this in the background
unawaited(keyVerificationManager.handleToDeviceEvent(event));
}
}
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
return await olmManager.decryptToDeviceEvent(event);
}
Event decryptRoomEventSync(String roomId, Event event) {
if (event.type != EventTypes.Encrypted ||
event.content['ciphertext'] == null) return event;
Map<String, dynamic> decryptedPayload;
try {
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
throw (DecryptError.UNKNOWN_ALGORITHM);
}
final String sessionId = event.content['session_id'];
final String senderKey = event.content['sender_key'];
final inboundGroupSession =
keyManager.getInboundGroupSession(roomId, sessionId, senderKey);
if (inboundGroupSession == null) {
throw (DecryptError.UNKNOWN_SESSION);
}
final decryptResult = inboundGroupSession.inboundGroupSession
.decrypt(event.content['ciphertext']);
final messageIndexKey = event.eventId +
event.originServerTs.millisecondsSinceEpoch.toString();
var haveIndex = inboundGroupSession.indexes.containsKey(messageIndexKey);
if (haveIndex &&
inboundGroupSession.indexes[messageIndexKey] !=
decryptResult.message_index) {
// TODO: maybe clear outbound session, if it is ours
throw (DecryptError.CHANNEL_CORRUPTED);
}
inboundGroupSession.indexes[messageIndexKey] =
decryptResult.message_index;
if (!haveIndex) {
// now we persist the udpated indexes into the database.
// the entry should always exist. In the case it doesn't, the following
// line *could* throw an error. As that is a future, though, and we call
// it un-awaited here, nothing happens, which is exactly the result we want
client.database?.updateInboundGroupSessionIndexes(
json.encode(inboundGroupSession.indexes),
client.id,
roomId,
sessionId);
}
decryptedPayload = json.decode(decryptResult.plaintext);
} catch (exception) {
// alright, if this was actually by our own outbound group session, we might as well clear it
if (client.enableE2eeRecovery &&
(keyManager
.getOutboundGroupSession(roomId)
?.outboundGroupSession
?.session_id() ??
'') ==
event.content['session_id']) {
keyManager.clearOutboundGroupSession(roomId, wipe: true);
}
if (exception.toString() == DecryptError.UNKNOWN_SESSION) {
decryptedPayload = {
'content': event.content,
'type': EventTypes.Encrypted,
};
decryptedPayload['content']['body'] = exception.toString();
decryptedPayload['content']['msgtype'] = 'm.bad.encrypted';
} else {
decryptedPayload = {
'content': <String, dynamic>{
'msgtype': 'm.bad.encrypted',
'body': exception.toString(),
},
'type': EventTypes.Encrypted,
};
}
}
if (event.content['m.relates_to'] != null) {
decryptedPayload['content']['m.relates_to'] =
event.content['m.relates_to'];
}
return Event(
content: decryptedPayload['content'],
type: decryptedPayload['type'],
senderId: event.senderId,
eventId: event.eventId,
roomId: event.roomId,
room: event.room,
originServerTs: event.originServerTs,
unsigned: event.unsigned,
stateKey: event.stateKey,
prevContent: event.prevContent,
status: event.status,
sortOrder: event.sortOrder,
);
}
Future<Event> decryptRoomEvent(String roomId, Event event,
{bool store = false, String updateType = 'timeline'}) async {
final doStore = () async {
await client.database?.storeEventUpdate(
client.id,
EventUpdate(
eventType: event.type,
content: event.toJson(),
roomID: event.roomId,
type: updateType,
sortOrder: event.sortOrder,
),
);
if (updateType != 'history') {
event.room?.setState(event);
}
};
if (event.type != EventTypes.Encrypted) {
return event;
}
event = decryptRoomEventSync(roomId, event);
if (event.type != EventTypes.Encrypted) {
if (store) {
await doStore();
}
return event;
}
if (client.database == null) {
return event;
}
await keyManager.loadInboundGroupSession(
roomId, event.content['session_id'], event.content['sender_key']);
event = decryptRoomEventSync(roomId, event);
if (event.type != EventTypes.Encrypted && store) {
await doStore();
}
return event;
}
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
/// payload. This will create a new outgoingGroupSession if necessary.
Future<Map<String, dynamic>> encryptGroupMessagePayload(
String roomId, Map<String, dynamic> payload,
{String type = EventTypes.Message}) async {
final room = client.getRoomById(roomId);
if (room == null || !room.encrypted || !enabled) {
return payload;
}
if (room.encryptionAlgorithm != 'm.megolm.v1.aes-sha2') {
throw ('Unknown encryption algorithm');
}
if (keyManager.getOutboundGroupSession(roomId) == null) {
await keyManager.loadOutboundGroupSession(roomId);
}
await keyManager.clearOutboundGroupSession(roomId);
if (keyManager.getOutboundGroupSession(roomId) == null) {
await keyManager.createOutboundGroupSession(roomId);
}
final sess = keyManager.getOutboundGroupSession(roomId);
if (sess == null) {
throw ('Unable to create new outbound group session');
}
final Map<String, dynamic> mRelatesTo = payload.remove('m.relates_to');
final payloadContent = {
'content': payload,
'type': type,
'room_id': roomId,
};
var encryptedPayload = <String, dynamic>{
'algorithm': 'm.megolm.v1.aes-sha2',
'ciphertext':
sess.outboundGroupSession.encrypt(json.encode(payloadContent)),
'device_id': client.deviceID,
'sender_key': identityKey,
'session_id': sess.outboundGroupSession.session_id(),
if (mRelatesTo != null) 'm.relates_to': mRelatesTo,
};
sess.sentMessages++;
await keyManager.storeOutboundGroupSession(roomId, sess);
return encryptedPayload;
}
Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
DeviceKeys device, String type, Map<String, dynamic> payload) async {
return await olmManager.encryptToDeviceMessagePayload(
device, type, payload);
}
Future<Map<String, dynamic>> encryptToDeviceMessage(
List<DeviceKeys> deviceKeys,
String type,
Map<String, dynamic> payload) async {
return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
}
void dispose() {
keyManager.dispose();
olmManager.dispose();
keyVerificationManager.dispose();
}
}
abstract class DecryptError {
static const String NOT_ENABLED = 'Encryption is not enabled in your client.';
static const String UNKNOWN_ALGORITHM = 'Unknown encryption algorithm.';
static const String UNKNOWN_SESSION =
'The sender has not sent us the session key.';
static const String CHANNEL_CORRUPTED =
'The secure channel with the sender was corrupted.';
}

View file

@ -0,0 +1,517 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 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:pedantic/pedantic.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart';
import 'package:olm/olm.dart' as olm;
import './encryption.dart';
import './utils/session_key.dart';
import './utils/outbound_group_session.dart';
class KeyManager {
final Encryption encryption;
Client get client => encryption.client;
final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
final _inboundGroupSessions = <String, Map<String, SessionKey>>{};
final _outboundGroupSessions = <String, OutboundGroupSession>{};
final Set<String> _loadedOutboundGroupSessions = <String>{};
final Set<String> _requestedSessionIds = <String>{};
KeyManager(this.encryption);
/// clear all cached inbound group sessions. useful for testing
void clearInboundGroupSessions() {
_inboundGroupSessions.clear();
}
void setInboundGroupSession(String roomId, String sessionId, String senderKey,
Map<String, dynamic> content,
{bool forwarded = false}) {
final oldSession =
getInboundGroupSession(roomId, sessionId, senderKey, otherRooms: false);
if (oldSession != null) {
return;
}
if (content['algorithm'] != 'm.megolm.v1.aes-sha2') {
return;
}
olm.InboundGroupSession inboundGroupSession;
try {
inboundGroupSession = olm.InboundGroupSession();
if (forwarded) {
inboundGroupSession.import_session(content['session_key']);
} else {
inboundGroupSession.create(content['session_key']);
}
} catch (e) {
inboundGroupSession.free();
print(
'[LibOlm] Could not create new InboundGroupSession: ' + e.toString());
return;
}
if (!_inboundGroupSessions.containsKey(roomId)) {
_inboundGroupSessions[roomId] = <String, SessionKey>{};
}
_inboundGroupSessions[roomId][sessionId] = SessionKey(
content: content,
inboundGroupSession: inboundGroupSession,
indexes: {},
key: client.userID,
);
client.database?.storeInboundGroupSession(
client.id,
roomId,
sessionId,
inboundGroupSession.pickle(client.userID),
json.encode(content),
json.encode({}),
);
// TODO: somehow try to decrypt last message again
final room = client.getRoomById(roomId);
if (room != null) {
room.onSessionKeyReceived.add(sessionId);
}
}
SessionKey getInboundGroupSession(
String roomId, String sessionId, String senderKey,
{bool otherRooms = true}) {
if (_inboundGroupSessions.containsKey(roomId) &&
_inboundGroupSessions[roomId].containsKey(sessionId)) {
return _inboundGroupSessions[roomId][sessionId];
}
if (!otherRooms) {
return null;
}
// search if this session id is *somehow* found in another room
for (final val in _inboundGroupSessions.values) {
if (val.containsKey(sessionId)) {
return val[sessionId];
}
}
return null;
}
/// Loads an inbound group session
Future<SessionKey> loadInboundGroupSession(
String roomId, String sessionId, String senderKey) async {
if (roomId == null || sessionId == null || senderKey == null) {
return null;
}
if (_inboundGroupSessions.containsKey(roomId) &&
_inboundGroupSessions[roomId].containsKey(sessionId)) {
return _inboundGroupSessions[roomId][sessionId]; // nothing to do
}
final session = await client.database
?.getDbInboundGroupSession(client.id, roomId, sessionId);
if (session == null) {
final room = client.getRoomById(roomId);
final requestIdent = '$roomId|$sessionId|$senderKey';
if (client.enableE2eeRecovery &&
room != null &&
!_requestedSessionIds.contains(requestIdent)) {
// do e2ee recovery
_requestedSessionIds.add(requestIdent);
unawaited(request(room, sessionId, senderKey));
}
return null;
}
if (!_inboundGroupSessions.containsKey(roomId)) {
_inboundGroupSessions[roomId] = <String, SessionKey>{};
}
final sess = SessionKey.fromDb(session, client.userID);
if (!sess.isValid) {
return null;
}
_inboundGroupSessions[roomId][sessionId] = sess;
return sess;
}
/// clear all cached inbound group sessions. useful for testing
void clearOutboundGroupSessions() {
_outboundGroupSessions.clear();
}
/// Clears the existing outboundGroupSession but first checks if the participating
/// devices have been changed. Returns false if the session has not been cleared because
/// it wasn't necessary.
Future<bool> clearOutboundGroupSession(String roomId,
{bool wipe = false}) async {
final room = client.getRoomById(roomId);
final sess = getOutboundGroupSession(roomId);
if (room == null || sess == null) {
return true;
}
if (!wipe) {
// first check if the devices in the room changed
final deviceKeys = await room.getUserDeviceKeys();
deviceKeys.removeWhere((k) => k.blocked);
final deviceKeyIds = deviceKeys.map((k) => k.deviceId).toList();
deviceKeyIds.sort();
if (deviceKeyIds.toString() != sess.devices.toString()) {
wipe = true;
}
// next check if it needs to be rotated
final encryptionContent = room.getState(EventTypes.Encryption)?.content;
final maxMessages = encryptionContent != null &&
encryptionContent['rotation_period_msgs'] is int
? encryptionContent['rotation_period_msgs']
: 100;
final maxAge = encryptionContent != null &&
encryptionContent['rotation_period_ms'] is int
? encryptionContent['rotation_period_ms']
: 604800000; // default of one week
if (sess.sentMessages >= maxMessages ||
sess.creationTime
.add(Duration(milliseconds: maxAge))
.isBefore(DateTime.now())) {
wipe = true;
}
if (!wipe) {
return false;
}
}
sess.dispose();
_outboundGroupSessions.remove(roomId);
await client.database?.removeOutboundGroupSession(client.id, roomId);
return true;
}
Future<void> storeOutboundGroupSession(
String roomId, OutboundGroupSession sess) async {
if (sess == null) {
return;
}
await client.database?.storeOutboundGroupSession(
client.id,
roomId,
sess.outboundGroupSession.pickle(client.userID),
json.encode(sess.devices),
sess.creationTime,
sess.sentMessages);
}
Future<OutboundGroupSession> createOutboundGroupSession(String roomId) async {
await clearOutboundGroupSession(roomId, wipe: true);
final room = client.getRoomById(roomId);
if (room == null) {
return null;
}
final deviceKeys = await room.getUserDeviceKeys();
deviceKeys.removeWhere((k) => k.blocked);
final deviceKeyIds = deviceKeys.map((k) => k.deviceId).toList();
deviceKeyIds.sort();
final outboundGroupSession = olm.OutboundGroupSession();
try {
outboundGroupSession.create();
} catch (e) {
outboundGroupSession.free();
print('[LibOlm] Unable to create new outboundGroupSession: ' +
e.toString());
return null;
}
final rawSession = <String, dynamic>{
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': room.id,
'session_id': outboundGroupSession.session_id(),
'session_key': outboundGroupSession.session_key(),
};
setInboundGroupSession(
roomId, rawSession['session_id'], encryption.identityKey, rawSession);
final sess = OutboundGroupSession(
devices: deviceKeyIds,
creationTime: DateTime.now(),
outboundGroupSession: outboundGroupSession,
sentMessages: 0,
key: client.userID,
);
try {
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
await storeOutboundGroupSession(roomId, sess);
_outboundGroupSessions[roomId] = sess;
} catch (e, s) {
print(
'[LibOlm] Unable to send the session key to the participating devices: ' +
e.toString());
print(s);
sess.dispose();
return null;
}
return sess;
}
OutboundGroupSession getOutboundGroupSession(String roomId) {
return _outboundGroupSessions[roomId];
}
Future<void> loadOutboundGroupSession(String roomId) async {
if (_loadedOutboundGroupSessions.contains(roomId) ||
_outboundGroupSessions.containsKey(roomId) ||
client.database == null) {
return; // nothing to do
}
_loadedOutboundGroupSessions.add(roomId);
final session =
await client.database.getDbOutboundGroupSession(client.id, roomId);
if (session == null) {
return;
}
final sess = OutboundGroupSession.fromDb(session, client.userID);
if (!sess.isValid) {
return;
}
_outboundGroupSessions[roomId] = sess;
}
/// Request a certain key from another device
Future<void> request(Room room, String sessionId, String senderKey) async {
// while we just send the to-device event to '*', we still need to save the
// devices themself to know where to send the cancel to after receiving a reply
final devices = await room.getUserDeviceKeys();
final requestId = client.generateUniqueTransactionId();
final request = KeyManagerKeyShareRequest(
requestId: requestId,
devices: devices,
room: room,
sessionId: sessionId,
senderKey: senderKey,
);
await client.sendToDevice(
[],
'm.room_key_request',
{
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': room.id,
'sender_key': senderKey,
'session_id': sessionId,
},
'request_id': requestId,
'requesting_device_id': client.deviceID,
},
encrypted: false,
toUsers: await room.requestParticipants());
outgoingShareRequests[request.requestId] = request;
}
/// Handle an incoming to_device event that is related to key sharing
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
if (event.type == 'm.room_key_request') {
if (!event.content.containsKey('request_id')) {
return; // invalid event
}
if (event.content['action'] == 'request') {
// we are *receiving* a request
if (!event.content.containsKey('body')) {
return; // no body
}
if (!client.userDeviceKeys.containsKey(event.sender) ||
!client.userDeviceKeys[event.sender].deviceKeys
.containsKey(event.content['requesting_device_id'])) {
return; // device not found
}
final device = client.userDeviceKeys[event.sender]
.deviceKeys[event.content['requesting_device_id']];
if (device.userId == client.userID &&
device.deviceId == client.deviceID) {
return; // ignore requests by ourself
}
final room = client.getRoomById(event.content['body']['room_id']);
if (room == null) {
return; // unknown room
}
final sessionId = event.content['body']['session_id'];
final senderKey = event.content['body']['sender_key'];
// okay, let's see if we have this session at all
if ((await loadInboundGroupSession(room.id, sessionId, senderKey)) ==
null) {
return; // we don't have this session anyways
}
final request = KeyManagerKeyShareRequest(
requestId: event.content['request_id'],
devices: [device],
room: room,
sessionId: sessionId,
senderKey: senderKey,
);
if (incomingShareRequests.containsKey(request.requestId)) {
return; // we don't want to process one and the same request multiple times
}
incomingShareRequests[request.requestId] = request;
final roomKeyRequest =
RoomKeyRequest.fromToDeviceEvent(event, this, request);
if (device.userId == client.userID &&
device.verified &&
!device.blocked) {
// alright, we can forward the key
await roomKeyRequest.forwardKey();
} else {
client.onRoomKeyRequest
.add(roomKeyRequest); // let the client handle this
}
} else if (event.content['action'] == 'request_cancellation') {
// we got told to cancel an incoming request
if (!incomingShareRequests.containsKey(event.content['request_id'])) {
return; // we don't know this request anyways
}
// alright, let's just cancel this request
final request = incomingShareRequests[event.content['request_id']];
request.canceled = true;
incomingShareRequests.remove(request.requestId);
}
} else if (event.type == 'm.forwarded_room_key') {
// we *received* an incoming key request
if (event.encryptedContent == null) {
return; // event wasn't encrypted, this is a security risk
}
final request = outgoingShareRequests.values.firstWhere(
(r) =>
r.room.id == event.content['room_id'] &&
r.sessionId == event.content['session_id'] &&
r.senderKey == event.content['sender_key'],
orElse: () => null);
if (request == null || request.canceled) {
return; // no associated request found or it got canceled
}
final device = request.devices.firstWhere(
(d) =>
d.userId == event.sender &&
d.curve25519Key == event.encryptedContent['sender_key'],
orElse: () => null);
if (device == null) {
return; // someone we didn't send our request to replied....better ignore this
}
// TODO: verify that the keys work to decrypt a message
// alright, all checks out, let's go ahead and store this session
setInboundGroupSession(
request.room.id, request.sessionId, request.senderKey, event.content,
forwarded: true);
request.devices.removeWhere(
(k) => k.userId == device.userId && k.deviceId == device.deviceId);
outgoingShareRequests.remove(request.requestId);
// send cancel to all other devices
if (request.devices.isEmpty) {
return; // no need to send any cancellation
}
await client.sendToDevice(
request.devices,
'm.room_key_request',
{
'action': 'request_cancellation',
'request_id': request.requestId,
'requesting_device_id': client.deviceID,
},
encrypted: false);
} else if (event.type == 'm.room_key') {
if (event.encryptedContent == null) {
return; // the event wasn't encrypted, this is a security risk;
}
final String roomId = event.content['room_id'];
final String sessionId = event.content['session_id'];
if (client.userDeviceKeys.containsKey(event.sender) &&
client.userDeviceKeys[event.sender].deviceKeys
.containsKey(event.content['requesting_device_id'])) {
event.content['sender_claimed_ed25519_key'] = client
.userDeviceKeys[event.sender]
.deviceKeys[event.content['requesting_device_id']]
.ed25519Key;
}
setInboundGroupSession(roomId, sessionId,
event.encryptedContent['sender_key'], event.content,
forwarded: false);
}
}
void dispose() {
for (final sess in _outboundGroupSessions.values) {
sess.dispose();
}
for (final entries in _inboundGroupSessions.values) {
for (final sess in entries.values) {
sess.dispose();
}
}
}
}
class KeyManagerKeyShareRequest {
final String requestId;
final List<DeviceKeys> devices;
final Room room;
final String sessionId;
final String senderKey;
bool canceled;
KeyManagerKeyShareRequest(
{this.requestId,
this.devices,
this.room,
this.sessionId,
this.senderKey,
this.canceled = false});
}
class RoomKeyRequest extends ToDeviceEvent {
KeyManager keyManager;
KeyManagerKeyShareRequest request;
RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent,
KeyManager keyManager, KeyManagerKeyShareRequest request) {
this.keyManager = keyManager;
this.request = request;
sender = toDeviceEvent.sender;
content = toDeviceEvent.content;
type = toDeviceEvent.type;
}
Room get room => request.room;
DeviceKeys get requestingDevice => request.devices.first;
Future<void> forwardKey() async {
if (request.canceled) {
keyManager.incomingShareRequests.remove(request.requestId);
return; // request is canceled, don't send anything
}
var room = this.room;
final session = await keyManager.loadInboundGroupSession(
room.id, request.sessionId, request.senderKey);
var forwardedKeys = <dynamic>[keyManager.encryption.identityKey];
for (final key in session.forwardingCurve25519KeyChain) {
forwardedKeys.add(key);
}
await requestingDevice.setVerified(true, keyManager.client);
var message = session.content;
message['forwarding_curve25519_key_chain'] = forwardedKeys;
message['session_key'] = session.inboundGroupSession
.export_session(session.inboundGroupSession.first_known_index());
// send the actual reply of the key back to the requester
await keyManager.client.sendToDevice(
[requestingDevice],
'm.forwarded_room_key',
message,
);
keyManager.incomingShareRequests.remove(request.requestId);
}
}

View file

@ -0,0 +1,83 @@
/*
* 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:famedlysdk/famedlysdk.dart';
import './encryption.dart';
import './utils/key_verification.dart';
class KeyVerificationManager {
final Encryption encryption;
Client get client => encryption.client;
KeyVerificationManager(this.encryption);
final Map<String, KeyVerification> _requests = {};
Future<void> cleanup() async {
for (final entry in _requests.entries) {
var dispose = entry.value.canceled ||
entry.value.state == KeyVerificationState.done ||
entry.value.state == KeyVerificationState.error;
if (!dispose) {
dispose = !(await entry.value.verifyActivity());
}
if (dispose) {
entry.value.dispose();
_requests.remove(entry.key);
}
}
}
void addRequest(KeyVerification request) {
if (request.transactionId == null) {
return;
}
_requests[request.transactionId] = request;
}
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
if (!event.type.startsWith('m.key.verification')) {
return;
}
// we have key verification going on!
final transactionId = KeyVerification.getTransactionId(event.content);
if (transactionId == null) {
return; // TODO: send cancel with unknown transaction id
}
if (_requests.containsKey(transactionId)) {
await _requests[transactionId].handlePayload(event.type, event.content);
} else {
final newKeyRequest =
KeyVerification(encryption: encryption, userId: event.sender);
await newKeyRequest.handlePayload(event.type, event.content);
if (newKeyRequest.state != KeyVerificationState.askAccept) {
// okay, something went wrong (unknown transaction id?), just dispose it
newKeyRequest.dispose();
} else {
_requests[transactionId] = newKeyRequest;
client.onKeyVerificationRequest.add(newKeyRequest);
}
}
}
void dispose() {
for (final req in _requests.values) {
req.dispose();
}
}
}

View file

@ -0,0 +1,433 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 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: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';
class OlmManager {
final Encryption encryption;
Client get client => encryption.client;
olm.Account _olmAccount;
/// Returns the base64 encoded keys to store them in a store.
/// This String should **never** leave the device!
String get pickledOlmAccount =>
enabled ? _olmAccount.pickle(client.userID) : null;
String get fingerprintKey =>
enabled ? json.decode(_olmAccount.identity_keys())['ed25519'] : null;
String get identityKey =>
enabled ? json.decode(_olmAccount.identity_keys())['curve25519'] : null;
bool get enabled => _olmAccount != null;
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 = {};
Future<void> init(String olmAccount) async {
if (olmAccount == null) {
try {
await olm.init();
_olmAccount = olm.Account();
_olmAccount.create();
if (await uploadKeys(uploadDeviceKeys: true) == false) {
throw ('Upload key failed');
}
} catch (_) {
_olmAccount?.free();
_olmAccount = null;
}
} else {
try {
await olm.init();
_olmAccount = olm.Account();
_olmAccount.unpickle(client.userID, olmAccount);
} catch (_) {
_olmAccount?.free();
_olmAccount = null;
}
}
}
/// Adds a signature to this json from this olm account.
Map<String, dynamic> signJson(Map<String, dynamic> payload) {
if (!enabled) throw ('Encryption is disabled');
final Map<String, dynamic> unsigned = payload['unsigned'];
final Map<String, dynamic> signatures = payload['signatures'];
payload.remove('unsigned');
payload.remove('signatures');
final canonical = canonicalJson.encode(payload);
final signature = _olmAccount.sign(String.fromCharCodes(canonical));
if (signatures != null) {
payload['signatures'] = signatures;
} else {
payload['signatures'] = <String, dynamic>{};
}
if (!payload['signatures'].containsKey(client.userID)) {
payload['signatures'][client.userID] = <String, dynamic>{};
}
payload['signatures'][client.userID]['ed25519:${client.deviceID}'] =
signature;
if (unsigned != null) {
payload['unsigned'] = unsigned;
}
return payload;
}
/// Checks the signature of a signed json object.
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
String userId, String deviceId) {
if (!enabled) throw ('Encryption is disabled');
final Map<String, dynamic> signatures = signedJson['signatures'];
if (signatures == null || !signatures.containsKey(userId)) return false;
signedJson.remove('unsigned');
signedJson.remove('signatures');
if (!signatures[userId].containsKey('ed25519:$deviceId')) return false;
final String signature = signatures[userId]['ed25519:$deviceId'];
final canonical = canonicalJson.encode(signedJson);
final message = String.fromCharCodes(canonical);
var isValid = false;
final olmutil = olm.Utility();
try {
olmutil.ed25519_verify(key, message, signature);
isValid = true;
} catch (e) {
isValid = false;
print('[LibOlm] Signature check failed: ' + e.toString());
} finally {
olmutil.free();
}
return isValid;
}
/// Generates new one time keys, signs everything and upload it to the server.
Future<bool> uploadKeys(
{bool uploadDeviceKeys = false, int oldKeyCount = 0}) async {
if (!enabled) {
return true;
}
// generate one-time keys
// we generate 2/3rds of max, so that other keys people may still have can
// still be used
final oneTimeKeysCount =
(_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() -
oldKeyCount;
_olmAccount.generate_one_time_keys(oneTimeKeysCount);
final Map<String, dynamic> oneTimeKeys =
json.decode(_olmAccount.one_time_keys());
// now sign all the one-time keys
final signedOneTimeKeys = <String, dynamic>{};
for (final entry in oneTimeKeys['curve25519'].entries) {
final key = entry.key;
final value = entry.value;
signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{};
signedOneTimeKeys['signed_curve25519:$key'] = signJson({
'key': value,
});
}
// and now generate the payload to upload
final keysContent = <String, dynamic>{
if (uploadDeviceKeys)
'device_keys': {
'user_id': client.userID,
'device_id': client.deviceID,
'algorithms': [
'm.olm.v1.curve25519-aes-sha2',
'm.megolm.v1.aes-sha2'
],
'keys': <String, dynamic>{},
},
};
if (uploadDeviceKeys) {
final Map<String, dynamic> keys =
json.decode(_olmAccount.identity_keys());
for (final entry in keys.entries) {
final algorithm = entry.key;
final value = entry.value;
keysContent['device_keys']['keys']['$algorithm:${client.deviceID}'] =
value;
}
keysContent['device_keys'] =
signJson(keysContent['device_keys'] as Map<String, dynamic>);
}
final response = await client.api.uploadDeviceKeys(
deviceKeys: uploadDeviceKeys
? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
: null,
oneTimeKeys: signedOneTimeKeys,
);
_olmAccount.mark_keys_as_published();
await client.database?.updateClientKeys(pickledOlmAccount, client.id);
return response['signed_curve25519'] == oneTimeKeysCount;
}
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
if (!enabled) {
return;
}
// Check if there are at least half of max_number_of_one_time_keys left on the server
// and generate and upload more if not.
if (countJson.containsKey('signed_curve25519') &&
countJson['signed_curve25519'] <
(_olmAccount.max_number_of_one_time_keys() / 2)) {
uploadKeys(oldKeyCount: countJson['signed_curve25519']);
}
}
void storeOlmSession(String curve25519IdentityKey, olm.Session session) {
if (client.database == null) {
return;
}
if (!_olmSessions.containsKey(curve25519IdentityKey)) {
_olmSessions[curve25519IdentityKey] = [];
}
final ix = _olmSessions[curve25519IdentityKey]
.indexWhere((s) => s.session_id() == session.session_id());
if (ix == -1) {
// add a new session
_olmSessions[curve25519IdentityKey].add(session);
} else {
// update an existing session
_olmSessions[curve25519IdentityKey][ix] = session;
}
final pickle = session.pickle(client.userID);
client.database.storeOlmSession(
client.id, curve25519IdentityKey, session.session_id(), pickle);
}
ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) {
if (event.type != EventTypes.Encrypted) {
return event;
}
if (event.content['algorithm'] != 'm.olm.v1.curve25519-aes-sha2') {
throw ('Unknown algorithm: ${event.content}');
}
if (!event.content['ciphertext'].containsKey(identityKey)) {
throw ("The message isn't sent for this device");
}
String plaintext;
final String senderKey = event.content['sender_key'];
final String body = event.content['ciphertext'][identityKey]['body'];
final int type = event.content['ciphertext'][identityKey]['type'];
if (type != 0 && type != 1) {
throw ('Unknown message type');
}
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);
break;
} else if (type == 1) {
try {
plaintext = session.decrypt(type, body);
storeOlmSession(senderKey, session);
break;
} catch (_) {
plaintext = null;
}
}
}
}
if (plaintext == null && type != 0) {
return event;
}
if (plaintext == null) {
var newSession = olm.Session();
try {
newSession.create_inbound_from(_olmAccount, senderKey, body);
_olmAccount.remove_one_time_keys(newSession);
client.database?.updateClientKeys(pickledOlmAccount, client.id);
plaintext = newSession.decrypt(type, body);
storeOlmSession(senderKey, newSession);
} catch (_) {
newSession?.free();
rethrow;
}
}
final Map<String, dynamic> plainContent = json.decode(plaintext);
if (plainContent.containsKey('sender') &&
plainContent['sender'] != event.sender) {
throw ("Message was decrypted but sender doesn't match");
}
if (plainContent.containsKey('recipient') &&
plainContent['recipient'] != client.userID) {
throw ("Message was decrypted but recipient doesn't match");
}
if (plainContent['recipient_keys'] is Map &&
plainContent['recipient_keys']['ed25519'] is String &&
plainContent['recipient_keys']['ed25519'] != fingerprintKey) {
throw ("Message was decrypted but own fingerprint Key doesn't match");
}
return ToDeviceEvent(
content: plainContent['content'],
encryptedContent: event.content,
type: plainContent['type'],
sender: event.sender,
);
}
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
if (event.type != EventTypes.Encrypted) {
return event;
}
final senderKey = event.content['sender_key'];
final loadFromDb = () async {
if (client.database == null) {
return false;
}
final sessions = await client.database
.getSingleOlmSessions(client.id, senderKey, client.userID);
if (sessions.isEmpty) {
return false; // okay, can't do anything
}
_olmSessions[senderKey] = sessions;
return true;
};
if (!_olmSessions.containsKey(senderKey)) {
await loadFromDb();
}
event = _decryptToDeviceEvent(event);
if (event.type != EventTypes.Encrypted || !(await loadFromDb())) {
return event;
}
// retry to decrypt!
return _decryptToDeviceEvent(event);
}
Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys) async {
var requestingKeysFrom = <String, Map<String, String>>{};
for (var device in deviceKeys) {
if (requestingKeysFrom[device.userId] == null) {
requestingKeysFrom[device.userId] = {};
}
requestingKeysFrom[device.userId][device.deviceId] = 'signed_curve25519';
}
final response =
await client.api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000);
for (var userKeysEntry in response.oneTimeKeys.entries) {
final userId = userKeysEntry.key;
for (var deviceKeysEntry in userKeysEntry.value.entries) {
final deviceId = deviceKeysEntry.key;
final fingerprintKey =
client.userDeviceKeys[userId].deviceKeys[deviceId].ed25519Key;
final identityKey =
client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key;
for (Map<String, dynamic> deviceKey in deviceKeysEntry.value.values) {
if (!checkJsonSignature(
fingerprintKey, deviceKey, userId, deviceId)) {
continue;
}
try {
var session = olm.Session();
session.create_outbound(_olmAccount, identityKey, deviceKey['key']);
await storeOlmSession(identityKey, session);
} catch (e) {
print('[LibOlm] Could not create new outbound olm session: ' +
e.toString());
}
}
}
}
}
Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
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);
if (sessions.isEmpty) {
throw ('No olm session found');
}
sess = _olmSessions[device.curve25519Key] = sessions;
}
sess.sort((a, b) => a.session_id().compareTo(b.session_id()));
final fullPayload = {
'type': type,
'content': payload,
'sender': client.userID,
'keys': {'ed25519': fingerprintKey},
'recipient': device.userId,
'recipient_keys': {'ed25519': device.ed25519Key},
};
final encryptResult = sess.first.encrypt(json.encode(fullPayload));
storeOlmSession(device.curve25519Key, sess.first);
final encryptedBody = <String, dynamic>{
'algorithm': 'm.olm.v1.curve25519-aes-sha2',
'sender_key': identityKey,
'ciphertext': <String, dynamic>{},
};
encryptedBody['ciphertext'][device.curve25519Key] = {
'type': encryptResult.type,
'body': encryptResult.body,
};
return encryptedBody;
}
Future<Map<String, dynamic>> encryptToDeviceMessage(
List<DeviceKeys> deviceKeys,
String type,
Map<String, dynamic> payload) async {
var data = <String, Map<String, Map<String, dynamic>>>{};
final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
olmSessions.containsKey(deviceKeys.curve25519Key));
if (deviceKeysWithoutSession.isNotEmpty) {
await startOutgoingOlmSessions(deviceKeysWithoutSession);
}
for (final device in deviceKeys) {
if (!data.containsKey(device.userId)) {
data[device.userId] = {};
}
try {
data[device.userId][device.deviceId] =
await encryptToDeviceMessagePayload(device, type, payload);
} catch (e) {
print('[LibOlm] Error encrypting to-device event: ' + e.toString());
continue;
}
}
return data;
}
void dispose() {
for (final sessions in olmSessions.values) {
for (final sess in sessions) {
sess.free();
}
}
_olmAccount?.free();
_olmAccount = null;
}
}

View file

@ -1,11 +1,29 @@
/*
* 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 'package:random_string/random_string.dart';
import 'package:canonical_json/canonical_json.dart';
import 'package:olm/olm.dart' as olm;
import '../../matrix_api.dart';
import 'device_keys_list.dart';
import '../client.dart';
import '../room.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart';
import '../encryption.dart';
/*
+-------------+ +-----------+
@ -53,6 +71,9 @@ enum KeyVerificationState {
}
List<String> _intersect(List<String> a, List<dynamic> b) {
if (b == null || a == null) {
return [];
}
final res = <String>[];
for (final v in a) {
if (b.contains(v)) {
@ -94,7 +115,8 @@ _KeyVerificationMethod _makeVerificationMethod(
class KeyVerification {
String transactionId;
final Client client;
final Encryption encryption;
Client get client => encryption.client;
final Room room;
final String userId;
void Function() onUpdate;
@ -114,7 +136,11 @@ class KeyVerification {
String canceledReason;
KeyVerification(
{this.client, this.room, this.userId, String deviceId, this.onUpdate}) {
{this.encryption,
this.room,
this.userId,
String deviceId,
this.onUpdate}) {
lastActivity = DateTime.now();
_deviceId ??= deviceId;
}
@ -384,7 +410,7 @@ class KeyVerification {
final newTransactionId = await room.sendEvent(payload, type: type);
if (transactionId == null) {
transactionId = newTransactionId;
client.addKeyVerificationRequest(this);
encryption.keyVerificationManager.addRequest(this);
}
} else {
await client.sendToDevice(
@ -404,10 +430,9 @@ class KeyVerification {
abstract class _KeyVerificationMethod {
KeyVerification request;
Client client;
_KeyVerificationMethod({this.request}) {
client = request.client;
}
Encryption get encryption => request.encryption;
Client get client => request.client;
_KeyVerificationMethod({this.request});
Future<void> handlePayload(String type, Map<String, dynamic> payload);
bool validateStart(Map<String, dynamic> payload) {
@ -662,7 +687,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
// we would also add the cross signing key here
final deviceKeyId = 'ed25519:${client.deviceID}';
mac[deviceKeyId] =
_calculateMac(client.fingerprintKey, baseInfo + deviceKeyId);
_calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId);
keyList.add(deviceKeyId);
keyList.sort();

View file

@ -0,0 +1,58 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 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:olm/olm.dart' as olm;
import '../../src/database/database.dart' show DbOutboundGroupSession;
class OutboundGroupSession {
List<String> devices;
DateTime creationTime;
olm.OutboundGroupSession outboundGroupSession;
int sentMessages;
bool get isValid => outboundGroupSession != null;
final String key;
OutboundGroupSession(
{this.devices,
this.creationTime,
this.outboundGroupSession,
this.sentMessages,
this.key});
OutboundGroupSession.fromDb(DbOutboundGroupSession dbEntry, String key)
: key = key {
outboundGroupSession = olm.OutboundGroupSession();
try {
outboundGroupSession.unpickle(key, dbEntry.pickle);
devices = List<String>.from(json.decode(dbEntry.deviceIds));
creationTime = dbEntry.creationTime;
sentMessages = dbEntry.sentMessages;
} catch (e) {
dispose();
print(
'[LibOlm] Unable to unpickle outboundGroupSession: ' + e.toString());
}
}
void dispose() {
outboundGroupSession?.free();
outboundGroupSession = null;
}
}

View file

@ -0,0 +1,76 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 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:olm/olm.dart' as olm;
import 'package:famedlysdk/famedlysdk.dart';
import '../../src/database/database.dart' show DbInboundGroupSession;
class SessionKey {
Map<String, dynamic> content;
Map<String, int> indexes;
olm.InboundGroupSession inboundGroupSession;
final String key;
List<dynamic> get forwardingCurve25519KeyChain =>
content['forwarding_curve25519_key_chain'] ?? [];
String get senderClaimedEd25519Key =>
content['sender_claimed_ed25519_key'] ?? '';
String get senderKey => content['sender_key'] ?? '';
bool get isValid => inboundGroupSession != null;
SessionKey({this.content, this.inboundGroupSession, this.key, this.indexes});
SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key {
final parsedContent = Event.getMapFromPayload(dbEntry.content);
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes);
content =
parsedContent != null ? Map<String, dynamic>.from(parsedContent) : null;
indexes = parsedIndexes != null
? Map<String, int>.from(parsedIndexes)
: <String, int>{};
inboundGroupSession = olm.InboundGroupSession();
try {
inboundGroupSession.unpickle(key, dbEntry.pickle);
} catch (e) {
dispose();
print('[LibOlm] Unable to unpickle inboundGroupSession: ' + e.toString());
}
}
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
if (content != null) {
data['content'] = content;
}
if (indexes != null) {
data['indexes'] = indexes;
}
data['inboundGroupSession'] = inboundGroupSession.pickle(key);
return data;
}
void dispose() {
inboundGroupSession?.free();
inboundGroupSession = null;
}
@override
String toString() => json.encode(toJson());
}

View file

@ -22,7 +22,6 @@ export 'matrix_api.dart';
export 'package:famedlysdk/src/utils/room_update.dart';
export 'package:famedlysdk/src/utils/event_update.dart';
export 'package:famedlysdk/src/utils/device_keys_list.dart';
export 'package:famedlysdk/src/utils/key_verification.dart';
export 'package:famedlysdk/src/utils/matrix_file.dart';
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
export 'package:famedlysdk/src/utils/uri_extension.dart';
@ -32,7 +31,6 @@ export 'package:famedlysdk/src/utils/states_map.dart';
export 'package:famedlysdk/src/utils/to_device_event.dart';
export 'package:famedlysdk/src/client.dart';
export 'package:famedlysdk/src/event.dart';
export 'package:famedlysdk/src/key_manager.dart';
export 'package:famedlysdk/src/room.dart';
export 'package:famedlysdk/src/timeline.dart';
export 'package:famedlysdk/src/user.dart';

View file

@ -20,16 +20,14 @@ import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'package:canonical_json/canonical_json.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';
import 'package:famedlysdk/src/utils/session_key.dart';
import 'package:famedlysdk/src/utils/to_device_event.dart';
import 'package:http/http.dart' as http;
import 'package:olm/olm.dart' as olm;
import 'package:pedantic/pedantic.dart';
import 'event.dart';
@ -38,8 +36,6 @@ import 'utils/event_update.dart';
import 'utils/room_update.dart';
import 'user.dart';
import 'database/database.dart' show Database;
import 'utils/key_verification.dart';
import 'key_manager.dart';
typedef RoomSorter = int Function(Room a, Room b);
@ -53,12 +49,13 @@ class Client {
int get id => _id;
Database database;
KeyManager keyManager;
bool enableE2eeRecovery;
MatrixApi api;
Encryption encryption;
/// Create a client
/// clientName = unique identifier of this client
/// debug: Print debug output?
@ -70,7 +67,6 @@ class Client {
this.enableE2eeRecovery = false,
http.Client httpClient}) {
api = MatrixApi(debug: debug, httpClient: httpClient);
keyManager = KeyManager(this);
onLoginStateChanged.stream.listen((loginState) {
if (debug) {
print('[LoginState]: ${loginState.toString()}');
@ -106,18 +102,14 @@ class Client {
List<Room> get rooms => _rooms;
List<Room> _rooms = [];
olm.Account _olmAccount;
/// Returns the base64 encoded keys to store them in a store.
/// This String should **never** leave the device!
String get pickledOlmAccount =>
encryptionEnabled ? _olmAccount.pickle(userID) : null;
/// Whether this client supports end-to-end encryption using olm.
bool get encryptionEnabled => _olmAccount != null;
bool get encryptionEnabled => encryption != null && encryption.enabled;
/// Whether this client is able to encrypt and decrypt files.
bool get fileEncryptionEnabled => true;
bool get fileEncryptionEnabled => encryptionEnabled && true;
String get identityKey => encryption?.identityKey ?? '';
String get fingerprintKey => encryption?.fingerprintKey ?? '';
/// Warning! This endpoint is for testing only!
set rooms(List<Room> newList) {
@ -529,8 +521,6 @@ class Client {
final StreamController<KeyVerification> onKeyVerificationRequest =
StreamController.broadcast();
final Map<String, KeyVerification> _keyVerificationRequests = {};
/// Matrix synchronisation is done with https long polling. This needs a
/// timeout which is usually 30 seconds.
int syncTimeoutSec = 30;
@ -604,31 +594,15 @@ class Client {
if (api.accessToken == null || api.homeserver == null || _userID == null) {
// we aren't logged in
encryption?.dispose();
encryption = null;
onLoginStateChanged.add(LoginState.loggedOut);
return;
}
// Try to create a new olm account or restore a previous one.
if (olmAccount == null) {
try {
await olm.init();
_olmAccount = olm.Account();
_olmAccount.create();
if (await _uploadKeys(uploadDeviceKeys: true) == false) {
throw ('Upload key failed');
}
} catch (_) {
_olmAccount = null;
}
} else {
try {
await olm.init();
_olmAccount = olm.Account();
_olmAccount.unpickle(userID, olmAccount);
} catch (_) {
_olmAccount = null;
}
}
encryption = Encryption(
debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery);
await encryption.init(olmAccount);
if (database != null) {
if (id != null) {
@ -639,7 +613,7 @@ class Client {
_deviceID,
_deviceName,
prevBatch,
pickledOlmAccount,
encryption?.pickledOlmAccount,
id,
);
} else {
@ -651,11 +625,10 @@ class Client {
_deviceID,
_deviceName,
prevBatch,
pickledOlmAccount,
encryption?.pickledOlmAccount,
);
}
_userDeviceKeys = await database.getUserDeviceKeys(id);
_olmSessions = await database.getOlmSessions(id, _userID);
_rooms = await database.getRoomList(this, onlyLeft: false);
_sortRooms();
accountData = await database.getAccountData(id);
@ -674,20 +647,12 @@ class Client {
/// Resets all settings and stops the synchronisation.
void clear() {
olmSessions.values.forEach((List<olm.Session> sessions) {
sessions.forEach((olm.Session session) => session?.free());
});
rooms.forEach((Room room) {
room.clearOutboundGroupSession(wipe: true);
room.inboundGroupSessions.values.forEach((SessionKey sessionKey) {
sessionKey.inboundGroupSession?.free();
});
});
_olmAccount?.free();
database?.clear(id);
_id = api.accessToken =
api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
_rooms = [];
encryption?.dispose();
encryption = null;
onLoginStateChanged.add(LoginState.loggedOut);
}
@ -723,7 +688,9 @@ class Client {
}
prevBatch = syncResp.nextBatch;
await _updateUserDeviceKeys();
_cleanupKeyVerificationRequests();
if (encryptionEnabled) {
encryption.onSync();
}
if (hash == _syncRequest.hashCode) unawaited(_sync());
} on MatrixException catch (exception) {
onError.add(exception);
@ -740,7 +707,7 @@ class Client {
/// Use this method only for testing utilities!
Future<void> handleSync(SyncUpdate sync) async {
if (sync.toDevice != null) {
_handleToDeviceEvents(sync.toDevice);
await _handleToDeviceEvents(sync.toDevice);
}
if (sync.rooms != null) {
if (sync.rooms.join != null) {
@ -784,31 +751,12 @@ class Client {
if (sync.deviceLists != null) {
await _handleDeviceListsEvents(sync.deviceLists);
}
if (sync.deviceOneTimeKeysCount != null) {
_handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount);
}
while (_pendingToDeviceEvents.isNotEmpty) {
_updateRoomsByToDeviceEvent(
_pendingToDeviceEvents.removeLast(),
addToPendingIfNotFound: false,
);
if (sync.deviceOneTimeKeysCount != null && encryptionEnabled) {
encryption.handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount);
}
onSync.add(sync);
}
void _handleDeviceOneTimeKeysCount(Map<String, int> deviceOneTimeKeysCount) {
if (!encryptionEnabled) return;
// Check if there are at least half of max_number_of_one_time_keys left on the server
// and generate and upload more if not.
if (deviceOneTimeKeysCount['signed_curve25519'] != null) {
final oneTimeKeysCount = deviceOneTimeKeysCount['signed_curve25519'];
if (oneTimeKeysCount < (_olmAccount.max_number_of_one_time_keys() / 2)) {
// Generate and upload more one time keys:
_uploadKeys();
}
}
}
Future<void> _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async {
if (deviceLists.changed is List) {
for (final userId in deviceLists.changed) {
@ -827,36 +775,12 @@ class Client {
}
}
void _cleanupKeyVerificationRequests() {
for (final entry in _keyVerificationRequests.entries) {
(() async {
var dispose = entry.value.canceled ||
entry.value.state == KeyVerificationState.done ||
entry.value.state == KeyVerificationState.error;
if (!dispose) {
dispose = !(await entry.value.verifyActivity());
}
if (dispose) {
entry.value.dispose();
_keyVerificationRequests.remove(entry.key);
}
})();
}
}
void addKeyVerificationRequest(KeyVerification request) {
if (request.transactionId == null) {
return;
}
_keyVerificationRequests[request.transactionId] = request;
}
void _handleToDeviceEvents(List<BasicEventWithSender> events) {
Future<void> _handleToDeviceEvents(List<BasicEventWithSender> events) async {
for (var i = 0; i < events.length; i++) {
var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
if (toDeviceEvent.type == EventTypes.Encrypted) {
if (toDeviceEvent.type == EventTypes.Encrypted && encryptionEnabled) {
try {
toDeviceEvent = decryptToDeviceEvent(toDeviceEvent);
toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent);
} catch (e, s) {
print(
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}');
@ -872,48 +796,13 @@ class Client {
toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
}
}
_updateRoomsByToDeviceEvent(toDeviceEvent);
if (toDeviceEvent.type.startsWith('m.key.verification.')) {
_handleToDeviceKeyVerificationRequest(toDeviceEvent);
}
if (['m.room_key_request', 'm.forwarded_room_key']
.contains(toDeviceEvent.type)) {
keyManager.handleToDeviceEvent(toDeviceEvent);
if (encryptionEnabled) {
await encryption.handleToDeviceEvent(toDeviceEvent);
}
onToDeviceEvent.add(toDeviceEvent);
}
}
void _handleToDeviceKeyVerificationRequest(ToDeviceEvent toDeviceEvent) {
if (!toDeviceEvent.type.startsWith('m.key.verification.')) {
return;
}
// we have key verification going on!
final transactionId =
KeyVerification.getTransactionId(toDeviceEvent.content);
if (transactionId != null) {
if (_keyVerificationRequests.containsKey(transactionId)) {
_keyVerificationRequests[transactionId]
.handlePayload(toDeviceEvent.type, toDeviceEvent.content);
} else {
final newKeyRequest =
KeyVerification(client: this, userId: toDeviceEvent.sender);
newKeyRequest
.handlePayload(toDeviceEvent.type, toDeviceEvent.content)
.then((res) {
if (newKeyRequest.state != KeyVerificationState.askAccept) {
// okay, something went wrong (unknown transaction id?), just dispose it
newKeyRequest.dispose();
} else {
// we have a new request! Let's broadcast it!
_keyVerificationRequests[transactionId] = newKeyRequest;
onKeyVerificationRequest.add(newKeyRequest);
}
});
}
}
}
Future<void> _handleRooms(
Map<String, SyncRoomUpdate> rooms, Membership membership) async {
for (final entry in rooms.entries) {
@ -1056,14 +945,8 @@ class Client {
content: event,
sortOrder: sortOrder,
);
if (event['type'] == EventTypes.Encrypted) {
update = update.decrypt(room);
}
if (update.eventType == EventTypes.Encrypted && database != null) {
// the event is still encrytped....let's try fetching the keys from the database!
await room.loadInboundGroupSessionKey(
event['content']['session_id'], event['content']['sender_key']);
update = update.decrypt(room);
if (event['type'] == EventTypes.Encrypted && encryptionEnabled) {
update = await update.decrypt(room);
}
if (type != 'ephemeral' && database != null) {
await database.storeEventUpdate(id, update);
@ -1187,42 +1070,6 @@ class Client {
if (eventUpdate.type == 'timeline') _sortRooms();
}
final List<ToDeviceEvent> _pendingToDeviceEvents = [];
void _updateRoomsByToDeviceEvent(ToDeviceEvent toDeviceEvent,
{addToPendingIfNotFound = true}) async {
try {
switch (toDeviceEvent.type) {
case 'm.room_key':
final roomId = toDeviceEvent.content['room_id'];
var room = getRoomById(roomId);
if (room == null && addToPendingIfNotFound) {
_pendingToDeviceEvents.add(toDeviceEvent);
break;
}
room ??= Room(client: this, id: roomId);
final String sessionId = toDeviceEvent.content['session_id'];
if (userDeviceKeys.containsKey(toDeviceEvent.sender) &&
userDeviceKeys[toDeviceEvent.sender]
.deviceKeys
.containsKey(toDeviceEvent.content['requesting_device_id'])) {
toDeviceEvent.content['sender_claimed_ed25519_key'] =
userDeviceKeys[toDeviceEvent.sender]
.deviceKeys[toDeviceEvent.content['requesting_device_id']]
.ed25519Key;
}
room.setInboundGroupSession(
sessionId,
toDeviceEvent.content,
forwarded: false,
);
break;
}
} catch (e) {
print('[Matrix] Error while processing to-device-event: ' + e.toString());
}
}
bool _sortLock = false;
/// The compare function how the rooms should be sorted internally. By default
@ -1287,6 +1134,9 @@ class Client {
for (final rawDeviceKeyListEntry in response.deviceKeys.entries) {
final userId = rawDeviceKeyListEntry.key;
if (!userDeviceKeys.containsKey(userId)) {
_userDeviceKeys[userId] = DeviceKeysList(userId);
}
final oldKeys =
Map<String, DeviceKeys>.from(_userDeviceKeys[userId].deviceKeys);
_userDeviceKeys[userId].deviceKeys = {};
@ -1301,7 +1151,7 @@ class Client {
if (entry.isValid) {
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
if (deviceId == deviceID &&
entry.ed25519Key == fingerprintKey) {
entry.ed25519Key == encryption?.fingerprintKey) {
// Always trust the own device
entry.verified = true;
}
@ -1349,213 +1199,6 @@ class Client {
}
}
String get fingerprintKey => encryptionEnabled
? json.decode(_olmAccount.identity_keys())['ed25519']
: null;
String get identityKey => encryptionEnabled
? json.decode(_olmAccount.identity_keys())['curve25519']
: null;
/// Adds a signature to this json from this olm account.
Map<String, dynamic> signJson(Map<String, dynamic> payload) {
if (!encryptionEnabled) throw ('Encryption is disabled');
final Map<String, dynamic> unsigned = payload['unsigned'];
final Map<String, dynamic> signatures = payload['signatures'];
payload.remove('unsigned');
payload.remove('signatures');
final canonical = canonicalJson.encode(payload);
final signature = _olmAccount.sign(String.fromCharCodes(canonical));
if (signatures != null) {
payload['signatures'] = signatures;
} else {
payload['signatures'] = <String, dynamic>{};
}
payload['signatures'][userID] = <String, dynamic>{};
payload['signatures'][userID]['ed25519:$deviceID'] = signature;
if (unsigned != null) {
payload['unsigned'] = unsigned;
}
return payload;
}
/// Checks the signature of a signed json object.
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
String userId, String deviceId) {
if (!encryptionEnabled) throw ('Encryption is disabled');
final Map<String, dynamic> signatures = signedJson['signatures'];
if (signatures == null || !signatures.containsKey(userId)) return false;
signedJson.remove('unsigned');
signedJson.remove('signatures');
if (!signatures[userId].containsKey('ed25519:$deviceId')) return false;
final String signature = signatures[userId]['ed25519:$deviceId'];
final canonical = canonicalJson.encode(signedJson);
final message = String.fromCharCodes(canonical);
var isValid = true;
try {
olm.Utility()
..ed25519_verify(key, message, signature)
..free();
} catch (e) {
isValid = false;
print('[LibOlm] Signature check failed: ' + e.toString());
}
return isValid;
}
DateTime lastTimeKeysUploaded;
/// Generates new one time keys, signs everything and upload it to the server.
Future<bool> _uploadKeys({bool uploadDeviceKeys = false}) async {
if (!encryptionEnabled) return true;
final oneTimeKeysCount = _olmAccount.max_number_of_one_time_keys();
_olmAccount.generate_one_time_keys(oneTimeKeysCount);
final Map<String, dynamic> oneTimeKeys =
json.decode(_olmAccount.one_time_keys());
var signedOneTimeKeys = <String, dynamic>{};
for (String key in oneTimeKeys['curve25519'].keys) {
signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{};
signedOneTimeKeys['signed_curve25519:$key']['key'] =
oneTimeKeys['curve25519'][key];
signedOneTimeKeys['signed_curve25519:$key'] =
signJson(signedOneTimeKeys['signed_curve25519:$key']);
}
var keysContent = <String, dynamic>{
if (uploadDeviceKeys)
'device_keys': {
'user_id': userID,
'device_id': deviceID,
'algorithms': [
'm.olm.v1.curve25519-aes-sha2',
'm.megolm.v1.aes-sha2'
],
'keys': <String, dynamic>{},
},
};
if (uploadDeviceKeys) {
final Map<String, dynamic> keys =
json.decode(_olmAccount.identity_keys());
for (var algorithm in keys.keys) {
keysContent['device_keys']['keys']['$algorithm:$deviceID'] =
keys[algorithm];
}
keysContent['device_keys'] =
signJson(keysContent['device_keys'] as Map<String, dynamic>);
}
_olmAccount.mark_keys_as_published();
final response = await api.uploadDeviceKeys(
deviceKeys: uploadDeviceKeys
? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
: null,
oneTimeKeys: signedOneTimeKeys,
);
if (response['signed_curve25519'] != oneTimeKeysCount) {
return false;
}
await database?.updateClientKeys(pickledOlmAccount, id);
lastTimeKeysUploaded = DateTime.now();
return true;
}
/// Try to decrypt a ToDeviceEvent encrypted with olm.
ToDeviceEvent decryptToDeviceEvent(ToDeviceEvent toDeviceEvent) {
if (toDeviceEvent.type != EventTypes.Encrypted) {
print(
'[LibOlm] Warning! Tried to decrypt a not-encrypted to-device-event');
return toDeviceEvent;
}
if (toDeviceEvent.content['algorithm'] != 'm.olm.v1.curve25519-aes-sha2') {
throw ('Unknown algorithm: ${toDeviceEvent.content}');
}
if (!toDeviceEvent.content['ciphertext'].containsKey(identityKey)) {
throw ("The message isn't sent for this device");
}
String plaintext;
final String senderKey = toDeviceEvent.content['sender_key'];
final String body =
toDeviceEvent.content['ciphertext'][identityKey]['body'];
final int type = toDeviceEvent.content['ciphertext'][identityKey]['type'];
if (type != 0 && type != 1) {
throw ('Unknown message type');
}
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);
break;
} else if (type == 1) {
try {
plaintext = session.decrypt(type, body);
storeOlmSession(senderKey, session);
break;
} catch (_) {
plaintext = null;
}
}
}
}
if (plaintext == null && type != 0) {
throw ('No existing sessions found');
}
if (plaintext == null) {
var newSession = olm.Session();
newSession.create_inbound_from(_olmAccount, senderKey, body);
_olmAccount.remove_one_time_keys(newSession);
database?.updateClientKeys(pickledOlmAccount, id);
plaintext = newSession.decrypt(type, body);
storeOlmSession(senderKey, newSession);
}
final Map<String, dynamic> plainContent = json.decode(plaintext);
if (plainContent.containsKey('sender') &&
plainContent['sender'] != toDeviceEvent.sender) {
throw ("Message was decrypted but sender doesn't match");
}
if (plainContent.containsKey('recipient') &&
plainContent['recipient'] != userID) {
throw ("Message was decrypted but recipient doesn't match");
}
if (plainContent['recipient_keys'] is Map &&
plainContent['recipient_keys']['ed25519'] is String &&
plainContent['recipient_keys']['ed25519'] != fingerprintKey) {
throw ("Message was decrypted but own fingerprint Key doesn't match");
}
return ToDeviceEvent(
content: plainContent['content'],
encryptedContent: toDeviceEvent.content,
type: plainContent['type'],
sender: toDeviceEvent.sender,
);
}
/// A map from Curve25519 identity keys to existing olm sessions.
Map<String, List<olm.Session>> get olmSessions => _olmSessions;
Map<String, List<olm.Session>> _olmSessions = {};
void storeOlmSession(String curve25519IdentityKey, olm.Session session) {
if (!_olmSessions.containsKey(curve25519IdentityKey)) {
_olmSessions[curve25519IdentityKey] = [];
}
final ix = _olmSessions[curve25519IdentityKey]
.indexWhere((s) => s.session_id() == session.session_id());
if (ix == -1) {
// add a new session
_olmSessions[curve25519IdentityKey].add(session);
} else {
// update an existing session
_olmSessions[curve25519IdentityKey][ix] = session;
}
final pickle = session.pickle(userID);
database?.storeOlmSession(
id, curve25519IdentityKey, session.session_id(), pickle);
}
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send
/// the request to all devices of the current user, pass an empty list to [deviceKeys].
Future<void> sendToDevice(
@ -1589,96 +1232,22 @@ class Client {
}
} else {
if (encrypted) {
// Create new sessions with devices if there is no existing session yet.
var deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
olmSessions.containsKey(deviceKeys.curve25519Key));
if (deviceKeysWithoutSession.isNotEmpty) {
await startOutgoingOlmSessions(deviceKeysWithoutSession);
data =
await encryption.encryptToDeviceMessage(deviceKeys, type, message);
} else {
for (final device in deviceKeys) {
if (!data.containsKey(device.userId)) {
data[device.userId] = {};
}
data[device.userId][device.deviceId] = sendToDeviceMessage;
}
}
for (var i = 0; i < deviceKeys.length; i++) {
var device = deviceKeys[i];
if (!data.containsKey(device.userId)) {
data[device.userId] = {};
}
if (encrypted) {
var existingSessions = olmSessions[device.curve25519Key];
if (existingSessions == null || existingSessions.isEmpty) continue;
existingSessions
.sort((a, b) => a.session_id().compareTo(b.session_id()));
final payload = {
'type': type,
'content': message,
'sender': userID,
'keys': {'ed25519': fingerprintKey},
'recipient': device.userId,
'recipient_keys': {'ed25519': device.ed25519Key},
};
final encryptResult =
existingSessions.first.encrypt(json.encode(payload));
storeOlmSession(device.curve25519Key, existingSessions.first);
sendToDeviceMessage = {
'algorithm': 'm.olm.v1.curve25519-aes-sha2',
'sender_key': identityKey,
'ciphertext': <String, dynamic>{},
};
sendToDeviceMessage['ciphertext'][device.curve25519Key] = {
'type': encryptResult.type,
'body': encryptResult.body,
};
}
data[device.userId][device.deviceId] = sendToDeviceMessage;
}
}
if (encrypted) type = EventTypes.Encrypted;
final messageID = generateUniqueTransactionId();
await api.sendToDevice(type, messageID, data);
}
Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys,
{bool checkSignature = true}) async {
var requestingKeysFrom = <String, Map<String, String>>{};
for (var device in deviceKeys) {
if (requestingKeysFrom[device.userId] == null) {
requestingKeysFrom[device.userId] = {};
}
requestingKeysFrom[device.userId][device.deviceId] = 'signed_curve25519';
}
final response =
await api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000);
for (var userKeysEntry in response.oneTimeKeys.entries) {
final userId = userKeysEntry.key;
for (var deviceKeysEntry in userKeysEntry.value.entries) {
final deviceId = deviceKeysEntry.key;
final fingerprintKey =
userDeviceKeys[userId].deviceKeys[deviceId].ed25519Key;
final identityKey =
userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key;
for (Map<String, dynamic> deviceKey in deviceKeysEntry.value.values) {
if (checkSignature &&
checkJsonSignature(fingerprintKey, deviceKey, userId, deviceId) ==
false) {
continue;
}
try {
var session = olm.Session();
session.create_outbound(_olmAccount, identityKey, deviceKey['key']);
await storeOlmSession(identityKey, session);
} catch (e) {
print('[LibOlm] Could not create new outbound olm session: ' +
e.toString());
}
}
}
}
}
/// Whether all push notifications are muted using the [.m.rule.master]
/// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master
bool get allPushNotificationsMuted {

View file

@ -91,6 +91,22 @@ 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();

View file

@ -4851,6 +4851,19 @@ abstract class _$Database extends GeneratedDatabase {
readsFrom: {olmSessions}).map(_rowToDbOlmSessions);
}
Selectable<DbOlmSessions> dbGetOlmSessions(
int client_id, String identity_key) {
return customSelect(
'SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key',
variables: [
Variable.withInt(client_id),
Variable.withString(identity_key)
],
readsFrom: {
olmSessions
}).map(_rowToDbOlmSessions);
}
Future<int> storeOlmSession(
int client_id, String identitiy_key, String session_id, String pickle) {
return customInsert(

View file

@ -155,6 +155,7 @@ storePrevBatch: UPDATE clients SET prev_batch = :prev_batch WHERE client_id = :c
getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id;
getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key 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);
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;

View file

@ -19,6 +19,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/src/utils/receipt.dart';
import 'package:http/http.dart' as http;
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
@ -333,36 +334,6 @@ class Event extends MatrixEvent {
return await timeline.getEventById(replyEventId);
}
Future<void> loadSession() {
return room.loadInboundGroupSessionKeyForEvent(this);
}
/// Trys to decrypt this event. Returns a m.bad.encrypted event
/// if it fails and does nothing if the event was not encrypted.
Event get decrypted => room.decryptGroupMessage(this);
/// Trys to decrypt this event and persists it in the database afterwards
Future<Event> decryptAndStore([String updateType = 'timeline']) async {
final newEvent = decrypted;
if (newEvent.type == EventTypes.Encrypted) {
return newEvent; // decryption failed
}
await room.client.database?.storeEventUpdate(
room.client.id,
EventUpdate(
eventType: newEvent.type,
content: newEvent.toJson(),
roomID: newEvent.roomId,
type: updateType,
sortOrder: newEvent.sortOrder,
),
);
if (updateType != 'history') {
room.setState(newEvent);
}
return newEvent;
}
/// If this event is encrypted and the decryption was not successful because
/// the session is unknown, this requests the session key from other devices
/// in the room. If the event is not encrypted or the decryption failed because

View file

@ -1,214 +0,0 @@
import 'client.dart';
import 'room.dart';
import 'utils/to_device_event.dart';
import 'utils/device_keys_list.dart';
class KeyManager {
final Client client;
final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
KeyManager(this.client);
/// Request a certain key from another device
Future<void> request(Room room, String sessionId, String senderKey) async {
// while we just send the to-device event to '*', we still need to save the
// devices themself to know where to send the cancel to after receiving a reply
final devices = await room.getUserDeviceKeys();
final requestId = client.generateUniqueTransactionId();
final request = KeyManagerKeyShareRequest(
requestId: requestId,
devices: devices,
room: room,
sessionId: sessionId,
senderKey: senderKey,
);
await client.sendToDevice(
[],
'm.room_key_request',
{
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': room.id,
'sender_key': senderKey,
'session_id': sessionId,
},
'request_id': requestId,
'requesting_device_id': client.deviceID,
},
encrypted: false,
toUsers: await room.requestParticipants());
outgoingShareRequests[request.requestId] = request;
}
/// Handle an incoming to_device event that is related to key sharing
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
if (event.type == 'm.room_key_request') {
if (!event.content.containsKey('request_id')) {
return; // invalid event
}
if (event.content['action'] == 'request') {
// we are *receiving* a request
if (!event.content.containsKey('body')) {
return; // no body
}
if (!client.userDeviceKeys.containsKey(event.sender) ||
!client.userDeviceKeys[event.sender].deviceKeys
.containsKey(event.content['requesting_device_id'])) {
return; // device not found
}
final device = client.userDeviceKeys[event.sender]
.deviceKeys[event.content['requesting_device_id']];
if (device.userId == client.userID &&
device.deviceId == client.deviceID) {
return; // ignore requests by ourself
}
final room = client.getRoomById(event.content['body']['room_id']);
if (room == null) {
return; // unknown room
}
final sessionId = event.content['body']['session_id'];
// okay, let's see if we have this session at all
await room.loadInboundGroupSessionKey(sessionId);
if (!room.inboundGroupSessions.containsKey(sessionId)) {
return; // we don't have this session anyways
}
final request = KeyManagerKeyShareRequest(
requestId: event.content['request_id'],
devices: [device],
room: room,
sessionId: event.content['body']['session_id'],
senderKey: event.content['body']['sender_key'],
);
if (incomingShareRequests.containsKey(request.requestId)) {
return; // we don't want to process one and the same request multiple times
}
incomingShareRequests[request.requestId] = request;
final roomKeyRequest =
RoomKeyRequest.fromToDeviceEvent(event, this, request);
if (device.userId == client.userID &&
device.verified &&
!device.blocked) {
// alright, we can forward the key
await roomKeyRequest.forwardKey();
} else {
client.onRoomKeyRequest
.add(roomKeyRequest); // let the client handle this
}
} else if (event.content['action'] == 'request_cancellation') {
// we got told to cancel an incoming request
if (!incomingShareRequests.containsKey(event.content['request_id'])) {
return; // we don't know this request anyways
}
// alright, let's just cancel this request
final request = incomingShareRequests[event.content['request_id']];
request.canceled = true;
incomingShareRequests.remove(request.requestId);
}
} else if (event.type == 'm.forwarded_room_key') {
// we *received* an incoming key request
if (event.encryptedContent == null) {
return; // event wasn't encrypted, this is a security risk
}
final request = outgoingShareRequests.values.firstWhere(
(r) =>
r.room.id == event.content['room_id'] &&
r.sessionId == event.content['session_id'] &&
r.senderKey == event.content['sender_key'],
orElse: () => null);
if (request == null || request.canceled) {
return; // no associated request found or it got canceled
}
final device = request.devices.firstWhere(
(d) =>
d.userId == event.sender &&
d.curve25519Key == event.encryptedContent['sender_key'],
orElse: () => null);
if (device == null) {
return; // someone we didn't send our request to replied....better ignore this
}
// TODO: verify that the keys work to decrypt a message
// alright, all checks out, let's go ahead and store this session
request.room.setInboundGroupSession(request.sessionId, event.content,
forwarded: true);
request.devices.removeWhere(
(k) => k.userId == device.userId && k.deviceId == device.deviceId);
outgoingShareRequests.remove(request.requestId);
// send cancel to all other devices
if (request.devices.isEmpty) {
return; // no need to send any cancellation
}
await client.sendToDevice(
request.devices,
'm.room_key_request',
{
'action': 'request_cancellation',
'request_id': request.requestId,
'requesting_device_id': client.deviceID,
},
encrypted: false);
}
}
}
class KeyManagerKeyShareRequest {
final String requestId;
final List<DeviceKeys> devices;
final Room room;
final String sessionId;
final String senderKey;
bool canceled;
KeyManagerKeyShareRequest(
{this.requestId,
this.devices,
this.room,
this.sessionId,
this.senderKey,
this.canceled = false});
}
class RoomKeyRequest extends ToDeviceEvent {
KeyManager keyManager;
KeyManagerKeyShareRequest request;
RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent,
KeyManager keyManager, KeyManagerKeyShareRequest request) {
this.keyManager = keyManager;
this.request = request;
sender = toDeviceEvent.sender;
content = toDeviceEvent.content;
type = toDeviceEvent.type;
}
Room get room => request.room;
DeviceKeys get requestingDevice => request.devices.first;
Future<void> forwardKey() async {
if (request.canceled) {
keyManager.incomingShareRequests.remove(request.requestId);
return; // request is canceled, don't send anything
}
var room = this.room;
await room.loadInboundGroupSessionKey(request.sessionId);
final session = room.inboundGroupSessions[request.sessionId];
var forwardedKeys = <dynamic>[keyManager.client.identityKey];
for (final key in session.forwardingCurve25519KeyChain) {
forwardedKeys.add(key);
}
await requestingDevice.setVerified(true, keyManager.client);
var message = session.content;
message['forwarding_curve25519_key_chain'] = forwardedKeys;
message['session_key'] = session.inboundGroupSession
.export_session(session.inboundGroupSession.first_known_index());
// send the actual reply of the key back to the requester
await keyManager.client.sendToDevice(
[requestingDevice],
'm.forwarded_room_key',
message,
);
keyManager.incomingShareRequests.remove(request.requestId);
}
}

View file

@ -17,21 +17,18 @@
*/
import 'dart:async';
import 'dart:convert';
import 'package:famedlysdk/matrix_api.dart';
import 'package:pedantic/pedantic.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/client.dart';
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:famedlysdk/src/utils/session_key.dart';
import 'package:image/image.dart';
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
import 'package:mime_type/mime_type.dart';
import 'package:olm/olm.dart' as olm;
import 'package:html_unescape/html_unescape.dart';
import './user.dart';
@ -81,13 +78,6 @@ class Room {
/// Key-Value store for private account data only visible for this user.
Map<String, BasicRoomEvent> roomAccountData = {};
olm.OutboundGroupSession get outboundGroupSession => _outboundGroupSession;
olm.OutboundGroupSession _outboundGroupSession;
List<String> _outboundGroupSessionDevices;
DateTime _outboundGroupSessionCreationTime;
int _outboundGroupSessionSentMessages;
double _newestSortOrder;
double _oldestSortOrder;
@ -110,168 +100,6 @@ class Room {
_oldestSortOrder, _newestSortOrder, client.id, id);
}
/// Clears the existing outboundGroupSession, tries to create a new one and
/// stores it as an ingoingGroupSession in the [sessionKeys]. Then sends the
/// new session encrypted with olm to all non-blocked devices using
/// to-device-messaging.
Future<void> createOutboundGroupSession() async {
await clearOutboundGroupSession(wipe: true);
var deviceKeys = await getUserDeviceKeys();
olm.OutboundGroupSession outboundGroupSession;
var outboundGroupSessionDevices = <String>[];
for (var keys in deviceKeys) {
if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId);
}
outboundGroupSessionDevices.sort();
try {
outboundGroupSession = olm.OutboundGroupSession();
outboundGroupSession.create();
} catch (e) {
outboundGroupSession = null;
print('[LibOlm] Unable to create new outboundGroupSession: ' +
e.toString());
}
if (outboundGroupSession == null) return;
// Add as an inboundSession to the [sessionKeys].
var rawSession = <String, dynamic>{
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': id,
'session_id': outboundGroupSession.session_id(),
'session_key': outboundGroupSession.session_key(),
};
setInboundGroupSession(rawSession['session_id'], rawSession);
try {
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
_outboundGroupSession = outboundGroupSession;
_outboundGroupSessionDevices = outboundGroupSessionDevices;
_outboundGroupSessionCreationTime = DateTime.now();
_outboundGroupSessionSentMessages = 0;
await _storeOutboundGroupSession();
} catch (e, s) {
print(
'[LibOlm] Unable to send the session key to the participating devices: ' +
e.toString());
print(s);
await clearOutboundGroupSession();
}
return;
}
Future<void> _storeOutboundGroupSession() async {
if (_outboundGroupSession == null) return;
await client.database?.storeOutboundGroupSession(
client.id,
id,
_outboundGroupSession.pickle(client.userID),
json.encode(_outboundGroupSessionDevices),
_outboundGroupSessionCreationTime,
_outboundGroupSessionSentMessages);
return;
}
/// Clears the existing outboundGroupSession but first checks if the participating
/// devices have been changed. Returns false if the session has not been cleared because
/// it wasn't necessary.
Future<bool> clearOutboundGroupSession({bool wipe = false}) async {
if (!wipe && _outboundGroupSessionDevices != null) {
// first check if the devices in the room changed
var deviceKeys = await getUserDeviceKeys();
var outboundGroupSessionDevices = <String>[];
for (var keys in deviceKeys) {
if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId);
}
outboundGroupSessionDevices.sort();
if (outboundGroupSessionDevices.toString() !=
_outboundGroupSessionDevices.toString()) {
wipe = true;
}
// next check if it needs to be rotated
final encryptionContent = getState(EventTypes.Encryption)?.content;
final maxMessages = encryptionContent != null &&
encryptionContent['rotation_period_msgs'] is int
? encryptionContent['rotation_period_msgs']
: 100;
final maxAge = encryptionContent != null &&
encryptionContent['rotation_period_ms'] is int
? encryptionContent['rotation_period_ms']
: 604800000; // default of one week
if (_outboundGroupSessionSentMessages >= maxMessages ||
_outboundGroupSessionCreationTime
.add(Duration(milliseconds: maxAge))
.isBefore(DateTime.now())) {
wipe = true;
}
if (!wipe) {
return false;
}
}
if (!wipe &&
_outboundGroupSessionDevices == null &&
_outboundGroupSession == null) {
return true; // let's just short-circuit out of here, no need to do DB stuff
}
_outboundGroupSessionDevices = null;
await client.database?.removeOutboundGroupSession(client.id, id);
_outboundGroupSession?.free();
_outboundGroupSession = null;
return true;
}
/// Key-Value store of session ids to the session keys. Only m.megolm.v1.aes-sha2
/// session keys are supported. They are stored as a Map with the following keys:
/// {
/// "algorithm": "m.megolm.v1.aes-sha2",
/// "room_id": "!Cuyf34gef24t:localhost",
/// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
/// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
/// }
Map<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions;
final _inboundGroupSessions = <String, SessionKey>{};
/// Add a new session key to the [sessionKeys].
void setInboundGroupSession(String sessionId, Map<String, dynamic> content,
{bool forwarded = false}) {
if (inboundGroupSessions.containsKey(sessionId)) return;
olm.InboundGroupSession inboundGroupSession;
if (content['algorithm'] == 'm.megolm.v1.aes-sha2') {
try {
inboundGroupSession = olm.InboundGroupSession();
if (forwarded) {
inboundGroupSession.import_session(content['session_key']);
} else {
inboundGroupSession.create(content['session_key']);
}
} catch (e) {
inboundGroupSession = null;
print('[LibOlm] Could not create new InboundGroupSession: ' +
e.toString());
}
}
_inboundGroupSessions[sessionId] = SessionKey(
content: content,
inboundGroupSession: inboundGroupSession,
indexes: {},
key: client.userID,
);
client.database?.storeInboundGroupSession(
client.id,
id,
sessionId,
inboundGroupSession.pickle(client.userID),
json.encode(content),
json.encode({}),
);
_tryAgainDecryptLastMessage();
onSessionKeyReceived.add(sessionId);
}
Future<void> _tryAgainDecryptLastMessage() async {
if (getState(EventTypes.Encrypted) != null) {
await getState(EventTypes.Encrypted).decryptAndStore();
}
}
/// 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 = '']) =>
@ -281,23 +109,13 @@ class Room {
/// typeKey/stateKey key pair if there is one.
void setState(Event state) {
// Decrypt if necessary
if (state.type == EventTypes.Encrypted) {
if (state.type == EventTypes.Encrypted && client.encryptionEnabled) {
try {
state = decryptGroupMessage(state);
state = client.encryption.decryptRoomEventSync(id, state);
} catch (e) {
print('[LibOlm] Could not decrypt room state: ' + e.toString());
}
}
// Check if this is a member change and we need to clear the outboundGroupSession.
if (encrypted &&
outboundGroupSession != null &&
state.type == EventTypes.RoomMember) {
var newUser = state.asUser;
var oldUser = getState(EventTypes.RoomMember, newUser.id)?.asUser;
if (oldUser == null || oldUser.membership != newUser.membership) {
clearOutboundGroupSession();
}
}
if ((getState(state.type)?.originServerTs?.millisecondsSinceEpoch ?? 0) >
(state.originServerTs?.millisecondsSinceEpoch ?? 1)) {
return;
@ -883,7 +701,8 @@ class Room {
// Send the text and on success, store and display a *sent* event.
try {
final sendMessageContent = encrypted && client.encryptionEnabled
? await encryptGroupMessagePayload(content, type: type)
? await client.encryption
.encryptGroupMessagePayload(id, content, type: type)
: content;
final res = await client.api.sendMessage(
id,
@ -999,55 +818,42 @@ class Room {
if (onHistoryReceived != null) onHistoryReceived();
prev_batch = resp.end;
final dbActions = <Future<dynamic> Function()>[];
if (client.database != null) {
dbActions.add(
() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
}
final loadFn = () async {
if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return;
if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return;
if (resp.state != null) {
for (final state in resp.state) {
var eventUpdate = EventUpdate(
type: 'state',
roomID: id,
eventType: state.type,
content: state.toJson(),
sortOrder: oldSortOrder,
).decrypt(this);
client.onEvent.add(eventUpdate);
if (client.database != null) {
dbActions.add(
() => client.database.storeEventUpdate(client.id, eventUpdate));
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) {
var eventUpdate = EventUpdate(
type: 'history',
roomID: id,
eventType: hist.type,
content: hist.toJson(),
sortOrder: oldSortOrder,
).decrypt(this);
client.onEvent.add(eventUpdate);
if (client.database != null) {
dbActions.add(
() => client.database.storeEventUpdate(client.id, eventUpdate));
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);
}
}
};
if (client.database != null) {
dbActions
.add(() => client.database.setRoomPrevBatch(resp.end, client.id, id));
await client.database.transaction(() async {
await client.database.setRoomPrevBatch(resp.end, client.id, id);
await loadFn();
await updateSortOrder();
});
} else {
await loadFn();
}
await client.database?.transaction(() async {
for (final f in dbActions) {
await f();
}
await updateSortOrder();
});
client.onRoomUpdate.add(
RoomUpdate(
id: id,
@ -1147,7 +953,6 @@ class Room {
}
for (final rawState in rawStates) {
final newState = Event.fromDb(rawState, newRoom);
;
newRoom.setState(newState);
}
}
@ -1187,13 +992,13 @@ class Room {
}
// Try again to decrypt encrypted events and update the database.
if (encrypted && client.database != null) {
if (encrypted && client.database != null && client.encryptionEnabled) {
await client.database.transaction(() async {
for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted &&
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
await events[i].loadSession();
events[i] = await events[i].decryptAndStore();
events[i] = await client.encryption
.decryptRoomEvent(id, events[i], store: true);
}
}
});
@ -1746,209 +1551,10 @@ class Room {
return deviceKeys;
}
bool _restoredOutboundGroupSession = false;
Future<void> restoreOutboundGroupSession() async {
if (_restoredOutboundGroupSession || client.database == null) {
return;
}
final outboundSession =
await client.database.getDbOutboundGroupSession(client.id, id);
if (outboundSession != null) {
try {
_outboundGroupSession = olm.OutboundGroupSession();
_outboundGroupSession.unpickle(client.userID, outboundSession.pickle);
_outboundGroupSessionDevices =
List<String>.from(json.decode(outboundSession.deviceIds));
_outboundGroupSessionCreationTime = outboundSession.creationTime;
_outboundGroupSessionSentMessages = outboundSession.sentMessages;
} catch (e) {
_outboundGroupSession = null;
_outboundGroupSessionDevices = null;
print('[LibOlm] Unable to unpickle outboundGroupSession: ' +
e.toString());
}
}
_restoredOutboundGroupSession = true;
}
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
/// payload. This will create a new outgoingGroupSession if necessary.
Future<Map<String, dynamic>> encryptGroupMessagePayload(
Map<String, dynamic> payload,
{String type = EventTypes.Message}) async {
if (!encrypted || !client.encryptionEnabled) return payload;
if (encryptionAlgorithm != 'm.megolm.v1.aes-sha2') {
throw ('Unknown encryption algorithm');
}
if (!_restoredOutboundGroupSession && client.database != null) {
// try to restore an outbound group session from the database
await restoreOutboundGroupSession();
}
// and clear the outbound session, if it needs clearing
await clearOutboundGroupSession();
// create a new one if none exists...
if (_outboundGroupSession == null) {
await createOutboundGroupSession();
}
final Map<String, dynamic> mRelatesTo = payload.remove('m.relates_to');
final payloadContent = {
'content': payload,
'type': type,
'room_id': id,
};
var encryptedPayload = <String, dynamic>{
'algorithm': 'm.megolm.v1.aes-sha2',
'ciphertext': _outboundGroupSession.encrypt(json.encode(payloadContent)),
'device_id': client.deviceID,
'sender_key': client.identityKey,
'session_id': _outboundGroupSession.session_id(),
if (mRelatesTo != null) 'm.relates_to': mRelatesTo,
};
_outboundGroupSessionSentMessages++;
await _storeOutboundGroupSession();
return encryptedPayload;
}
final Set<String> _requestedSessionIds = <String>{};
Future<void> requestSessionKey(String sessionId, String senderKey) async {
await client.keyManager.request(this, sessionId, senderKey);
}
Future<void> loadInboundGroupSessionKey(String sessionId,
[String senderKey]) async {
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) {
return;
} // nothing to do
final session = await client.database
.getDbInboundGroupSession(client.id, id, sessionId);
if (session == null) {
// no session found, let's request it!
if (client.enableE2eeRecovery &&
!_requestedSessionIds.contains(sessionId) &&
senderKey != null) {
unawaited(requestSessionKey(sessionId, senderKey));
_requestedSessionIds.add(sessionId);
}
return;
}
try {
_inboundGroupSessions[sessionId] =
SessionKey.fromDb(session, client.userID);
} catch (e) {
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
}
}
Future<void> loadInboundGroupSessionKeyForEvent(Event event) async {
if (client.database == null) return; // nothing to do, no database
if (event.type != EventTypes.Encrypted) return;
if (!client.encryptionEnabled) {
throw (DecryptError.NOT_ENABLED);
return;
}
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
throw (DecryptError.UNKNOWN_ALGORITHM);
}
final String sessionId = event.content['session_id'];
return loadInboundGroupSessionKey(sessionId, event.content['sender_key']);
}
/// Decrypts the given [event] with one of the available ingoingGroupSessions.
/// Returns a m.bad.encrypted event if it fails and does nothing if the event
/// was not encrypted.
Event decryptGroupMessage(Event event) {
if (event.type != EventTypes.Encrypted ||
event.content['ciphertext'] == null) return event;
Map<String, dynamic> decryptedPayload;
try {
if (!client.encryptionEnabled) {
throw (DecryptError.NOT_ENABLED);
}
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
throw (DecryptError.UNKNOWN_ALGORITHM);
}
final String sessionId = event.content['session_id'];
if (!inboundGroupSessions.containsKey(sessionId)) {
throw (DecryptError.UNKNOWN_SESSION);
}
final decryptResult = inboundGroupSessions[sessionId]
.inboundGroupSession
.decrypt(event.content['ciphertext']);
final messageIndexKey = event.eventId +
event.originServerTs.millisecondsSinceEpoch.toString();
if (inboundGroupSessions[sessionId]
.indexes
.containsKey(messageIndexKey) &&
inboundGroupSessions[sessionId].indexes[messageIndexKey] !=
decryptResult.message_index) {
if ((_outboundGroupSession?.session_id() ?? '') == sessionId) {
clearOutboundGroupSession();
}
throw (DecryptError.CHANNEL_CORRUPTED);
}
inboundGroupSessions[sessionId].indexes[messageIndexKey] =
decryptResult.message_index;
// now we persist the udpated indexes into the database.
// the entry should always exist. In the case it doesn't, the following
// line *could* throw an error. As that is a future, though, and we call
// it un-awaited here, nothing happens, which is exactly the result we want
client.database?.updateInboundGroupSessionIndexes(
json.encode(inboundGroupSessions[sessionId].indexes),
client.id,
id,
sessionId);
decryptedPayload = json.decode(decryptResult.plaintext);
} catch (exception) {
// alright, if this was actually by our own outbound group session, we might as well clear it
if (client.enableE2eeRecovery &&
(_outboundGroupSession?.session_id() ?? '') ==
event.content['session_id']) {
clearOutboundGroupSession(wipe: true);
}
if (exception.toString() == DecryptError.UNKNOWN_SESSION) {
decryptedPayload = {
'content': event.content,
'type': EventTypes.Encrypted,
};
decryptedPayload['content']['body'] = exception.toString();
decryptedPayload['content']['msgtype'] = 'm.bad.encrypted';
} else {
decryptedPayload = {
'content': <String, dynamic>{
'msgtype': 'm.bad.encrypted',
'body': exception.toString(),
},
'type': EventTypes.Encrypted,
};
}
}
if (event.content['m.relates_to'] != null) {
decryptedPayload['content']['m.relates_to'] =
event.content['m.relates_to'];
}
return Event(
content: decryptedPayload['content'],
type: decryptedPayload['type'],
senderId: event.senderId,
eventId: event.eventId,
roomId: event.roomId,
room: event.room,
originServerTs: event.originServerTs,
unsigned: event.unsigned,
stateKey: event.stateKey,
prevContent: event.prevContent,
status: event.status,
sortOrder: event.sortOrder,
);
await client.encryption.keyManager.request(this, sessionId, senderKey);
}
}
abstract class DecryptError {
static const String NOT_ENABLED = 'Encryption is not enabled in your client.';
static const String UNKNOWN_ALGORITHM = 'Unknown encryption algorithm.';
static const String UNKNOWN_SESSION =
'The sender has not sent us the session key.';
static const String CHANNEL_CORRUPTED =
'The secure channel with the sender was corrupted.';
}

View file

@ -19,6 +19,7 @@
import 'dart:async';
import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/encryption.dart';
import 'event.dart';
import 'room.dart';
@ -97,12 +98,16 @@ class Timeline {
void _sessionKeyReceived(String sessionId) async {
var decryptAtLeastOneEvent = false;
final decryptFn = () async {
if (!room.client.encryptionEnabled) {
return;
}
for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted &&
events[i].messageType == MessageTypes.BadEncrypted &&
events[i].content['body'] == DecryptError.UNKNOWN_SESSION &&
events[i].content['session_id'] == sessionId) {
events[i] = await events[i].decryptAndStore();
events[i] = await room.client.encryption
.decryptRoomEvent(room.id, events[i], store: true);
if (events[i].type != EventTypes.Encrypted) {
decryptAtLeastOneEvent = true;
}

View file

@ -1,11 +1,11 @@
import 'dart:convert';
import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/encryption.dart';
import '../client.dart';
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
import '../event.dart';
import 'key_verification.dart';
class DeviceKeysList {
String userId;
@ -78,12 +78,6 @@ class DeviceKeys extends MatrixDeviceKeys {
Future<void> setBlocked(bool newBlocked, Client client) {
blocked = newBlocked;
for (var room in client.rooms) {
if (!room.encrypted) continue;
if (room.getParticipants().indexWhere((u) => u.id == userId) != -1) {
room.clearOutboundGroupSession();
}
}
return client.database
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
}
@ -157,10 +151,10 @@ class DeviceKeys extends MatrixDeviceKeys {
}
KeyVerification startVerification(Client client) {
final request =
KeyVerification(client: client, userId: userId, deviceId: deviceId);
final request = KeyVerification(
encryption: client.encryption, userId: userId, deviceId: deviceId);
request.start();
client.addKeyVerificationRequest(request);
client.encryption.keyVerificationManager.addRequest(request);
return request;
}
}

View file

@ -42,13 +42,14 @@ class EventUpdate {
EventUpdate(
{this.eventType, this.roomID, this.type, this.content, this.sortOrder});
EventUpdate decrypt(Room room) {
if (eventType != EventTypes.Encrypted) {
Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
if (eventType != EventTypes.Encrypted || !room.client.encryptionEnabled) {
return this;
}
try {
var decrpytedEvent =
room.decryptGroupMessage(Event.fromJson(content, room, sortOrder));
var decrpytedEvent = await room.client.encryption.decryptRoomEvent(
room.id, Event.fromJson(content, room, sortOrder),
store: store, updateType: type);
return EventUpdate(
eventType: decrpytedEvent.type,
roomID: roomID,

View file

@ -1,59 +0,0 @@
import 'dart:convert';
import 'package:olm/olm.dart';
import '../database/database.dart' show DbInboundGroupSession;
import '../event.dart';
class SessionKey {
Map<String, dynamic> content;
Map<String, int> indexes;
InboundGroupSession inboundGroupSession;
final String key;
List<dynamic> get forwardingCurve25519KeyChain =>
content['forwarding_curve25519_key_chain'] ?? [];
String get senderClaimedEd25519Key =>
content['sender_claimed_ed25519_key'] ?? '';
SessionKey({this.content, this.inboundGroupSession, this.key, this.indexes});
SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key {
final parsedContent = Event.getMapFromPayload(dbEntry.content);
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes);
content =
parsedContent != null ? Map<String, dynamic>.from(parsedContent) : null;
indexes = parsedIndexes != null
? Map<String, int>.from(parsedIndexes)
: <String, int>{};
var newInboundGroupSession = InboundGroupSession();
newInboundGroupSession.unpickle(key, dbEntry.pickle);
inboundGroupSession = newInboundGroupSession;
}
SessionKey.fromJson(Map<String, dynamic> json, String key) : key = key {
content = json['content'] != null
? Map<String, dynamic>.from(json['content'])
: null;
indexes = json['indexes'] != null
? Map<String, int>.from(json['indexes'])
: <String, int>{};
var newInboundGroupSession = InboundGroupSession();
newInboundGroupSession.unpickle(key, json['inboundGroupSession']);
inboundGroupSession = newInboundGroupSession;
}
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
if (content != null) {
data['content'] = content;
}
if (indexes != null) {
data['indexes'] = indexes;
}
data['inboundGroupSession'] = inboundGroupSession.pickle(key);
return data;
}
@override
String toString() => json.encode(toJson());
}

View file

@ -17,7 +17,6 @@
*/
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:famedlysdk/famedlysdk.dart';
@ -39,8 +38,9 @@ void main() {
Future<List<EventUpdate>> eventUpdateListFuture;
Future<List<ToDeviceEvent>> toDeviceUpdateListFuture;
// key @test:fakeServer.notExisting
const pickledOlmAccount =
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuweStA+EKZvvHZO0SnwRp0Hw7sv8UMYvXw';
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
const identityKey = '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk';
const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo';
@ -134,24 +134,6 @@ void main() {
expect(matrix.directChats, matrix.accountData['m.direct'].content);
expect(matrix.presences.length, 1);
expect(matrix.rooms[1].ephemerals.length, 2);
expect(matrix.rooms[1].inboundGroupSessions.length, 1);
expect(
matrix
.rooms[1]
.inboundGroupSessions[
'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
.content['session_key'],
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw');
if (olmEnabled) {
expect(
matrix
.rooms[1]
.inboundGroupSessions[
'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
.inboundGroupSession !=
null,
true);
}
expect(matrix.rooms[1].typingUsers.length, 1);
expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com');
expect(matrix.rooms[1].roomAccountData.length, 3);
@ -177,7 +159,7 @@ void main() {
expect(presenceCounter, 1);
expect(accountDataCounter, 3);
await Future.delayed(Duration(milliseconds: 50));
expect(matrix.userDeviceKeys.length, 3);
expect(matrix.userDeviceKeys.length, 4);
expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false);
expect(matrix.userDeviceKeys['@alice:example.com'].deviceKeys.length, 2);
expect(
@ -196,7 +178,7 @@ void main() {
}
}));
await Future.delayed(Duration(milliseconds: 50));
expect(matrix.userDeviceKeys.length, 2);
expect(matrix.userDeviceKeys.length, 3);
expect(matrix.userDeviceKeys['@alice:example.com'].outdated, true);
await matrix.handleSync(SyncUpdate.fromJson({
@ -335,7 +317,11 @@ void main() {
expect(eventUpdateList.length, 2);
expect(eventUpdateList[0].type, 'm.new_device');
expect(eventUpdateList[1].type, 'm.room_key');
if (olmEnabled) {
expect(eventUpdateList[1].type, 'm.room_key');
} else {
expect(eventUpdateList[1].type, 'm.room.encrypted');
}
});
test('Login', () async {
@ -388,115 +374,6 @@ void main() {
'mxc://example.org/SEsfnsuifSDFSSEF');
expect(aliceProfile.displayname, 'Alice Margatroid');
});
test('signJson', () {
if (matrix.encryptionEnabled) {
expect(matrix.fingerprintKey.isNotEmpty, true);
expect(matrix.identityKey.isNotEmpty, true);
var payload = <String, dynamic>{
'unsigned': {
'foo': 'bar',
},
'auth': {
'success': true,
'mxid': '@john.doe:example.com',
'profile': {
'display_name': 'John Doe',
'three_pids': [
{'medium': 'email', 'address': 'john.doe@example.org'},
{'medium': 'msisdn', 'address': '123456789'}
]
}
}
};
var payloadWithoutUnsigned = Map<String, dynamic>.from(payload);
payloadWithoutUnsigned.remove('unsigned');
expect(
matrix.checkJsonSignature(
matrix.fingerprintKey, payload, matrix.userID, matrix.deviceID),
false);
expect(
matrix.checkJsonSignature(matrix.fingerprintKey,
payloadWithoutUnsigned, matrix.userID, matrix.deviceID),
false);
payload = matrix.signJson(payload);
payloadWithoutUnsigned = matrix.signJson(payloadWithoutUnsigned);
expect(payload['signatures'], payloadWithoutUnsigned['signatures']);
print(payload['signatures']);
expect(
matrix.checkJsonSignature(
matrix.fingerprintKey, payload, matrix.userID, matrix.deviceID),
true);
expect(
matrix.checkJsonSignature(matrix.fingerprintKey,
payloadWithoutUnsigned, matrix.userID, matrix.deviceID),
true);
}
});
test('Track oneTimeKeys', () async {
if (matrix.encryptionEnabled) {
var last = matrix.lastTimeKeysUploaded ?? DateTime.now();
await matrix.handleSync(SyncUpdate.fromJson({
'device_one_time_keys_count': {'signed_curve25519': 49}
}));
await Future.delayed(Duration(milliseconds: 50));
expect(
matrix.lastTimeKeysUploaded.millisecondsSinceEpoch >
last.millisecondsSinceEpoch,
true);
}
});
test('Test invalidate outboundGroupSessions', () async {
if (matrix.encryptionEnabled) {
expect(matrix.rooms[1].outboundGroupSession == null, true);
await matrix.rooms[1].createOutboundGroupSession();
expect(matrix.rooms[1].outboundGroupSession != null, true);
await matrix.handleSync(SyncUpdate.fromJson({
'device_lists': {
'changed': [
'@alice:example.com',
],
'left': [
'@bob:example.com',
],
}
}));
await Future.delayed(Duration(milliseconds: 50));
expect(matrix.rooms[1].outboundGroupSession != null, true);
}
});
test('Test invalidate outboundGroupSessions', () async {
if (matrix.encryptionEnabled) {
await matrix.rooms[1].clearOutboundGroupSession(wipe: true);
expect(matrix.rooms[1].outboundGroupSession == null, true);
await matrix.rooms[1].createOutboundGroupSession();
expect(matrix.rooms[1].outboundGroupSession != null, true);
await matrix.handleSync(SyncUpdate.fromJson({
'rooms': {
'join': {
'!726s6s6q:example.com': {
'state': {
'events': [
{
'content': {'membership': 'leave'},
'event_id': '143273582443PhrSn:example.org',
'origin_server_ts': 1432735824653,
'room_id': '!726s6s6q:example.com',
'sender': '@alice:example.com',
'state_key': '@alice:example.com',
'type': 'm.room.member'
}
]
}
}
}
}
}));
await Future.delayed(Duration(milliseconds: 50));
expect(matrix.rooms[1].outboundGroupSession != null, true);
}
});
var deviceKeys = DeviceKeys.fromJson({
'user_id': '@alice:example.com',
'device_id': 'JLAFKJWSCS',
@ -512,16 +389,6 @@ void main() {
}
}
});
test('startOutgoingOlmSessions', () async {
expect(matrix.olmSessions.length, 0);
if (olmEnabled) {
await matrix
.startOutgoingOlmSessions([deviceKeys], checkSignature: false);
expect(matrix.olmSessions.length, 1);
expect(matrix.olmSessions.entries.first.key,
'3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI');
}
});
test('sendToDevice', () async {
await matrix.sendToDevice(
[deviceKeys],
@ -547,13 +414,6 @@ void main() {
await Future.delayed(Duration(milliseconds: 50));
String sessionKey;
if (client1.encryptionEnabled) {
await client1.rooms[1].createOutboundGroupSession();
sessionKey = client1.rooms[1].outboundGroupSession.session_key();
}
expect(client1.isLogged(), true);
expect(client1.rooms.length, 2);
@ -571,12 +431,9 @@ void main() {
expect(client2.deviceID, client1.deviceID);
expect(client2.deviceName, client1.deviceName);
if (client2.encryptionEnabled) {
await client2.rooms[1].restoreOutboundGroupSession();
expect(client2.pickledOlmAccount, client1.pickledOlmAccount);
expect(json.encode(client2.rooms[1].inboundGroupSessions[sessionKey]),
json.encode(client1.rooms[1].inboundGroupSessions[sessionKey]));
expect(client2.encryption.pickledOlmAccount,
client1.encryption.pickledOlmAccount);
expect(client2.rooms[1].id, client1.rooms[1].id);
expect(client2.rooms[1].outboundGroupSession.session_key(), sessionKey);
}
await client1.logout();

View file

@ -0,0 +1,99 @@
/*
* 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('Encrypt/Decrypt room message', () {
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';
Room room;
Map<String, dynamic> payload;
final now = DateTime.now();
test('setupClient', () async {
client = await getClient();
room = client.getRoomById(roomId);
});
test('encrypt payload', () async {
payload = await client.encryption.encryptGroupMessagePayload(roomId, {
'msgtype': 'm.text',
'text': 'Hello foxies!',
});
expect(payload['algorithm'], 'm.megolm.v1.aes-sha2');
expect(payload['ciphertext'] is String, true);
expect(payload['device_id'], client.deviceID);
expect(payload['sender_key'], client.identityKey);
expect(payload['session_id'] is String, true);
});
test('decrypt payload', () async {
final encryptedEvent = Event(
type: EventTypes.Encrypted,
content: payload,
roomId: roomId,
room: room,
originServerTs: now,
eventId: '\$event',
);
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!');
});
test('decrypt payload nocache', () async {
client.encryption.keyManager.clearInboundGroupSessions();
final encryptedEvent = Event(
type: EventTypes.Encrypted,
content: payload,
roomId: roomId,
room: room,
originServerTs: now,
eventId: '\$event',
);
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!');
});
test('dispose client', () async {
await client.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,120 @@
/*
* 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';
import '../fake_matrix_api.dart';
void main() {
// key @othertest:fakeServer.notExisting
const otherPickledOlmAccount =
'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA';
group('Encrypt/Decrypt to-device messages', () {
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;
var otherClient =
Client('othertestclient', debug: true, httpClient: FakeMatrixApi());
DeviceKeys device;
Map<String, dynamic> payload;
test('setupClient', () async {
client = await getClient();
otherClient.database = client.database;
await otherClient.checkServer('https://fakeServer.notExisting');
otherClient.connect(
newToken: 'abc',
newUserID: '@othertest:fakeServer.notExisting',
newHomeserver: otherClient.api.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: 'FOXDEVICE',
newOlmAccount: otherPickledOlmAccount,
);
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: {
'curve25519:${client.deviceID}': client.identityKey,
'ed25519:${client.deviceID}': client.fingerprintKey,
},
verified: true,
blocked: false,
);
});
test('encryptToDeviceMessage', () async {
payload = await otherClient.encryption
.encryptToDeviceMessage([device], 'm.to_device', {'hello': 'foxies'});
});
test('encryptToDeviceMessagePayload', () async {
// just a hard test if nothing errors
await otherClient.encryption.encryptToDeviceMessagePayload(
device, 'm.to_device', {'hello': 'foxies'});
});
test('decryptToDeviceEvent', () async {
final encryptedEvent = ToDeviceEvent(
sender: '@othertest:fakeServer.notExisting',
type: EventTypes.Encrypted,
content: payload[client.userID][client.deviceID],
);
final decryptedEvent =
await client.encryption.decryptToDeviceEvent(encryptedEvent);
expect(decryptedEvent.type, 'm.to_device');
expect(decryptedEvent.content['hello'], 'foxies');
});
test('decryptToDeviceEvent nocache', () async {
client.encryption.olmManager.olmSessions.clear();
payload = await otherClient.encryption.encryptToDeviceMessage(
[device], 'm.to_device', {'hello': 'superfoxies'});
final encryptedEvent = ToDeviceEvent(
sender: '@othertest:fakeServer.notExisting',
type: EventTypes.Encrypted,
content: payload[client.userID][client.deviceID],
);
final decryptedEvent =
await client.encryption.decryptToDeviceEvent(encryptedEvent);
expect(decryptedEvent.type, 'm.to_device');
expect(decryptedEvent.content['hello'], 'superfoxies');
});
test('dispose client', () async {
await client.dispose(closeDatabase: true);
await otherClient.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,223 @@
/*
* 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('Key Manager', () {
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('handle new m.room_key', () async {
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
final sessionKey =
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw';
client.encryption.keyManager.clearInboundGroupSessions();
var event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
},
encryptedContent: {
'sender_key': validSessionId,
});
await client.encryption.keyManager.handleToDeviceEvent(event);
expect(
client.encryption.keyManager.getInboundGroupSession(
'!726s6s6q:example.com', validSessionId, validSenderKey) !=
null,
true);
// now test a few invalid scenarios
// not encrypted
client.encryption.keyManager.clearInboundGroupSessions();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
});
await client.encryption.keyManager.handleToDeviceEvent(event);
expect(
client.encryption.keyManager.getInboundGroupSession(
'!726s6s6q:example.com', validSessionId, validSenderKey) !=
null,
false);
});
test('outbound group session', () async {
final roomId = '!726s6s6q:example.com';
expect(
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
false);
var sess =
await client.encryption.keyManager.createOutboundGroupSession(roomId);
expect(
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
true);
await client.encryption.keyManager.clearOutboundGroupSession(roomId);
expect(
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
true);
expect(
client.encryption.keyManager.getInboundGroupSession(roomId,
sess.outboundGroupSession.session_id(), client.identityKey) !=
null,
true);
// rotate after too many messages
sess.sentMessages = 300;
await client.encryption.keyManager.clearOutboundGroupSession(roomId);
expect(
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
false);
// rotate if devices in room change
sess =
await client.encryption.keyManager.createOutboundGroupSession(roomId);
client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS']
.blocked = true;
await client.encryption.keyManager.clearOutboundGroupSession(roomId);
expect(
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
false);
client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS']
.blocked = false;
// rotate if too far in the past
sess =
await client.encryption.keyManager.createOutboundGroupSession(roomId);
sess.creationTime = DateTime.now().subtract(Duration(days: 30));
await client.encryption.keyManager.clearOutboundGroupSession(roomId);
expect(
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
false);
// force wipe
sess =
await client.encryption.keyManager.createOutboundGroupSession(roomId);
await client.encryption.keyManager
.clearOutboundGroupSession(roomId, wipe: true);
expect(
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
false);
// load from database
sess =
await client.encryption.keyManager.createOutboundGroupSession(roomId);
client.encryption.keyManager.clearOutboundGroupSessions();
expect(
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
false);
await client.encryption.keyManager.loadOutboundGroupSession(roomId);
expect(
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
true);
});
test('inbound group session', () async {
final roomId = '!726s6s6q:example.com';
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
final sessionContent = <String, dynamic>{
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU',
'session_key':
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'
};
client.encryption.keyManager.clearInboundGroupSessions();
expect(
client.encryption.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
false);
client.encryption.keyManager
.setInboundGroupSession(roomId, sessionId, senderKey, sessionContent);
await Future.delayed(Duration(milliseconds: 10));
expect(
client.encryption.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
true);
expect(
client.encryption.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
true);
expect(
client.encryption.keyManager
.getInboundGroupSession('otherroom', sessionId, senderKey) !=
null,
true);
expect(
client.encryption.keyManager
.getInboundGroupSession('otherroom', 'invalid', senderKey) !=
null,
false);
client.encryption.keyManager.clearInboundGroupSessions();
expect(
client.encryption.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
false);
await client.encryption.keyManager
.loadInboundGroupSession(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,331 @@
/*
* 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';
Map<String, dynamic> jsonDecode(dynamic payload) {
if (payload is String) {
try {
return json.decode(payload);
} catch (e) {
return {};
}
}
if (payload is Map<String, dynamic>) return payload;
return {};
}
void main() {
/// All Tests related to device keys
group('Key Request', () {
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;
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
final validSenderKey = '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI';
test('Create Request', () async {
var matrix = await getClient();
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
await matrix.encryption.keyManager
.request(requestRoom, 'sessionId', validSenderKey);
var foundEvent = false;
for (var entry in FakeMatrixApi.calledEndpoints.entries) {
final payload = jsonDecode(entry.value.first);
if (entry.key
.startsWith('/client/r0/sendToDevice/m.room_key_request') &&
(payload['messages'] is Map) &&
(payload['messages']['@alice:example.com'] is Map) &&
(payload['messages']['@alice:example.com']['*'] is Map)) {
final content = payload['messages']['@alice:example.com']['*'];
if (content['action'] == 'request' &&
content['body']['room_id'] == '!726s6s6q:example.com' &&
content['body']['sender_key'] == validSenderKey &&
content['body']['session_id'] == 'sessionId') {
foundEvent = true;
break;
}
}
}
expect(foundEvent, true);
await matrix.dispose(closeDatabase: true);
});
test('Reply To Request', () async {
var matrix = await getClient();
matrix.setUserId('@alice:example.com'); // we need to pretend to be alice
FakeMatrixApi.calledEndpoints.clear();
await matrix
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
.setBlocked(false, matrix);
await matrix
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
.setVerified(true, matrix);
// test a successful share
var event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_1',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
print(FakeMatrixApi.calledEndpoints.keys.toString());
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
true);
// test various fail scenarios
// no body
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'request_id': 'request_2',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// request by ourself
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_3',
'requesting_device_id': 'JLAFKJWSCS',
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// device not found
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_4',
'requesting_device_id': 'blubb',
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// unknown room
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!invalid:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_5',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// unknwon session
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': 'invalid',
},
'request_id': 'request_6',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
FakeMatrixApi.calledEndpoints.clear();
await matrix.dispose(closeDatabase: true);
});
test('Receive shared keys', () async {
var matrix = await getClient();
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
await matrix.encryption.keyManager
.request(requestRoom, validSessionId, validSenderKey);
final session = await matrix.encryption.keyManager
.loadInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey);
final sessionKey = session.inboundGroupSession
.export_session(session.inboundGroupSession.first_known_index());
matrix.encryption.keyManager.clearInboundGroupSessions();
var event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': validSenderKey,
'forwarding_curve25519_key_chain': [],
},
encryptedContent: {
'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
expect(
matrix.encryption.keyManager.getInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey) !=
null,
true);
// now test a few invalid scenarios
// request not found
matrix.encryption.keyManager.clearInboundGroupSessions();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': validSenderKey,
'forwarding_curve25519_key_chain': [],
},
encryptedContent: {
'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
expect(
matrix.encryption.keyManager.getInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey) !=
null,
false);
// unknown device
await matrix.encryption.keyManager
.request(requestRoom, validSessionId, validSenderKey);
matrix.encryption.keyManager.clearInboundGroupSessions();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': validSenderKey,
'forwarding_curve25519_key_chain': [],
},
encryptedContent: {
'sender_key': 'invalid',
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
expect(
matrix.encryption.keyManager.getInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey) !=
null,
false);
// no encrypted content
await matrix.encryption.keyManager
.request(requestRoom, validSessionId, validSenderKey);
matrix.encryption.keyManager.clearInboundGroupSessions();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': validSenderKey,
'forwarding_curve25519_key_chain': [],
});
await matrix.encryption.keyManager.handleToDeviceEvent(event);
expect(
matrix.encryption.keyManager.getInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey) !=
null,
false);
await matrix.dispose(closeDatabase: true);
});
});
}

View file

@ -1,6 +1,6 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Copyright (C) 2019, 2020 Famedly GmbH
* 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
@ -17,10 +17,11 @@
*/
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import 'fake_matrix_api.dart';
import '../fake_client.dart';
void main() {
/// All Tests related to the ChatTime
@ -35,20 +36,25 @@ void main() {
}
print('[LibOlm] Enabled: $olmEnabled');
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
client.api.homeserver = Uri.parse('https://fakeserver.notexisting');
var room = Room(id: '!localpart:server.abc', client: client);
var updateCounter = 0;
final keyVerification = KeyVerification(
client: client,
room: room,
userId: '@alice:example.com',
deviceId: 'ABCD',
onUpdate: () => updateCounter++,
);
if (!olmEnabled) return;
Client client;
Room room;
var updateCounter = 0;
KeyVerification keyVerification;
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++,
);
});
test('acceptSas', () async {
await keyVerification.acceptSas();
});
@ -91,7 +97,11 @@ void main() {
test('verifyActivity', () async {
final verified = await keyVerification.verifyActivity();
expect(verified, true);
keyVerification?.dispose();
});
test('dispose client', () async {
await client.dispose(closeDatabase: true);
});
keyVerification.dispose();
});
}

View file

@ -0,0 +1,116 @@
/*
* 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('Olm Manager', () {
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('signatures', () async {
final payload = <String, dynamic>{
'fox': 'floof',
};
final signedPayload = client.encryption.olmManager.signJson(payload);
expect(
client.encryption.olmManager.checkJsonSignature(client.fingerprintKey,
signedPayload, client.userID, client.deviceID),
true);
expect(
client.encryption.olmManager.checkJsonSignature(
client.fingerprintKey, payload, client.userID, client.deviceID),
false);
});
test('uploadKeys', () async {
FakeMatrixApi.calledEndpoints.clear();
final res =
await client.encryption.olmManager.uploadKeys(uploadDeviceKeys: true);
expect(res, true);
var sent = json.decode(
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first);
expect(sent['device_keys'] != null, true);
expect(sent['one_time_keys'] != null, true);
expect(sent['one_time_keys'].keys.length, 66);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption.olmManager.uploadKeys();
sent = json.decode(
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first);
expect(sent['device_keys'] != null, false);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption.olmManager.uploadKeys(oldKeyCount: 20);
sent = json.decode(
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first);
expect(sent['one_time_keys'].keys.length, 46);
});
test('handleDeviceOneTimeKeysCount', () async {
FakeMatrixApi.calledEndpoints.clear();
client.encryption.olmManager
.handleDeviceOneTimeKeysCount({'signed_curve25519': 20});
await Future.delayed(Duration(milliseconds: 50));
expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
true);
FakeMatrixApi.calledEndpoints.clear();
client.encryption.olmManager
.handleDeviceOneTimeKeysCount({'signed_curve25519': 70});
await Future.delayed(Duration(milliseconds: 50));
expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
false);
});
test('startOutgoingOlmSessions', () async {
// start an olm session.....with ourself!
await client.encryption.olmManager.startOutgoingOlmSessions(
[client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]]);
expect(
client.encryption.olmManager.olmSessions
.containsKey(client.identityKey),
true);
});
test('dispose client', () async {
await client.dispose(closeDatabase: true);
});
});
}

View file

@ -20,6 +20,7 @@ import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/src/event.dart';
import 'package:test/test.dart';
@ -219,7 +220,7 @@ void main() {
event.status = -1;
final resp2 = await event.sendAgain(txid: '1234');
expect(resp1, null);
expect(resp2, '42');
expect(resp2.startsWith('\$event'), true);
await matrix.dispose(closeDatabase: true);
});

48
test/fake_client.dart Normal file
View file

@ -0,0 +1,48 @@
/*
* 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 'fake_matrix_api.dart';
import 'fake_database.dart';
// key @test:fakeServer.notExisting
const pickledOlmAccount =
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
Future<Client> getClient() async {
final client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
client.database = getDatabase();
await client.checkServer('https://fakeServer.notExisting');
final resp = await client.api.login(
type: 'm.login.password',
user: 'test',
password: '1234',
initialDeviceDisplayName: 'Fluffy Matrix Client',
);
client.connect(
newToken: resp.accessToken,
newUserID: resp.userId,
newHomeserver: client.api.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: resp.deviceId,
newOlmAccount: pickledOlmAccount,
);
await Future.delayed(Duration(milliseconds: 10));
return client;
}

View file

@ -25,6 +25,7 @@ import 'package:http/testing.dart';
class FakeMatrixApi extends MockClient {
static final calledEndpoints = <String, List<dynamic>>{};
static int eventCounter = 0;
FakeMatrixApi()
: super((request) async {
@ -527,16 +528,32 @@ class FakeMatrixApi extends MockClient {
'rooms': ['!726s6s6q:example.com']
}
},
// {
// 'sender': '@othertest:fakeServer.notExisting',
// 'content': {
// 'algorithm': 'm.megolm.v1.aes-sha2',
// 'room_id': '!726s6s6q:example.com',
// 'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU',
// 'session_key':
// 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'
// },
// 'type': 'm.room_key'
// },
{
'sender': '@alice:example.com',
// this is the commented out m.room_key event - only encrypted
'sender': '@othertest:fakeServer.notExisting',
'content': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU',
'session_key':
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'
'algorithm': 'm.olm.v1.curve25519-aes-sha2',
'sender_key': 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg',
'ciphertext': {
'7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk': {
'type': 0,
'body':
'Awogyh7K4iLUQjcOxIfi7q7LhBBqv9w0mQ6JI9+U9tv7iF4SIHC6xb5YFWf9voRnmDBbd+0vxD/xDlVNRDlPIKliLGkYGiAkEbtlo+fng4ELtO4gSLKVbcFn7tZwZCEUE8H2miBsCCKABgMKIFrKDJwB7gM3lXPt9yVoh6gQksafKt7VFCNRN5KLKqsDEAAi0AX5EfTV7jJ1ZWAbxftjoSN6kCVIxzGclbyg1HjchmNCX7nxNCHWl+q5ZgqHYZVu2n2mCVmIaKD0kvoEZeY3tV1Itb6zf67BLaU0qgW/QzHCHg5a44tNLjucvL2mumHjIG8k0BY2uh+52HeiMCvSOvtDwHg7nzCASGdqPVCj9Kzw6z7F6nL4e3mYim8zvJd7f+mD9z3ARrypUOLGkTGYbB2PQOovf0Do8WzcaRzfaUCnuu/YVZWKK7DPgG8uhw/TjR6XtraAKZysF+4DJYMG9SQWx558r6s7Z5EUOF5CU2M35w1t1Xxllb3vrS83dtf9LPCrBhLsEBeYEUBE2+bTBfl0BDKqLiB0Cc0N0ixOcHIt6e40wAvW622/gMgHlpNSx8xG12u0s6h6EMWdCXXLWd9fy2q6glFUHvA67A35q7O+M8DVml7Y9xG55Y3DHkMDc9cwgwFkBDCAYQe6pQF1nlKytcVCGREpBs/gq69gHAStMQ8WEg38Lf8u8eBr2DFexrN4U+QAk+S//P3fJgf0bQx/Eosx4fvWSz9En41iC+ADCsWQpMbwHn4JWvtAbn3oW0XmL/OgThTkJMLiCymduYAa1Hnt7a3tP0KTL2/x11F02ggQHL28cCjq5W4zUGjWjl5wo2PsKB6t8aAvMg2ujGD2rCjb4yrv5VIzAKMOZLyj7K0vSK9gwDLQ/4vq+QnKUBG5zrcOze0hX+kz2909/tmAdeCH61Ypw7gbPUJAKnmKYUiB/UgwkJvzMJSsk/SEs5SXosHDI+HsJHJp4Mp4iKD0xRMst+8f9aTjaWwh8ZvELE1ZOhhCbF3RXhxi3x2Nu8ORIz+vhEQ1NOlMc7UIo98Fk/96T36vL/fviowT4C/0AlaapZDJBmKwhmwqisMjY2n1vY29oM2p5BzY1iwP7q9BYdRFst6xwo57TNSuRwQw7IhFsf0k+ABuPEZy5xB5nPHyIRTf/pr3Hw',
},
},
},
'type': 'm.room_key'
'type': 'm.room.encrypted',
},
]
},
@ -1567,7 +1584,20 @@ class FakeMatrixApi extends MockClient {
}
}
}
}
},
'@test:fakeServer.notExisting': {
'GHTYAJCE': {
'signed_curve25519:AAAAAQ': {
'key': 'qc72ve94cA28iuE0fXa98QO3uls39DHWdQlYyvvhGh0',
'signatures': {
'@test:fakeServer.notExisting': {
'ed25519:GHTYAJCE':
'dFwffr5kTKefO7sjnWLMhTzw7oV31nkPIDRxFy5OQT2OP5++Ao0KRbaBZ6qfuT7lW1owKK0Xk3s7QTBvc/eNDA',
},
},
},
},
},
}
},
'/client/r0/rooms/!localpart%3Aexample.com/invite': (var req) => {},
@ -1584,7 +1614,8 @@ class FakeMatrixApi extends MockClient {
'/client/r0/keys/upload': (var req) => {
'one_time_key_counts': {
'curve25519': 10,
'signed_curve25519': 100,
'signed_curve25519':
json.decode(req)['one_time_keys']?.keys?.length ?? 0,
}
},
'/client/r0/keys/query': (var req) => {
@ -1625,8 +1656,42 @@ class FakeMatrixApi extends MockClient {
},
'signatures': {},
},
}
}
},
'@test:fakeServer.notExisting': {
'GHTYAJCE': {
'user_id': '@test:fakeServer.notExisting',
'device_id': 'GHTYAJCE',
'algorithms': [
'm.olm.v1.curve25519-aes-sha2',
'm.megolm.v1.aes-sha2'
],
'keys': {
'curve25519:GHTYAJCE':
'7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk',
'ed25519:GHTYAJCE':
'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo'
},
'signatures': {},
},
},
'@othertest:fakeServer.notExisting': {
'FOXDEVICE': {
'user_id': '@othertest:fakeServer.notExisting',
'device_id': 'FOXDEVICE',
'algorithms': [
'm.olm.v1.curve25519-aes-sha2',
'm.megolm.v1.aes-sha2'
],
'keys': {
'curve25519:FOXDEVICE':
'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg',
'ed25519:FOXDEVICE':
'R5/p04tticvdlNIxiiBIP0j9OQWv8ep6eEU6/lWKDxw',
},
'signatures': {},
},
},
},
},
'/client/r0/register': (var req) => {
'user_id': '@testuser:example.com',
@ -1706,13 +1771,13 @@ class FakeMatrixApi extends MockClient {
'/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/testtxid':
(var reqI) => {
'event_id': '42',
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
},
'/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com':
(var req) => {},
'/client/r0/rooms/%211234%3Aexample.com/send/m.room.message/1234':
(var reqI) => {
'event_id': '42',
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
},
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag':
(var req) => {},

View file

@ -1,367 +0,0 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Copyright (C) 2019, 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 'fake_matrix_api.dart';
import 'fake_database.dart';
Map<String, dynamic> jsonDecode(dynamic payload) {
if (payload is String) {
try {
return json.decode(payload);
} catch (e) {
return {};
}
}
if (payload is Map<String, dynamic>) return payload;
return {};
}
void main() {
/// All Tests related to device keys
test('fromJson', () async {
var rawJson = <String, dynamic>{
'content': {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'sender_key': 'RF3s+E7RkTQTGF2d8Deol0FkQvgII2aJDf3/Jp5mxVU',
'session_id': 'X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ'
},
'request_id': '1495474790150.19',
'requesting_device_id': 'JLAFKJWSCS'
},
'type': 'm.room_key_request',
'sender': '@alice:example.com'
};
var toDeviceEvent = ToDeviceEvent.fromJson(rawJson);
expect(toDeviceEvent.content, rawJson['content']);
expect(toDeviceEvent.sender, rawJson['sender']);
expect(toDeviceEvent.type, rawJson['type']);
expect(
ToDeviceEventDecryptionError(
exception: Exception('test'),
stackTrace: null,
toDeviceEvent: toDeviceEvent)
.sender,
rawJson['sender'],
);
var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
matrix.database = getDatabase();
await matrix.checkServer('https://fakeServer.notExisting');
await matrix.login('test', '1234');
var room = matrix.getRoomById('!726s6s6q:example.com');
if (matrix.encryptionEnabled) {
await room.createOutboundGroupSession();
rawJson['content']['body']['session_id'] =
room.inboundGroupSessions.keys.first;
var roomKeyRequest = RoomKeyRequest.fromToDeviceEvent(
ToDeviceEvent.fromJson(rawJson),
matrix.keyManager,
KeyManagerKeyShareRequest(
room: room,
sessionId: rawJson['content']['body']['session_id'],
senderKey: rawJson['content']['body']['sender_key'],
devices: [
matrix.userDeviceKeys[rawJson['sender']]
.deviceKeys[rawJson['content']['requesting_device_id']]
],
));
await roomKeyRequest.forwardKey();
}
await matrix.dispose(closeDatabase: true);
});
test('Create Request', () async {
var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
matrix.database = getDatabase();
await matrix.checkServer('https://fakeServer.notExisting');
await matrix.login('test', '1234');
if (!matrix.encryptionEnabled) {
await matrix.dispose(closeDatabase: true);
return;
}
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
await matrix.keyManager.request(requestRoom, 'sessionId', 'senderKey');
var foundEvent = false;
for (var entry in FakeMatrixApi.calledEndpoints.entries) {
final payload = jsonDecode(entry.value.first);
if (entry.key.startsWith('/client/r0/sendToDevice/m.room_key_request') &&
(payload['messages'] is Map) &&
(payload['messages']['@alice:example.com'] is Map) &&
(payload['messages']['@alice:example.com']['*'] is Map)) {
final content = payload['messages']['@alice:example.com']['*'];
if (content['action'] == 'request' &&
content['body']['room_id'] == '!726s6s6q:example.com' &&
content['body']['sender_key'] == 'senderKey' &&
content['body']['session_id'] == 'sessionId') {
foundEvent = true;
break;
}
}
}
expect(foundEvent, true);
await matrix.dispose(closeDatabase: true);
});
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
test('Reply To Request', () async {
var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
matrix.database = getDatabase();
await matrix.checkServer('https://fakeServer.notExisting');
await matrix.login('test', '1234');
if (!matrix.encryptionEnabled) {
await matrix.dispose(closeDatabase: true);
return;
}
matrix.setUserId('@alice:example.com'); // we need to pretend to be alice
FakeMatrixApi.calledEndpoints.clear();
await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
.setBlocked(false, matrix);
await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
.setVerified(true, matrix);
await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
.startVerification(matrix);
// test a successful share
var event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'sender_key': 'senderKey',
'session_id': validSessionId,
},
'request_id': 'request_1',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
true);
// test various fail scenarios
// no body
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'request_id': 'request_2',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// request by ourself
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'sender_key': 'senderKey',
'session_id': validSessionId,
},
'request_id': 'request_3',
'requesting_device_id': 'JLAFKJWSCS',
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// device not found
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'sender_key': 'senderKey',
'session_id': validSessionId,
},
'request_id': 'request_4',
'requesting_device_id': 'blubb',
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// unknown room
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!invalid:example.com',
'sender_key': 'senderKey',
'session_id': validSessionId,
},
'request_id': 'request_5',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// unknwon session
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'sender_key': 'senderKey',
'session_id': 'invalid',
},
'request_id': 'request_6',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
FakeMatrixApi.calledEndpoints.clear();
await matrix.dispose(closeDatabase: true);
});
test('Receive shared keys', () async {
var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
matrix.database = getDatabase();
await matrix.checkServer('https://fakeServer.notExisting');
await matrix.login('test', '1234');
if (!matrix.encryptionEnabled) {
await matrix.dispose(closeDatabase: true);
return;
}
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
await matrix.keyManager.request(requestRoom, validSessionId, 'senderKey');
final session = requestRoom.inboundGroupSessions[validSessionId];
final sessionKey = session.inboundGroupSession
.export_session(session.inboundGroupSession.first_known_index());
requestRoom.inboundGroupSessions.clear();
var event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': 'senderKey',
'forwarding_curve25519_key_chain': [],
},
encryptedContent: {
'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), true);
// now test a few invalid scenarios
// request not found
requestRoom.inboundGroupSessions.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': 'senderKey',
'forwarding_curve25519_key_chain': [],
},
encryptedContent: {
'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), false);
// unknown device
await matrix.keyManager.request(requestRoom, validSessionId, 'senderKey');
requestRoom.inboundGroupSessions.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': 'senderKey',
'forwarding_curve25519_key_chain': [],
},
encryptedContent: {
'sender_key': 'invalid',
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), false);
// no encrypted content
await matrix.keyManager.request(requestRoom, validSessionId, 'senderKey');
requestRoom.inboundGroupSessions.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': 'senderKey',
'forwarding_curve25519_key_chain': [],
});
await matrix.keyManager.handleToDeviceEvent(event);
expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), false);
await matrix.dispose(closeDatabase: true);
});
}

View file

@ -26,7 +26,7 @@ import 'package:famedlysdk/src/database/database.dart'
show DbRoom, DbRoomState, DbRoomAccountData;
import 'package:test/test.dart';
import 'fake_matrix_api.dart';
import 'fake_client.dart';
import 'dart:typed_data';
@ -37,15 +37,7 @@ void main() {
/// All Tests related to the Event
group('Room', () {
test('Login', () async {
matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
final checkResp =
await matrix.checkServer('https://fakeServer.notExisting');
final loginResp = await matrix.login('test', '1234');
expect(checkResp, true);
expect(loginResp, true);
matrix = await getClient();
});
test('Create from json', () async {
@ -315,7 +307,7 @@ void main() {
test('getTimeline', () async {
final timeline = await room.getTimeline();
expect(timeline.events, []);
expect(timeline.events.length, 0);
});
test('getUserByMXID', () async {
@ -338,13 +330,13 @@ void main() {
final dynamic resp = await room.sendEvent(
{'msgtype': 'm.text', 'body': 'hello world'},
txid: 'testtxid');
expect(resp, '42');
expect(resp.startsWith('\$event'), true);
});
test('sendEvent', () async {
final dynamic resp =
await room.sendTextEvent('Hello world', txid: 'testtxid');
expect(resp, '42');
expect(resp.startsWith('\$event'), true);
});
// Not working because there is no real file to test it...
@ -388,60 +380,6 @@ void main() {
);
expect(room.encrypted, true);
expect(room.encryptionAlgorithm, 'm.megolm.v1.aes-sha2');
expect(room.outboundGroupSession, null);
});
test('createOutboundGroupSession', () async {
if (!room.client.encryptionEnabled) return;
await room.createOutboundGroupSession();
expect(room.outboundGroupSession != null, true);
expect(room.outboundGroupSession.session_id().isNotEmpty, true);
expect(
room.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()),
true);
expect(
room.inboundGroupSessions[room.outboundGroupSession.session_id()]
.content['session_key'],
room.outboundGroupSession.session_key());
expect(
room.inboundGroupSessions[room.outboundGroupSession.session_id()]
.indexes.length,
0);
});
test('clearOutboundGroupSession', () async {
if (!room.client.encryptionEnabled) return;
await room.clearOutboundGroupSession(wipe: true);
expect(room.outboundGroupSession == null, true);
});
test('encryptGroupMessagePayload and decryptGroupMessage', () async {
if (!room.client.encryptionEnabled) return;
final payload = {
'msgtype': 'm.text',
'body': 'Hello world',
};
final encryptedPayload = await room.encryptGroupMessagePayload(payload);
expect(encryptedPayload['algorithm'], 'm.megolm.v1.aes-sha2');
expect(encryptedPayload['ciphertext'].isNotEmpty, true);
expect(encryptedPayload['device_id'], room.client.deviceID);
expect(encryptedPayload['sender_key'], room.client.identityKey);
expect(encryptedPayload['session_id'],
room.outboundGroupSession.session_id());
var encryptedEvent = Event(
content: encryptedPayload,
type: 'm.room.encrypted',
senderId: room.client.userID,
eventId: '1234',
roomId: room.id,
room: room,
originServerTs: DateTime.now(),
);
var decryptedEvent = room.decryptGroupMessage(encryptedEvent);
expect(decryptedEvent.type, 'm.room.message');
expect(decryptedEvent.content, payload);
});
test('setPushRuleState', () async {

View file

@ -1,56 +0,0 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Copyright (C) 2019, 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/src/utils/session_key.dart';
import 'package:olm/olm.dart' as olm;
import 'package:test/test.dart';
void main() {
/// All Tests related to the ChatTime
group('SessionKey', () {
var olmEnabled = true;
try {
olm.init();
olm.Account();
} catch (_) {
olmEnabled = false;
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
}
print('[LibOlm] Enabled: $olmEnabled');
test('SessionKey test', () {
if (olmEnabled) {
final sessionKey = SessionKey(
content: {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': '!Cuyf34gef24t:localhost',
'session_id': 'X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ',
'session_key':
'AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY...'
},
inboundGroupSession: olm.InboundGroupSession(),
key: '1234',
indexes: {},
);
expect(sessionKey.senderClaimedEd25519Key, '');
expect(sessionKey.toJson(),
SessionKey.fromJson(sessionKey.toJson(), '1234').toJson());
expect(sessionKey.toString(), json.encode(sessionKey.toJson()));
}
});
});
}

View file

@ -143,7 +143,8 @@ void main() {
expect(updateCount, 5);
expect(insertList, [0, 0, 0]);
expect(insertList.length, timeline.events.length);
expect(timeline.events[0].eventId, '42');
final eventId = timeline.events[0].eventId;
expect(eventId.startsWith('\$event'), true);
expect(timeline.events[0].status, 1);
client.onEvent.add(EventUpdate(
@ -155,7 +156,7 @@ void main() {
'content': {'msgtype': 'm.text', 'body': 'test'},
'sender': '@alice:example.com',
'status': 2,
'event_id': '42',
'event_id': eventId,
'unsigned': {'transaction_id': '1234'},
'origin_server_ts': DateTime.now().millisecondsSinceEpoch
},
@ -166,7 +167,7 @@ void main() {
expect(updateCount, 6);
expect(insertList, [0, 0, 0]);
expect(insertList.length, timeline.events.length);
expect(timeline.events[0].eventId, '42');
expect(timeline.events[0].eventId, eventId);
expect(timeline.events[0].status, 2);
});

View file

@ -88,7 +88,8 @@ void test() async {
await room.enableEncryption();
await Future.delayed(Duration(seconds: 5));
assert(room.encrypted == true);
assert(room.outboundGroupSession == null);
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) ==
null);
print('++++ ($testUserA) Check known olm devices ++++');
assert(testClientA.userDeviceKeys.containsKey(testUserB));
@ -123,16 +124,30 @@ void test() async {
print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++");
await room.sendTextEvent(testMessage);
await Future.delayed(Duration(seconds: 5));
assert(room.outboundGroupSession != null);
var currentSessionIdA = room.outboundGroupSession.session_id();
assert(room.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
testClientB.olmSessions[testClientA.identityKey].first.session_id());
assert(inviteRoom.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) !=
null);
var currentSessionIdA = room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
.outboundGroupSession
.session_id();
assert(room.client.encryption.keyManager
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
null);
assert(testClientA
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
1);
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(inviteRoom.client.encryption.keyManager
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
null);
assert(room.lastMessage == testMessage);
assert(inviteRoom.lastMessage == testMessage);
print(
@ -141,14 +156,27 @@ void test() async {
print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++");
await room.sendTextEvent(testMessage2);
await Future.delayed(Duration(seconds: 5));
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
testClientB.olmSessions[testClientA.identityKey].first.session_id());
assert(testClientA
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
1);
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(room.outboundGroupSession.session_id() == currentSessionIdA);
assert(inviteRoom.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
.outboundGroupSession
.session_id() ==
currentSessionIdA);
assert(room.client.encryption.keyManager
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
null);
assert(room.lastMessage == testMessage2);
assert(inviteRoom.lastMessage == testMessage2);
print(
@ -157,14 +185,31 @@ void test() async {
print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++");
await inviteRoom.sendTextEvent(testMessage3);
await Future.delayed(Duration(seconds: 5));
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(room.outboundGroupSession.session_id() == currentSessionIdA);
assert(inviteRoom.outboundGroupSession != null);
assert(inviteRoom.inboundGroupSessions
.containsKey(inviteRoom.outboundGroupSession.session_id()));
assert(room.inboundGroupSessions
.containsKey(inviteRoom.outboundGroupSession.session_id()));
assert(testClientA
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
1);
assert(testClientB
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
1);
assert(room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
.outboundGroupSession
.session_id() ==
currentSessionIdA);
var inviteRoomOutboundGroupSession = inviteRoom.client.encryption.keyManager
.getOutboundGroupSession(inviteRoom.id);
assert(inviteRoomOutboundGroupSession != null);
assert(inviteRoom.client.encryption.keyManager.getInboundGroupSession(
inviteRoom.id,
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
'') !=
null);
assert(room.client.encryption.keyManager.getInboundGroupSession(
room.id,
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
'') !=
null);
assert(inviteRoom.lastMessage == testMessage3);
assert(room.lastMessage == testMessage3);
print(
@ -180,18 +225,42 @@ void test() async {
print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++");
await room.sendTextEvent(testMessage4);
await Future.delayed(Duration(seconds: 5));
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
testClientB.olmSessions[testClientA.identityKey].first.session_id());
assert(testClientA.olmSessions[testClientC.identityKey].length == 1);
assert(testClientC.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientC.identityKey].first.session_id() ==
testClientC.olmSessions[testClientA.identityKey].first.session_id());
assert(room.outboundGroupSession.session_id() != currentSessionIdA);
currentSessionIdA = room.outboundGroupSession.session_id();
assert(inviteRoom.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(testClientA
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
1);
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[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(room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
.outboundGroupSession
.session_id() !=
currentSessionIdA);
currentSessionIdA = room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
.outboundGroupSession
.session_id();
assert(inviteRoom.client.encryption.keyManager
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
null);
assert(room.lastMessage == testMessage4);
assert(inviteRoom.lastMessage == testMessage4);
print(
@ -206,14 +275,30 @@ void test() async {
print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++");
await room.sendTextEvent(testMessage6);
await Future.delayed(Duration(seconds: 5));
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
testClientB.olmSessions[testClientA.identityKey].first.session_id());
assert(room.outboundGroupSession.session_id() != currentSessionIdA);
currentSessionIdA = room.outboundGroupSession.session_id();
assert(inviteRoom.inboundGroupSessions
.containsKey(room.outboundGroupSession.session_id()));
assert(testClientA
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
1);
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(room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
.outboundGroupSession
.session_id() !=
currentSessionIdA);
currentSessionIdA = room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
.outboundGroupSession
.session_id();
assert(inviteRoom.client.encryption.keyManager
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
null);
assert(room.lastMessage == testMessage6);
assert(inviteRoom.lastMessage == testMessage6);
print(
@ -241,18 +326,18 @@ void test() async {
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
room.inboundGroupSessions.keys.toList()[i]);
}
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
testClientB.olmSessions[testClientA.identityKey].first.session_id());
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
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());
print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++");
await restoredRoom.sendTextEvent(testMessage5);
await Future.delayed(Duration(seconds: 5));
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
testClientB.olmSessions[testClientA.identityKey].first.session_id());
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
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(restoredRoom.lastMessage == testMessage5);
assert(inviteRoom.lastMessage == testMessage5);
assert(testClientB.getRoomById(roomId).lastMessage == testMessage5);