split encryption stuff to other library

This commit is contained in:
Sorunome 2020-06-04 13:39:51 +02:00
parent e84126f3c5
commit fcde6a2459
No known key found for this signature in database
GPG key ID: B19471D07FC9BE9C
28 changed files with 2113 additions and 1930 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,279 @@
/*
* 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)) {
await keyManager.handleToDeviceEvent(event);
}
if (event.type.startsWith('m.key.verification.')) {
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,512 @@
/*
* 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;
}
/// 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;
}
// event.encryptedContent['sender_key']
setInboundGroupSession(roomId, sessionId, '', 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,418 @@
/*
* 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}) async {
if (!enabled) {
return true;
}
// generate one-time keys
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());
// 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,
);
if (response['signed_curve25519'] != oneTimeKeysCount) {
return false;
}
_olmAccount.mark_keys_as_published();
await client.database?.updateClientKeys(pickledOlmAccount, client.id);
return true;
}
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();
}
}
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();
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);
}
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;
}
event = _decryptToDeviceEvent(event);
if (event.type != EventTypes.Encrypted || client.database == null) {
return event;
}
// load the olm session from the database and re-try to decrypt it
final sessions = await client.database.getSingleOlmSessions(
client.id, event.content['sender_key'], client.userID);
if (sessions.isEmpty) {
return event; // okay, can't do anything
}
_olmSessions[event.content['sender_key']] = sessions;
return _decryptToDeviceEvent(event);
}
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 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 (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());
}
}
}
}
}
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 'dart:typed_data';
import 'package:random_string/random_string.dart'; import 'package:random_string/random_string.dart';
import 'package:canonical_json/canonical_json.dart'; import 'package:canonical_json/canonical_json.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import '../../matrix_api.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'device_keys_list.dart'; import 'package:famedlysdk/matrix_api.dart';
import '../client.dart';
import '../room.dart'; import '../encryption.dart';
/* /*
+-------------+ +-----------+ +-------------+ +-----------+
@ -53,6 +71,9 @@ enum KeyVerificationState {
} }
List<String> _intersect(List<String> a, List<dynamic> b) { List<String> _intersect(List<String> a, List<dynamic> b) {
if (b == null || a == null) {
return [];
}
final res = <String>[]; final res = <String>[];
for (final v in a) { for (final v in a) {
if (b.contains(v)) { if (b.contains(v)) {
@ -94,7 +115,8 @@ _KeyVerificationMethod _makeVerificationMethod(
class KeyVerification { class KeyVerification {
String transactionId; String transactionId;
final Client client; final Encryption encryption;
Client get client => encryption.client;
final Room room; final Room room;
final String userId; final String userId;
void Function() onUpdate; void Function() onUpdate;
@ -114,7 +136,11 @@ class KeyVerification {
String canceledReason; String canceledReason;
KeyVerification( KeyVerification(
{this.client, this.room, this.userId, String deviceId, this.onUpdate}) { {this.encryption,
this.room,
this.userId,
String deviceId,
this.onUpdate}) {
lastActivity = DateTime.now(); lastActivity = DateTime.now();
_deviceId ??= deviceId; _deviceId ??= deviceId;
} }
@ -384,7 +410,7 @@ class KeyVerification {
final newTransactionId = await room.sendEvent(payload, type: type); final newTransactionId = await room.sendEvent(payload, type: type);
if (transactionId == null) { if (transactionId == null) {
transactionId = newTransactionId; transactionId = newTransactionId;
client.addKeyVerificationRequest(this); encryption.keyVerificationManager.addRequest(this);
} }
} else { } else {
await client.sendToDevice( await client.sendToDevice(
@ -404,10 +430,9 @@ class KeyVerification {
abstract class _KeyVerificationMethod { abstract class _KeyVerificationMethod {
KeyVerification request; KeyVerification request;
Client client; Encryption get encryption => request.encryption;
_KeyVerificationMethod({this.request}) { Client get client => request.client;
client = request.client; _KeyVerificationMethod({this.request});
}
Future<void> handlePayload(String type, Map<String, dynamic> payload); Future<void> handlePayload(String type, Map<String, dynamic> payload);
bool validateStart(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 // we would also add the cross signing key here
final deviceKeyId = 'ed25519:${client.deviceID}'; final deviceKeyId = 'ed25519:${client.deviceID}';
mac[deviceKeyId] = mac[deviceKeyId] =
_calculateMac(client.fingerprintKey, baseInfo + deviceKeyId); _calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId);
keyList.add(deviceKeyId); keyList.add(deviceKeyId);
keyList.sort(); 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/room_update.dart';
export 'package:famedlysdk/src/utils/event_update.dart'; export 'package:famedlysdk/src/utils/event_update.dart';
export 'package:famedlysdk/src/utils/device_keys_list.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_file.dart';
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart'; export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
export 'package:famedlysdk/src/utils/uri_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/utils/to_device_event.dart';
export 'package:famedlysdk/src/client.dart'; export 'package:famedlysdk/src/client.dart';
export 'package:famedlysdk/src/event.dart'; export 'package:famedlysdk/src/event.dart';
export 'package:famedlysdk/src/key_manager.dart';
export 'package:famedlysdk/src/room.dart'; export 'package:famedlysdk/src/room.dart';
export 'package:famedlysdk/src/timeline.dart'; export 'package:famedlysdk/src/timeline.dart';
export 'package:famedlysdk/src/user.dart'; export 'package:famedlysdk/src/user.dart';

View file

@ -20,16 +20,14 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:core'; import 'dart:core';
import 'package:canonical_json/canonical_json.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart'; import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/src/room.dart'; import 'package:famedlysdk/src/room.dart';
import 'package:famedlysdk/src/utils/device_keys_list.dart'; import 'package:famedlysdk/src/utils/device_keys_list.dart';
import 'package:famedlysdk/src/utils/matrix_file.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:famedlysdk/src/utils/to_device_event.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:olm/olm.dart' as olm;
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'event.dart'; import 'event.dart';
@ -38,8 +36,6 @@ import 'utils/event_update.dart';
import 'utils/room_update.dart'; import 'utils/room_update.dart';
import 'user.dart'; import 'user.dart';
import 'database/database.dart' show Database; import 'database/database.dart' show Database;
import 'utils/key_verification.dart';
import 'key_manager.dart';
typedef RoomSorter = int Function(Room a, Room b); typedef RoomSorter = int Function(Room a, Room b);
@ -53,12 +49,13 @@ class Client {
int get id => _id; int get id => _id;
Database database; Database database;
KeyManager keyManager;
bool enableE2eeRecovery; bool enableE2eeRecovery;
MatrixApi api; MatrixApi api;
Encryption encryption;
/// Create a client /// Create a client
/// clientName = unique identifier of this client /// clientName = unique identifier of this client
/// debug: Print debug output? /// debug: Print debug output?
@ -70,7 +67,6 @@ class Client {
this.enableE2eeRecovery = false, this.enableE2eeRecovery = false,
http.Client httpClient}) { http.Client httpClient}) {
api = MatrixApi(debug: debug, httpClient: httpClient); api = MatrixApi(debug: debug, httpClient: httpClient);
keyManager = KeyManager(this);
onLoginStateChanged.stream.listen((loginState) { onLoginStateChanged.stream.listen((loginState) {
if (debug) { if (debug) {
print('[LoginState]: ${loginState.toString()}'); print('[LoginState]: ${loginState.toString()}');
@ -106,18 +102,14 @@ class Client {
List<Room> get rooms => _rooms; List<Room> get rooms => _rooms;
List<Room> _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. /// 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. /// 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! /// Warning! This endpoint is for testing only!
set rooms(List<Room> newList) { set rooms(List<Room> newList) {
@ -529,8 +521,6 @@ class Client {
final StreamController<KeyVerification> onKeyVerificationRequest = final StreamController<KeyVerification> onKeyVerificationRequest =
StreamController.broadcast(); StreamController.broadcast();
final Map<String, KeyVerification> _keyVerificationRequests = {};
/// Matrix synchronisation is done with https long polling. This needs a /// Matrix synchronisation is done with https long polling. This needs a
/// timeout which is usually 30 seconds. /// timeout which is usually 30 seconds.
int syncTimeoutSec = 30; int syncTimeoutSec = 30;
@ -604,31 +594,15 @@ class Client {
if (api.accessToken == null || api.homeserver == null || _userID == null) { if (api.accessToken == null || api.homeserver == null || _userID == null) {
// we aren't logged in // we aren't logged in
encryption?.dispose();
encryption = null;
onLoginStateChanged.add(LoginState.loggedOut); onLoginStateChanged.add(LoginState.loggedOut);
return; return;
} }
// Try to create a new olm account or restore a previous one. encryption = Encryption(
if (olmAccount == null) { debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery);
try { await encryption.init(olmAccount);
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;
}
}
if (database != null) { if (database != null) {
if (id != null) { if (id != null) {
@ -639,7 +613,7 @@ class Client {
_deviceID, _deviceID,
_deviceName, _deviceName,
prevBatch, prevBatch,
pickledOlmAccount, encryption?.pickledOlmAccount,
id, id,
); );
} else { } else {
@ -651,11 +625,10 @@ class Client {
_deviceID, _deviceID,
_deviceName, _deviceName,
prevBatch, prevBatch,
pickledOlmAccount, encryption?.pickledOlmAccount,
); );
} }
_userDeviceKeys = await database.getUserDeviceKeys(id); _userDeviceKeys = await database.getUserDeviceKeys(id);
_olmSessions = await database.getOlmSessions(id, _userID);
_rooms = await database.getRoomList(this, onlyLeft: false); _rooms = await database.getRoomList(this, onlyLeft: false);
_sortRooms(); _sortRooms();
accountData = await database.getAccountData(id); accountData = await database.getAccountData(id);
@ -674,20 +647,12 @@ class Client {
/// Resets all settings and stops the synchronisation. /// Resets all settings and stops the synchronisation.
void clear() { 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); database?.clear(id);
_id = api.accessToken = _id = api.accessToken =
api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null; api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
_rooms = []; _rooms = [];
encryption?.dispose();
encryption = null;
onLoginStateChanged.add(LoginState.loggedOut); onLoginStateChanged.add(LoginState.loggedOut);
} }
@ -723,7 +688,9 @@ class Client {
} }
prevBatch = syncResp.nextBatch; prevBatch = syncResp.nextBatch;
await _updateUserDeviceKeys(); await _updateUserDeviceKeys();
_cleanupKeyVerificationRequests(); if (encryptionEnabled) {
encryption.onSync();
}
if (hash == _syncRequest.hashCode) unawaited(_sync()); if (hash == _syncRequest.hashCode) unawaited(_sync());
} on MatrixException catch (exception) { } on MatrixException catch (exception) {
onError.add(exception); onError.add(exception);
@ -740,7 +707,7 @@ class Client {
/// Use this method only for testing utilities! /// Use this method only for testing utilities!
Future<void> handleSync(SyncUpdate sync) async { Future<void> handleSync(SyncUpdate sync) async {
if (sync.toDevice != null) { if (sync.toDevice != null) {
_handleToDeviceEvents(sync.toDevice); await _handleToDeviceEvents(sync.toDevice);
} }
if (sync.rooms != null) { if (sync.rooms != null) {
if (sync.rooms.join != null) { if (sync.rooms.join != null) {
@ -784,31 +751,12 @@ class Client {
if (sync.deviceLists != null) { if (sync.deviceLists != null) {
await _handleDeviceListsEvents(sync.deviceLists); await _handleDeviceListsEvents(sync.deviceLists);
} }
if (sync.deviceOneTimeKeysCount != null) { if (sync.deviceOneTimeKeysCount != null && encryptionEnabled) {
_handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount); encryption.handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount);
}
while (_pendingToDeviceEvents.isNotEmpty) {
_updateRoomsByToDeviceEvent(
_pendingToDeviceEvents.removeLast(),
addToPendingIfNotFound: false,
);
} }
onSync.add(sync); 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 { Future<void> _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async {
if (deviceLists.changed is List) { if (deviceLists.changed is List) {
for (final userId in deviceLists.changed) { for (final userId in deviceLists.changed) {
@ -827,36 +775,12 @@ class Client {
} }
} }
void _cleanupKeyVerificationRequests() { Future<void> _handleToDeviceEvents(List<BasicEventWithSender> events) async {
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) {
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson()); var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
if (toDeviceEvent.type == EventTypes.Encrypted) { if (toDeviceEvent.type == EventTypes.Encrypted && encryptionEnabled) {
try { try {
toDeviceEvent = decryptToDeviceEvent(toDeviceEvent); toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent);
} catch (e, s) { } catch (e, s) {
print( print(
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}'); '[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()); toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
} }
} }
_updateRoomsByToDeviceEvent(toDeviceEvent); if (encryptionEnabled) {
if (toDeviceEvent.type.startsWith('m.key.verification.')) { await encryption.handleToDeviceEvent(toDeviceEvent);
_handleToDeviceKeyVerificationRequest(toDeviceEvent);
}
if (['m.room_key_request', 'm.forwarded_room_key']
.contains(toDeviceEvent.type)) {
keyManager.handleToDeviceEvent(toDeviceEvent);
} }
onToDeviceEvent.add(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( Future<void> _handleRooms(
Map<String, SyncRoomUpdate> rooms, Membership membership) async { Map<String, SyncRoomUpdate> rooms, Membership membership) async {
for (final entry in rooms.entries) { for (final entry in rooms.entries) {
@ -1056,14 +945,8 @@ class Client {
content: event, content: event,
sortOrder: sortOrder, sortOrder: sortOrder,
); );
if (event['type'] == EventTypes.Encrypted) { if (event['type'] == EventTypes.Encrypted && encryptionEnabled) {
update = update.decrypt(room); update = await 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 (type != 'ephemeral' && database != null) { if (type != 'ephemeral' && database != null) {
await database.storeEventUpdate(id, update); await database.storeEventUpdate(id, update);
@ -1187,42 +1070,6 @@ class Client {
if (eventUpdate.type == 'timeline') _sortRooms(); 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; bool _sortLock = false;
/// The compare function how the rooms should be sorted internally. By default /// The compare function how the rooms should be sorted internally. By default
@ -1301,7 +1148,7 @@ class Client {
if (entry.isValid) { if (entry.isValid) {
_userDeviceKeys[userId].deviceKeys[deviceId] = entry; _userDeviceKeys[userId].deviceKeys[deviceId] = entry;
if (deviceId == deviceID && if (deviceId == deviceID &&
entry.ed25519Key == fingerprintKey) { entry.ed25519Key == encryption?.fingerprintKey) {
// Always trust the own device // Always trust the own device
entry.verified = true; entry.verified = true;
} }
@ -1349,213 +1196,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 /// 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]. /// the request to all devices of the current user, pass an empty list to [deviceKeys].
Future<void> sendToDevice( Future<void> sendToDevice(
@ -1589,96 +1229,22 @@ class Client {
} }
} else { } else {
if (encrypted) { if (encrypted) {
// Create new sessions with devices if there is no existing session yet. data =
var deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys); await encryption.encryptToDeviceMessage(deviceKeys, type, message);
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) => } else {
olmSessions.containsKey(deviceKeys.curve25519Key)); for (final device in deviceKeys) {
if (deviceKeysWithoutSession.isNotEmpty) { if (!data.containsKey(device.userId)) {
await startOutgoingOlmSessions(deviceKeysWithoutSession); 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; if (encrypted) type = EventTypes.Encrypted;
final messageID = generateUniqueTransactionId(); final messageID = generateUniqueTransactionId();
await api.sendToDevice(type, messageID, data); 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] /// 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 /// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master
bool get allPushNotificationsMuted { bool get allPushNotificationsMuted {

View file

@ -91,6 +91,22 @@ class Database extends _$Database {
return res; 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( Future<DbOutboundGroupSession> getDbOutboundGroupSession(
int clientId, String roomId) async { int clientId, String roomId) async {
final res = await dbGetOutboundGroupSession(clientId, roomId).get(); final res = await dbGetOutboundGroupSession(clientId, roomId).get();

View file

@ -4851,6 +4851,19 @@ abstract class _$Database extends GeneratedDatabase {
readsFrom: {olmSessions}).map(_rowToDbOlmSessions); 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( Future<int> storeOlmSession(
int client_id, String identitiy_key, String session_id, String pickle) { int client_id, String identitiy_key, String session_id, String pickle) {
return customInsert( 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; getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id;
getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key 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; getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id;
dbGetOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key;
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle); storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle);
getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id; 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; 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:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/src/utils/receipt.dart'; import 'package:famedlysdk/src/utils/receipt.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
@ -333,36 +334,6 @@ class Event extends MatrixEvent {
return await timeline.getEventById(replyEventId); 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 /// If this event is encrypted and the decryption was not successful because
/// the session is unknown, this requests the session key from other devices /// 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 /// 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:async';
import 'dart:convert';
import 'package:famedlysdk/matrix_api.dart'; import 'package:famedlysdk/matrix_api.dart';
import 'package:pedantic/pedantic.dart'; import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/client.dart'; import 'package:famedlysdk/src/client.dart';
import 'package:famedlysdk/src/event.dart'; import 'package:famedlysdk/src/event.dart';
import 'package:famedlysdk/src/utils/event_update.dart'; import 'package:famedlysdk/src/utils/event_update.dart';
import 'package:famedlysdk/src/utils/room_update.dart'; import 'package:famedlysdk/src/utils/room_update.dart';
import 'package:famedlysdk/src/utils/matrix_file.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart';
import 'package:famedlysdk/src/utils/session_key.dart';
import 'package:image/image.dart'; import 'package:image/image.dart';
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
import 'package:mime_type/mime_type.dart'; import 'package:mime_type/mime_type.dart';
import 'package:olm/olm.dart' as olm;
import 'package:html_unescape/html_unescape.dart'; import 'package:html_unescape/html_unescape.dart';
import './user.dart'; import './user.dart';
@ -81,13 +78,6 @@ class Room {
/// Key-Value store for private account data only visible for this user. /// Key-Value store for private account data only visible for this user.
Map<String, BasicRoomEvent> roomAccountData = {}; Map<String, BasicRoomEvent> roomAccountData = {};
olm.OutboundGroupSession get outboundGroupSession => _outboundGroupSession;
olm.OutboundGroupSession _outboundGroupSession;
List<String> _outboundGroupSessionDevices;
DateTime _outboundGroupSessionCreationTime;
int _outboundGroupSessionSentMessages;
double _newestSortOrder; double _newestSortOrder;
double _oldestSortOrder; double _oldestSortOrder;
@ -110,168 +100,6 @@ class Room {
_oldestSortOrder, _newestSortOrder, client.id, id); _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]. /// Returns the [Event] for the given [typeKey] and optional [stateKey].
/// If no [stateKey] is provided, it defaults to an empty string. /// If no [stateKey] is provided, it defaults to an empty string.
Event getState(String typeKey, [String stateKey = '']) => Event getState(String typeKey, [String stateKey = '']) =>
@ -281,23 +109,13 @@ class Room {
/// typeKey/stateKey key pair if there is one. /// typeKey/stateKey key pair if there is one.
void setState(Event state) { void setState(Event state) {
// Decrypt if necessary // Decrypt if necessary
if (state.type == EventTypes.Encrypted) { if (state.type == EventTypes.Encrypted && client.encryptionEnabled) {
try { try {
state = decryptGroupMessage(state); state = client.encryption.decryptRoomEventSync(id, state);
} catch (e) { } catch (e) {
print('[LibOlm] Could not decrypt room state: ' + e.toString()); 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) > if ((getState(state.type)?.originServerTs?.millisecondsSinceEpoch ?? 0) >
(state.originServerTs?.millisecondsSinceEpoch ?? 1)) { (state.originServerTs?.millisecondsSinceEpoch ?? 1)) {
return; return;
@ -882,7 +700,8 @@ class Room {
// Send the text and on success, store and display a *sent* event. // Send the text and on success, store and display a *sent* event.
try { try {
final sendMessageContent = encrypted && client.encryptionEnabled final sendMessageContent = encrypted && client.encryptionEnabled
? await encryptGroupMessagePayload(content, type: type) ? await client.encryption
.encryptGroupMessagePayload(id, content, type: type)
: content; : content;
final res = await client.api.sendMessage( final res = await client.api.sendMessage(
id, id,
@ -998,55 +817,42 @@ class Room {
if (onHistoryReceived != null) onHistoryReceived(); if (onHistoryReceived != null) onHistoryReceived();
prev_batch = resp.end; prev_batch = resp.end;
final dbActions = <Future<dynamic> Function()>[]; final loadFn = () async {
if (client.database != null) { if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return;
dbActions.add(
() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
}
if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return; if (resp.state != null) {
for (final state in resp.state) {
if (resp.state != null) { await EventUpdate(
for (final state in resp.state) { type: 'state',
var eventUpdate = EventUpdate( roomID: id,
type: 'state', eventType: state.type,
roomID: id, content: state.toJson(),
eventType: state.type, sortOrder: oldSortOrder,
content: state.toJson(), ).decrypt(this, store: true);
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) { for (final hist in resp.chunk) {
var eventUpdate = EventUpdate( final eventUpdate = await EventUpdate(
type: 'history', type: 'history',
roomID: id, roomID: id,
eventType: hist.type, eventType: hist.type,
content: hist.toJson(), content: hist.toJson(),
sortOrder: oldSortOrder, sortOrder: oldSortOrder,
).decrypt(this); ).decrypt(this, store: true);
client.onEvent.add(eventUpdate); client.onEvent.add(eventUpdate);
if (client.database != null) {
dbActions.add(
() => client.database.storeEventUpdate(client.id, eventUpdate));
} }
} };
if (client.database != null) { if (client.database != null) {
dbActions await client.database.transaction(() async {
.add(() => client.database.setRoomPrevBatch(resp.end, client.id, id)); 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( client.onRoomUpdate.add(
RoomUpdate( RoomUpdate(
id: id, id: id,
@ -1146,7 +952,6 @@ class Room {
} }
for (final rawState in rawStates) { for (final rawState in rawStates) {
final newState = Event.fromDb(rawState, newRoom); final newState = Event.fromDb(rawState, newRoom);
;
newRoom.setState(newState); newRoom.setState(newState);
} }
} }
@ -1186,13 +991,13 @@ class Room {
} }
// Try again to decrypt encrypted events and update the database. // 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 { await client.database.transaction(() async {
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted && if (events[i].type == EventTypes.Encrypted &&
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) { events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
await events[i].loadSession(); events[i] = await client.encryption
events[i] = await events[i].decryptAndStore(); .decryptRoomEvent(id, events[i], store: true);
} }
} }
}); });
@ -1745,209 +1550,10 @@ class Room {
return deviceKeys; 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 { 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) { if (!client.encryptionEnabled) {
throw (DecryptError.NOT_ENABLED); return;
} }
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') { await client.encryption.keyManager.request(this, sessionId, senderKey);
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,
);
} }
} }
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 'dart:async';
import 'package:famedlysdk/matrix_api.dart'; import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/encryption.dart';
import 'event.dart'; import 'event.dart';
import 'room.dart'; import 'room.dart';
@ -97,12 +98,16 @@ class Timeline {
void _sessionKeyReceived(String sessionId) async { void _sessionKeyReceived(String sessionId) async {
var decryptAtLeastOneEvent = false; var decryptAtLeastOneEvent = false;
final decryptFn = () async { final decryptFn = () async {
if (!room.client.encryptionEnabled) {
return;
}
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted && if (events[i].type == EventTypes.Encrypted &&
events[i].messageType == MessageTypes.BadEncrypted && events[i].messageType == MessageTypes.BadEncrypted &&
events[i].content['body'] == DecryptError.UNKNOWN_SESSION && events[i].content['body'] == DecryptError.UNKNOWN_SESSION &&
events[i].content['session_id'] == sessionId) { 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) { if (events[i].type != EventTypes.Encrypted) {
decryptAtLeastOneEvent = true; decryptAtLeastOneEvent = true;
} }

View file

@ -1,11 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'package:famedlysdk/matrix_api.dart'; import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/encryption.dart';
import '../client.dart'; import '../client.dart';
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey; import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
import '../event.dart'; import '../event.dart';
import 'key_verification.dart';
class DeviceKeysList { class DeviceKeysList {
String userId; String userId;
@ -78,12 +78,6 @@ class DeviceKeys extends MatrixDeviceKeys {
Future<void> setBlocked(bool newBlocked, Client client) { Future<void> setBlocked(bool newBlocked, Client client) {
blocked = newBlocked; 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 return client.database
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); ?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
} }
@ -157,10 +151,10 @@ class DeviceKeys extends MatrixDeviceKeys {
} }
KeyVerification startVerification(Client client) { KeyVerification startVerification(Client client) {
final request = final request = KeyVerification(
KeyVerification(client: client, userId: userId, deviceId: deviceId); encryption: client.encryption, userId: userId, deviceId: deviceId);
request.start(); request.start();
client.addKeyVerificationRequest(request); client.encryption.keyVerificationManager.addRequest(request);
return request; return request;
} }
} }

View file

@ -42,13 +42,14 @@ class EventUpdate {
EventUpdate( EventUpdate(
{this.eventType, this.roomID, this.type, this.content, this.sortOrder}); {this.eventType, this.roomID, this.type, this.content, this.sortOrder});
EventUpdate decrypt(Room room) { Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
if (eventType != EventTypes.Encrypted) { if (eventType != EventTypes.Encrypted || !room.client.encryptionEnabled) {
return this; return this;
} }
try { try {
var decrpytedEvent = var decrpytedEvent = await room.client.encryption.decryptRoomEvent(
room.decryptGroupMessage(Event.fromJson(content, room, sortOrder)); room.id, Event.fromJson(content, room, sortOrder),
store: store, updateType: type);
return EventUpdate( return EventUpdate(
eventType: decrpytedEvent.type, eventType: decrpytedEvent.type,
roomID: roomID, 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:async';
import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
@ -134,24 +133,6 @@ void main() {
expect(matrix.directChats, matrix.accountData['m.direct'].content); expect(matrix.directChats, matrix.accountData['m.direct'].content);
expect(matrix.presences.length, 1); expect(matrix.presences.length, 1);
expect(matrix.rooms[1].ephemerals.length, 2); 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.length, 1);
expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com'); expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com');
expect(matrix.rooms[1].roomAccountData.length, 3); expect(matrix.rooms[1].roomAccountData.length, 3);
@ -388,115 +369,6 @@ void main() {
'mxc://example.org/SEsfnsuifSDFSSEF'); 'mxc://example.org/SEsfnsuifSDFSSEF');
expect(aliceProfile.displayname, 'Alice Margatroid'); 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({ var deviceKeys = DeviceKeys.fromJson({
'user_id': '@alice:example.com', 'user_id': '@alice:example.com',
'device_id': 'JLAFKJWSCS', 'device_id': 'JLAFKJWSCS',
@ -512,16 +384,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 { test('sendToDevice', () async {
await matrix.sendToDevice( await matrix.sendToDevice(
[deviceKeys], [deviceKeys],
@ -547,13 +409,6 @@ void main() {
await Future.delayed(Duration(milliseconds: 50)); 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.isLogged(), true);
expect(client1.rooms.length, 2); expect(client1.rooms.length, 2);
@ -571,12 +426,9 @@ void main() {
expect(client2.deviceID, client1.deviceID); expect(client2.deviceID, client1.deviceID);
expect(client2.deviceName, client1.deviceName); expect(client2.deviceName, client1.deviceName);
if (client2.encryptionEnabled) { if (client2.encryptionEnabled) {
await client2.rooms[1].restoreOutboundGroupSession(); expect(client2.encryption.pickledOlmAccount,
expect(client2.pickledOlmAccount, client1.pickledOlmAccount); client1.encryption.pickledOlmAccount);
expect(json.encode(client2.rooms[1].inboundGroupSessions[sessionKey]),
json.encode(client1.rooms[1].inboundGroupSessions[sessionKey]));
expect(client2.rooms[1].id, client1.rooms[1].id); expect(client2.rooms[1].id, client1.rooms[1].id);
expect(client2.rooms[1].outboundGroupSession.session_key(), sessionKey);
} }
await client1.logout(); await client1.logout();

View file

@ -0,0 +1,342 @@
/*
* 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
group('Key Request', () {
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
final validSenderKey = '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI';
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.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 =
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);
// 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 =
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.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

@ -17,10 +17,12 @@
*/ */
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import 'fake_matrix_api.dart'; import '../fake_matrix_api.dart';
import '../fake_database.dart';
void main() { void main() {
/// All Tests related to the ChatTime /// All Tests related to the ChatTime
@ -36,19 +38,25 @@ void main() {
print('[LibOlm] Enabled: $olmEnabled'); print('[LibOlm] Enabled: $olmEnabled');
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); 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 room = Room(id: '!localpart:server.abc', client: client);
var updateCounter = 0; var updateCounter = 0;
final keyVerification = KeyVerification( KeyVerification keyVerification;
client: client,
room: room,
userId: '@alice:example.com',
deviceId: 'ABCD',
onUpdate: () => updateCounter++,
);
if (!olmEnabled) return; if (!olmEnabled) return;
test('setupClient', () async {
client.database = getDatabase();
await client.checkServer('https://fakeServer.notExisting');
await client.login('test', '1234');
keyVerification = KeyVerification(
encryption: client.encryption,
room: room,
userId: '@alice:example.com',
deviceId: 'ABCD',
onUpdate: () => updateCounter++,
);
});
test('acceptSas', () async { test('acceptSas', () async {
await keyVerification.acceptSas(); await keyVerification.acceptSas();
}); });
@ -91,7 +99,7 @@ void main() {
test('verifyActivity', () async { test('verifyActivity', () async {
final verified = await keyVerification.verifyActivity(); final verified = await keyVerification.verifyActivity();
expect(verified, true); expect(verified, true);
keyVerification?.dispose();
}); });
keyVerification.dispose();
}); });
} }

View file

@ -20,6 +20,7 @@ import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart'; import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/src/event.dart'; import 'package:famedlysdk/src/event.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';

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

@ -315,7 +315,7 @@ void main() {
test('getTimeline', () async { test('getTimeline', () async {
final timeline = await room.getTimeline(); final timeline = await room.getTimeline();
expect(timeline.events, []); expect(timeline.events.length, 1);
}); });
test('getUserByMXID', () async { test('getUserByMXID', () async {
@ -388,60 +388,6 @@ void main() {
); );
expect(room.encrypted, true); expect(room.encrypted, true);
expect(room.encryptionAlgorithm, 'm.megolm.v1.aes-sha2'); 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 { 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

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