split encryption stuff to other library
This commit is contained in:
parent
e84126f3c5
commit
fcde6a2459
23
lib/encryption.dart
Normal file
23
lib/encryption.dart
Normal 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';
|
279
lib/encryption/encryption.dart
Normal file
279
lib/encryption/encryption.dart
Normal 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.';
|
||||
}
|
512
lib/encryption/key_manager.dart
Normal file
512
lib/encryption/key_manager.dart
Normal 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);
|
||||
}
|
||||
}
|
83
lib/encryption/key_verification_manager.dart
Normal file
83
lib/encryption/key_verification_manager.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
418
lib/encryption/olm_manager.dart
Normal file
418
lib/encryption/olm_manager.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,29 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'package:random_string/random_string.dart';
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import '../../matrix_api.dart';
|
||||
import 'device_keys_list.dart';
|
||||
import '../client.dart';
|
||||
import '../room.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
|
||||
import '../encryption.dart';
|
||||
|
||||
/*
|
||||
+-------------+ +-----------+
|
||||
|
@ -53,6 +71,9 @@ enum KeyVerificationState {
|
|||
}
|
||||
|
||||
List<String> _intersect(List<String> a, List<dynamic> b) {
|
||||
if (b == null || a == null) {
|
||||
return [];
|
||||
}
|
||||
final res = <String>[];
|
||||
for (final v in a) {
|
||||
if (b.contains(v)) {
|
||||
|
@ -94,7 +115,8 @@ _KeyVerificationMethod _makeVerificationMethod(
|
|||
|
||||
class KeyVerification {
|
||||
String transactionId;
|
||||
final Client client;
|
||||
final Encryption encryption;
|
||||
Client get client => encryption.client;
|
||||
final Room room;
|
||||
final String userId;
|
||||
void Function() onUpdate;
|
||||
|
@ -114,7 +136,11 @@ class KeyVerification {
|
|||
String canceledReason;
|
||||
|
||||
KeyVerification(
|
||||
{this.client, this.room, this.userId, String deviceId, this.onUpdate}) {
|
||||
{this.encryption,
|
||||
this.room,
|
||||
this.userId,
|
||||
String deviceId,
|
||||
this.onUpdate}) {
|
||||
lastActivity = DateTime.now();
|
||||
_deviceId ??= deviceId;
|
||||
}
|
||||
|
@ -384,7 +410,7 @@ class KeyVerification {
|
|||
final newTransactionId = await room.sendEvent(payload, type: type);
|
||||
if (transactionId == null) {
|
||||
transactionId = newTransactionId;
|
||||
client.addKeyVerificationRequest(this);
|
||||
encryption.keyVerificationManager.addRequest(this);
|
||||
}
|
||||
} else {
|
||||
await client.sendToDevice(
|
||||
|
@ -404,10 +430,9 @@ class KeyVerification {
|
|||
|
||||
abstract class _KeyVerificationMethod {
|
||||
KeyVerification request;
|
||||
Client client;
|
||||
_KeyVerificationMethod({this.request}) {
|
||||
client = request.client;
|
||||
}
|
||||
Encryption get encryption => request.encryption;
|
||||
Client get client => request.client;
|
||||
_KeyVerificationMethod({this.request});
|
||||
|
||||
Future<void> handlePayload(String type, Map<String, dynamic> payload);
|
||||
bool validateStart(Map<String, dynamic> payload) {
|
||||
|
@ -662,7 +687,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
// we would also add the cross signing key here
|
||||
final deviceKeyId = 'ed25519:${client.deviceID}';
|
||||
mac[deviceKeyId] =
|
||||
_calculateMac(client.fingerprintKey, baseInfo + deviceKeyId);
|
||||
_calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId);
|
||||
keyList.add(deviceKeyId);
|
||||
|
||||
keyList.sort();
|
58
lib/encryption/utils/outbound_group_session.dart
Normal file
58
lib/encryption/utils/outbound_group_session.dart
Normal 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;
|
||||
}
|
||||
}
|
76
lib/encryption/utils/session_key.dart
Normal file
76
lib/encryption/utils/session_key.dart
Normal 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());
|
||||
}
|
|
@ -22,7 +22,6 @@ export 'matrix_api.dart';
|
|||
export 'package:famedlysdk/src/utils/room_update.dart';
|
||||
export 'package:famedlysdk/src/utils/event_update.dart';
|
||||
export 'package:famedlysdk/src/utils/device_keys_list.dart';
|
||||
export 'package:famedlysdk/src/utils/key_verification.dart';
|
||||
export 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
|
||||
export 'package:famedlysdk/src/utils/uri_extension.dart';
|
||||
|
@ -32,7 +31,6 @@ export 'package:famedlysdk/src/utils/states_map.dart';
|
|||
export 'package:famedlysdk/src/utils/to_device_event.dart';
|
||||
export 'package:famedlysdk/src/client.dart';
|
||||
export 'package:famedlysdk/src/event.dart';
|
||||
export 'package:famedlysdk/src/key_manager.dart';
|
||||
export 'package:famedlysdk/src/room.dart';
|
||||
export 'package:famedlysdk/src/timeline.dart';
|
||||
export 'package:famedlysdk/src/user.dart';
|
||||
|
|
|
@ -20,16 +20,14 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/src/room.dart';
|
||||
import 'package:famedlysdk/src/utils/device_keys_list.dart';
|
||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
import 'package:famedlysdk/src/utils/session_key.dart';
|
||||
import 'package:famedlysdk/src/utils/to_device_event.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
import 'event.dart';
|
||||
|
@ -38,8 +36,6 @@ import 'utils/event_update.dart';
|
|||
import 'utils/room_update.dart';
|
||||
import 'user.dart';
|
||||
import 'database/database.dart' show Database;
|
||||
import 'utils/key_verification.dart';
|
||||
import 'key_manager.dart';
|
||||
|
||||
typedef RoomSorter = int Function(Room a, Room b);
|
||||
|
||||
|
@ -53,12 +49,13 @@ class Client {
|
|||
int get id => _id;
|
||||
|
||||
Database database;
|
||||
KeyManager keyManager;
|
||||
|
||||
bool enableE2eeRecovery;
|
||||
|
||||
MatrixApi api;
|
||||
|
||||
Encryption encryption;
|
||||
|
||||
/// Create a client
|
||||
/// clientName = unique identifier of this client
|
||||
/// debug: Print debug output?
|
||||
|
@ -70,7 +67,6 @@ class Client {
|
|||
this.enableE2eeRecovery = false,
|
||||
http.Client httpClient}) {
|
||||
api = MatrixApi(debug: debug, httpClient: httpClient);
|
||||
keyManager = KeyManager(this);
|
||||
onLoginStateChanged.stream.listen((loginState) {
|
||||
if (debug) {
|
||||
print('[LoginState]: ${loginState.toString()}');
|
||||
|
@ -106,18 +102,14 @@ class Client {
|
|||
List<Room> get rooms => _rooms;
|
||||
List<Room> _rooms = [];
|
||||
|
||||
olm.Account _olmAccount;
|
||||
|
||||
/// Returns the base64 encoded keys to store them in a store.
|
||||
/// This String should **never** leave the device!
|
||||
String get pickledOlmAccount =>
|
||||
encryptionEnabled ? _olmAccount.pickle(userID) : null;
|
||||
|
||||
/// Whether this client supports end-to-end encryption using olm.
|
||||
bool get encryptionEnabled => _olmAccount != null;
|
||||
bool get encryptionEnabled => encryption != null && encryption.enabled;
|
||||
|
||||
/// Whether this client is able to encrypt and decrypt files.
|
||||
bool get fileEncryptionEnabled => true;
|
||||
bool get fileEncryptionEnabled => encryptionEnabled && true;
|
||||
|
||||
String get identityKey => encryption?.identityKey ?? '';
|
||||
String get fingerprintKey => encryption?.fingerprintKey ?? '';
|
||||
|
||||
/// Warning! This endpoint is for testing only!
|
||||
set rooms(List<Room> newList) {
|
||||
|
@ -529,8 +521,6 @@ class Client {
|
|||
final StreamController<KeyVerification> onKeyVerificationRequest =
|
||||
StreamController.broadcast();
|
||||
|
||||
final Map<String, KeyVerification> _keyVerificationRequests = {};
|
||||
|
||||
/// Matrix synchronisation is done with https long polling. This needs a
|
||||
/// timeout which is usually 30 seconds.
|
||||
int syncTimeoutSec = 30;
|
||||
|
@ -604,31 +594,15 @@ class Client {
|
|||
|
||||
if (api.accessToken == null || api.homeserver == null || _userID == null) {
|
||||
// we aren't logged in
|
||||
encryption?.dispose();
|
||||
encryption = null;
|
||||
onLoginStateChanged.add(LoginState.loggedOut);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to create a new olm account or restore a previous one.
|
||||
if (olmAccount == null) {
|
||||
try {
|
||||
await olm.init();
|
||||
_olmAccount = olm.Account();
|
||||
_olmAccount.create();
|
||||
if (await _uploadKeys(uploadDeviceKeys: true) == false) {
|
||||
throw ('Upload key failed');
|
||||
}
|
||||
} catch (_) {
|
||||
_olmAccount = null;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await olm.init();
|
||||
_olmAccount = olm.Account();
|
||||
_olmAccount.unpickle(userID, olmAccount);
|
||||
} catch (_) {
|
||||
_olmAccount = null;
|
||||
}
|
||||
}
|
||||
encryption = Encryption(
|
||||
debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery);
|
||||
await encryption.init(olmAccount);
|
||||
|
||||
if (database != null) {
|
||||
if (id != null) {
|
||||
|
@ -639,7 +613,7 @@ class Client {
|
|||
_deviceID,
|
||||
_deviceName,
|
||||
prevBatch,
|
||||
pickledOlmAccount,
|
||||
encryption?.pickledOlmAccount,
|
||||
id,
|
||||
);
|
||||
} else {
|
||||
|
@ -651,11 +625,10 @@ class Client {
|
|||
_deviceID,
|
||||
_deviceName,
|
||||
prevBatch,
|
||||
pickledOlmAccount,
|
||||
encryption?.pickledOlmAccount,
|
||||
);
|
||||
}
|
||||
_userDeviceKeys = await database.getUserDeviceKeys(id);
|
||||
_olmSessions = await database.getOlmSessions(id, _userID);
|
||||
_rooms = await database.getRoomList(this, onlyLeft: false);
|
||||
_sortRooms();
|
||||
accountData = await database.getAccountData(id);
|
||||
|
@ -674,20 +647,12 @@ class Client {
|
|||
|
||||
/// Resets all settings and stops the synchronisation.
|
||||
void clear() {
|
||||
olmSessions.values.forEach((List<olm.Session> sessions) {
|
||||
sessions.forEach((olm.Session session) => session?.free());
|
||||
});
|
||||
rooms.forEach((Room room) {
|
||||
room.clearOutboundGroupSession(wipe: true);
|
||||
room.inboundGroupSessions.values.forEach((SessionKey sessionKey) {
|
||||
sessionKey.inboundGroupSession?.free();
|
||||
});
|
||||
});
|
||||
_olmAccount?.free();
|
||||
database?.clear(id);
|
||||
_id = api.accessToken =
|
||||
api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
|
||||
_rooms = [];
|
||||
encryption?.dispose();
|
||||
encryption = null;
|
||||
onLoginStateChanged.add(LoginState.loggedOut);
|
||||
}
|
||||
|
||||
|
@ -723,7 +688,9 @@ class Client {
|
|||
}
|
||||
prevBatch = syncResp.nextBatch;
|
||||
await _updateUserDeviceKeys();
|
||||
_cleanupKeyVerificationRequests();
|
||||
if (encryptionEnabled) {
|
||||
encryption.onSync();
|
||||
}
|
||||
if (hash == _syncRequest.hashCode) unawaited(_sync());
|
||||
} on MatrixException catch (exception) {
|
||||
onError.add(exception);
|
||||
|
@ -740,7 +707,7 @@ class Client {
|
|||
/// Use this method only for testing utilities!
|
||||
Future<void> handleSync(SyncUpdate sync) async {
|
||||
if (sync.toDevice != null) {
|
||||
_handleToDeviceEvents(sync.toDevice);
|
||||
await _handleToDeviceEvents(sync.toDevice);
|
||||
}
|
||||
if (sync.rooms != null) {
|
||||
if (sync.rooms.join != null) {
|
||||
|
@ -784,31 +751,12 @@ class Client {
|
|||
if (sync.deviceLists != null) {
|
||||
await _handleDeviceListsEvents(sync.deviceLists);
|
||||
}
|
||||
if (sync.deviceOneTimeKeysCount != null) {
|
||||
_handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount);
|
||||
}
|
||||
while (_pendingToDeviceEvents.isNotEmpty) {
|
||||
_updateRoomsByToDeviceEvent(
|
||||
_pendingToDeviceEvents.removeLast(),
|
||||
addToPendingIfNotFound: false,
|
||||
);
|
||||
if (sync.deviceOneTimeKeysCount != null && encryptionEnabled) {
|
||||
encryption.handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount);
|
||||
}
|
||||
onSync.add(sync);
|
||||
}
|
||||
|
||||
void _handleDeviceOneTimeKeysCount(Map<String, int> deviceOneTimeKeysCount) {
|
||||
if (!encryptionEnabled) return;
|
||||
// Check if there are at least half of max_number_of_one_time_keys left on the server
|
||||
// and generate and upload more if not.
|
||||
if (deviceOneTimeKeysCount['signed_curve25519'] != null) {
|
||||
final oneTimeKeysCount = deviceOneTimeKeysCount['signed_curve25519'];
|
||||
if (oneTimeKeysCount < (_olmAccount.max_number_of_one_time_keys() / 2)) {
|
||||
// Generate and upload more one time keys:
|
||||
_uploadKeys();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async {
|
||||
if (deviceLists.changed is List) {
|
||||
for (final userId in deviceLists.changed) {
|
||||
|
@ -827,36 +775,12 @@ class Client {
|
|||
}
|
||||
}
|
||||
|
||||
void _cleanupKeyVerificationRequests() {
|
||||
for (final entry in _keyVerificationRequests.entries) {
|
||||
(() async {
|
||||
var dispose = entry.value.canceled ||
|
||||
entry.value.state == KeyVerificationState.done ||
|
||||
entry.value.state == KeyVerificationState.error;
|
||||
if (!dispose) {
|
||||
dispose = !(await entry.value.verifyActivity());
|
||||
}
|
||||
if (dispose) {
|
||||
entry.value.dispose();
|
||||
_keyVerificationRequests.remove(entry.key);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
void addKeyVerificationRequest(KeyVerification request) {
|
||||
if (request.transactionId == null) {
|
||||
return;
|
||||
}
|
||||
_keyVerificationRequests[request.transactionId] = request;
|
||||
}
|
||||
|
||||
void _handleToDeviceEvents(List<BasicEventWithSender> events) {
|
||||
Future<void> _handleToDeviceEvents(List<BasicEventWithSender> events) async {
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
|
||||
if (toDeviceEvent.type == EventTypes.Encrypted) {
|
||||
if (toDeviceEvent.type == EventTypes.Encrypted && encryptionEnabled) {
|
||||
try {
|
||||
toDeviceEvent = decryptToDeviceEvent(toDeviceEvent);
|
||||
toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent);
|
||||
} catch (e, s) {
|
||||
print(
|
||||
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}');
|
||||
|
@ -872,48 +796,13 @@ class Client {
|
|||
toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
|
||||
}
|
||||
}
|
||||
_updateRoomsByToDeviceEvent(toDeviceEvent);
|
||||
if (toDeviceEvent.type.startsWith('m.key.verification.')) {
|
||||
_handleToDeviceKeyVerificationRequest(toDeviceEvent);
|
||||
}
|
||||
if (['m.room_key_request', 'm.forwarded_room_key']
|
||||
.contains(toDeviceEvent.type)) {
|
||||
keyManager.handleToDeviceEvent(toDeviceEvent);
|
||||
if (encryptionEnabled) {
|
||||
await encryption.handleToDeviceEvent(toDeviceEvent);
|
||||
}
|
||||
onToDeviceEvent.add(toDeviceEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleToDeviceKeyVerificationRequest(ToDeviceEvent toDeviceEvent) {
|
||||
if (!toDeviceEvent.type.startsWith('m.key.verification.')) {
|
||||
return;
|
||||
}
|
||||
// we have key verification going on!
|
||||
final transactionId =
|
||||
KeyVerification.getTransactionId(toDeviceEvent.content);
|
||||
if (transactionId != null) {
|
||||
if (_keyVerificationRequests.containsKey(transactionId)) {
|
||||
_keyVerificationRequests[transactionId]
|
||||
.handlePayload(toDeviceEvent.type, toDeviceEvent.content);
|
||||
} else {
|
||||
final newKeyRequest =
|
||||
KeyVerification(client: this, userId: toDeviceEvent.sender);
|
||||
newKeyRequest
|
||||
.handlePayload(toDeviceEvent.type, toDeviceEvent.content)
|
||||
.then((res) {
|
||||
if (newKeyRequest.state != KeyVerificationState.askAccept) {
|
||||
// okay, something went wrong (unknown transaction id?), just dispose it
|
||||
newKeyRequest.dispose();
|
||||
} else {
|
||||
// we have a new request! Let's broadcast it!
|
||||
_keyVerificationRequests[transactionId] = newKeyRequest;
|
||||
onKeyVerificationRequest.add(newKeyRequest);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRooms(
|
||||
Map<String, SyncRoomUpdate> rooms, Membership membership) async {
|
||||
for (final entry in rooms.entries) {
|
||||
|
@ -1056,14 +945,8 @@ class Client {
|
|||
content: event,
|
||||
sortOrder: sortOrder,
|
||||
);
|
||||
if (event['type'] == EventTypes.Encrypted) {
|
||||
update = update.decrypt(room);
|
||||
}
|
||||
if (update.eventType == EventTypes.Encrypted && database != null) {
|
||||
// the event is still encrytped....let's try fetching the keys from the database!
|
||||
await room.loadInboundGroupSessionKey(
|
||||
event['content']['session_id'], event['content']['sender_key']);
|
||||
update = update.decrypt(room);
|
||||
if (event['type'] == EventTypes.Encrypted && encryptionEnabled) {
|
||||
update = await update.decrypt(room);
|
||||
}
|
||||
if (type != 'ephemeral' && database != null) {
|
||||
await database.storeEventUpdate(id, update);
|
||||
|
@ -1187,42 +1070,6 @@ class Client {
|
|||
if (eventUpdate.type == 'timeline') _sortRooms();
|
||||
}
|
||||
|
||||
final List<ToDeviceEvent> _pendingToDeviceEvents = [];
|
||||
|
||||
void _updateRoomsByToDeviceEvent(ToDeviceEvent toDeviceEvent,
|
||||
{addToPendingIfNotFound = true}) async {
|
||||
try {
|
||||
switch (toDeviceEvent.type) {
|
||||
case 'm.room_key':
|
||||
final roomId = toDeviceEvent.content['room_id'];
|
||||
var room = getRoomById(roomId);
|
||||
if (room == null && addToPendingIfNotFound) {
|
||||
_pendingToDeviceEvents.add(toDeviceEvent);
|
||||
break;
|
||||
}
|
||||
room ??= Room(client: this, id: roomId);
|
||||
final String sessionId = toDeviceEvent.content['session_id'];
|
||||
if (userDeviceKeys.containsKey(toDeviceEvent.sender) &&
|
||||
userDeviceKeys[toDeviceEvent.sender]
|
||||
.deviceKeys
|
||||
.containsKey(toDeviceEvent.content['requesting_device_id'])) {
|
||||
toDeviceEvent.content['sender_claimed_ed25519_key'] =
|
||||
userDeviceKeys[toDeviceEvent.sender]
|
||||
.deviceKeys[toDeviceEvent.content['requesting_device_id']]
|
||||
.ed25519Key;
|
||||
}
|
||||
room.setInboundGroupSession(
|
||||
sessionId,
|
||||
toDeviceEvent.content,
|
||||
forwarded: false,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
print('[Matrix] Error while processing to-device-event: ' + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
bool _sortLock = false;
|
||||
|
||||
/// The compare function how the rooms should be sorted internally. By default
|
||||
|
@ -1301,7 +1148,7 @@ class Client {
|
|||
if (entry.isValid) {
|
||||
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
|
||||
if (deviceId == deviceID &&
|
||||
entry.ed25519Key == fingerprintKey) {
|
||||
entry.ed25519Key == encryption?.fingerprintKey) {
|
||||
// Always trust the own device
|
||||
entry.verified = true;
|
||||
}
|
||||
|
@ -1349,213 +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
|
||||
/// the request to all devices of the current user, pass an empty list to [deviceKeys].
|
||||
Future<void> sendToDevice(
|
||||
|
@ -1589,96 +1229,22 @@ class Client {
|
|||
}
|
||||
} else {
|
||||
if (encrypted) {
|
||||
// Create new sessions with devices if there is no existing session yet.
|
||||
var deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
|
||||
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
|
||||
olmSessions.containsKey(deviceKeys.curve25519Key));
|
||||
if (deviceKeysWithoutSession.isNotEmpty) {
|
||||
await startOutgoingOlmSessions(deviceKeysWithoutSession);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < deviceKeys.length; i++) {
|
||||
var device = deviceKeys[i];
|
||||
data =
|
||||
await encryption.encryptToDeviceMessage(deviceKeys, type, message);
|
||||
} else {
|
||||
for (final device in deviceKeys) {
|
||||
if (!data.containsKey(device.userId)) {
|
||||
data[device.userId] = {};
|
||||
}
|
||||
|
||||
if (encrypted) {
|
||||
var existingSessions = olmSessions[device.curve25519Key];
|
||||
if (existingSessions == null || existingSessions.isEmpty) continue;
|
||||
existingSessions
|
||||
.sort((a, b) => a.session_id().compareTo(b.session_id()));
|
||||
|
||||
final payload = {
|
||||
'type': type,
|
||||
'content': message,
|
||||
'sender': userID,
|
||||
'keys': {'ed25519': fingerprintKey},
|
||||
'recipient': device.userId,
|
||||
'recipient_keys': {'ed25519': device.ed25519Key},
|
||||
};
|
||||
final encryptResult =
|
||||
existingSessions.first.encrypt(json.encode(payload));
|
||||
storeOlmSession(device.curve25519Key, existingSessions.first);
|
||||
sendToDeviceMessage = {
|
||||
'algorithm': 'm.olm.v1.curve25519-aes-sha2',
|
||||
'sender_key': identityKey,
|
||||
'ciphertext': <String, dynamic>{},
|
||||
};
|
||||
sendToDeviceMessage['ciphertext'][device.curve25519Key] = {
|
||||
'type': encryptResult.type,
|
||||
'body': encryptResult.body,
|
||||
};
|
||||
}
|
||||
|
||||
data[device.userId][device.deviceId] = sendToDeviceMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (encrypted) type = EventTypes.Encrypted;
|
||||
final messageID = generateUniqueTransactionId();
|
||||
await api.sendToDevice(type, messageID, data);
|
||||
}
|
||||
|
||||
Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys,
|
||||
{bool checkSignature = true}) async {
|
||||
var requestingKeysFrom = <String, Map<String, String>>{};
|
||||
for (var device in deviceKeys) {
|
||||
if (requestingKeysFrom[device.userId] == null) {
|
||||
requestingKeysFrom[device.userId] = {};
|
||||
}
|
||||
requestingKeysFrom[device.userId][device.deviceId] = 'signed_curve25519';
|
||||
}
|
||||
|
||||
final response =
|
||||
await api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000);
|
||||
|
||||
for (var userKeysEntry in response.oneTimeKeys.entries) {
|
||||
final userId = userKeysEntry.key;
|
||||
for (var deviceKeysEntry in userKeysEntry.value.entries) {
|
||||
final deviceId = deviceKeysEntry.key;
|
||||
final fingerprintKey =
|
||||
userDeviceKeys[userId].deviceKeys[deviceId].ed25519Key;
|
||||
final identityKey =
|
||||
userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key;
|
||||
for (Map<String, dynamic> deviceKey in deviceKeysEntry.value.values) {
|
||||
if (checkSignature &&
|
||||
checkJsonSignature(fingerprintKey, deviceKey, userId, deviceId) ==
|
||||
false) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
var session = olm.Session();
|
||||
session.create_outbound(_olmAccount, identityKey, deviceKey['key']);
|
||||
await storeOlmSession(identityKey, session);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not create new outbound olm session: ' +
|
||||
e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether all push notifications are muted using the [.m.rule.master]
|
||||
/// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master
|
||||
bool get allPushNotificationsMuted {
|
||||
|
|
|
@ -91,6 +91,22 @@ class Database extends _$Database {
|
|||
return res;
|
||||
}
|
||||
|
||||
Future<List<olm.Session>> getSingleOlmSessions(
|
||||
int clientId, String identityKey, String userId) async {
|
||||
final rows = await dbGetOlmSessions(clientId, identityKey).get();
|
||||
final res = <olm.Session>[];
|
||||
for (final row in rows) {
|
||||
try {
|
||||
var session = olm.Session();
|
||||
session.unpickle(userId, row.pickle);
|
||||
res.add(session);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<DbOutboundGroupSession> getDbOutboundGroupSession(
|
||||
int clientId, String roomId) async {
|
||||
final res = await dbGetOutboundGroupSession(clientId, roomId).get();
|
||||
|
|
|
@ -4851,6 +4851,19 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
readsFrom: {olmSessions}).map(_rowToDbOlmSessions);
|
||||
}
|
||||
|
||||
Selectable<DbOlmSessions> dbGetOlmSessions(
|
||||
int client_id, String identity_key) {
|
||||
return customSelect(
|
||||
'SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key',
|
||||
variables: [
|
||||
Variable.withInt(client_id),
|
||||
Variable.withString(identity_key)
|
||||
],
|
||||
readsFrom: {
|
||||
olmSessions
|
||||
}).map(_rowToDbOlmSessions);
|
||||
}
|
||||
|
||||
Future<int> storeOlmSession(
|
||||
int client_id, String identitiy_key, String session_id, String pickle) {
|
||||
return customInsert(
|
||||
|
|
|
@ -155,6 +155,7 @@ storePrevBatch: UPDATE clients SET prev_batch = :prev_batch WHERE client_id = :c
|
|||
getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id;
|
||||
getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key WHERE client_id = :client_id;
|
||||
getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id;
|
||||
dbGetOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key;
|
||||
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle);
|
||||
getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id;
|
||||
dbGetOutboundGroupSession: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/src/utils/receipt.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||
|
@ -333,36 +334,6 @@ class Event extends MatrixEvent {
|
|||
return await timeline.getEventById(replyEventId);
|
||||
}
|
||||
|
||||
Future<void> loadSession() {
|
||||
return room.loadInboundGroupSessionKeyForEvent(this);
|
||||
}
|
||||
|
||||
/// Trys to decrypt this event. Returns a m.bad.encrypted event
|
||||
/// if it fails and does nothing if the event was not encrypted.
|
||||
Event get decrypted => room.decryptGroupMessage(this);
|
||||
|
||||
/// Trys to decrypt this event and persists it in the database afterwards
|
||||
Future<Event> decryptAndStore([String updateType = 'timeline']) async {
|
||||
final newEvent = decrypted;
|
||||
if (newEvent.type == EventTypes.Encrypted) {
|
||||
return newEvent; // decryption failed
|
||||
}
|
||||
await room.client.database?.storeEventUpdate(
|
||||
room.client.id,
|
||||
EventUpdate(
|
||||
eventType: newEvent.type,
|
||||
content: newEvent.toJson(),
|
||||
roomID: newEvent.roomId,
|
||||
type: updateType,
|
||||
sortOrder: newEvent.sortOrder,
|
||||
),
|
||||
);
|
||||
if (updateType != 'history') {
|
||||
room.setState(newEvent);
|
||||
}
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
/// If this event is encrypted and the decryption was not successful because
|
||||
/// the session is unknown, this requests the session key from other devices
|
||||
/// in the room. If the event is not encrypted or the decryption failed because
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -17,21 +17,18 @@
|
|||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/client.dart';
|
||||
import 'package:famedlysdk/src/event.dart';
|
||||
import 'package:famedlysdk/src/utils/event_update.dart';
|
||||
import 'package:famedlysdk/src/utils/room_update.dart';
|
||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
import 'package:famedlysdk/src/utils/session_key.dart';
|
||||
import 'package:image/image.dart';
|
||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||
import 'package:mime_type/mime_type.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
|
||||
import './user.dart';
|
||||
|
@ -81,13 +78,6 @@ class Room {
|
|||
/// Key-Value store for private account data only visible for this user.
|
||||
Map<String, BasicRoomEvent> roomAccountData = {};
|
||||
|
||||
olm.OutboundGroupSession get outboundGroupSession => _outboundGroupSession;
|
||||
olm.OutboundGroupSession _outboundGroupSession;
|
||||
|
||||
List<String> _outboundGroupSessionDevices;
|
||||
DateTime _outboundGroupSessionCreationTime;
|
||||
int _outboundGroupSessionSentMessages;
|
||||
|
||||
double _newestSortOrder;
|
||||
double _oldestSortOrder;
|
||||
|
||||
|
@ -110,168 +100,6 @@ class Room {
|
|||
_oldestSortOrder, _newestSortOrder, client.id, id);
|
||||
}
|
||||
|
||||
/// Clears the existing outboundGroupSession, tries to create a new one and
|
||||
/// stores it as an ingoingGroupSession in the [sessionKeys]. Then sends the
|
||||
/// new session encrypted with olm to all non-blocked devices using
|
||||
/// to-device-messaging.
|
||||
Future<void> createOutboundGroupSession() async {
|
||||
await clearOutboundGroupSession(wipe: true);
|
||||
var deviceKeys = await getUserDeviceKeys();
|
||||
olm.OutboundGroupSession outboundGroupSession;
|
||||
var outboundGroupSessionDevices = <String>[];
|
||||
for (var keys in deviceKeys) {
|
||||
if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId);
|
||||
}
|
||||
outboundGroupSessionDevices.sort();
|
||||
try {
|
||||
outboundGroupSession = olm.OutboundGroupSession();
|
||||
outboundGroupSession.create();
|
||||
} catch (e) {
|
||||
outboundGroupSession = null;
|
||||
print('[LibOlm] Unable to create new outboundGroupSession: ' +
|
||||
e.toString());
|
||||
}
|
||||
|
||||
if (outboundGroupSession == null) return;
|
||||
// Add as an inboundSession to the [sessionKeys].
|
||||
var rawSession = <String, dynamic>{
|
||||
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||
'room_id': id,
|
||||
'session_id': outboundGroupSession.session_id(),
|
||||
'session_key': outboundGroupSession.session_key(),
|
||||
};
|
||||
setInboundGroupSession(rawSession['session_id'], rawSession);
|
||||
try {
|
||||
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
|
||||
_outboundGroupSession = outboundGroupSession;
|
||||
_outboundGroupSessionDevices = outboundGroupSessionDevices;
|
||||
_outboundGroupSessionCreationTime = DateTime.now();
|
||||
_outboundGroupSessionSentMessages = 0;
|
||||
await _storeOutboundGroupSession();
|
||||
} catch (e, s) {
|
||||
print(
|
||||
'[LibOlm] Unable to send the session key to the participating devices: ' +
|
||||
e.toString());
|
||||
print(s);
|
||||
await clearOutboundGroupSession();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> _storeOutboundGroupSession() async {
|
||||
if (_outboundGroupSession == null) return;
|
||||
await client.database?.storeOutboundGroupSession(
|
||||
client.id,
|
||||
id,
|
||||
_outboundGroupSession.pickle(client.userID),
|
||||
json.encode(_outboundGroupSessionDevices),
|
||||
_outboundGroupSessionCreationTime,
|
||||
_outboundGroupSessionSentMessages);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Clears the existing outboundGroupSession but first checks if the participating
|
||||
/// devices have been changed. Returns false if the session has not been cleared because
|
||||
/// it wasn't necessary.
|
||||
Future<bool> clearOutboundGroupSession({bool wipe = false}) async {
|
||||
if (!wipe && _outboundGroupSessionDevices != null) {
|
||||
// first check if the devices in the room changed
|
||||
var deviceKeys = await getUserDeviceKeys();
|
||||
var outboundGroupSessionDevices = <String>[];
|
||||
for (var keys in deviceKeys) {
|
||||
if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId);
|
||||
}
|
||||
outboundGroupSessionDevices.sort();
|
||||
if (outboundGroupSessionDevices.toString() !=
|
||||
_outboundGroupSessionDevices.toString()) {
|
||||
wipe = true;
|
||||
}
|
||||
// next check if it needs to be rotated
|
||||
final encryptionContent = getState(EventTypes.Encryption)?.content;
|
||||
final maxMessages = encryptionContent != null &&
|
||||
encryptionContent['rotation_period_msgs'] is int
|
||||
? encryptionContent['rotation_period_msgs']
|
||||
: 100;
|
||||
final maxAge = encryptionContent != null &&
|
||||
encryptionContent['rotation_period_ms'] is int
|
||||
? encryptionContent['rotation_period_ms']
|
||||
: 604800000; // default of one week
|
||||
if (_outboundGroupSessionSentMessages >= maxMessages ||
|
||||
_outboundGroupSessionCreationTime
|
||||
.add(Duration(milliseconds: maxAge))
|
||||
.isBefore(DateTime.now())) {
|
||||
wipe = true;
|
||||
}
|
||||
if (!wipe) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!wipe &&
|
||||
_outboundGroupSessionDevices == null &&
|
||||
_outboundGroupSession == null) {
|
||||
return true; // let's just short-circuit out of here, no need to do DB stuff
|
||||
}
|
||||
_outboundGroupSessionDevices = null;
|
||||
await client.database?.removeOutboundGroupSession(client.id, id);
|
||||
_outboundGroupSession?.free();
|
||||
_outboundGroupSession = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Key-Value store of session ids to the session keys. Only m.megolm.v1.aes-sha2
|
||||
/// session keys are supported. They are stored as a Map with the following keys:
|
||||
/// {
|
||||
/// "algorithm": "m.megolm.v1.aes-sha2",
|
||||
/// "room_id": "!Cuyf34gef24t:localhost",
|
||||
/// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
|
||||
/// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
|
||||
/// }
|
||||
Map<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions;
|
||||
final _inboundGroupSessions = <String, SessionKey>{};
|
||||
|
||||
/// Add a new session key to the [sessionKeys].
|
||||
void setInboundGroupSession(String sessionId, Map<String, dynamic> content,
|
||||
{bool forwarded = false}) {
|
||||
if (inboundGroupSessions.containsKey(sessionId)) return;
|
||||
olm.InboundGroupSession inboundGroupSession;
|
||||
if (content['algorithm'] == 'm.megolm.v1.aes-sha2') {
|
||||
try {
|
||||
inboundGroupSession = olm.InboundGroupSession();
|
||||
if (forwarded) {
|
||||
inboundGroupSession.import_session(content['session_key']);
|
||||
} else {
|
||||
inboundGroupSession.create(content['session_key']);
|
||||
}
|
||||
} catch (e) {
|
||||
inboundGroupSession = null;
|
||||
print('[LibOlm] Could not create new InboundGroupSession: ' +
|
||||
e.toString());
|
||||
}
|
||||
}
|
||||
_inboundGroupSessions[sessionId] = SessionKey(
|
||||
content: content,
|
||||
inboundGroupSession: inboundGroupSession,
|
||||
indexes: {},
|
||||
key: client.userID,
|
||||
);
|
||||
client.database?.storeInboundGroupSession(
|
||||
client.id,
|
||||
id,
|
||||
sessionId,
|
||||
inboundGroupSession.pickle(client.userID),
|
||||
json.encode(content),
|
||||
json.encode({}),
|
||||
);
|
||||
_tryAgainDecryptLastMessage();
|
||||
onSessionKeyReceived.add(sessionId);
|
||||
}
|
||||
|
||||
Future<void> _tryAgainDecryptLastMessage() async {
|
||||
if (getState(EventTypes.Encrypted) != null) {
|
||||
await getState(EventTypes.Encrypted).decryptAndStore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [Event] for the given [typeKey] and optional [stateKey].
|
||||
/// If no [stateKey] is provided, it defaults to an empty string.
|
||||
Event getState(String typeKey, [String stateKey = '']) =>
|
||||
|
@ -281,23 +109,13 @@ class Room {
|
|||
/// typeKey/stateKey key pair if there is one.
|
||||
void setState(Event state) {
|
||||
// Decrypt if necessary
|
||||
if (state.type == EventTypes.Encrypted) {
|
||||
if (state.type == EventTypes.Encrypted && client.encryptionEnabled) {
|
||||
try {
|
||||
state = decryptGroupMessage(state);
|
||||
state = client.encryption.decryptRoomEventSync(id, state);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not decrypt room state: ' + e.toString());
|
||||
}
|
||||
}
|
||||
// Check if this is a member change and we need to clear the outboundGroupSession.
|
||||
if (encrypted &&
|
||||
outboundGroupSession != null &&
|
||||
state.type == EventTypes.RoomMember) {
|
||||
var newUser = state.asUser;
|
||||
var oldUser = getState(EventTypes.RoomMember, newUser.id)?.asUser;
|
||||
if (oldUser == null || oldUser.membership != newUser.membership) {
|
||||
clearOutboundGroupSession();
|
||||
}
|
||||
}
|
||||
if ((getState(state.type)?.originServerTs?.millisecondsSinceEpoch ?? 0) >
|
||||
(state.originServerTs?.millisecondsSinceEpoch ?? 1)) {
|
||||
return;
|
||||
|
@ -882,7 +700,8 @@ class Room {
|
|||
// Send the text and on success, store and display a *sent* event.
|
||||
try {
|
||||
final sendMessageContent = encrypted && client.encryptionEnabled
|
||||
? await encryptGroupMessagePayload(content, type: type)
|
||||
? await client.encryption
|
||||
.encryptGroupMessagePayload(id, content, type: type)
|
||||
: content;
|
||||
final res = await client.api.sendMessage(
|
||||
id,
|
||||
|
@ -998,55 +817,42 @@ class Room {
|
|||
if (onHistoryReceived != null) onHistoryReceived();
|
||||
prev_batch = resp.end;
|
||||
|
||||
final dbActions = <Future<dynamic> Function()>[];
|
||||
if (client.database != null) {
|
||||
dbActions.add(
|
||||
() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
|
||||
}
|
||||
|
||||
final loadFn = () async {
|
||||
if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return;
|
||||
|
||||
if (resp.state != null) {
|
||||
for (final state in resp.state) {
|
||||
var eventUpdate = EventUpdate(
|
||||
await EventUpdate(
|
||||
type: 'state',
|
||||
roomID: id,
|
||||
eventType: state.type,
|
||||
content: state.toJson(),
|
||||
sortOrder: oldSortOrder,
|
||||
).decrypt(this);
|
||||
client.onEvent.add(eventUpdate);
|
||||
if (client.database != null) {
|
||||
dbActions.add(
|
||||
() => client.database.storeEventUpdate(client.id, eventUpdate));
|
||||
}
|
||||
).decrypt(this, store: true);
|
||||
}
|
||||
}
|
||||
|
||||
for (final hist in resp.chunk) {
|
||||
var eventUpdate = EventUpdate(
|
||||
final eventUpdate = await EventUpdate(
|
||||
type: 'history',
|
||||
roomID: id,
|
||||
eventType: hist.type,
|
||||
content: hist.toJson(),
|
||||
sortOrder: oldSortOrder,
|
||||
).decrypt(this);
|
||||
).decrypt(this, store: true);
|
||||
client.onEvent.add(eventUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
if (client.database != null) {
|
||||
dbActions.add(
|
||||
() => client.database.storeEventUpdate(client.id, eventUpdate));
|
||||
}
|
||||
}
|
||||
if (client.database != null) {
|
||||
dbActions
|
||||
.add(() => client.database.setRoomPrevBatch(resp.end, client.id, id));
|
||||
}
|
||||
await client.database?.transaction(() async {
|
||||
for (final f in dbActions) {
|
||||
await f();
|
||||
}
|
||||
await client.database.transaction(() async {
|
||||
await client.database.setRoomPrevBatch(resp.end, client.id, id);
|
||||
await loadFn();
|
||||
await updateSortOrder();
|
||||
});
|
||||
} else {
|
||||
await loadFn();
|
||||
}
|
||||
client.onRoomUpdate.add(
|
||||
RoomUpdate(
|
||||
id: id,
|
||||
|
@ -1146,7 +952,6 @@ class Room {
|
|||
}
|
||||
for (final rawState in rawStates) {
|
||||
final newState = Event.fromDb(rawState, newRoom);
|
||||
;
|
||||
newRoom.setState(newState);
|
||||
}
|
||||
}
|
||||
|
@ -1186,13 +991,13 @@ class Room {
|
|||
}
|
||||
|
||||
// Try again to decrypt encrypted events and update the database.
|
||||
if (encrypted && client.database != null) {
|
||||
if (encrypted && client.database != null && client.encryptionEnabled) {
|
||||
await client.database.transaction(() async {
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
if (events[i].type == EventTypes.Encrypted &&
|
||||
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
|
||||
await events[i].loadSession();
|
||||
events[i] = await events[i].decryptAndStore();
|
||||
events[i] = await client.encryption
|
||||
.decryptRoomEvent(id, events[i], store: true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1745,209 +1550,10 @@ class Room {
|
|||
return deviceKeys;
|
||||
}
|
||||
|
||||
bool _restoredOutboundGroupSession = false;
|
||||
|
||||
Future<void> restoreOutboundGroupSession() async {
|
||||
if (_restoredOutboundGroupSession || client.database == null) {
|
||||
return;
|
||||
}
|
||||
final outboundSession =
|
||||
await client.database.getDbOutboundGroupSession(client.id, id);
|
||||
if (outboundSession != null) {
|
||||
try {
|
||||
_outboundGroupSession = olm.OutboundGroupSession();
|
||||
_outboundGroupSession.unpickle(client.userID, outboundSession.pickle);
|
||||
_outboundGroupSessionDevices =
|
||||
List<String>.from(json.decode(outboundSession.deviceIds));
|
||||
_outboundGroupSessionCreationTime = outboundSession.creationTime;
|
||||
_outboundGroupSessionSentMessages = outboundSession.sentMessages;
|
||||
} catch (e) {
|
||||
_outboundGroupSession = null;
|
||||
_outboundGroupSessionDevices = null;
|
||||
print('[LibOlm] Unable to unpickle outboundGroupSession: ' +
|
||||
e.toString());
|
||||
}
|
||||
}
|
||||
_restoredOutboundGroupSession = true;
|
||||
}
|
||||
|
||||
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
|
||||
/// payload. This will create a new outgoingGroupSession if necessary.
|
||||
Future<Map<String, dynamic>> encryptGroupMessagePayload(
|
||||
Map<String, dynamic> payload,
|
||||
{String type = EventTypes.Message}) async {
|
||||
if (!encrypted || !client.encryptionEnabled) return payload;
|
||||
if (encryptionAlgorithm != 'm.megolm.v1.aes-sha2') {
|
||||
throw ('Unknown encryption algorithm');
|
||||
}
|
||||
if (!_restoredOutboundGroupSession && client.database != null) {
|
||||
// try to restore an outbound group session from the database
|
||||
await restoreOutboundGroupSession();
|
||||
}
|
||||
// and clear the outbound session, if it needs clearing
|
||||
await clearOutboundGroupSession();
|
||||
// create a new one if none exists...
|
||||
if (_outboundGroupSession == null) {
|
||||
await createOutboundGroupSession();
|
||||
}
|
||||
final Map<String, dynamic> mRelatesTo = payload.remove('m.relates_to');
|
||||
final payloadContent = {
|
||||
'content': payload,
|
||||
'type': type,
|
||||
'room_id': id,
|
||||
};
|
||||
var encryptedPayload = <String, dynamic>{
|
||||
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||
'ciphertext': _outboundGroupSession.encrypt(json.encode(payloadContent)),
|
||||
'device_id': client.deviceID,
|
||||
'sender_key': client.identityKey,
|
||||
'session_id': _outboundGroupSession.session_id(),
|
||||
if (mRelatesTo != null) 'm.relates_to': mRelatesTo,
|
||||
};
|
||||
_outboundGroupSessionSentMessages++;
|
||||
await _storeOutboundGroupSession();
|
||||
return encryptedPayload;
|
||||
}
|
||||
|
||||
final Set<String> _requestedSessionIds = <String>{};
|
||||
|
||||
Future<void> requestSessionKey(String sessionId, String senderKey) async {
|
||||
await client.keyManager.request(this, sessionId, senderKey);
|
||||
}
|
||||
|
||||
Future<void> loadInboundGroupSessionKey(String sessionId,
|
||||
[String senderKey]) async {
|
||||
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) {
|
||||
return;
|
||||
} // nothing to do
|
||||
final session = await client.database
|
||||
.getDbInboundGroupSession(client.id, id, sessionId);
|
||||
if (session == null) {
|
||||
// no session found, let's request it!
|
||||
if (client.enableE2eeRecovery &&
|
||||
!_requestedSessionIds.contains(sessionId) &&
|
||||
senderKey != null) {
|
||||
unawaited(requestSessionKey(sessionId, senderKey));
|
||||
_requestedSessionIds.add(sessionId);
|
||||
}
|
||||
if (!client.encryptionEnabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_inboundGroupSessions[sessionId] =
|
||||
SessionKey.fromDb(session, client.userID);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
|
||||
await client.encryption.keyManager.request(this, sessionId, senderKey);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadInboundGroupSessionKeyForEvent(Event event) async {
|
||||
if (client.database == null) return; // nothing to do, no database
|
||||
if (event.type != EventTypes.Encrypted) return;
|
||||
if (!client.encryptionEnabled) {
|
||||
throw (DecryptError.NOT_ENABLED);
|
||||
}
|
||||
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
|
||||
throw (DecryptError.UNKNOWN_ALGORITHM);
|
||||
}
|
||||
final String sessionId = event.content['session_id'];
|
||||
return loadInboundGroupSessionKey(sessionId, event.content['sender_key']);
|
||||
}
|
||||
|
||||
/// Decrypts the given [event] with one of the available ingoingGroupSessions.
|
||||
/// Returns a m.bad.encrypted event if it fails and does nothing if the event
|
||||
/// was not encrypted.
|
||||
Event decryptGroupMessage(Event event) {
|
||||
if (event.type != EventTypes.Encrypted ||
|
||||
event.content['ciphertext'] == null) return event;
|
||||
Map<String, dynamic> decryptedPayload;
|
||||
try {
|
||||
if (!client.encryptionEnabled) {
|
||||
throw (DecryptError.NOT_ENABLED);
|
||||
}
|
||||
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
|
||||
throw (DecryptError.UNKNOWN_ALGORITHM);
|
||||
}
|
||||
final String sessionId = event.content['session_id'];
|
||||
if (!inboundGroupSessions.containsKey(sessionId)) {
|
||||
throw (DecryptError.UNKNOWN_SESSION);
|
||||
}
|
||||
final decryptResult = inboundGroupSessions[sessionId]
|
||||
.inboundGroupSession
|
||||
.decrypt(event.content['ciphertext']);
|
||||
final messageIndexKey = event.eventId +
|
||||
event.originServerTs.millisecondsSinceEpoch.toString();
|
||||
if (inboundGroupSessions[sessionId]
|
||||
.indexes
|
||||
.containsKey(messageIndexKey) &&
|
||||
inboundGroupSessions[sessionId].indexes[messageIndexKey] !=
|
||||
decryptResult.message_index) {
|
||||
if ((_outboundGroupSession?.session_id() ?? '') == sessionId) {
|
||||
clearOutboundGroupSession();
|
||||
}
|
||||
throw (DecryptError.CHANNEL_CORRUPTED);
|
||||
}
|
||||
inboundGroupSessions[sessionId].indexes[messageIndexKey] =
|
||||
decryptResult.message_index;
|
||||
// now we persist the udpated indexes into the database.
|
||||
// the entry should always exist. In the case it doesn't, the following
|
||||
// line *could* throw an error. As that is a future, though, and we call
|
||||
// it un-awaited here, nothing happens, which is exactly the result we want
|
||||
client.database?.updateInboundGroupSessionIndexes(
|
||||
json.encode(inboundGroupSessions[sessionId].indexes),
|
||||
client.id,
|
||||
id,
|
||||
sessionId);
|
||||
decryptedPayload = json.decode(decryptResult.plaintext);
|
||||
} catch (exception) {
|
||||
// alright, if this was actually by our own outbound group session, we might as well clear it
|
||||
if (client.enableE2eeRecovery &&
|
||||
(_outboundGroupSession?.session_id() ?? '') ==
|
||||
event.content['session_id']) {
|
||||
clearOutboundGroupSession(wipe: true);
|
||||
}
|
||||
if (exception.toString() == DecryptError.UNKNOWN_SESSION) {
|
||||
decryptedPayload = {
|
||||
'content': event.content,
|
||||
'type': EventTypes.Encrypted,
|
||||
};
|
||||
decryptedPayload['content']['body'] = exception.toString();
|
||||
decryptedPayload['content']['msgtype'] = 'm.bad.encrypted';
|
||||
} else {
|
||||
decryptedPayload = {
|
||||
'content': <String, dynamic>{
|
||||
'msgtype': 'm.bad.encrypted',
|
||||
'body': exception.toString(),
|
||||
},
|
||||
'type': EventTypes.Encrypted,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (event.content['m.relates_to'] != null) {
|
||||
decryptedPayload['content']['m.relates_to'] =
|
||||
event.content['m.relates_to'];
|
||||
}
|
||||
return Event(
|
||||
content: decryptedPayload['content'],
|
||||
type: decryptedPayload['type'],
|
||||
senderId: event.senderId,
|
||||
eventId: event.eventId,
|
||||
roomId: event.roomId,
|
||||
room: event.room,
|
||||
originServerTs: event.originServerTs,
|
||||
unsigned: event.unsigned,
|
||||
stateKey: event.stateKey,
|
||||
prevContent: event.prevContent,
|
||||
status: event.status,
|
||||
sortOrder: event.sortOrder,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.';
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
|
||||
import 'event.dart';
|
||||
import 'room.dart';
|
||||
|
@ -97,12 +98,16 @@ class Timeline {
|
|||
void _sessionKeyReceived(String sessionId) async {
|
||||
var decryptAtLeastOneEvent = false;
|
||||
final decryptFn = () async {
|
||||
if (!room.client.encryptionEnabled) {
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
if (events[i].type == EventTypes.Encrypted &&
|
||||
events[i].messageType == MessageTypes.BadEncrypted &&
|
||||
events[i].content['body'] == DecryptError.UNKNOWN_SESSION &&
|
||||
events[i].content['session_id'] == sessionId) {
|
||||
events[i] = await events[i].decryptAndStore();
|
||||
events[i] = await room.client.encryption
|
||||
.decryptRoomEvent(room.id, events[i], store: true);
|
||||
if (events[i].type != EventTypes.Encrypted) {
|
||||
decryptAtLeastOneEvent = true;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
|
||||
import '../client.dart';
|
||||
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
|
||||
import '../event.dart';
|
||||
import 'key_verification.dart';
|
||||
|
||||
class DeviceKeysList {
|
||||
String userId;
|
||||
|
@ -78,12 +78,6 @@ class DeviceKeys extends MatrixDeviceKeys {
|
|||
|
||||
Future<void> setBlocked(bool newBlocked, Client client) {
|
||||
blocked = newBlocked;
|
||||
for (var room in client.rooms) {
|
||||
if (!room.encrypted) continue;
|
||||
if (room.getParticipants().indexWhere((u) => u.id == userId) != -1) {
|
||||
room.clearOutboundGroupSession();
|
||||
}
|
||||
}
|
||||
return client.database
|
||||
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
|
||||
}
|
||||
|
@ -157,10 +151,10 @@ class DeviceKeys extends MatrixDeviceKeys {
|
|||
}
|
||||
|
||||
KeyVerification startVerification(Client client) {
|
||||
final request =
|
||||
KeyVerification(client: client, userId: userId, deviceId: deviceId);
|
||||
final request = KeyVerification(
|
||||
encryption: client.encryption, userId: userId, deviceId: deviceId);
|
||||
request.start();
|
||||
client.addKeyVerificationRequest(request);
|
||||
client.encryption.keyVerificationManager.addRequest(request);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,13 +42,14 @@ class EventUpdate {
|
|||
EventUpdate(
|
||||
{this.eventType, this.roomID, this.type, this.content, this.sortOrder});
|
||||
|
||||
EventUpdate decrypt(Room room) {
|
||||
if (eventType != EventTypes.Encrypted) {
|
||||
Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
|
||||
if (eventType != EventTypes.Encrypted || !room.client.encryptionEnabled) {
|
||||
return this;
|
||||
}
|
||||
try {
|
||||
var decrpytedEvent =
|
||||
room.decryptGroupMessage(Event.fromJson(content, room, sortOrder));
|
||||
var decrpytedEvent = await room.client.encryption.decryptRoomEvent(
|
||||
room.id, Event.fromJson(content, room, sortOrder),
|
||||
store: store, updateType: type);
|
||||
return EventUpdate(
|
||||
eventType: decrpytedEvent.type,
|
||||
roomID: roomID,
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
@ -134,24 +133,6 @@ void main() {
|
|||
expect(matrix.directChats, matrix.accountData['m.direct'].content);
|
||||
expect(matrix.presences.length, 1);
|
||||
expect(matrix.rooms[1].ephemerals.length, 2);
|
||||
expect(matrix.rooms[1].inboundGroupSessions.length, 1);
|
||||
expect(
|
||||
matrix
|
||||
.rooms[1]
|
||||
.inboundGroupSessions[
|
||||
'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
|
||||
.content['session_key'],
|
||||
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw');
|
||||
if (olmEnabled) {
|
||||
expect(
|
||||
matrix
|
||||
.rooms[1]
|
||||
.inboundGroupSessions[
|
||||
'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
|
||||
.inboundGroupSession !=
|
||||
null,
|
||||
true);
|
||||
}
|
||||
expect(matrix.rooms[1].typingUsers.length, 1);
|
||||
expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com');
|
||||
expect(matrix.rooms[1].roomAccountData.length, 3);
|
||||
|
@ -388,115 +369,6 @@ void main() {
|
|||
'mxc://example.org/SEsfnsuifSDFSSEF');
|
||||
expect(aliceProfile.displayname, 'Alice Margatroid');
|
||||
});
|
||||
|
||||
test('signJson', () {
|
||||
if (matrix.encryptionEnabled) {
|
||||
expect(matrix.fingerprintKey.isNotEmpty, true);
|
||||
expect(matrix.identityKey.isNotEmpty, true);
|
||||
var payload = <String, dynamic>{
|
||||
'unsigned': {
|
||||
'foo': 'bar',
|
||||
},
|
||||
'auth': {
|
||||
'success': true,
|
||||
'mxid': '@john.doe:example.com',
|
||||
'profile': {
|
||||
'display_name': 'John Doe',
|
||||
'three_pids': [
|
||||
{'medium': 'email', 'address': 'john.doe@example.org'},
|
||||
{'medium': 'msisdn', 'address': '123456789'}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
var payloadWithoutUnsigned = Map<String, dynamic>.from(payload);
|
||||
payloadWithoutUnsigned.remove('unsigned');
|
||||
|
||||
expect(
|
||||
matrix.checkJsonSignature(
|
||||
matrix.fingerprintKey, payload, matrix.userID, matrix.deviceID),
|
||||
false);
|
||||
expect(
|
||||
matrix.checkJsonSignature(matrix.fingerprintKey,
|
||||
payloadWithoutUnsigned, matrix.userID, matrix.deviceID),
|
||||
false);
|
||||
payload = matrix.signJson(payload);
|
||||
payloadWithoutUnsigned = matrix.signJson(payloadWithoutUnsigned);
|
||||
expect(payload['signatures'], payloadWithoutUnsigned['signatures']);
|
||||
print(payload['signatures']);
|
||||
expect(
|
||||
matrix.checkJsonSignature(
|
||||
matrix.fingerprintKey, payload, matrix.userID, matrix.deviceID),
|
||||
true);
|
||||
expect(
|
||||
matrix.checkJsonSignature(matrix.fingerprintKey,
|
||||
payloadWithoutUnsigned, matrix.userID, matrix.deviceID),
|
||||
true);
|
||||
}
|
||||
});
|
||||
test('Track oneTimeKeys', () async {
|
||||
if (matrix.encryptionEnabled) {
|
||||
var last = matrix.lastTimeKeysUploaded ?? DateTime.now();
|
||||
await matrix.handleSync(SyncUpdate.fromJson({
|
||||
'device_one_time_keys_count': {'signed_curve25519': 49}
|
||||
}));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(
|
||||
matrix.lastTimeKeysUploaded.millisecondsSinceEpoch >
|
||||
last.millisecondsSinceEpoch,
|
||||
true);
|
||||
}
|
||||
});
|
||||
test('Test invalidate outboundGroupSessions', () async {
|
||||
if (matrix.encryptionEnabled) {
|
||||
expect(matrix.rooms[1].outboundGroupSession == null, true);
|
||||
await matrix.rooms[1].createOutboundGroupSession();
|
||||
expect(matrix.rooms[1].outboundGroupSession != null, true);
|
||||
await matrix.handleSync(SyncUpdate.fromJson({
|
||||
'device_lists': {
|
||||
'changed': [
|
||||
'@alice:example.com',
|
||||
],
|
||||
'left': [
|
||||
'@bob:example.com',
|
||||
],
|
||||
}
|
||||
}));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(matrix.rooms[1].outboundGroupSession != null, true);
|
||||
}
|
||||
});
|
||||
test('Test invalidate outboundGroupSessions', () async {
|
||||
if (matrix.encryptionEnabled) {
|
||||
await matrix.rooms[1].clearOutboundGroupSession(wipe: true);
|
||||
expect(matrix.rooms[1].outboundGroupSession == null, true);
|
||||
await matrix.rooms[1].createOutboundGroupSession();
|
||||
expect(matrix.rooms[1].outboundGroupSession != null, true);
|
||||
await matrix.handleSync(SyncUpdate.fromJson({
|
||||
'rooms': {
|
||||
'join': {
|
||||
'!726s6s6q:example.com': {
|
||||
'state': {
|
||||
'events': [
|
||||
{
|
||||
'content': {'membership': 'leave'},
|
||||
'event_id': '143273582443PhrSn:example.org',
|
||||
'origin_server_ts': 1432735824653,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'sender': '@alice:example.com',
|
||||
'state_key': '@alice:example.com',
|
||||
'type': 'm.room.member'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(matrix.rooms[1].outboundGroupSession != null, true);
|
||||
}
|
||||
});
|
||||
var deviceKeys = DeviceKeys.fromJson({
|
||||
'user_id': '@alice:example.com',
|
||||
'device_id': 'JLAFKJWSCS',
|
||||
|
@ -512,16 +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 {
|
||||
await matrix.sendToDevice(
|
||||
[deviceKeys],
|
||||
|
@ -547,13 +409,6 @@ void main() {
|
|||
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
|
||||
String sessionKey;
|
||||
if (client1.encryptionEnabled) {
|
||||
await client1.rooms[1].createOutboundGroupSession();
|
||||
|
||||
sessionKey = client1.rooms[1].outboundGroupSession.session_key();
|
||||
}
|
||||
|
||||
expect(client1.isLogged(), true);
|
||||
expect(client1.rooms.length, 2);
|
||||
|
||||
|
@ -571,12 +426,9 @@ void main() {
|
|||
expect(client2.deviceID, client1.deviceID);
|
||||
expect(client2.deviceName, client1.deviceName);
|
||||
if (client2.encryptionEnabled) {
|
||||
await client2.rooms[1].restoreOutboundGroupSession();
|
||||
expect(client2.pickledOlmAccount, client1.pickledOlmAccount);
|
||||
expect(json.encode(client2.rooms[1].inboundGroupSessions[sessionKey]),
|
||||
json.encode(client1.rooms[1].inboundGroupSessions[sessionKey]));
|
||||
expect(client2.encryption.pickledOlmAccount,
|
||||
client1.encryption.pickledOlmAccount);
|
||||
expect(client2.rooms[1].id, client1.rooms[1].id);
|
||||
expect(client2.rooms[1].outboundGroupSession.session_key(), sessionKey);
|
||||
}
|
||||
|
||||
await client1.logout();
|
||||
|
|
342
test/encryption/key_request_test.dart
Normal file
342
test/encryption/key_request_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -17,10 +17,12 @@
|
|||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import 'fake_matrix_api.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
import '../fake_database.dart';
|
||||
|
||||
void main() {
|
||||
/// All Tests related to the ChatTime
|
||||
|
@ -36,18 +38,24 @@ void main() {
|
|||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
client.api.homeserver = Uri.parse('https://fakeserver.notexisting');
|
||||
var room = Room(id: '!localpart:server.abc', client: client);
|
||||
var updateCounter = 0;
|
||||
final keyVerification = KeyVerification(
|
||||
client: client,
|
||||
KeyVerification keyVerification;
|
||||
|
||||
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++,
|
||||
);
|
||||
|
||||
if (!olmEnabled) return;
|
||||
});
|
||||
|
||||
test('acceptSas', () async {
|
||||
await keyVerification.acceptSas();
|
||||
|
@ -91,7 +99,7 @@ void main() {
|
|||
test('verifyActivity', () async {
|
||||
final verified = await keyVerification.verifyActivity();
|
||||
expect(verified, true);
|
||||
keyVerification?.dispose();
|
||||
});
|
||||
keyVerification.dispose();
|
||||
});
|
||||
}
|
|
@ -20,6 +20,7 @@ import 'dart:convert';
|
|||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/src/event.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -315,7 +315,7 @@ void main() {
|
|||
|
||||
test('getTimeline', () async {
|
||||
final timeline = await room.getTimeline();
|
||||
expect(timeline.events, []);
|
||||
expect(timeline.events.length, 1);
|
||||
});
|
||||
|
||||
test('getUserByMXID', () async {
|
||||
|
@ -388,60 +388,6 @@ void main() {
|
|||
);
|
||||
expect(room.encrypted, true);
|
||||
expect(room.encryptionAlgorithm, 'm.megolm.v1.aes-sha2');
|
||||
expect(room.outboundGroupSession, null);
|
||||
});
|
||||
|
||||
test('createOutboundGroupSession', () async {
|
||||
if (!room.client.encryptionEnabled) return;
|
||||
await room.createOutboundGroupSession();
|
||||
expect(room.outboundGroupSession != null, true);
|
||||
expect(room.outboundGroupSession.session_id().isNotEmpty, true);
|
||||
expect(
|
||||
room.inboundGroupSessions
|
||||
.containsKey(room.outboundGroupSession.session_id()),
|
||||
true);
|
||||
expect(
|
||||
room.inboundGroupSessions[room.outboundGroupSession.session_id()]
|
||||
.content['session_key'],
|
||||
room.outboundGroupSession.session_key());
|
||||
expect(
|
||||
room.inboundGroupSessions[room.outboundGroupSession.session_id()]
|
||||
.indexes.length,
|
||||
0);
|
||||
});
|
||||
|
||||
test('clearOutboundGroupSession', () async {
|
||||
if (!room.client.encryptionEnabled) return;
|
||||
await room.clearOutboundGroupSession(wipe: true);
|
||||
expect(room.outboundGroupSession == null, true);
|
||||
});
|
||||
|
||||
test('encryptGroupMessagePayload and decryptGroupMessage', () async {
|
||||
if (!room.client.encryptionEnabled) return;
|
||||
final payload = {
|
||||
'msgtype': 'm.text',
|
||||
'body': 'Hello world',
|
||||
};
|
||||
final encryptedPayload = await room.encryptGroupMessagePayload(payload);
|
||||
expect(encryptedPayload['algorithm'], 'm.megolm.v1.aes-sha2');
|
||||
expect(encryptedPayload['ciphertext'].isNotEmpty, true);
|
||||
expect(encryptedPayload['device_id'], room.client.deviceID);
|
||||
expect(encryptedPayload['sender_key'], room.client.identityKey);
|
||||
expect(encryptedPayload['session_id'],
|
||||
room.outboundGroupSession.session_id());
|
||||
|
||||
var encryptedEvent = Event(
|
||||
content: encryptedPayload,
|
||||
type: 'm.room.encrypted',
|
||||
senderId: room.client.userID,
|
||||
eventId: '1234',
|
||||
roomId: room.id,
|
||||
room: room,
|
||||
originServerTs: DateTime.now(),
|
||||
);
|
||||
var decryptedEvent = room.decryptGroupMessage(encryptedEvent);
|
||||
expect(decryptedEvent.type, 'm.room.message');
|
||||
expect(decryptedEvent.content, payload);
|
||||
});
|
||||
|
||||
test('setPushRuleState', () async {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -88,7 +88,8 @@ void test() async {
|
|||
await room.enableEncryption();
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(room.encrypted == true);
|
||||
assert(room.outboundGroupSession == null);
|
||||
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) ==
|
||||
null);
|
||||
|
||||
print('++++ ($testUserA) Check known olm devices ++++');
|
||||
assert(testClientA.userDeviceKeys.containsKey(testUserB));
|
||||
|
@ -123,16 +124,30 @@ void test() async {
|
|||
print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++");
|
||||
await room.sendTextEvent(testMessage);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(room.outboundGroupSession != null);
|
||||
var currentSessionIdA = room.outboundGroupSession.session_id();
|
||||
assert(room.inboundGroupSessions
|
||||
.containsKey(room.outboundGroupSession.session_id()));
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
||||
assert(inviteRoom.inboundGroupSessions
|
||||
.containsKey(room.outboundGroupSession.session_id()));
|
||||
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) !=
|
||||
null);
|
||||
var currentSessionIdA = room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id();
|
||||
assert(room.client.encryption.keyManager
|
||||
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||
.session_id() ==
|
||||
testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
assert(inviteRoom.client.encryption.keyManager
|
||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(room.lastMessage == testMessage);
|
||||
assert(inviteRoom.lastMessage == testMessage);
|
||||
print(
|
||||
|
@ -141,14 +156,27 @@ void test() async {
|
|||
print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++");
|
||||
await room.sendTextEvent(testMessage2);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||
.session_id() ==
|
||||
testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
|
||||
assert(room.outboundGroupSession.session_id() == currentSessionIdA);
|
||||
assert(inviteRoom.inboundGroupSessions
|
||||
.containsKey(room.outboundGroupSession.session_id()));
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() ==
|
||||
currentSessionIdA);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(room.lastMessage == testMessage2);
|
||||
assert(inviteRoom.lastMessage == testMessage2);
|
||||
print(
|
||||
|
@ -157,14 +185,31 @@ void test() async {
|
|||
print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++");
|
||||
await inviteRoom.sendTextEvent(testMessage3);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(room.outboundGroupSession.session_id() == currentSessionIdA);
|
||||
assert(inviteRoom.outboundGroupSession != null);
|
||||
assert(inviteRoom.inboundGroupSessions
|
||||
.containsKey(inviteRoom.outboundGroupSession.session_id()));
|
||||
assert(room.inboundGroupSessions
|
||||
.containsKey(inviteRoom.outboundGroupSession.session_id()));
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() ==
|
||||
currentSessionIdA);
|
||||
var inviteRoomOutboundGroupSession = inviteRoom.client.encryption.keyManager
|
||||
.getOutboundGroupSession(inviteRoom.id);
|
||||
|
||||
assert(inviteRoomOutboundGroupSession != null);
|
||||
assert(inviteRoom.client.encryption.keyManager.getInboundGroupSession(
|
||||
inviteRoom.id,
|
||||
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
||||
'') !=
|
||||
null);
|
||||
assert(room.client.encryption.keyManager.getInboundGroupSession(
|
||||
room.id,
|
||||
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
||||
'') !=
|
||||
null);
|
||||
assert(inviteRoom.lastMessage == testMessage3);
|
||||
assert(room.lastMessage == testMessage3);
|
||||
print(
|
||||
|
@ -180,18 +225,42 @@ void test() async {
|
|||
print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++");
|
||||
await room.sendTextEvent(testMessage4);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
||||
assert(testClientA.olmSessions[testClientC.identityKey].length == 1);
|
||||
assert(testClientC.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.olmSessions[testClientC.identityKey].first.session_id() ==
|
||||
testClientC.olmSessions[testClientA.identityKey].first.session_id());
|
||||
assert(room.outboundGroupSession.session_id() != currentSessionIdA);
|
||||
currentSessionIdA = room.outboundGroupSession.session_id();
|
||||
assert(inviteRoom.inboundGroupSessions
|
||||
.containsKey(room.outboundGroupSession.session_id()));
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||
.session_id() ==
|
||||
testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientC.identityKey].length ==
|
||||
1);
|
||||
assert(testClientC
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientC.identityKey].first
|
||||
.session_id() ==
|
||||
testClientC
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() !=
|
||||
currentSessionIdA);
|
||||
currentSessionIdA = room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id();
|
||||
assert(inviteRoom.client.encryption.keyManager
|
||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(room.lastMessage == testMessage4);
|
||||
assert(inviteRoom.lastMessage == testMessage4);
|
||||
print(
|
||||
|
@ -206,14 +275,30 @@ void test() async {
|
|||
print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++");
|
||||
await room.sendTextEvent(testMessage6);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
||||
assert(room.outboundGroupSession.session_id() != currentSessionIdA);
|
||||
currentSessionIdA = room.outboundGroupSession.session_id();
|
||||
assert(inviteRoom.inboundGroupSessions
|
||||
.containsKey(room.outboundGroupSession.session_id()));
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||
.session_id() ==
|
||||
testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||
.session_id());
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() !=
|
||||
currentSessionIdA);
|
||||
currentSessionIdA = room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id();
|
||||
assert(inviteRoom.client.encryption.keyManager
|
||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(room.lastMessage == testMessage6);
|
||||
assert(inviteRoom.lastMessage == testMessage6);
|
||||
print(
|
||||
|
@ -241,18 +326,18 @@ void test() async {
|
|||
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
|
||||
room.inboundGroupSessions.keys.toList()[i]);
|
||||
}
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id());
|
||||
|
||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++");
|
||||
await restoredRoom.sendTextEvent(testMessage5);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id());
|
||||
assert(restoredRoom.lastMessage == testMessage5);
|
||||
assert(inviteRoom.lastMessage == testMessage5);
|
||||
assert(testClientB.getRoomById(roomId).lastMessage == testMessage5);
|
||||
|
|
Loading…
Reference in a new issue