Merge branch 'soru/modularize-e2ee' into 'master'
split encryption stuff to other library See merge request famedly/famedlysdk!333
This commit is contained in:
commit
b8c58faaab
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';
|
283
lib/encryption/encryption.dart
Normal file
283
lib/encryption/encryption.dart
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2019, 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
import 'key_manager.dart';
|
||||||
|
import 'olm_manager.dart';
|
||||||
|
import 'key_verification_manager.dart';
|
||||||
|
|
||||||
|
class Encryption {
|
||||||
|
final Client client;
|
||||||
|
final bool debug;
|
||||||
|
final bool enableE2eeRecovery;
|
||||||
|
|
||||||
|
bool get enabled => olmManager.enabled;
|
||||||
|
|
||||||
|
/// Returns the base64 encoded keys to store them in a store.
|
||||||
|
/// This String should **never** leave the device!
|
||||||
|
String get pickledOlmAccount => olmManager.pickledOlmAccount;
|
||||||
|
|
||||||
|
String get fingerprintKey => olmManager.fingerprintKey;
|
||||||
|
String get identityKey => olmManager.identityKey;
|
||||||
|
|
||||||
|
KeyManager keyManager;
|
||||||
|
OlmManager olmManager;
|
||||||
|
KeyVerificationManager keyVerificationManager;
|
||||||
|
|
||||||
|
Encryption({
|
||||||
|
this.client,
|
||||||
|
this.debug,
|
||||||
|
this.enableE2eeRecovery,
|
||||||
|
}) {
|
||||||
|
keyManager = KeyManager(this);
|
||||||
|
olmManager = OlmManager(this);
|
||||||
|
keyVerificationManager = KeyVerificationManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> init(String olmAccount) async {
|
||||||
|
await olmManager.init(olmAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
||||||
|
olmManager.handleDeviceOneTimeKeysCount(countJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSync() {
|
||||||
|
keyVerificationManager.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||||
|
if (['m.room_key', 'm.room_key_request', 'm.forwarded_room_key']
|
||||||
|
.contains(event.type)) {
|
||||||
|
// a new room key or thelike. We need to handle this asap, before other
|
||||||
|
// events in /sync are handled
|
||||||
|
await keyManager.handleToDeviceEvent(event);
|
||||||
|
}
|
||||||
|
if (event.type.startsWith('m.key.verification.')) {
|
||||||
|
// some key verification event. No need to handle it now, we can easily
|
||||||
|
// do this in the background
|
||||||
|
unawaited(keyVerificationManager.handleToDeviceEvent(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
|
||||||
|
return await olmManager.decryptToDeviceEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
Event decryptRoomEventSync(String roomId, Event event) {
|
||||||
|
if (event.type != EventTypes.Encrypted ||
|
||||||
|
event.content['ciphertext'] == null) return event;
|
||||||
|
Map<String, dynamic> decryptedPayload;
|
||||||
|
try {
|
||||||
|
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
|
||||||
|
throw (DecryptError.UNKNOWN_ALGORITHM);
|
||||||
|
}
|
||||||
|
final String sessionId = event.content['session_id'];
|
||||||
|
final String senderKey = event.content['sender_key'];
|
||||||
|
final inboundGroupSession =
|
||||||
|
keyManager.getInboundGroupSession(roomId, sessionId, senderKey);
|
||||||
|
if (inboundGroupSession == null) {
|
||||||
|
throw (DecryptError.UNKNOWN_SESSION);
|
||||||
|
}
|
||||||
|
final decryptResult = inboundGroupSession.inboundGroupSession
|
||||||
|
.decrypt(event.content['ciphertext']);
|
||||||
|
final messageIndexKey = event.eventId +
|
||||||
|
event.originServerTs.millisecondsSinceEpoch.toString();
|
||||||
|
var haveIndex = inboundGroupSession.indexes.containsKey(messageIndexKey);
|
||||||
|
if (haveIndex &&
|
||||||
|
inboundGroupSession.indexes[messageIndexKey] !=
|
||||||
|
decryptResult.message_index) {
|
||||||
|
// TODO: maybe clear outbound session, if it is ours
|
||||||
|
throw (DecryptError.CHANNEL_CORRUPTED);
|
||||||
|
}
|
||||||
|
inboundGroupSession.indexes[messageIndexKey] =
|
||||||
|
decryptResult.message_index;
|
||||||
|
if (!haveIndex) {
|
||||||
|
// now we persist the udpated indexes into the database.
|
||||||
|
// the entry should always exist. In the case it doesn't, the following
|
||||||
|
// line *could* throw an error. As that is a future, though, and we call
|
||||||
|
// it un-awaited here, nothing happens, which is exactly the result we want
|
||||||
|
client.database?.updateInboundGroupSessionIndexes(
|
||||||
|
json.encode(inboundGroupSession.indexes),
|
||||||
|
client.id,
|
||||||
|
roomId,
|
||||||
|
sessionId);
|
||||||
|
}
|
||||||
|
decryptedPayload = json.decode(decryptResult.plaintext);
|
||||||
|
} catch (exception) {
|
||||||
|
// alright, if this was actually by our own outbound group session, we might as well clear it
|
||||||
|
if (client.enableE2eeRecovery &&
|
||||||
|
(keyManager
|
||||||
|
.getOutboundGroupSession(roomId)
|
||||||
|
?.outboundGroupSession
|
||||||
|
?.session_id() ??
|
||||||
|
'') ==
|
||||||
|
event.content['session_id']) {
|
||||||
|
keyManager.clearOutboundGroupSession(roomId, wipe: true);
|
||||||
|
}
|
||||||
|
if (exception.toString() == DecryptError.UNKNOWN_SESSION) {
|
||||||
|
decryptedPayload = {
|
||||||
|
'content': event.content,
|
||||||
|
'type': EventTypes.Encrypted,
|
||||||
|
};
|
||||||
|
decryptedPayload['content']['body'] = exception.toString();
|
||||||
|
decryptedPayload['content']['msgtype'] = 'm.bad.encrypted';
|
||||||
|
} else {
|
||||||
|
decryptedPayload = {
|
||||||
|
'content': <String, dynamic>{
|
||||||
|
'msgtype': 'm.bad.encrypted',
|
||||||
|
'body': exception.toString(),
|
||||||
|
},
|
||||||
|
'type': EventTypes.Encrypted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.content['m.relates_to'] != null) {
|
||||||
|
decryptedPayload['content']['m.relates_to'] =
|
||||||
|
event.content['m.relates_to'];
|
||||||
|
}
|
||||||
|
return Event(
|
||||||
|
content: decryptedPayload['content'],
|
||||||
|
type: decryptedPayload['type'],
|
||||||
|
senderId: event.senderId,
|
||||||
|
eventId: event.eventId,
|
||||||
|
roomId: event.roomId,
|
||||||
|
room: event.room,
|
||||||
|
originServerTs: event.originServerTs,
|
||||||
|
unsigned: event.unsigned,
|
||||||
|
stateKey: event.stateKey,
|
||||||
|
prevContent: event.prevContent,
|
||||||
|
status: event.status,
|
||||||
|
sortOrder: event.sortOrder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Event> decryptRoomEvent(String roomId, Event event,
|
||||||
|
{bool store = false, String updateType = 'timeline'}) async {
|
||||||
|
final doStore = () async {
|
||||||
|
await client.database?.storeEventUpdate(
|
||||||
|
client.id,
|
||||||
|
EventUpdate(
|
||||||
|
eventType: event.type,
|
||||||
|
content: event.toJson(),
|
||||||
|
roomID: event.roomId,
|
||||||
|
type: updateType,
|
||||||
|
sortOrder: event.sortOrder,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (updateType != 'history') {
|
||||||
|
event.room?.setState(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (event.type != EventTypes.Encrypted) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
event = decryptRoomEventSync(roomId, event);
|
||||||
|
if (event.type != EventTypes.Encrypted) {
|
||||||
|
if (store) {
|
||||||
|
await doStore();
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
if (client.database == null) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
await keyManager.loadInboundGroupSession(
|
||||||
|
roomId, event.content['session_id'], event.content['sender_key']);
|
||||||
|
event = decryptRoomEventSync(roomId, event);
|
||||||
|
if (event.type != EventTypes.Encrypted && store) {
|
||||||
|
await doStore();
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
|
||||||
|
/// payload. This will create a new outgoingGroupSession if necessary.
|
||||||
|
Future<Map<String, dynamic>> encryptGroupMessagePayload(
|
||||||
|
String roomId, Map<String, dynamic> payload,
|
||||||
|
{String type = EventTypes.Message}) async {
|
||||||
|
final room = client.getRoomById(roomId);
|
||||||
|
if (room == null || !room.encrypted || !enabled) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
if (room.encryptionAlgorithm != 'm.megolm.v1.aes-sha2') {
|
||||||
|
throw ('Unknown encryption algorithm');
|
||||||
|
}
|
||||||
|
if (keyManager.getOutboundGroupSession(roomId) == null) {
|
||||||
|
await keyManager.loadOutboundGroupSession(roomId);
|
||||||
|
}
|
||||||
|
await keyManager.clearOutboundGroupSession(roomId);
|
||||||
|
if (keyManager.getOutboundGroupSession(roomId) == null) {
|
||||||
|
await keyManager.createOutboundGroupSession(roomId);
|
||||||
|
}
|
||||||
|
final sess = keyManager.getOutboundGroupSession(roomId);
|
||||||
|
if (sess == null) {
|
||||||
|
throw ('Unable to create new outbound group session');
|
||||||
|
}
|
||||||
|
final Map<String, dynamic> mRelatesTo = payload.remove('m.relates_to');
|
||||||
|
final payloadContent = {
|
||||||
|
'content': payload,
|
||||||
|
'type': type,
|
||||||
|
'room_id': roomId,
|
||||||
|
};
|
||||||
|
var encryptedPayload = <String, dynamic>{
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'ciphertext':
|
||||||
|
sess.outboundGroupSession.encrypt(json.encode(payloadContent)),
|
||||||
|
'device_id': client.deviceID,
|
||||||
|
'sender_key': identityKey,
|
||||||
|
'session_id': sess.outboundGroupSession.session_id(),
|
||||||
|
if (mRelatesTo != null) 'm.relates_to': mRelatesTo,
|
||||||
|
};
|
||||||
|
sess.sentMessages++;
|
||||||
|
await keyManager.storeOutboundGroupSession(roomId, sess);
|
||||||
|
return encryptedPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
|
||||||
|
DeviceKeys device, String type, Map<String, dynamic> payload) async {
|
||||||
|
return await olmManager.encryptToDeviceMessagePayload(
|
||||||
|
device, type, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> encryptToDeviceMessage(
|
||||||
|
List<DeviceKeys> deviceKeys,
|
||||||
|
String type,
|
||||||
|
Map<String, dynamic> payload) async {
|
||||||
|
return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
keyManager.dispose();
|
||||||
|
olmManager.dispose();
|
||||||
|
keyVerificationManager.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class DecryptError {
|
||||||
|
static const String NOT_ENABLED = 'Encryption is not enabled in your client.';
|
||||||
|
static const String UNKNOWN_ALGORITHM = 'Unknown encryption algorithm.';
|
||||||
|
static const String UNKNOWN_SESSION =
|
||||||
|
'The sender has not sent us the session key.';
|
||||||
|
static const String CHANNEL_CORRUPTED =
|
||||||
|
'The secure channel with the sender was corrupted.';
|
||||||
|
}
|
517
lib/encryption/key_manager.dart
Normal file
517
lib/encryption/key_manager.dart
Normal file
|
@ -0,0 +1,517 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2019, 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import './encryption.dart';
|
||||||
|
import './utils/session_key.dart';
|
||||||
|
import './utils/outbound_group_session.dart';
|
||||||
|
|
||||||
|
class KeyManager {
|
||||||
|
final Encryption encryption;
|
||||||
|
Client get client => encryption.client;
|
||||||
|
final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
||||||
|
final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
||||||
|
final _inboundGroupSessions = <String, Map<String, SessionKey>>{};
|
||||||
|
final _outboundGroupSessions = <String, OutboundGroupSession>{};
|
||||||
|
final Set<String> _loadedOutboundGroupSessions = <String>{};
|
||||||
|
final Set<String> _requestedSessionIds = <String>{};
|
||||||
|
|
||||||
|
KeyManager(this.encryption);
|
||||||
|
|
||||||
|
/// clear all cached inbound group sessions. useful for testing
|
||||||
|
void clearInboundGroupSessions() {
|
||||||
|
_inboundGroupSessions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInboundGroupSession(String roomId, String sessionId, String senderKey,
|
||||||
|
Map<String, dynamic> content,
|
||||||
|
{bool forwarded = false}) {
|
||||||
|
final oldSession =
|
||||||
|
getInboundGroupSession(roomId, sessionId, senderKey, otherRooms: false);
|
||||||
|
if (oldSession != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (content['algorithm'] != 'm.megolm.v1.aes-sha2') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
olm.InboundGroupSession inboundGroupSession;
|
||||||
|
try {
|
||||||
|
inboundGroupSession = olm.InboundGroupSession();
|
||||||
|
if (forwarded) {
|
||||||
|
inboundGroupSession.import_session(content['session_key']);
|
||||||
|
} else {
|
||||||
|
inboundGroupSession.create(content['session_key']);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
inboundGroupSession.free();
|
||||||
|
print(
|
||||||
|
'[LibOlm] Could not create new InboundGroupSession: ' + e.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_inboundGroupSessions.containsKey(roomId)) {
|
||||||
|
_inboundGroupSessions[roomId] = <String, SessionKey>{};
|
||||||
|
}
|
||||||
|
_inboundGroupSessions[roomId][sessionId] = SessionKey(
|
||||||
|
content: content,
|
||||||
|
inboundGroupSession: inboundGroupSession,
|
||||||
|
indexes: {},
|
||||||
|
key: client.userID,
|
||||||
|
);
|
||||||
|
client.database?.storeInboundGroupSession(
|
||||||
|
client.id,
|
||||||
|
roomId,
|
||||||
|
sessionId,
|
||||||
|
inboundGroupSession.pickle(client.userID),
|
||||||
|
json.encode(content),
|
||||||
|
json.encode({}),
|
||||||
|
);
|
||||||
|
// TODO: somehow try to decrypt last message again
|
||||||
|
final room = client.getRoomById(roomId);
|
||||||
|
if (room != null) {
|
||||||
|
room.onSessionKeyReceived.add(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionKey getInboundGroupSession(
|
||||||
|
String roomId, String sessionId, String senderKey,
|
||||||
|
{bool otherRooms = true}) {
|
||||||
|
if (_inboundGroupSessions.containsKey(roomId) &&
|
||||||
|
_inboundGroupSessions[roomId].containsKey(sessionId)) {
|
||||||
|
return _inboundGroupSessions[roomId][sessionId];
|
||||||
|
}
|
||||||
|
if (!otherRooms) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// search if this session id is *somehow* found in another room
|
||||||
|
for (final val in _inboundGroupSessions.values) {
|
||||||
|
if (val.containsKey(sessionId)) {
|
||||||
|
return val[sessionId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads an inbound group session
|
||||||
|
Future<SessionKey> loadInboundGroupSession(
|
||||||
|
String roomId, String sessionId, String senderKey) async {
|
||||||
|
if (roomId == null || sessionId == null || senderKey == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (_inboundGroupSessions.containsKey(roomId) &&
|
||||||
|
_inboundGroupSessions[roomId].containsKey(sessionId)) {
|
||||||
|
return _inboundGroupSessions[roomId][sessionId]; // nothing to do
|
||||||
|
}
|
||||||
|
final session = await client.database
|
||||||
|
?.getDbInboundGroupSession(client.id, roomId, sessionId);
|
||||||
|
if (session == null) {
|
||||||
|
final room = client.getRoomById(roomId);
|
||||||
|
final requestIdent = '$roomId|$sessionId|$senderKey';
|
||||||
|
if (client.enableE2eeRecovery &&
|
||||||
|
room != null &&
|
||||||
|
!_requestedSessionIds.contains(requestIdent)) {
|
||||||
|
// do e2ee recovery
|
||||||
|
_requestedSessionIds.add(requestIdent);
|
||||||
|
unawaited(request(room, sessionId, senderKey));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!_inboundGroupSessions.containsKey(roomId)) {
|
||||||
|
_inboundGroupSessions[roomId] = <String, SessionKey>{};
|
||||||
|
}
|
||||||
|
final sess = SessionKey.fromDb(session, client.userID);
|
||||||
|
if (!sess.isValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_inboundGroupSessions[roomId][sessionId] = sess;
|
||||||
|
return sess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// clear all cached inbound group sessions. useful for testing
|
||||||
|
void clearOutboundGroupSessions() {
|
||||||
|
_outboundGroupSessions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the existing outboundGroupSession but first checks if the participating
|
||||||
|
/// devices have been changed. Returns false if the session has not been cleared because
|
||||||
|
/// it wasn't necessary.
|
||||||
|
Future<bool> clearOutboundGroupSession(String roomId,
|
||||||
|
{bool wipe = false}) async {
|
||||||
|
final room = client.getRoomById(roomId);
|
||||||
|
final sess = getOutboundGroupSession(roomId);
|
||||||
|
if (room == null || sess == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!wipe) {
|
||||||
|
// first check if the devices in the room changed
|
||||||
|
final deviceKeys = await room.getUserDeviceKeys();
|
||||||
|
deviceKeys.removeWhere((k) => k.blocked);
|
||||||
|
final deviceKeyIds = deviceKeys.map((k) => k.deviceId).toList();
|
||||||
|
deviceKeyIds.sort();
|
||||||
|
if (deviceKeyIds.toString() != sess.devices.toString()) {
|
||||||
|
wipe = true;
|
||||||
|
}
|
||||||
|
// next check if it needs to be rotated
|
||||||
|
final encryptionContent = room.getState(EventTypes.Encryption)?.content;
|
||||||
|
final maxMessages = encryptionContent != null &&
|
||||||
|
encryptionContent['rotation_period_msgs'] is int
|
||||||
|
? encryptionContent['rotation_period_msgs']
|
||||||
|
: 100;
|
||||||
|
final maxAge = encryptionContent != null &&
|
||||||
|
encryptionContent['rotation_period_ms'] is int
|
||||||
|
? encryptionContent['rotation_period_ms']
|
||||||
|
: 604800000; // default of one week
|
||||||
|
if (sess.sentMessages >= maxMessages ||
|
||||||
|
sess.creationTime
|
||||||
|
.add(Duration(milliseconds: maxAge))
|
||||||
|
.isBefore(DateTime.now())) {
|
||||||
|
wipe = true;
|
||||||
|
}
|
||||||
|
if (!wipe) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sess.dispose();
|
||||||
|
_outboundGroupSessions.remove(roomId);
|
||||||
|
await client.database?.removeOutboundGroupSession(client.id, roomId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> storeOutboundGroupSession(
|
||||||
|
String roomId, OutboundGroupSession sess) async {
|
||||||
|
if (sess == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await client.database?.storeOutboundGroupSession(
|
||||||
|
client.id,
|
||||||
|
roomId,
|
||||||
|
sess.outboundGroupSession.pickle(client.userID),
|
||||||
|
json.encode(sess.devices),
|
||||||
|
sess.creationTime,
|
||||||
|
sess.sentMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OutboundGroupSession> createOutboundGroupSession(String roomId) async {
|
||||||
|
await clearOutboundGroupSession(roomId, wipe: true);
|
||||||
|
final room = client.getRoomById(roomId);
|
||||||
|
if (room == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final deviceKeys = await room.getUserDeviceKeys();
|
||||||
|
deviceKeys.removeWhere((k) => k.blocked);
|
||||||
|
final deviceKeyIds = deviceKeys.map((k) => k.deviceId).toList();
|
||||||
|
deviceKeyIds.sort();
|
||||||
|
final outboundGroupSession = olm.OutboundGroupSession();
|
||||||
|
try {
|
||||||
|
outboundGroupSession.create();
|
||||||
|
} catch (e) {
|
||||||
|
outboundGroupSession.free();
|
||||||
|
print('[LibOlm] Unable to create new outboundGroupSession: ' +
|
||||||
|
e.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final rawSession = <String, dynamic>{
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': room.id,
|
||||||
|
'session_id': outboundGroupSession.session_id(),
|
||||||
|
'session_key': outboundGroupSession.session_key(),
|
||||||
|
};
|
||||||
|
setInboundGroupSession(
|
||||||
|
roomId, rawSession['session_id'], encryption.identityKey, rawSession);
|
||||||
|
final sess = OutboundGroupSession(
|
||||||
|
devices: deviceKeyIds,
|
||||||
|
creationTime: DateTime.now(),
|
||||||
|
outboundGroupSession: outboundGroupSession,
|
||||||
|
sentMessages: 0,
|
||||||
|
key: client.userID,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
|
||||||
|
await storeOutboundGroupSession(roomId, sess);
|
||||||
|
_outboundGroupSessions[roomId] = sess;
|
||||||
|
} catch (e, s) {
|
||||||
|
print(
|
||||||
|
'[LibOlm] Unable to send the session key to the participating devices: ' +
|
||||||
|
e.toString());
|
||||||
|
print(s);
|
||||||
|
sess.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sess;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutboundGroupSession getOutboundGroupSession(String roomId) {
|
||||||
|
return _outboundGroupSessions[roomId];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadOutboundGroupSession(String roomId) async {
|
||||||
|
if (_loadedOutboundGroupSessions.contains(roomId) ||
|
||||||
|
_outboundGroupSessions.containsKey(roomId) ||
|
||||||
|
client.database == null) {
|
||||||
|
return; // nothing to do
|
||||||
|
}
|
||||||
|
_loadedOutboundGroupSessions.add(roomId);
|
||||||
|
final session =
|
||||||
|
await client.database.getDbOutboundGroupSession(client.id, roomId);
|
||||||
|
if (session == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final sess = OutboundGroupSession.fromDb(session, client.userID);
|
||||||
|
if (!sess.isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_outboundGroupSessions[roomId] = sess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request a certain key from another device
|
||||||
|
Future<void> request(Room room, String sessionId, String senderKey) async {
|
||||||
|
// while we just send the to-device event to '*', we still need to save the
|
||||||
|
// devices themself to know where to send the cancel to after receiving a reply
|
||||||
|
final devices = await room.getUserDeviceKeys();
|
||||||
|
final requestId = client.generateUniqueTransactionId();
|
||||||
|
final request = KeyManagerKeyShareRequest(
|
||||||
|
requestId: requestId,
|
||||||
|
devices: devices,
|
||||||
|
room: room,
|
||||||
|
sessionId: sessionId,
|
||||||
|
senderKey: senderKey,
|
||||||
|
);
|
||||||
|
await client.sendToDevice(
|
||||||
|
[],
|
||||||
|
'm.room_key_request',
|
||||||
|
{
|
||||||
|
'action': 'request',
|
||||||
|
'body': {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': room.id,
|
||||||
|
'sender_key': senderKey,
|
||||||
|
'session_id': sessionId,
|
||||||
|
},
|
||||||
|
'request_id': requestId,
|
||||||
|
'requesting_device_id': client.deviceID,
|
||||||
|
},
|
||||||
|
encrypted: false,
|
||||||
|
toUsers: await room.requestParticipants());
|
||||||
|
outgoingShareRequests[request.requestId] = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle an incoming to_device event that is related to key sharing
|
||||||
|
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||||
|
if (event.type == 'm.room_key_request') {
|
||||||
|
if (!event.content.containsKey('request_id')) {
|
||||||
|
return; // invalid event
|
||||||
|
}
|
||||||
|
if (event.content['action'] == 'request') {
|
||||||
|
// we are *receiving* a request
|
||||||
|
if (!event.content.containsKey('body')) {
|
||||||
|
return; // no body
|
||||||
|
}
|
||||||
|
if (!client.userDeviceKeys.containsKey(event.sender) ||
|
||||||
|
!client.userDeviceKeys[event.sender].deviceKeys
|
||||||
|
.containsKey(event.content['requesting_device_id'])) {
|
||||||
|
return; // device not found
|
||||||
|
}
|
||||||
|
final device = client.userDeviceKeys[event.sender]
|
||||||
|
.deviceKeys[event.content['requesting_device_id']];
|
||||||
|
if (device.userId == client.userID &&
|
||||||
|
device.deviceId == client.deviceID) {
|
||||||
|
return; // ignore requests by ourself
|
||||||
|
}
|
||||||
|
final room = client.getRoomById(event.content['body']['room_id']);
|
||||||
|
if (room == null) {
|
||||||
|
return; // unknown room
|
||||||
|
}
|
||||||
|
final sessionId = event.content['body']['session_id'];
|
||||||
|
final senderKey = event.content['body']['sender_key'];
|
||||||
|
// okay, let's see if we have this session at all
|
||||||
|
if ((await loadInboundGroupSession(room.id, sessionId, senderKey)) ==
|
||||||
|
null) {
|
||||||
|
return; // we don't have this session anyways
|
||||||
|
}
|
||||||
|
final request = KeyManagerKeyShareRequest(
|
||||||
|
requestId: event.content['request_id'],
|
||||||
|
devices: [device],
|
||||||
|
room: room,
|
||||||
|
sessionId: sessionId,
|
||||||
|
senderKey: senderKey,
|
||||||
|
);
|
||||||
|
if (incomingShareRequests.containsKey(request.requestId)) {
|
||||||
|
return; // we don't want to process one and the same request multiple times
|
||||||
|
}
|
||||||
|
incomingShareRequests[request.requestId] = request;
|
||||||
|
final roomKeyRequest =
|
||||||
|
RoomKeyRequest.fromToDeviceEvent(event, this, request);
|
||||||
|
if (device.userId == client.userID &&
|
||||||
|
device.verified &&
|
||||||
|
!device.blocked) {
|
||||||
|
// alright, we can forward the key
|
||||||
|
await roomKeyRequest.forwardKey();
|
||||||
|
} else {
|
||||||
|
client.onRoomKeyRequest
|
||||||
|
.add(roomKeyRequest); // let the client handle this
|
||||||
|
}
|
||||||
|
} else if (event.content['action'] == 'request_cancellation') {
|
||||||
|
// we got told to cancel an incoming request
|
||||||
|
if (!incomingShareRequests.containsKey(event.content['request_id'])) {
|
||||||
|
return; // we don't know this request anyways
|
||||||
|
}
|
||||||
|
// alright, let's just cancel this request
|
||||||
|
final request = incomingShareRequests[event.content['request_id']];
|
||||||
|
request.canceled = true;
|
||||||
|
incomingShareRequests.remove(request.requestId);
|
||||||
|
}
|
||||||
|
} else if (event.type == 'm.forwarded_room_key') {
|
||||||
|
// we *received* an incoming key request
|
||||||
|
if (event.encryptedContent == null) {
|
||||||
|
return; // event wasn't encrypted, this is a security risk
|
||||||
|
}
|
||||||
|
final request = outgoingShareRequests.values.firstWhere(
|
||||||
|
(r) =>
|
||||||
|
r.room.id == event.content['room_id'] &&
|
||||||
|
r.sessionId == event.content['session_id'] &&
|
||||||
|
r.senderKey == event.content['sender_key'],
|
||||||
|
orElse: () => null);
|
||||||
|
if (request == null || request.canceled) {
|
||||||
|
return; // no associated request found or it got canceled
|
||||||
|
}
|
||||||
|
final device = request.devices.firstWhere(
|
||||||
|
(d) =>
|
||||||
|
d.userId == event.sender &&
|
||||||
|
d.curve25519Key == event.encryptedContent['sender_key'],
|
||||||
|
orElse: () => null);
|
||||||
|
if (device == null) {
|
||||||
|
return; // someone we didn't send our request to replied....better ignore this
|
||||||
|
}
|
||||||
|
// TODO: verify that the keys work to decrypt a message
|
||||||
|
// alright, all checks out, let's go ahead and store this session
|
||||||
|
setInboundGroupSession(
|
||||||
|
request.room.id, request.sessionId, request.senderKey, event.content,
|
||||||
|
forwarded: true);
|
||||||
|
request.devices.removeWhere(
|
||||||
|
(k) => k.userId == device.userId && k.deviceId == device.deviceId);
|
||||||
|
outgoingShareRequests.remove(request.requestId);
|
||||||
|
// send cancel to all other devices
|
||||||
|
if (request.devices.isEmpty) {
|
||||||
|
return; // no need to send any cancellation
|
||||||
|
}
|
||||||
|
await client.sendToDevice(
|
||||||
|
request.devices,
|
||||||
|
'm.room_key_request',
|
||||||
|
{
|
||||||
|
'action': 'request_cancellation',
|
||||||
|
'request_id': request.requestId,
|
||||||
|
'requesting_device_id': client.deviceID,
|
||||||
|
},
|
||||||
|
encrypted: false);
|
||||||
|
} else if (event.type == 'm.room_key') {
|
||||||
|
if (event.encryptedContent == null) {
|
||||||
|
return; // the event wasn't encrypted, this is a security risk;
|
||||||
|
}
|
||||||
|
final String roomId = event.content['room_id'];
|
||||||
|
final String sessionId = event.content['session_id'];
|
||||||
|
if (client.userDeviceKeys.containsKey(event.sender) &&
|
||||||
|
client.userDeviceKeys[event.sender].deviceKeys
|
||||||
|
.containsKey(event.content['requesting_device_id'])) {
|
||||||
|
event.content['sender_claimed_ed25519_key'] = client
|
||||||
|
.userDeviceKeys[event.sender]
|
||||||
|
.deviceKeys[event.content['requesting_device_id']]
|
||||||
|
.ed25519Key;
|
||||||
|
}
|
||||||
|
setInboundGroupSession(roomId, sessionId,
|
||||||
|
event.encryptedContent['sender_key'], event.content,
|
||||||
|
forwarded: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
for (final sess in _outboundGroupSessions.values) {
|
||||||
|
sess.dispose();
|
||||||
|
}
|
||||||
|
for (final entries in _inboundGroupSessions.values) {
|
||||||
|
for (final sess in entries.values) {
|
||||||
|
sess.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KeyManagerKeyShareRequest {
|
||||||
|
final String requestId;
|
||||||
|
final List<DeviceKeys> devices;
|
||||||
|
final Room room;
|
||||||
|
final String sessionId;
|
||||||
|
final String senderKey;
|
||||||
|
bool canceled;
|
||||||
|
|
||||||
|
KeyManagerKeyShareRequest(
|
||||||
|
{this.requestId,
|
||||||
|
this.devices,
|
||||||
|
this.room,
|
||||||
|
this.sessionId,
|
||||||
|
this.senderKey,
|
||||||
|
this.canceled = false});
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomKeyRequest extends ToDeviceEvent {
|
||||||
|
KeyManager keyManager;
|
||||||
|
KeyManagerKeyShareRequest request;
|
||||||
|
RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent,
|
||||||
|
KeyManager keyManager, KeyManagerKeyShareRequest request) {
|
||||||
|
this.keyManager = keyManager;
|
||||||
|
this.request = request;
|
||||||
|
sender = toDeviceEvent.sender;
|
||||||
|
content = toDeviceEvent.content;
|
||||||
|
type = toDeviceEvent.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
Room get room => request.room;
|
||||||
|
|
||||||
|
DeviceKeys get requestingDevice => request.devices.first;
|
||||||
|
|
||||||
|
Future<void> forwardKey() async {
|
||||||
|
if (request.canceled) {
|
||||||
|
keyManager.incomingShareRequests.remove(request.requestId);
|
||||||
|
return; // request is canceled, don't send anything
|
||||||
|
}
|
||||||
|
var room = this.room;
|
||||||
|
final session = await keyManager.loadInboundGroupSession(
|
||||||
|
room.id, request.sessionId, request.senderKey);
|
||||||
|
var forwardedKeys = <dynamic>[keyManager.encryption.identityKey];
|
||||||
|
for (final key in session.forwardingCurve25519KeyChain) {
|
||||||
|
forwardedKeys.add(key);
|
||||||
|
}
|
||||||
|
await requestingDevice.setVerified(true, keyManager.client);
|
||||||
|
var message = session.content;
|
||||||
|
message['forwarding_curve25519_key_chain'] = forwardedKeys;
|
||||||
|
|
||||||
|
message['session_key'] = session.inboundGroupSession
|
||||||
|
.export_session(session.inboundGroupSession.first_known_index());
|
||||||
|
// send the actual reply of the key back to the requester
|
||||||
|
await keyManager.client.sendToDevice(
|
||||||
|
[requestingDevice],
|
||||||
|
'm.forwarded_room_key',
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
keyManager.incomingShareRequests.remove(request.requestId);
|
||||||
|
}
|
||||||
|
}
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
433
lib/encryption/olm_manager.dart
Normal file
433
lib/encryption/olm_manager.dart
Normal file
|
@ -0,0 +1,433 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2019, 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:canonical_json/canonical_json.dart';
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
import './encryption.dart';
|
||||||
|
|
||||||
|
class OlmManager {
|
||||||
|
final Encryption encryption;
|
||||||
|
Client get client => encryption.client;
|
||||||
|
olm.Account _olmAccount;
|
||||||
|
|
||||||
|
/// Returns the base64 encoded keys to store them in a store.
|
||||||
|
/// This String should **never** leave the device!
|
||||||
|
String get pickledOlmAccount =>
|
||||||
|
enabled ? _olmAccount.pickle(client.userID) : null;
|
||||||
|
String get fingerprintKey =>
|
||||||
|
enabled ? json.decode(_olmAccount.identity_keys())['ed25519'] : null;
|
||||||
|
String get identityKey =>
|
||||||
|
enabled ? json.decode(_olmAccount.identity_keys())['curve25519'] : null;
|
||||||
|
|
||||||
|
bool get enabled => _olmAccount != null;
|
||||||
|
|
||||||
|
OlmManager(this.encryption);
|
||||||
|
|
||||||
|
/// A map from Curve25519 identity keys to existing olm sessions.
|
||||||
|
Map<String, List<olm.Session>> get olmSessions => _olmSessions;
|
||||||
|
final Map<String, List<olm.Session>> _olmSessions = {};
|
||||||
|
|
||||||
|
Future<void> init(String olmAccount) async {
|
||||||
|
if (olmAccount == null) {
|
||||||
|
try {
|
||||||
|
await olm.init();
|
||||||
|
_olmAccount = olm.Account();
|
||||||
|
_olmAccount.create();
|
||||||
|
if (await uploadKeys(uploadDeviceKeys: true) == false) {
|
||||||
|
throw ('Upload key failed');
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
_olmAccount?.free();
|
||||||
|
_olmAccount = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await olm.init();
|
||||||
|
_olmAccount = olm.Account();
|
||||||
|
_olmAccount.unpickle(client.userID, olmAccount);
|
||||||
|
} catch (_) {
|
||||||
|
_olmAccount?.free();
|
||||||
|
_olmAccount = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a signature to this json from this olm account.
|
||||||
|
Map<String, dynamic> signJson(Map<String, dynamic> payload) {
|
||||||
|
if (!enabled) throw ('Encryption is disabled');
|
||||||
|
final Map<String, dynamic> unsigned = payload['unsigned'];
|
||||||
|
final Map<String, dynamic> signatures = payload['signatures'];
|
||||||
|
payload.remove('unsigned');
|
||||||
|
payload.remove('signatures');
|
||||||
|
final canonical = canonicalJson.encode(payload);
|
||||||
|
final signature = _olmAccount.sign(String.fromCharCodes(canonical));
|
||||||
|
if (signatures != null) {
|
||||||
|
payload['signatures'] = signatures;
|
||||||
|
} else {
|
||||||
|
payload['signatures'] = <String, dynamic>{};
|
||||||
|
}
|
||||||
|
if (!payload['signatures'].containsKey(client.userID)) {
|
||||||
|
payload['signatures'][client.userID] = <String, dynamic>{};
|
||||||
|
}
|
||||||
|
payload['signatures'][client.userID]['ed25519:${client.deviceID}'] =
|
||||||
|
signature;
|
||||||
|
if (unsigned != null) {
|
||||||
|
payload['unsigned'] = unsigned;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the signature of a signed json object.
|
||||||
|
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
|
||||||
|
String userId, String deviceId) {
|
||||||
|
if (!enabled) throw ('Encryption is disabled');
|
||||||
|
final Map<String, dynamic> signatures = signedJson['signatures'];
|
||||||
|
if (signatures == null || !signatures.containsKey(userId)) return false;
|
||||||
|
signedJson.remove('unsigned');
|
||||||
|
signedJson.remove('signatures');
|
||||||
|
if (!signatures[userId].containsKey('ed25519:$deviceId')) return false;
|
||||||
|
final String signature = signatures[userId]['ed25519:$deviceId'];
|
||||||
|
final canonical = canonicalJson.encode(signedJson);
|
||||||
|
final message = String.fromCharCodes(canonical);
|
||||||
|
var isValid = false;
|
||||||
|
final olmutil = olm.Utility();
|
||||||
|
try {
|
||||||
|
olmutil.ed25519_verify(key, message, signature);
|
||||||
|
isValid = true;
|
||||||
|
} catch (e) {
|
||||||
|
isValid = false;
|
||||||
|
print('[LibOlm] Signature check failed: ' + e.toString());
|
||||||
|
} finally {
|
||||||
|
olmutil.free();
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates new one time keys, signs everything and upload it to the server.
|
||||||
|
Future<bool> uploadKeys(
|
||||||
|
{bool uploadDeviceKeys = false, int oldKeyCount = 0}) async {
|
||||||
|
if (!enabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate one-time keys
|
||||||
|
// we generate 2/3rds of max, so that other keys people may still have can
|
||||||
|
// still be used
|
||||||
|
final oneTimeKeysCount =
|
||||||
|
(_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() -
|
||||||
|
oldKeyCount;
|
||||||
|
_olmAccount.generate_one_time_keys(oneTimeKeysCount);
|
||||||
|
final Map<String, dynamic> oneTimeKeys =
|
||||||
|
json.decode(_olmAccount.one_time_keys());
|
||||||
|
|
||||||
|
// now sign all the one-time keys
|
||||||
|
final signedOneTimeKeys = <String, dynamic>{};
|
||||||
|
for (final entry in oneTimeKeys['curve25519'].entries) {
|
||||||
|
final key = entry.key;
|
||||||
|
final value = entry.value;
|
||||||
|
signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{};
|
||||||
|
signedOneTimeKeys['signed_curve25519:$key'] = signJson({
|
||||||
|
'key': value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// and now generate the payload to upload
|
||||||
|
final keysContent = <String, dynamic>{
|
||||||
|
if (uploadDeviceKeys)
|
||||||
|
'device_keys': {
|
||||||
|
'user_id': client.userID,
|
||||||
|
'device_id': client.deviceID,
|
||||||
|
'algorithms': [
|
||||||
|
'm.olm.v1.curve25519-aes-sha2',
|
||||||
|
'm.megolm.v1.aes-sha2'
|
||||||
|
],
|
||||||
|
'keys': <String, dynamic>{},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (uploadDeviceKeys) {
|
||||||
|
final Map<String, dynamic> keys =
|
||||||
|
json.decode(_olmAccount.identity_keys());
|
||||||
|
for (final entry in keys.entries) {
|
||||||
|
final algorithm = entry.key;
|
||||||
|
final value = entry.value;
|
||||||
|
keysContent['device_keys']['keys']['$algorithm:${client.deviceID}'] =
|
||||||
|
value;
|
||||||
|
}
|
||||||
|
keysContent['device_keys'] =
|
||||||
|
signJson(keysContent['device_keys'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await client.api.uploadDeviceKeys(
|
||||||
|
deviceKeys: uploadDeviceKeys
|
||||||
|
? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
|
||||||
|
: null,
|
||||||
|
oneTimeKeys: signedOneTimeKeys,
|
||||||
|
);
|
||||||
|
_olmAccount.mark_keys_as_published();
|
||||||
|
await client.database?.updateClientKeys(pickledOlmAccount, client.id);
|
||||||
|
return response['signed_curve25519'] == oneTimeKeysCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check if there are at least half of max_number_of_one_time_keys left on the server
|
||||||
|
// and generate and upload more if not.
|
||||||
|
if (countJson.containsKey('signed_curve25519') &&
|
||||||
|
countJson['signed_curve25519'] <
|
||||||
|
(_olmAccount.max_number_of_one_time_keys() / 2)) {
|
||||||
|
uploadKeys(oldKeyCount: countJson['signed_curve25519']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void storeOlmSession(String curve25519IdentityKey, olm.Session session) {
|
||||||
|
if (client.database == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_olmSessions.containsKey(curve25519IdentityKey)) {
|
||||||
|
_olmSessions[curve25519IdentityKey] = [];
|
||||||
|
}
|
||||||
|
final ix = _olmSessions[curve25519IdentityKey]
|
||||||
|
.indexWhere((s) => s.session_id() == session.session_id());
|
||||||
|
if (ix == -1) {
|
||||||
|
// add a new session
|
||||||
|
_olmSessions[curve25519IdentityKey].add(session);
|
||||||
|
} else {
|
||||||
|
// update an existing session
|
||||||
|
_olmSessions[curve25519IdentityKey][ix] = session;
|
||||||
|
}
|
||||||
|
final pickle = session.pickle(client.userID);
|
||||||
|
client.database.storeOlmSession(
|
||||||
|
client.id, curve25519IdentityKey, session.session_id(), pickle);
|
||||||
|
}
|
||||||
|
|
||||||
|
ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) {
|
||||||
|
if (event.type != EventTypes.Encrypted) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
if (event.content['algorithm'] != 'm.olm.v1.curve25519-aes-sha2') {
|
||||||
|
throw ('Unknown algorithm: ${event.content}');
|
||||||
|
}
|
||||||
|
if (!event.content['ciphertext'].containsKey(identityKey)) {
|
||||||
|
throw ("The message isn't sent for this device");
|
||||||
|
}
|
||||||
|
String plaintext;
|
||||||
|
final String senderKey = event.content['sender_key'];
|
||||||
|
final String body = event.content['ciphertext'][identityKey]['body'];
|
||||||
|
final int type = event.content['ciphertext'][identityKey]['type'];
|
||||||
|
if (type != 0 && type != 1) {
|
||||||
|
throw ('Unknown message type');
|
||||||
|
}
|
||||||
|
var existingSessions = olmSessions[senderKey];
|
||||||
|
if (existingSessions != null) {
|
||||||
|
for (var session in existingSessions) {
|
||||||
|
if (type == 0 && session.matches_inbound(body) == true) {
|
||||||
|
plaintext = session.decrypt(type, body);
|
||||||
|
storeOlmSession(senderKey, session);
|
||||||
|
break;
|
||||||
|
} else if (type == 1) {
|
||||||
|
try {
|
||||||
|
plaintext = session.decrypt(type, body);
|
||||||
|
storeOlmSession(senderKey, session);
|
||||||
|
break;
|
||||||
|
} catch (_) {
|
||||||
|
plaintext = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (plaintext == null && type != 0) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plaintext == null) {
|
||||||
|
var newSession = olm.Session();
|
||||||
|
try {
|
||||||
|
newSession.create_inbound_from(_olmAccount, senderKey, body);
|
||||||
|
_olmAccount.remove_one_time_keys(newSession);
|
||||||
|
client.database?.updateClientKeys(pickledOlmAccount, client.id);
|
||||||
|
plaintext = newSession.decrypt(type, body);
|
||||||
|
storeOlmSession(senderKey, newSession);
|
||||||
|
} catch (_) {
|
||||||
|
newSession?.free();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final Map<String, dynamic> plainContent = json.decode(plaintext);
|
||||||
|
if (plainContent.containsKey('sender') &&
|
||||||
|
plainContent['sender'] != event.sender) {
|
||||||
|
throw ("Message was decrypted but sender doesn't match");
|
||||||
|
}
|
||||||
|
if (plainContent.containsKey('recipient') &&
|
||||||
|
plainContent['recipient'] != client.userID) {
|
||||||
|
throw ("Message was decrypted but recipient doesn't match");
|
||||||
|
}
|
||||||
|
if (plainContent['recipient_keys'] is Map &&
|
||||||
|
plainContent['recipient_keys']['ed25519'] is String &&
|
||||||
|
plainContent['recipient_keys']['ed25519'] != fingerprintKey) {
|
||||||
|
throw ("Message was decrypted but own fingerprint Key doesn't match");
|
||||||
|
}
|
||||||
|
return ToDeviceEvent(
|
||||||
|
content: plainContent['content'],
|
||||||
|
encryptedContent: event.content,
|
||||||
|
type: plainContent['type'],
|
||||||
|
sender: event.sender,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
|
||||||
|
if (event.type != EventTypes.Encrypted) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
final senderKey = event.content['sender_key'];
|
||||||
|
final loadFromDb = () async {
|
||||||
|
if (client.database == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final sessions = await client.database
|
||||||
|
.getSingleOlmSessions(client.id, senderKey, client.userID);
|
||||||
|
if (sessions.isEmpty) {
|
||||||
|
return false; // okay, can't do anything
|
||||||
|
}
|
||||||
|
_olmSessions[senderKey] = sessions;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (!_olmSessions.containsKey(senderKey)) {
|
||||||
|
await loadFromDb();
|
||||||
|
}
|
||||||
|
event = _decryptToDeviceEvent(event);
|
||||||
|
if (event.type != EventTypes.Encrypted || !(await loadFromDb())) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
// retry to decrypt!
|
||||||
|
return _decryptToDeviceEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys) async {
|
||||||
|
var requestingKeysFrom = <String, Map<String, String>>{};
|
||||||
|
for (var device in deviceKeys) {
|
||||||
|
if (requestingKeysFrom[device.userId] == null) {
|
||||||
|
requestingKeysFrom[device.userId] = {};
|
||||||
|
}
|
||||||
|
requestingKeysFrom[device.userId][device.deviceId] = 'signed_curve25519';
|
||||||
|
}
|
||||||
|
|
||||||
|
final response =
|
||||||
|
await client.api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000);
|
||||||
|
|
||||||
|
for (var userKeysEntry in response.oneTimeKeys.entries) {
|
||||||
|
final userId = userKeysEntry.key;
|
||||||
|
for (var deviceKeysEntry in userKeysEntry.value.entries) {
|
||||||
|
final deviceId = deviceKeysEntry.key;
|
||||||
|
final fingerprintKey =
|
||||||
|
client.userDeviceKeys[userId].deviceKeys[deviceId].ed25519Key;
|
||||||
|
final identityKey =
|
||||||
|
client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key;
|
||||||
|
for (Map<String, dynamic> deviceKey in deviceKeysEntry.value.values) {
|
||||||
|
if (!checkJsonSignature(
|
||||||
|
fingerprintKey, deviceKey, userId, deviceId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var session = olm.Session();
|
||||||
|
session.create_outbound(_olmAccount, identityKey, deviceKey['key']);
|
||||||
|
await storeOlmSession(identityKey, session);
|
||||||
|
} catch (e) {
|
||||||
|
print('[LibOlm] Could not create new outbound olm session: ' +
|
||||||
|
e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
|
||||||
|
DeviceKeys device, String type, Map<String, dynamic> payload) async {
|
||||||
|
var sess = olmSessions[device.curve25519Key];
|
||||||
|
if (sess == null || sess.isEmpty) {
|
||||||
|
final sessions = await client.database
|
||||||
|
.getSingleOlmSessions(client.id, device.curve25519Key, client.userID);
|
||||||
|
if (sessions.isEmpty) {
|
||||||
|
throw ('No olm session found');
|
||||||
|
}
|
||||||
|
sess = _olmSessions[device.curve25519Key] = sessions;
|
||||||
|
}
|
||||||
|
sess.sort((a, b) => a.session_id().compareTo(b.session_id()));
|
||||||
|
final fullPayload = {
|
||||||
|
'type': type,
|
||||||
|
'content': payload,
|
||||||
|
'sender': client.userID,
|
||||||
|
'keys': {'ed25519': fingerprintKey},
|
||||||
|
'recipient': device.userId,
|
||||||
|
'recipient_keys': {'ed25519': device.ed25519Key},
|
||||||
|
};
|
||||||
|
final encryptResult = sess.first.encrypt(json.encode(fullPayload));
|
||||||
|
storeOlmSession(device.curve25519Key, sess.first);
|
||||||
|
final encryptedBody = <String, dynamic>{
|
||||||
|
'algorithm': 'm.olm.v1.curve25519-aes-sha2',
|
||||||
|
'sender_key': identityKey,
|
||||||
|
'ciphertext': <String, dynamic>{},
|
||||||
|
};
|
||||||
|
encryptedBody['ciphertext'][device.curve25519Key] = {
|
||||||
|
'type': encryptResult.type,
|
||||||
|
'body': encryptResult.body,
|
||||||
|
};
|
||||||
|
return encryptedBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> encryptToDeviceMessage(
|
||||||
|
List<DeviceKeys> deviceKeys,
|
||||||
|
String type,
|
||||||
|
Map<String, dynamic> payload) async {
|
||||||
|
var data = <String, Map<String, Map<String, dynamic>>>{};
|
||||||
|
final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
|
||||||
|
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
|
||||||
|
olmSessions.containsKey(deviceKeys.curve25519Key));
|
||||||
|
if (deviceKeysWithoutSession.isNotEmpty) {
|
||||||
|
await startOutgoingOlmSessions(deviceKeysWithoutSession);
|
||||||
|
}
|
||||||
|
for (final device in deviceKeys) {
|
||||||
|
if (!data.containsKey(device.userId)) {
|
||||||
|
data[device.userId] = {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
data[device.userId][device.deviceId] =
|
||||||
|
await encryptToDeviceMessagePayload(device, type, payload);
|
||||||
|
} catch (e) {
|
||||||
|
print('[LibOlm] Error encrypting to-device event: ' + e.toString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
for (final sessions in olmSessions.values) {
|
||||||
|
for (final sess in sessions) {
|
||||||
|
sess.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_olmAccount?.free();
|
||||||
|
_olmAccount = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:random_string/random_string.dart';
|
import 'package:random_string/random_string.dart';
|
||||||
import 'package:canonical_json/canonical_json.dart';
|
import 'package:canonical_json/canonical_json.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
import '../../matrix_api.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'device_keys_list.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
import '../client.dart';
|
|
||||||
import '../room.dart';
|
import '../encryption.dart';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
+-------------+ +-----------+
|
+-------------+ +-----------+
|
||||||
|
@ -53,6 +71,9 @@ enum KeyVerificationState {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _intersect(List<String> a, List<dynamic> b) {
|
List<String> _intersect(List<String> a, List<dynamic> b) {
|
||||||
|
if (b == null || a == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final res = <String>[];
|
final res = <String>[];
|
||||||
for (final v in a) {
|
for (final v in a) {
|
||||||
if (b.contains(v)) {
|
if (b.contains(v)) {
|
||||||
|
@ -94,7 +115,8 @@ _KeyVerificationMethod _makeVerificationMethod(
|
||||||
|
|
||||||
class KeyVerification {
|
class KeyVerification {
|
||||||
String transactionId;
|
String transactionId;
|
||||||
final Client client;
|
final Encryption encryption;
|
||||||
|
Client get client => encryption.client;
|
||||||
final Room room;
|
final Room room;
|
||||||
final String userId;
|
final String userId;
|
||||||
void Function() onUpdate;
|
void Function() onUpdate;
|
||||||
|
@ -114,7 +136,11 @@ class KeyVerification {
|
||||||
String canceledReason;
|
String canceledReason;
|
||||||
|
|
||||||
KeyVerification(
|
KeyVerification(
|
||||||
{this.client, this.room, this.userId, String deviceId, this.onUpdate}) {
|
{this.encryption,
|
||||||
|
this.room,
|
||||||
|
this.userId,
|
||||||
|
String deviceId,
|
||||||
|
this.onUpdate}) {
|
||||||
lastActivity = DateTime.now();
|
lastActivity = DateTime.now();
|
||||||
_deviceId ??= deviceId;
|
_deviceId ??= deviceId;
|
||||||
}
|
}
|
||||||
|
@ -384,7 +410,7 @@ class KeyVerification {
|
||||||
final newTransactionId = await room.sendEvent(payload, type: type);
|
final newTransactionId = await room.sendEvent(payload, type: type);
|
||||||
if (transactionId == null) {
|
if (transactionId == null) {
|
||||||
transactionId = newTransactionId;
|
transactionId = newTransactionId;
|
||||||
client.addKeyVerificationRequest(this);
|
encryption.keyVerificationManager.addRequest(this);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await client.sendToDevice(
|
await client.sendToDevice(
|
||||||
|
@ -404,10 +430,9 @@ class KeyVerification {
|
||||||
|
|
||||||
abstract class _KeyVerificationMethod {
|
abstract class _KeyVerificationMethod {
|
||||||
KeyVerification request;
|
KeyVerification request;
|
||||||
Client client;
|
Encryption get encryption => request.encryption;
|
||||||
_KeyVerificationMethod({this.request}) {
|
Client get client => request.client;
|
||||||
client = request.client;
|
_KeyVerificationMethod({this.request});
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> handlePayload(String type, Map<String, dynamic> payload);
|
Future<void> handlePayload(String type, Map<String, dynamic> payload);
|
||||||
bool validateStart(Map<String, dynamic> payload) {
|
bool validateStart(Map<String, dynamic> payload) {
|
||||||
|
@ -662,7 +687,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
// we would also add the cross signing key here
|
// we would also add the cross signing key here
|
||||||
final deviceKeyId = 'ed25519:${client.deviceID}';
|
final deviceKeyId = 'ed25519:${client.deviceID}';
|
||||||
mac[deviceKeyId] =
|
mac[deviceKeyId] =
|
||||||
_calculateMac(client.fingerprintKey, baseInfo + deviceKeyId);
|
_calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId);
|
||||||
keyList.add(deviceKeyId);
|
keyList.add(deviceKeyId);
|
||||||
|
|
||||||
keyList.sort();
|
keyList.sort();
|
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/room_update.dart';
|
||||||
export 'package:famedlysdk/src/utils/event_update.dart';
|
export 'package:famedlysdk/src/utils/event_update.dart';
|
||||||
export 'package:famedlysdk/src/utils/device_keys_list.dart';
|
export 'package:famedlysdk/src/utils/device_keys_list.dart';
|
||||||
export 'package:famedlysdk/src/utils/key_verification.dart';
|
|
||||||
export 'package:famedlysdk/src/utils/matrix_file.dart';
|
export 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||||
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
|
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
|
||||||
export 'package:famedlysdk/src/utils/uri_extension.dart';
|
export 'package:famedlysdk/src/utils/uri_extension.dart';
|
||||||
|
@ -32,7 +31,6 @@ export 'package:famedlysdk/src/utils/states_map.dart';
|
||||||
export 'package:famedlysdk/src/utils/to_device_event.dart';
|
export 'package:famedlysdk/src/utils/to_device_event.dart';
|
||||||
export 'package:famedlysdk/src/client.dart';
|
export 'package:famedlysdk/src/client.dart';
|
||||||
export 'package:famedlysdk/src/event.dart';
|
export 'package:famedlysdk/src/event.dart';
|
||||||
export 'package:famedlysdk/src/key_manager.dart';
|
|
||||||
export 'package:famedlysdk/src/room.dart';
|
export 'package:famedlysdk/src/room.dart';
|
||||||
export 'package:famedlysdk/src/timeline.dart';
|
export 'package:famedlysdk/src/timeline.dart';
|
||||||
export 'package:famedlysdk/src/user.dart';
|
export 'package:famedlysdk/src/user.dart';
|
||||||
|
|
|
@ -20,16 +20,14 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
import 'package:canonical_json/canonical_json.dart';
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
|
import 'package:famedlysdk/encryption.dart';
|
||||||
import 'package:famedlysdk/src/room.dart';
|
import 'package:famedlysdk/src/room.dart';
|
||||||
import 'package:famedlysdk/src/utils/device_keys_list.dart';
|
import 'package:famedlysdk/src/utils/device_keys_list.dart';
|
||||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||||
import 'package:famedlysdk/src/utils/session_key.dart';
|
|
||||||
import 'package:famedlysdk/src/utils/to_device_event.dart';
|
import 'package:famedlysdk/src/utils/to_device_event.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:olm/olm.dart' as olm;
|
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
|
||||||
import 'event.dart';
|
import 'event.dart';
|
||||||
|
@ -38,8 +36,6 @@ import 'utils/event_update.dart';
|
||||||
import 'utils/room_update.dart';
|
import 'utils/room_update.dart';
|
||||||
import 'user.dart';
|
import 'user.dart';
|
||||||
import 'database/database.dart' show Database;
|
import 'database/database.dart' show Database;
|
||||||
import 'utils/key_verification.dart';
|
|
||||||
import 'key_manager.dart';
|
|
||||||
|
|
||||||
typedef RoomSorter = int Function(Room a, Room b);
|
typedef RoomSorter = int Function(Room a, Room b);
|
||||||
|
|
||||||
|
@ -53,12 +49,13 @@ class Client {
|
||||||
int get id => _id;
|
int get id => _id;
|
||||||
|
|
||||||
Database database;
|
Database database;
|
||||||
KeyManager keyManager;
|
|
||||||
|
|
||||||
bool enableE2eeRecovery;
|
bool enableE2eeRecovery;
|
||||||
|
|
||||||
MatrixApi api;
|
MatrixApi api;
|
||||||
|
|
||||||
|
Encryption encryption;
|
||||||
|
|
||||||
/// Create a client
|
/// Create a client
|
||||||
/// clientName = unique identifier of this client
|
/// clientName = unique identifier of this client
|
||||||
/// debug: Print debug output?
|
/// debug: Print debug output?
|
||||||
|
@ -70,7 +67,6 @@ class Client {
|
||||||
this.enableE2eeRecovery = false,
|
this.enableE2eeRecovery = false,
|
||||||
http.Client httpClient}) {
|
http.Client httpClient}) {
|
||||||
api = MatrixApi(debug: debug, httpClient: httpClient);
|
api = MatrixApi(debug: debug, httpClient: httpClient);
|
||||||
keyManager = KeyManager(this);
|
|
||||||
onLoginStateChanged.stream.listen((loginState) {
|
onLoginStateChanged.stream.listen((loginState) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
print('[LoginState]: ${loginState.toString()}');
|
print('[LoginState]: ${loginState.toString()}');
|
||||||
|
@ -106,18 +102,14 @@ class Client {
|
||||||
List<Room> get rooms => _rooms;
|
List<Room> get rooms => _rooms;
|
||||||
List<Room> _rooms = [];
|
List<Room> _rooms = [];
|
||||||
|
|
||||||
olm.Account _olmAccount;
|
|
||||||
|
|
||||||
/// Returns the base64 encoded keys to store them in a store.
|
|
||||||
/// This String should **never** leave the device!
|
|
||||||
String get pickledOlmAccount =>
|
|
||||||
encryptionEnabled ? _olmAccount.pickle(userID) : null;
|
|
||||||
|
|
||||||
/// Whether this client supports end-to-end encryption using olm.
|
/// Whether this client supports end-to-end encryption using olm.
|
||||||
bool get encryptionEnabled => _olmAccount != null;
|
bool get encryptionEnabled => encryption != null && encryption.enabled;
|
||||||
|
|
||||||
/// Whether this client is able to encrypt and decrypt files.
|
/// Whether this client is able to encrypt and decrypt files.
|
||||||
bool get fileEncryptionEnabled => true;
|
bool get fileEncryptionEnabled => encryptionEnabled && true;
|
||||||
|
|
||||||
|
String get identityKey => encryption?.identityKey ?? '';
|
||||||
|
String get fingerprintKey => encryption?.fingerprintKey ?? '';
|
||||||
|
|
||||||
/// Warning! This endpoint is for testing only!
|
/// Warning! This endpoint is for testing only!
|
||||||
set rooms(List<Room> newList) {
|
set rooms(List<Room> newList) {
|
||||||
|
@ -529,8 +521,6 @@ class Client {
|
||||||
final StreamController<KeyVerification> onKeyVerificationRequest =
|
final StreamController<KeyVerification> onKeyVerificationRequest =
|
||||||
StreamController.broadcast();
|
StreamController.broadcast();
|
||||||
|
|
||||||
final Map<String, KeyVerification> _keyVerificationRequests = {};
|
|
||||||
|
|
||||||
/// Matrix synchronisation is done with https long polling. This needs a
|
/// Matrix synchronisation is done with https long polling. This needs a
|
||||||
/// timeout which is usually 30 seconds.
|
/// timeout which is usually 30 seconds.
|
||||||
int syncTimeoutSec = 30;
|
int syncTimeoutSec = 30;
|
||||||
|
@ -604,31 +594,15 @@ class Client {
|
||||||
|
|
||||||
if (api.accessToken == null || api.homeserver == null || _userID == null) {
|
if (api.accessToken == null || api.homeserver == null || _userID == null) {
|
||||||
// we aren't logged in
|
// we aren't logged in
|
||||||
|
encryption?.dispose();
|
||||||
|
encryption = null;
|
||||||
onLoginStateChanged.add(LoginState.loggedOut);
|
onLoginStateChanged.add(LoginState.loggedOut);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to create a new olm account or restore a previous one.
|
encryption = Encryption(
|
||||||
if (olmAccount == null) {
|
debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery);
|
||||||
try {
|
await encryption.init(olmAccount);
|
||||||
await olm.init();
|
|
||||||
_olmAccount = olm.Account();
|
|
||||||
_olmAccount.create();
|
|
||||||
if (await _uploadKeys(uploadDeviceKeys: true) == false) {
|
|
||||||
throw ('Upload key failed');
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
_olmAccount = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await olm.init();
|
|
||||||
_olmAccount = olm.Account();
|
|
||||||
_olmAccount.unpickle(userID, olmAccount);
|
|
||||||
} catch (_) {
|
|
||||||
_olmAccount = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (database != null) {
|
if (database != null) {
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
|
@ -639,7 +613,7 @@ class Client {
|
||||||
_deviceID,
|
_deviceID,
|
||||||
_deviceName,
|
_deviceName,
|
||||||
prevBatch,
|
prevBatch,
|
||||||
pickledOlmAccount,
|
encryption?.pickledOlmAccount,
|
||||||
id,
|
id,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -651,11 +625,10 @@ class Client {
|
||||||
_deviceID,
|
_deviceID,
|
||||||
_deviceName,
|
_deviceName,
|
||||||
prevBatch,
|
prevBatch,
|
||||||
pickledOlmAccount,
|
encryption?.pickledOlmAccount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_userDeviceKeys = await database.getUserDeviceKeys(id);
|
_userDeviceKeys = await database.getUserDeviceKeys(id);
|
||||||
_olmSessions = await database.getOlmSessions(id, _userID);
|
|
||||||
_rooms = await database.getRoomList(this, onlyLeft: false);
|
_rooms = await database.getRoomList(this, onlyLeft: false);
|
||||||
_sortRooms();
|
_sortRooms();
|
||||||
accountData = await database.getAccountData(id);
|
accountData = await database.getAccountData(id);
|
||||||
|
@ -674,20 +647,12 @@ class Client {
|
||||||
|
|
||||||
/// Resets all settings and stops the synchronisation.
|
/// Resets all settings and stops the synchronisation.
|
||||||
void clear() {
|
void clear() {
|
||||||
olmSessions.values.forEach((List<olm.Session> sessions) {
|
|
||||||
sessions.forEach((olm.Session session) => session?.free());
|
|
||||||
});
|
|
||||||
rooms.forEach((Room room) {
|
|
||||||
room.clearOutboundGroupSession(wipe: true);
|
|
||||||
room.inboundGroupSessions.values.forEach((SessionKey sessionKey) {
|
|
||||||
sessionKey.inboundGroupSession?.free();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
_olmAccount?.free();
|
|
||||||
database?.clear(id);
|
database?.clear(id);
|
||||||
_id = api.accessToken =
|
_id = api.accessToken =
|
||||||
api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
|
api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
|
||||||
_rooms = [];
|
_rooms = [];
|
||||||
|
encryption?.dispose();
|
||||||
|
encryption = null;
|
||||||
onLoginStateChanged.add(LoginState.loggedOut);
|
onLoginStateChanged.add(LoginState.loggedOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,7 +688,9 @@ class Client {
|
||||||
}
|
}
|
||||||
prevBatch = syncResp.nextBatch;
|
prevBatch = syncResp.nextBatch;
|
||||||
await _updateUserDeviceKeys();
|
await _updateUserDeviceKeys();
|
||||||
_cleanupKeyVerificationRequests();
|
if (encryptionEnabled) {
|
||||||
|
encryption.onSync();
|
||||||
|
}
|
||||||
if (hash == _syncRequest.hashCode) unawaited(_sync());
|
if (hash == _syncRequest.hashCode) unawaited(_sync());
|
||||||
} on MatrixException catch (exception) {
|
} on MatrixException catch (exception) {
|
||||||
onError.add(exception);
|
onError.add(exception);
|
||||||
|
@ -740,7 +707,7 @@ class Client {
|
||||||
/// Use this method only for testing utilities!
|
/// Use this method only for testing utilities!
|
||||||
Future<void> handleSync(SyncUpdate sync) async {
|
Future<void> handleSync(SyncUpdate sync) async {
|
||||||
if (sync.toDevice != null) {
|
if (sync.toDevice != null) {
|
||||||
_handleToDeviceEvents(sync.toDevice);
|
await _handleToDeviceEvents(sync.toDevice);
|
||||||
}
|
}
|
||||||
if (sync.rooms != null) {
|
if (sync.rooms != null) {
|
||||||
if (sync.rooms.join != null) {
|
if (sync.rooms.join != null) {
|
||||||
|
@ -784,31 +751,12 @@ class Client {
|
||||||
if (sync.deviceLists != null) {
|
if (sync.deviceLists != null) {
|
||||||
await _handleDeviceListsEvents(sync.deviceLists);
|
await _handleDeviceListsEvents(sync.deviceLists);
|
||||||
}
|
}
|
||||||
if (sync.deviceOneTimeKeysCount != null) {
|
if (sync.deviceOneTimeKeysCount != null && encryptionEnabled) {
|
||||||
_handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount);
|
encryption.handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount);
|
||||||
}
|
|
||||||
while (_pendingToDeviceEvents.isNotEmpty) {
|
|
||||||
_updateRoomsByToDeviceEvent(
|
|
||||||
_pendingToDeviceEvents.removeLast(),
|
|
||||||
addToPendingIfNotFound: false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
onSync.add(sync);
|
onSync.add(sync);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDeviceOneTimeKeysCount(Map<String, int> deviceOneTimeKeysCount) {
|
|
||||||
if (!encryptionEnabled) return;
|
|
||||||
// Check if there are at least half of max_number_of_one_time_keys left on the server
|
|
||||||
// and generate and upload more if not.
|
|
||||||
if (deviceOneTimeKeysCount['signed_curve25519'] != null) {
|
|
||||||
final oneTimeKeysCount = deviceOneTimeKeysCount['signed_curve25519'];
|
|
||||||
if (oneTimeKeysCount < (_olmAccount.max_number_of_one_time_keys() / 2)) {
|
|
||||||
// Generate and upload more one time keys:
|
|
||||||
_uploadKeys();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async {
|
Future<void> _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async {
|
||||||
if (deviceLists.changed is List) {
|
if (deviceLists.changed is List) {
|
||||||
for (final userId in deviceLists.changed) {
|
for (final userId in deviceLists.changed) {
|
||||||
|
@ -827,36 +775,12 @@ class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cleanupKeyVerificationRequests() {
|
Future<void> _handleToDeviceEvents(List<BasicEventWithSender> events) async {
|
||||||
for (final entry in _keyVerificationRequests.entries) {
|
|
||||||
(() async {
|
|
||||||
var dispose = entry.value.canceled ||
|
|
||||||
entry.value.state == KeyVerificationState.done ||
|
|
||||||
entry.value.state == KeyVerificationState.error;
|
|
||||||
if (!dispose) {
|
|
||||||
dispose = !(await entry.value.verifyActivity());
|
|
||||||
}
|
|
||||||
if (dispose) {
|
|
||||||
entry.value.dispose();
|
|
||||||
_keyVerificationRequests.remove(entry.key);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addKeyVerificationRequest(KeyVerification request) {
|
|
||||||
if (request.transactionId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_keyVerificationRequests[request.transactionId] = request;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleToDeviceEvents(List<BasicEventWithSender> events) {
|
|
||||||
for (var i = 0; i < events.length; i++) {
|
for (var i = 0; i < events.length; i++) {
|
||||||
var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
|
var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
|
||||||
if (toDeviceEvent.type == EventTypes.Encrypted) {
|
if (toDeviceEvent.type == EventTypes.Encrypted && encryptionEnabled) {
|
||||||
try {
|
try {
|
||||||
toDeviceEvent = decryptToDeviceEvent(toDeviceEvent);
|
toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
print(
|
print(
|
||||||
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}');
|
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}');
|
||||||
|
@ -872,48 +796,13 @@ class Client {
|
||||||
toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
|
toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_updateRoomsByToDeviceEvent(toDeviceEvent);
|
if (encryptionEnabled) {
|
||||||
if (toDeviceEvent.type.startsWith('m.key.verification.')) {
|
await encryption.handleToDeviceEvent(toDeviceEvent);
|
||||||
_handleToDeviceKeyVerificationRequest(toDeviceEvent);
|
|
||||||
}
|
|
||||||
if (['m.room_key_request', 'm.forwarded_room_key']
|
|
||||||
.contains(toDeviceEvent.type)) {
|
|
||||||
keyManager.handleToDeviceEvent(toDeviceEvent);
|
|
||||||
}
|
}
|
||||||
onToDeviceEvent.add(toDeviceEvent);
|
onToDeviceEvent.add(toDeviceEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleToDeviceKeyVerificationRequest(ToDeviceEvent toDeviceEvent) {
|
|
||||||
if (!toDeviceEvent.type.startsWith('m.key.verification.')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// we have key verification going on!
|
|
||||||
final transactionId =
|
|
||||||
KeyVerification.getTransactionId(toDeviceEvent.content);
|
|
||||||
if (transactionId != null) {
|
|
||||||
if (_keyVerificationRequests.containsKey(transactionId)) {
|
|
||||||
_keyVerificationRequests[transactionId]
|
|
||||||
.handlePayload(toDeviceEvent.type, toDeviceEvent.content);
|
|
||||||
} else {
|
|
||||||
final newKeyRequest =
|
|
||||||
KeyVerification(client: this, userId: toDeviceEvent.sender);
|
|
||||||
newKeyRequest
|
|
||||||
.handlePayload(toDeviceEvent.type, toDeviceEvent.content)
|
|
||||||
.then((res) {
|
|
||||||
if (newKeyRequest.state != KeyVerificationState.askAccept) {
|
|
||||||
// okay, something went wrong (unknown transaction id?), just dispose it
|
|
||||||
newKeyRequest.dispose();
|
|
||||||
} else {
|
|
||||||
// we have a new request! Let's broadcast it!
|
|
||||||
_keyVerificationRequests[transactionId] = newKeyRequest;
|
|
||||||
onKeyVerificationRequest.add(newKeyRequest);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleRooms(
|
Future<void> _handleRooms(
|
||||||
Map<String, SyncRoomUpdate> rooms, Membership membership) async {
|
Map<String, SyncRoomUpdate> rooms, Membership membership) async {
|
||||||
for (final entry in rooms.entries) {
|
for (final entry in rooms.entries) {
|
||||||
|
@ -1056,14 +945,8 @@ class Client {
|
||||||
content: event,
|
content: event,
|
||||||
sortOrder: sortOrder,
|
sortOrder: sortOrder,
|
||||||
);
|
);
|
||||||
if (event['type'] == EventTypes.Encrypted) {
|
if (event['type'] == EventTypes.Encrypted && encryptionEnabled) {
|
||||||
update = update.decrypt(room);
|
update = await update.decrypt(room);
|
||||||
}
|
|
||||||
if (update.eventType == EventTypes.Encrypted && database != null) {
|
|
||||||
// the event is still encrytped....let's try fetching the keys from the database!
|
|
||||||
await room.loadInboundGroupSessionKey(
|
|
||||||
event['content']['session_id'], event['content']['sender_key']);
|
|
||||||
update = update.decrypt(room);
|
|
||||||
}
|
}
|
||||||
if (type != 'ephemeral' && database != null) {
|
if (type != 'ephemeral' && database != null) {
|
||||||
await database.storeEventUpdate(id, update);
|
await database.storeEventUpdate(id, update);
|
||||||
|
@ -1187,42 +1070,6 @@ class Client {
|
||||||
if (eventUpdate.type == 'timeline') _sortRooms();
|
if (eventUpdate.type == 'timeline') _sortRooms();
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<ToDeviceEvent> _pendingToDeviceEvents = [];
|
|
||||||
|
|
||||||
void _updateRoomsByToDeviceEvent(ToDeviceEvent toDeviceEvent,
|
|
||||||
{addToPendingIfNotFound = true}) async {
|
|
||||||
try {
|
|
||||||
switch (toDeviceEvent.type) {
|
|
||||||
case 'm.room_key':
|
|
||||||
final roomId = toDeviceEvent.content['room_id'];
|
|
||||||
var room = getRoomById(roomId);
|
|
||||||
if (room == null && addToPendingIfNotFound) {
|
|
||||||
_pendingToDeviceEvents.add(toDeviceEvent);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
room ??= Room(client: this, id: roomId);
|
|
||||||
final String sessionId = toDeviceEvent.content['session_id'];
|
|
||||||
if (userDeviceKeys.containsKey(toDeviceEvent.sender) &&
|
|
||||||
userDeviceKeys[toDeviceEvent.sender]
|
|
||||||
.deviceKeys
|
|
||||||
.containsKey(toDeviceEvent.content['requesting_device_id'])) {
|
|
||||||
toDeviceEvent.content['sender_claimed_ed25519_key'] =
|
|
||||||
userDeviceKeys[toDeviceEvent.sender]
|
|
||||||
.deviceKeys[toDeviceEvent.content['requesting_device_id']]
|
|
||||||
.ed25519Key;
|
|
||||||
}
|
|
||||||
room.setInboundGroupSession(
|
|
||||||
sessionId,
|
|
||||||
toDeviceEvent.content,
|
|
||||||
forwarded: false,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('[Matrix] Error while processing to-device-event: ' + e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _sortLock = false;
|
bool _sortLock = false;
|
||||||
|
|
||||||
/// The compare function how the rooms should be sorted internally. By default
|
/// The compare function how the rooms should be sorted internally. By default
|
||||||
|
@ -1287,6 +1134,9 @@ class Client {
|
||||||
|
|
||||||
for (final rawDeviceKeyListEntry in response.deviceKeys.entries) {
|
for (final rawDeviceKeyListEntry in response.deviceKeys.entries) {
|
||||||
final userId = rawDeviceKeyListEntry.key;
|
final userId = rawDeviceKeyListEntry.key;
|
||||||
|
if (!userDeviceKeys.containsKey(userId)) {
|
||||||
|
_userDeviceKeys[userId] = DeviceKeysList(userId);
|
||||||
|
}
|
||||||
final oldKeys =
|
final oldKeys =
|
||||||
Map<String, DeviceKeys>.from(_userDeviceKeys[userId].deviceKeys);
|
Map<String, DeviceKeys>.from(_userDeviceKeys[userId].deviceKeys);
|
||||||
_userDeviceKeys[userId].deviceKeys = {};
|
_userDeviceKeys[userId].deviceKeys = {};
|
||||||
|
@ -1301,7 +1151,7 @@ class Client {
|
||||||
if (entry.isValid) {
|
if (entry.isValid) {
|
||||||
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
|
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
|
||||||
if (deviceId == deviceID &&
|
if (deviceId == deviceID &&
|
||||||
entry.ed25519Key == fingerprintKey) {
|
entry.ed25519Key == encryption?.fingerprintKey) {
|
||||||
// Always trust the own device
|
// Always trust the own device
|
||||||
entry.verified = true;
|
entry.verified = true;
|
||||||
}
|
}
|
||||||
|
@ -1349,213 +1199,6 @@ class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String get fingerprintKey => encryptionEnabled
|
|
||||||
? json.decode(_olmAccount.identity_keys())['ed25519']
|
|
||||||
: null;
|
|
||||||
String get identityKey => encryptionEnabled
|
|
||||||
? json.decode(_olmAccount.identity_keys())['curve25519']
|
|
||||||
: null;
|
|
||||||
|
|
||||||
/// Adds a signature to this json from this olm account.
|
|
||||||
Map<String, dynamic> signJson(Map<String, dynamic> payload) {
|
|
||||||
if (!encryptionEnabled) throw ('Encryption is disabled');
|
|
||||||
final Map<String, dynamic> unsigned = payload['unsigned'];
|
|
||||||
final Map<String, dynamic> signatures = payload['signatures'];
|
|
||||||
payload.remove('unsigned');
|
|
||||||
payload.remove('signatures');
|
|
||||||
final canonical = canonicalJson.encode(payload);
|
|
||||||
final signature = _olmAccount.sign(String.fromCharCodes(canonical));
|
|
||||||
if (signatures != null) {
|
|
||||||
payload['signatures'] = signatures;
|
|
||||||
} else {
|
|
||||||
payload['signatures'] = <String, dynamic>{};
|
|
||||||
}
|
|
||||||
payload['signatures'][userID] = <String, dynamic>{};
|
|
||||||
payload['signatures'][userID]['ed25519:$deviceID'] = signature;
|
|
||||||
if (unsigned != null) {
|
|
||||||
payload['unsigned'] = unsigned;
|
|
||||||
}
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the signature of a signed json object.
|
|
||||||
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
|
|
||||||
String userId, String deviceId) {
|
|
||||||
if (!encryptionEnabled) throw ('Encryption is disabled');
|
|
||||||
final Map<String, dynamic> signatures = signedJson['signatures'];
|
|
||||||
if (signatures == null || !signatures.containsKey(userId)) return false;
|
|
||||||
signedJson.remove('unsigned');
|
|
||||||
signedJson.remove('signatures');
|
|
||||||
if (!signatures[userId].containsKey('ed25519:$deviceId')) return false;
|
|
||||||
final String signature = signatures[userId]['ed25519:$deviceId'];
|
|
||||||
final canonical = canonicalJson.encode(signedJson);
|
|
||||||
final message = String.fromCharCodes(canonical);
|
|
||||||
var isValid = true;
|
|
||||||
try {
|
|
||||||
olm.Utility()
|
|
||||||
..ed25519_verify(key, message, signature)
|
|
||||||
..free();
|
|
||||||
} catch (e) {
|
|
||||||
isValid = false;
|
|
||||||
print('[LibOlm] Signature check failed: ' + e.toString());
|
|
||||||
}
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime lastTimeKeysUploaded;
|
|
||||||
|
|
||||||
/// Generates new one time keys, signs everything and upload it to the server.
|
|
||||||
Future<bool> _uploadKeys({bool uploadDeviceKeys = false}) async {
|
|
||||||
if (!encryptionEnabled) return true;
|
|
||||||
|
|
||||||
final oneTimeKeysCount = _olmAccount.max_number_of_one_time_keys();
|
|
||||||
_olmAccount.generate_one_time_keys(oneTimeKeysCount);
|
|
||||||
final Map<String, dynamic> oneTimeKeys =
|
|
||||||
json.decode(_olmAccount.one_time_keys());
|
|
||||||
|
|
||||||
var signedOneTimeKeys = <String, dynamic>{};
|
|
||||||
|
|
||||||
for (String key in oneTimeKeys['curve25519'].keys) {
|
|
||||||
signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{};
|
|
||||||
signedOneTimeKeys['signed_curve25519:$key']['key'] =
|
|
||||||
oneTimeKeys['curve25519'][key];
|
|
||||||
signedOneTimeKeys['signed_curve25519:$key'] =
|
|
||||||
signJson(signedOneTimeKeys['signed_curve25519:$key']);
|
|
||||||
}
|
|
||||||
|
|
||||||
var keysContent = <String, dynamic>{
|
|
||||||
if (uploadDeviceKeys)
|
|
||||||
'device_keys': {
|
|
||||||
'user_id': userID,
|
|
||||||
'device_id': deviceID,
|
|
||||||
'algorithms': [
|
|
||||||
'm.olm.v1.curve25519-aes-sha2',
|
|
||||||
'm.megolm.v1.aes-sha2'
|
|
||||||
],
|
|
||||||
'keys': <String, dynamic>{},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (uploadDeviceKeys) {
|
|
||||||
final Map<String, dynamic> keys =
|
|
||||||
json.decode(_olmAccount.identity_keys());
|
|
||||||
for (var algorithm in keys.keys) {
|
|
||||||
keysContent['device_keys']['keys']['$algorithm:$deviceID'] =
|
|
||||||
keys[algorithm];
|
|
||||||
}
|
|
||||||
keysContent['device_keys'] =
|
|
||||||
signJson(keysContent['device_keys'] as Map<String, dynamic>);
|
|
||||||
}
|
|
||||||
|
|
||||||
_olmAccount.mark_keys_as_published();
|
|
||||||
final response = await api.uploadDeviceKeys(
|
|
||||||
deviceKeys: uploadDeviceKeys
|
|
||||||
? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
|
|
||||||
: null,
|
|
||||||
oneTimeKeys: signedOneTimeKeys,
|
|
||||||
);
|
|
||||||
if (response['signed_curve25519'] != oneTimeKeysCount) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await database?.updateClientKeys(pickledOlmAccount, id);
|
|
||||||
lastTimeKeysUploaded = DateTime.now();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to decrypt a ToDeviceEvent encrypted with olm.
|
|
||||||
ToDeviceEvent decryptToDeviceEvent(ToDeviceEvent toDeviceEvent) {
|
|
||||||
if (toDeviceEvent.type != EventTypes.Encrypted) {
|
|
||||||
print(
|
|
||||||
'[LibOlm] Warning! Tried to decrypt a not-encrypted to-device-event');
|
|
||||||
return toDeviceEvent;
|
|
||||||
}
|
|
||||||
if (toDeviceEvent.content['algorithm'] != 'm.olm.v1.curve25519-aes-sha2') {
|
|
||||||
throw ('Unknown algorithm: ${toDeviceEvent.content}');
|
|
||||||
}
|
|
||||||
if (!toDeviceEvent.content['ciphertext'].containsKey(identityKey)) {
|
|
||||||
throw ("The message isn't sent for this device");
|
|
||||||
}
|
|
||||||
String plaintext;
|
|
||||||
final String senderKey = toDeviceEvent.content['sender_key'];
|
|
||||||
final String body =
|
|
||||||
toDeviceEvent.content['ciphertext'][identityKey]['body'];
|
|
||||||
final int type = toDeviceEvent.content['ciphertext'][identityKey]['type'];
|
|
||||||
if (type != 0 && type != 1) {
|
|
||||||
throw ('Unknown message type');
|
|
||||||
}
|
|
||||||
var existingSessions = olmSessions[senderKey];
|
|
||||||
if (existingSessions != null) {
|
|
||||||
for (var session in existingSessions) {
|
|
||||||
if (type == 0 && session.matches_inbound(body) == true) {
|
|
||||||
plaintext = session.decrypt(type, body);
|
|
||||||
storeOlmSession(senderKey, session);
|
|
||||||
break;
|
|
||||||
} else if (type == 1) {
|
|
||||||
try {
|
|
||||||
plaintext = session.decrypt(type, body);
|
|
||||||
storeOlmSession(senderKey, session);
|
|
||||||
break;
|
|
||||||
} catch (_) {
|
|
||||||
plaintext = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (plaintext == null && type != 0) {
|
|
||||||
throw ('No existing sessions found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plaintext == null) {
|
|
||||||
var newSession = olm.Session();
|
|
||||||
newSession.create_inbound_from(_olmAccount, senderKey, body);
|
|
||||||
_olmAccount.remove_one_time_keys(newSession);
|
|
||||||
database?.updateClientKeys(pickledOlmAccount, id);
|
|
||||||
plaintext = newSession.decrypt(type, body);
|
|
||||||
storeOlmSession(senderKey, newSession);
|
|
||||||
}
|
|
||||||
final Map<String, dynamic> plainContent = json.decode(plaintext);
|
|
||||||
if (plainContent.containsKey('sender') &&
|
|
||||||
plainContent['sender'] != toDeviceEvent.sender) {
|
|
||||||
throw ("Message was decrypted but sender doesn't match");
|
|
||||||
}
|
|
||||||
if (plainContent.containsKey('recipient') &&
|
|
||||||
plainContent['recipient'] != userID) {
|
|
||||||
throw ("Message was decrypted but recipient doesn't match");
|
|
||||||
}
|
|
||||||
if (plainContent['recipient_keys'] is Map &&
|
|
||||||
plainContent['recipient_keys']['ed25519'] is String &&
|
|
||||||
plainContent['recipient_keys']['ed25519'] != fingerprintKey) {
|
|
||||||
throw ("Message was decrypted but own fingerprint Key doesn't match");
|
|
||||||
}
|
|
||||||
return ToDeviceEvent(
|
|
||||||
content: plainContent['content'],
|
|
||||||
encryptedContent: toDeviceEvent.content,
|
|
||||||
type: plainContent['type'],
|
|
||||||
sender: toDeviceEvent.sender,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A map from Curve25519 identity keys to existing olm sessions.
|
|
||||||
Map<String, List<olm.Session>> get olmSessions => _olmSessions;
|
|
||||||
Map<String, List<olm.Session>> _olmSessions = {};
|
|
||||||
|
|
||||||
void storeOlmSession(String curve25519IdentityKey, olm.Session session) {
|
|
||||||
if (!_olmSessions.containsKey(curve25519IdentityKey)) {
|
|
||||||
_olmSessions[curve25519IdentityKey] = [];
|
|
||||||
}
|
|
||||||
final ix = _olmSessions[curve25519IdentityKey]
|
|
||||||
.indexWhere((s) => s.session_id() == session.session_id());
|
|
||||||
if (ix == -1) {
|
|
||||||
// add a new session
|
|
||||||
_olmSessions[curve25519IdentityKey].add(session);
|
|
||||||
} else {
|
|
||||||
// update an existing session
|
|
||||||
_olmSessions[curve25519IdentityKey][ix] = session;
|
|
||||||
}
|
|
||||||
final pickle = session.pickle(userID);
|
|
||||||
database?.storeOlmSession(
|
|
||||||
id, curve25519IdentityKey, session.session_id(), pickle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send
|
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send
|
||||||
/// the request to all devices of the current user, pass an empty list to [deviceKeys].
|
/// the request to all devices of the current user, pass an empty list to [deviceKeys].
|
||||||
Future<void> sendToDevice(
|
Future<void> sendToDevice(
|
||||||
|
@ -1589,96 +1232,22 @@ class Client {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (encrypted) {
|
if (encrypted) {
|
||||||
// Create new sessions with devices if there is no existing session yet.
|
data =
|
||||||
var deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
|
await encryption.encryptToDeviceMessage(deviceKeys, type, message);
|
||||||
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
|
} else {
|
||||||
olmSessions.containsKey(deviceKeys.curve25519Key));
|
for (final device in deviceKeys) {
|
||||||
if (deviceKeysWithoutSession.isNotEmpty) {
|
|
||||||
await startOutgoingOlmSessions(deviceKeysWithoutSession);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var i = 0; i < deviceKeys.length; i++) {
|
|
||||||
var device = deviceKeys[i];
|
|
||||||
if (!data.containsKey(device.userId)) {
|
if (!data.containsKey(device.userId)) {
|
||||||
data[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;
|
data[device.userId][device.deviceId] = sendToDeviceMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (encrypted) type = EventTypes.Encrypted;
|
if (encrypted) type = EventTypes.Encrypted;
|
||||||
final messageID = generateUniqueTransactionId();
|
final messageID = generateUniqueTransactionId();
|
||||||
await api.sendToDevice(type, messageID, data);
|
await api.sendToDevice(type, messageID, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys,
|
|
||||||
{bool checkSignature = true}) async {
|
|
||||||
var requestingKeysFrom = <String, Map<String, String>>{};
|
|
||||||
for (var device in deviceKeys) {
|
|
||||||
if (requestingKeysFrom[device.userId] == null) {
|
|
||||||
requestingKeysFrom[device.userId] = {};
|
|
||||||
}
|
|
||||||
requestingKeysFrom[device.userId][device.deviceId] = 'signed_curve25519';
|
|
||||||
}
|
|
||||||
|
|
||||||
final response =
|
|
||||||
await api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000);
|
|
||||||
|
|
||||||
for (var userKeysEntry in response.oneTimeKeys.entries) {
|
|
||||||
final userId = userKeysEntry.key;
|
|
||||||
for (var deviceKeysEntry in userKeysEntry.value.entries) {
|
|
||||||
final deviceId = deviceKeysEntry.key;
|
|
||||||
final fingerprintKey =
|
|
||||||
userDeviceKeys[userId].deviceKeys[deviceId].ed25519Key;
|
|
||||||
final identityKey =
|
|
||||||
userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key;
|
|
||||||
for (Map<String, dynamic> deviceKey in deviceKeysEntry.value.values) {
|
|
||||||
if (checkSignature &&
|
|
||||||
checkJsonSignature(fingerprintKey, deviceKey, userId, deviceId) ==
|
|
||||||
false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
var session = olm.Session();
|
|
||||||
session.create_outbound(_olmAccount, identityKey, deviceKey['key']);
|
|
||||||
await storeOlmSession(identityKey, session);
|
|
||||||
} catch (e) {
|
|
||||||
print('[LibOlm] Could not create new outbound olm session: ' +
|
|
||||||
e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether all push notifications are muted using the [.m.rule.master]
|
/// Whether all push notifications are muted using the [.m.rule.master]
|
||||||
/// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master
|
/// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master
|
||||||
bool get allPushNotificationsMuted {
|
bool get allPushNotificationsMuted {
|
||||||
|
|
|
@ -91,6 +91,22 @@ class Database extends _$Database {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<olm.Session>> getSingleOlmSessions(
|
||||||
|
int clientId, String identityKey, String userId) async {
|
||||||
|
final rows = await dbGetOlmSessions(clientId, identityKey).get();
|
||||||
|
final res = <olm.Session>[];
|
||||||
|
for (final row in rows) {
|
||||||
|
try {
|
||||||
|
var session = olm.Session();
|
||||||
|
session.unpickle(userId, row.pickle);
|
||||||
|
res.add(session);
|
||||||
|
} catch (e) {
|
||||||
|
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
Future<DbOutboundGroupSession> getDbOutboundGroupSession(
|
Future<DbOutboundGroupSession> getDbOutboundGroupSession(
|
||||||
int clientId, String roomId) async {
|
int clientId, String roomId) async {
|
||||||
final res = await dbGetOutboundGroupSession(clientId, roomId).get();
|
final res = await dbGetOutboundGroupSession(clientId, roomId).get();
|
||||||
|
|
|
@ -4851,6 +4851,19 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
readsFrom: {olmSessions}).map(_rowToDbOlmSessions);
|
readsFrom: {olmSessions}).map(_rowToDbOlmSessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selectable<DbOlmSessions> dbGetOlmSessions(
|
||||||
|
int client_id, String identity_key) {
|
||||||
|
return customSelect(
|
||||||
|
'SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key',
|
||||||
|
variables: [
|
||||||
|
Variable.withInt(client_id),
|
||||||
|
Variable.withString(identity_key)
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
olmSessions
|
||||||
|
}).map(_rowToDbOlmSessions);
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> storeOlmSession(
|
Future<int> storeOlmSession(
|
||||||
int client_id, String identitiy_key, String session_id, String pickle) {
|
int client_id, String identitiy_key, String session_id, String pickle) {
|
||||||
return customInsert(
|
return customInsert(
|
||||||
|
|
|
@ -155,6 +155,7 @@ storePrevBatch: UPDATE clients SET prev_batch = :prev_batch WHERE client_id = :c
|
||||||
getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id;
|
getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id;
|
||||||
getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key WHERE client_id = :client_id;
|
getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key WHERE client_id = :client_id;
|
||||||
getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id;
|
getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id;
|
||||||
|
dbGetOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key;
|
||||||
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle);
|
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle);
|
||||||
getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id;
|
getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id;
|
||||||
dbGetOutboundGroupSession: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
|
dbGetOutboundGroupSession: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/encryption.dart';
|
||||||
import 'package:famedlysdk/src/utils/receipt.dart';
|
import 'package:famedlysdk/src/utils/receipt.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||||
|
@ -333,36 +334,6 @@ class Event extends MatrixEvent {
|
||||||
return await timeline.getEventById(replyEventId);
|
return await timeline.getEventById(replyEventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadSession() {
|
|
||||||
return room.loadInboundGroupSessionKeyForEvent(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trys to decrypt this event. Returns a m.bad.encrypted event
|
|
||||||
/// if it fails and does nothing if the event was not encrypted.
|
|
||||||
Event get decrypted => room.decryptGroupMessage(this);
|
|
||||||
|
|
||||||
/// Trys to decrypt this event and persists it in the database afterwards
|
|
||||||
Future<Event> decryptAndStore([String updateType = 'timeline']) async {
|
|
||||||
final newEvent = decrypted;
|
|
||||||
if (newEvent.type == EventTypes.Encrypted) {
|
|
||||||
return newEvent; // decryption failed
|
|
||||||
}
|
|
||||||
await room.client.database?.storeEventUpdate(
|
|
||||||
room.client.id,
|
|
||||||
EventUpdate(
|
|
||||||
eventType: newEvent.type,
|
|
||||||
content: newEvent.toJson(),
|
|
||||||
roomID: newEvent.roomId,
|
|
||||||
type: updateType,
|
|
||||||
sortOrder: newEvent.sortOrder,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (updateType != 'history') {
|
|
||||||
room.setState(newEvent);
|
|
||||||
}
|
|
||||||
return newEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If this event is encrypted and the decryption was not successful because
|
/// If this event is encrypted and the decryption was not successful because
|
||||||
/// the session is unknown, this requests the session key from other devices
|
/// the session is unknown, this requests the session key from other devices
|
||||||
/// in the room. If the event is not encrypted or the decryption failed because
|
/// in the room. If the event is not encrypted or the decryption failed because
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:famedlysdk/encryption.dart';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/src/client.dart';
|
import 'package:famedlysdk/src/client.dart';
|
||||||
import 'package:famedlysdk/src/event.dart';
|
import 'package:famedlysdk/src/event.dart';
|
||||||
import 'package:famedlysdk/src/utils/event_update.dart';
|
import 'package:famedlysdk/src/utils/event_update.dart';
|
||||||
import 'package:famedlysdk/src/utils/room_update.dart';
|
import 'package:famedlysdk/src/utils/room_update.dart';
|
||||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||||
import 'package:famedlysdk/src/utils/session_key.dart';
|
|
||||||
import 'package:image/image.dart';
|
import 'package:image/image.dart';
|
||||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||||
import 'package:mime_type/mime_type.dart';
|
import 'package:mime_type/mime_type.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
|
||||||
import 'package:html_unescape/html_unescape.dart';
|
import 'package:html_unescape/html_unescape.dart';
|
||||||
|
|
||||||
import './user.dart';
|
import './user.dart';
|
||||||
|
@ -81,13 +78,6 @@ class Room {
|
||||||
/// Key-Value store for private account data only visible for this user.
|
/// Key-Value store for private account data only visible for this user.
|
||||||
Map<String, BasicRoomEvent> roomAccountData = {};
|
Map<String, BasicRoomEvent> roomAccountData = {};
|
||||||
|
|
||||||
olm.OutboundGroupSession get outboundGroupSession => _outboundGroupSession;
|
|
||||||
olm.OutboundGroupSession _outboundGroupSession;
|
|
||||||
|
|
||||||
List<String> _outboundGroupSessionDevices;
|
|
||||||
DateTime _outboundGroupSessionCreationTime;
|
|
||||||
int _outboundGroupSessionSentMessages;
|
|
||||||
|
|
||||||
double _newestSortOrder;
|
double _newestSortOrder;
|
||||||
double _oldestSortOrder;
|
double _oldestSortOrder;
|
||||||
|
|
||||||
|
@ -110,168 +100,6 @@ class Room {
|
||||||
_oldestSortOrder, _newestSortOrder, client.id, id);
|
_oldestSortOrder, _newestSortOrder, client.id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the existing outboundGroupSession, tries to create a new one and
|
|
||||||
/// stores it as an ingoingGroupSession in the [sessionKeys]. Then sends the
|
|
||||||
/// new session encrypted with olm to all non-blocked devices using
|
|
||||||
/// to-device-messaging.
|
|
||||||
Future<void> createOutboundGroupSession() async {
|
|
||||||
await clearOutboundGroupSession(wipe: true);
|
|
||||||
var deviceKeys = await getUserDeviceKeys();
|
|
||||||
olm.OutboundGroupSession outboundGroupSession;
|
|
||||||
var outboundGroupSessionDevices = <String>[];
|
|
||||||
for (var keys in deviceKeys) {
|
|
||||||
if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId);
|
|
||||||
}
|
|
||||||
outboundGroupSessionDevices.sort();
|
|
||||||
try {
|
|
||||||
outboundGroupSession = olm.OutboundGroupSession();
|
|
||||||
outboundGroupSession.create();
|
|
||||||
} catch (e) {
|
|
||||||
outboundGroupSession = null;
|
|
||||||
print('[LibOlm] Unable to create new outboundGroupSession: ' +
|
|
||||||
e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outboundGroupSession == null) return;
|
|
||||||
// Add as an inboundSession to the [sessionKeys].
|
|
||||||
var rawSession = <String, dynamic>{
|
|
||||||
'algorithm': 'm.megolm.v1.aes-sha2',
|
|
||||||
'room_id': id,
|
|
||||||
'session_id': outboundGroupSession.session_id(),
|
|
||||||
'session_key': outboundGroupSession.session_key(),
|
|
||||||
};
|
|
||||||
setInboundGroupSession(rawSession['session_id'], rawSession);
|
|
||||||
try {
|
|
||||||
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
|
|
||||||
_outboundGroupSession = outboundGroupSession;
|
|
||||||
_outboundGroupSessionDevices = outboundGroupSessionDevices;
|
|
||||||
_outboundGroupSessionCreationTime = DateTime.now();
|
|
||||||
_outboundGroupSessionSentMessages = 0;
|
|
||||||
await _storeOutboundGroupSession();
|
|
||||||
} catch (e, s) {
|
|
||||||
print(
|
|
||||||
'[LibOlm] Unable to send the session key to the participating devices: ' +
|
|
||||||
e.toString());
|
|
||||||
print(s);
|
|
||||||
await clearOutboundGroupSession();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _storeOutboundGroupSession() async {
|
|
||||||
if (_outboundGroupSession == null) return;
|
|
||||||
await client.database?.storeOutboundGroupSession(
|
|
||||||
client.id,
|
|
||||||
id,
|
|
||||||
_outboundGroupSession.pickle(client.userID),
|
|
||||||
json.encode(_outboundGroupSessionDevices),
|
|
||||||
_outboundGroupSessionCreationTime,
|
|
||||||
_outboundGroupSessionSentMessages);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears the existing outboundGroupSession but first checks if the participating
|
|
||||||
/// devices have been changed. Returns false if the session has not been cleared because
|
|
||||||
/// it wasn't necessary.
|
|
||||||
Future<bool> clearOutboundGroupSession({bool wipe = false}) async {
|
|
||||||
if (!wipe && _outboundGroupSessionDevices != null) {
|
|
||||||
// first check if the devices in the room changed
|
|
||||||
var deviceKeys = await getUserDeviceKeys();
|
|
||||||
var outboundGroupSessionDevices = <String>[];
|
|
||||||
for (var keys in deviceKeys) {
|
|
||||||
if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId);
|
|
||||||
}
|
|
||||||
outboundGroupSessionDevices.sort();
|
|
||||||
if (outboundGroupSessionDevices.toString() !=
|
|
||||||
_outboundGroupSessionDevices.toString()) {
|
|
||||||
wipe = true;
|
|
||||||
}
|
|
||||||
// next check if it needs to be rotated
|
|
||||||
final encryptionContent = getState(EventTypes.Encryption)?.content;
|
|
||||||
final maxMessages = encryptionContent != null &&
|
|
||||||
encryptionContent['rotation_period_msgs'] is int
|
|
||||||
? encryptionContent['rotation_period_msgs']
|
|
||||||
: 100;
|
|
||||||
final maxAge = encryptionContent != null &&
|
|
||||||
encryptionContent['rotation_period_ms'] is int
|
|
||||||
? encryptionContent['rotation_period_ms']
|
|
||||||
: 604800000; // default of one week
|
|
||||||
if (_outboundGroupSessionSentMessages >= maxMessages ||
|
|
||||||
_outboundGroupSessionCreationTime
|
|
||||||
.add(Duration(milliseconds: maxAge))
|
|
||||||
.isBefore(DateTime.now())) {
|
|
||||||
wipe = true;
|
|
||||||
}
|
|
||||||
if (!wipe) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!wipe &&
|
|
||||||
_outboundGroupSessionDevices == null &&
|
|
||||||
_outboundGroupSession == null) {
|
|
||||||
return true; // let's just short-circuit out of here, no need to do DB stuff
|
|
||||||
}
|
|
||||||
_outboundGroupSessionDevices = null;
|
|
||||||
await client.database?.removeOutboundGroupSession(client.id, id);
|
|
||||||
_outboundGroupSession?.free();
|
|
||||||
_outboundGroupSession = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Key-Value store of session ids to the session keys. Only m.megolm.v1.aes-sha2
|
|
||||||
/// session keys are supported. They are stored as a Map with the following keys:
|
|
||||||
/// {
|
|
||||||
/// "algorithm": "m.megolm.v1.aes-sha2",
|
|
||||||
/// "room_id": "!Cuyf34gef24t:localhost",
|
|
||||||
/// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
|
|
||||||
/// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
|
|
||||||
/// }
|
|
||||||
Map<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions;
|
|
||||||
final _inboundGroupSessions = <String, SessionKey>{};
|
|
||||||
|
|
||||||
/// Add a new session key to the [sessionKeys].
|
|
||||||
void setInboundGroupSession(String sessionId, Map<String, dynamic> content,
|
|
||||||
{bool forwarded = false}) {
|
|
||||||
if (inboundGroupSessions.containsKey(sessionId)) return;
|
|
||||||
olm.InboundGroupSession inboundGroupSession;
|
|
||||||
if (content['algorithm'] == 'm.megolm.v1.aes-sha2') {
|
|
||||||
try {
|
|
||||||
inboundGroupSession = olm.InboundGroupSession();
|
|
||||||
if (forwarded) {
|
|
||||||
inboundGroupSession.import_session(content['session_key']);
|
|
||||||
} else {
|
|
||||||
inboundGroupSession.create(content['session_key']);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
inboundGroupSession = null;
|
|
||||||
print('[LibOlm] Could not create new InboundGroupSession: ' +
|
|
||||||
e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_inboundGroupSessions[sessionId] = SessionKey(
|
|
||||||
content: content,
|
|
||||||
inboundGroupSession: inboundGroupSession,
|
|
||||||
indexes: {},
|
|
||||||
key: client.userID,
|
|
||||||
);
|
|
||||||
client.database?.storeInboundGroupSession(
|
|
||||||
client.id,
|
|
||||||
id,
|
|
||||||
sessionId,
|
|
||||||
inboundGroupSession.pickle(client.userID),
|
|
||||||
json.encode(content),
|
|
||||||
json.encode({}),
|
|
||||||
);
|
|
||||||
_tryAgainDecryptLastMessage();
|
|
||||||
onSessionKeyReceived.add(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _tryAgainDecryptLastMessage() async {
|
|
||||||
if (getState(EventTypes.Encrypted) != null) {
|
|
||||||
await getState(EventTypes.Encrypted).decryptAndStore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [Event] for the given [typeKey] and optional [stateKey].
|
/// Returns the [Event] for the given [typeKey] and optional [stateKey].
|
||||||
/// If no [stateKey] is provided, it defaults to an empty string.
|
/// If no [stateKey] is provided, it defaults to an empty string.
|
||||||
Event getState(String typeKey, [String stateKey = '']) =>
|
Event getState(String typeKey, [String stateKey = '']) =>
|
||||||
|
@ -281,23 +109,13 @@ class Room {
|
||||||
/// typeKey/stateKey key pair if there is one.
|
/// typeKey/stateKey key pair if there is one.
|
||||||
void setState(Event state) {
|
void setState(Event state) {
|
||||||
// Decrypt if necessary
|
// Decrypt if necessary
|
||||||
if (state.type == EventTypes.Encrypted) {
|
if (state.type == EventTypes.Encrypted && client.encryptionEnabled) {
|
||||||
try {
|
try {
|
||||||
state = decryptGroupMessage(state);
|
state = client.encryption.decryptRoomEventSync(id, state);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[LibOlm] Could not decrypt room state: ' + e.toString());
|
print('[LibOlm] Could not decrypt room state: ' + e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if this is a member change and we need to clear the outboundGroupSession.
|
|
||||||
if (encrypted &&
|
|
||||||
outboundGroupSession != null &&
|
|
||||||
state.type == EventTypes.RoomMember) {
|
|
||||||
var newUser = state.asUser;
|
|
||||||
var oldUser = getState(EventTypes.RoomMember, newUser.id)?.asUser;
|
|
||||||
if (oldUser == null || oldUser.membership != newUser.membership) {
|
|
||||||
clearOutboundGroupSession();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((getState(state.type)?.originServerTs?.millisecondsSinceEpoch ?? 0) >
|
if ((getState(state.type)?.originServerTs?.millisecondsSinceEpoch ?? 0) >
|
||||||
(state.originServerTs?.millisecondsSinceEpoch ?? 1)) {
|
(state.originServerTs?.millisecondsSinceEpoch ?? 1)) {
|
||||||
return;
|
return;
|
||||||
|
@ -883,7 +701,8 @@ class Room {
|
||||||
// Send the text and on success, store and display a *sent* event.
|
// Send the text and on success, store and display a *sent* event.
|
||||||
try {
|
try {
|
||||||
final sendMessageContent = encrypted && client.encryptionEnabled
|
final sendMessageContent = encrypted && client.encryptionEnabled
|
||||||
? await encryptGroupMessagePayload(content, type: type)
|
? await client.encryption
|
||||||
|
.encryptGroupMessagePayload(id, content, type: type)
|
||||||
: content;
|
: content;
|
||||||
final res = await client.api.sendMessage(
|
final res = await client.api.sendMessage(
|
||||||
id,
|
id,
|
||||||
|
@ -999,55 +818,42 @@ class Room {
|
||||||
if (onHistoryReceived != null) onHistoryReceived();
|
if (onHistoryReceived != null) onHistoryReceived();
|
||||||
prev_batch = resp.end;
|
prev_batch = resp.end;
|
||||||
|
|
||||||
final dbActions = <Future<dynamic> Function()>[];
|
final loadFn = () async {
|
||||||
if (client.database != null) {
|
|
||||||
dbActions.add(
|
|
||||||
() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return;
|
if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return;
|
||||||
|
|
||||||
if (resp.state != null) {
|
if (resp.state != null) {
|
||||||
for (final state in resp.state) {
|
for (final state in resp.state) {
|
||||||
var eventUpdate = EventUpdate(
|
await EventUpdate(
|
||||||
type: 'state',
|
type: 'state',
|
||||||
roomID: id,
|
roomID: id,
|
||||||
eventType: state.type,
|
eventType: state.type,
|
||||||
content: state.toJson(),
|
content: state.toJson(),
|
||||||
sortOrder: oldSortOrder,
|
sortOrder: oldSortOrder,
|
||||||
).decrypt(this);
|
).decrypt(this, store: true);
|
||||||
client.onEvent.add(eventUpdate);
|
|
||||||
if (client.database != null) {
|
|
||||||
dbActions.add(
|
|
||||||
() => client.database.storeEventUpdate(client.id, eventUpdate));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final hist in resp.chunk) {
|
for (final hist in resp.chunk) {
|
||||||
var eventUpdate = EventUpdate(
|
final eventUpdate = await EventUpdate(
|
||||||
type: 'history',
|
type: 'history',
|
||||||
roomID: id,
|
roomID: id,
|
||||||
eventType: hist.type,
|
eventType: hist.type,
|
||||||
content: hist.toJson(),
|
content: hist.toJson(),
|
||||||
sortOrder: oldSortOrder,
|
sortOrder: oldSortOrder,
|
||||||
).decrypt(this);
|
).decrypt(this, store: true);
|
||||||
client.onEvent.add(eventUpdate);
|
client.onEvent.add(eventUpdate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (client.database != null) {
|
if (client.database != null) {
|
||||||
dbActions.add(
|
await client.database.transaction(() async {
|
||||||
() => client.database.storeEventUpdate(client.id, eventUpdate));
|
await client.database.setRoomPrevBatch(resp.end, client.id, id);
|
||||||
}
|
await loadFn();
|
||||||
}
|
|
||||||
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 updateSortOrder();
|
await updateSortOrder();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await loadFn();
|
||||||
|
}
|
||||||
client.onRoomUpdate.add(
|
client.onRoomUpdate.add(
|
||||||
RoomUpdate(
|
RoomUpdate(
|
||||||
id: id,
|
id: id,
|
||||||
|
@ -1147,7 +953,6 @@ class Room {
|
||||||
}
|
}
|
||||||
for (final rawState in rawStates) {
|
for (final rawState in rawStates) {
|
||||||
final newState = Event.fromDb(rawState, newRoom);
|
final newState = Event.fromDb(rawState, newRoom);
|
||||||
;
|
|
||||||
newRoom.setState(newState);
|
newRoom.setState(newState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1187,13 +992,13 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try again to decrypt encrypted events and update the database.
|
// Try again to decrypt encrypted events and update the database.
|
||||||
if (encrypted && client.database != null) {
|
if (encrypted && client.database != null && client.encryptionEnabled) {
|
||||||
await client.database.transaction(() async {
|
await client.database.transaction(() async {
|
||||||
for (var i = 0; i < events.length; i++) {
|
for (var i = 0; i < events.length; i++) {
|
||||||
if (events[i].type == EventTypes.Encrypted &&
|
if (events[i].type == EventTypes.Encrypted &&
|
||||||
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
|
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
|
||||||
await events[i].loadSession();
|
events[i] = await client.encryption
|
||||||
events[i] = await events[i].decryptAndStore();
|
.decryptRoomEvent(id, events[i], store: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1746,209 +1551,10 @@ class Room {
|
||||||
return deviceKeys;
|
return deviceKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _restoredOutboundGroupSession = false;
|
|
||||||
|
|
||||||
Future<void> restoreOutboundGroupSession() async {
|
|
||||||
if (_restoredOutboundGroupSession || client.database == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final outboundSession =
|
|
||||||
await client.database.getDbOutboundGroupSession(client.id, id);
|
|
||||||
if (outboundSession != null) {
|
|
||||||
try {
|
|
||||||
_outboundGroupSession = olm.OutboundGroupSession();
|
|
||||||
_outboundGroupSession.unpickle(client.userID, outboundSession.pickle);
|
|
||||||
_outboundGroupSessionDevices =
|
|
||||||
List<String>.from(json.decode(outboundSession.deviceIds));
|
|
||||||
_outboundGroupSessionCreationTime = outboundSession.creationTime;
|
|
||||||
_outboundGroupSessionSentMessages = outboundSession.sentMessages;
|
|
||||||
} catch (e) {
|
|
||||||
_outboundGroupSession = null;
|
|
||||||
_outboundGroupSessionDevices = null;
|
|
||||||
print('[LibOlm] Unable to unpickle outboundGroupSession: ' +
|
|
||||||
e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_restoredOutboundGroupSession = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
|
|
||||||
/// payload. This will create a new outgoingGroupSession if necessary.
|
|
||||||
Future<Map<String, dynamic>> encryptGroupMessagePayload(
|
|
||||||
Map<String, dynamic> payload,
|
|
||||||
{String type = EventTypes.Message}) async {
|
|
||||||
if (!encrypted || !client.encryptionEnabled) return payload;
|
|
||||||
if (encryptionAlgorithm != 'm.megolm.v1.aes-sha2') {
|
|
||||||
throw ('Unknown encryption algorithm');
|
|
||||||
}
|
|
||||||
if (!_restoredOutboundGroupSession && client.database != null) {
|
|
||||||
// try to restore an outbound group session from the database
|
|
||||||
await restoreOutboundGroupSession();
|
|
||||||
}
|
|
||||||
// and clear the outbound session, if it needs clearing
|
|
||||||
await clearOutboundGroupSession();
|
|
||||||
// create a new one if none exists...
|
|
||||||
if (_outboundGroupSession == null) {
|
|
||||||
await createOutboundGroupSession();
|
|
||||||
}
|
|
||||||
final Map<String, dynamic> mRelatesTo = payload.remove('m.relates_to');
|
|
||||||
final payloadContent = {
|
|
||||||
'content': payload,
|
|
||||||
'type': type,
|
|
||||||
'room_id': id,
|
|
||||||
};
|
|
||||||
var encryptedPayload = <String, dynamic>{
|
|
||||||
'algorithm': 'm.megolm.v1.aes-sha2',
|
|
||||||
'ciphertext': _outboundGroupSession.encrypt(json.encode(payloadContent)),
|
|
||||||
'device_id': client.deviceID,
|
|
||||||
'sender_key': client.identityKey,
|
|
||||||
'session_id': _outboundGroupSession.session_id(),
|
|
||||||
if (mRelatesTo != null) 'm.relates_to': mRelatesTo,
|
|
||||||
};
|
|
||||||
_outboundGroupSessionSentMessages++;
|
|
||||||
await _storeOutboundGroupSession();
|
|
||||||
return encryptedPayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Set<String> _requestedSessionIds = <String>{};
|
|
||||||
|
|
||||||
Future<void> requestSessionKey(String sessionId, String senderKey) async {
|
Future<void> requestSessionKey(String sessionId, String senderKey) async {
|
||||||
await client.keyManager.request(this, sessionId, senderKey);
|
if (!client.encryptionEnabled) {
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadInboundGroupSessionKey(String sessionId,
|
|
||||||
[String senderKey]) async {
|
|
||||||
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) {
|
|
||||||
return;
|
|
||||||
} // nothing to do
|
|
||||||
final session = await client.database
|
|
||||||
.getDbInboundGroupSession(client.id, id, sessionId);
|
|
||||||
if (session == null) {
|
|
||||||
// no session found, let's request it!
|
|
||||||
if (client.enableE2eeRecovery &&
|
|
||||||
!_requestedSessionIds.contains(sessionId) &&
|
|
||||||
senderKey != null) {
|
|
||||||
unawaited(requestSessionKey(sessionId, senderKey));
|
|
||||||
_requestedSessionIds.add(sessionId);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
await client.encryption.keyManager.request(this, sessionId, senderKey);
|
||||||
_inboundGroupSessions[sessionId] =
|
|
||||||
SessionKey.fromDb(session, client.userID);
|
|
||||||
} catch (e) {
|
|
||||||
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadInboundGroupSessionKeyForEvent(Event event) async {
|
|
||||||
if (client.database == null) return; // nothing to do, no database
|
|
||||||
if (event.type != EventTypes.Encrypted) return;
|
|
||||||
if (!client.encryptionEnabled) {
|
|
||||||
throw (DecryptError.NOT_ENABLED);
|
|
||||||
}
|
|
||||||
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 'dart:async';
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
|
import 'package:famedlysdk/encryption.dart';
|
||||||
|
|
||||||
import 'event.dart';
|
import 'event.dart';
|
||||||
import 'room.dart';
|
import 'room.dart';
|
||||||
|
@ -97,12 +98,16 @@ class Timeline {
|
||||||
void _sessionKeyReceived(String sessionId) async {
|
void _sessionKeyReceived(String sessionId) async {
|
||||||
var decryptAtLeastOneEvent = false;
|
var decryptAtLeastOneEvent = false;
|
||||||
final decryptFn = () async {
|
final decryptFn = () async {
|
||||||
|
if (!room.client.encryptionEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (var i = 0; i < events.length; i++) {
|
for (var i = 0; i < events.length; i++) {
|
||||||
if (events[i].type == EventTypes.Encrypted &&
|
if (events[i].type == EventTypes.Encrypted &&
|
||||||
events[i].messageType == MessageTypes.BadEncrypted &&
|
events[i].messageType == MessageTypes.BadEncrypted &&
|
||||||
events[i].content['body'] == DecryptError.UNKNOWN_SESSION &&
|
events[i].content['body'] == DecryptError.UNKNOWN_SESSION &&
|
||||||
events[i].content['session_id'] == sessionId) {
|
events[i].content['session_id'] == sessionId) {
|
||||||
events[i] = await events[i].decryptAndStore();
|
events[i] = await room.client.encryption
|
||||||
|
.decryptRoomEvent(room.id, events[i], store: true);
|
||||||
if (events[i].type != EventTypes.Encrypted) {
|
if (events[i].type != EventTypes.Encrypted) {
|
||||||
decryptAtLeastOneEvent = true;
|
decryptAtLeastOneEvent = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
|
import 'package:famedlysdk/encryption.dart';
|
||||||
|
|
||||||
import '../client.dart';
|
import '../client.dart';
|
||||||
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
|
import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey;
|
||||||
import '../event.dart';
|
import '../event.dart';
|
||||||
import 'key_verification.dart';
|
|
||||||
|
|
||||||
class DeviceKeysList {
|
class DeviceKeysList {
|
||||||
String userId;
|
String userId;
|
||||||
|
@ -78,12 +78,6 @@ class DeviceKeys extends MatrixDeviceKeys {
|
||||||
|
|
||||||
Future<void> setBlocked(bool newBlocked, Client client) {
|
Future<void> setBlocked(bool newBlocked, Client client) {
|
||||||
blocked = newBlocked;
|
blocked = newBlocked;
|
||||||
for (var room in client.rooms) {
|
|
||||||
if (!room.encrypted) continue;
|
|
||||||
if (room.getParticipants().indexWhere((u) => u.id == userId) != -1) {
|
|
||||||
room.clearOutboundGroupSession();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return client.database
|
return client.database
|
||||||
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
|
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
|
||||||
}
|
}
|
||||||
|
@ -157,10 +151,10 @@ class DeviceKeys extends MatrixDeviceKeys {
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyVerification startVerification(Client client) {
|
KeyVerification startVerification(Client client) {
|
||||||
final request =
|
final request = KeyVerification(
|
||||||
KeyVerification(client: client, userId: userId, deviceId: deviceId);
|
encryption: client.encryption, userId: userId, deviceId: deviceId);
|
||||||
request.start();
|
request.start();
|
||||||
client.addKeyVerificationRequest(request);
|
client.encryption.keyVerificationManager.addRequest(request);
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,13 +42,14 @@ class EventUpdate {
|
||||||
EventUpdate(
|
EventUpdate(
|
||||||
{this.eventType, this.roomID, this.type, this.content, this.sortOrder});
|
{this.eventType, this.roomID, this.type, this.content, this.sortOrder});
|
||||||
|
|
||||||
EventUpdate decrypt(Room room) {
|
Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
|
||||||
if (eventType != EventTypes.Encrypted) {
|
if (eventType != EventTypes.Encrypted || !room.client.encryptionEnabled) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var decrpytedEvent =
|
var decrpytedEvent = await room.client.encryption.decryptRoomEvent(
|
||||||
room.decryptGroupMessage(Event.fromJson(content, room, sortOrder));
|
room.id, Event.fromJson(content, room, sortOrder),
|
||||||
|
store: store, updateType: type);
|
||||||
return EventUpdate(
|
return EventUpdate(
|
||||||
eventType: decrpytedEvent.type,
|
eventType: decrpytedEvent.type,
|
||||||
roomID: roomID,
|
roomID: roomID,
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
@ -39,8 +38,9 @@ void main() {
|
||||||
Future<List<EventUpdate>> eventUpdateListFuture;
|
Future<List<EventUpdate>> eventUpdateListFuture;
|
||||||
Future<List<ToDeviceEvent>> toDeviceUpdateListFuture;
|
Future<List<ToDeviceEvent>> toDeviceUpdateListFuture;
|
||||||
|
|
||||||
|
// key @test:fakeServer.notExisting
|
||||||
const pickledOlmAccount =
|
const pickledOlmAccount =
|
||||||
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuweStA+EKZvvHZO0SnwRp0Hw7sv8UMYvXw';
|
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
|
||||||
const identityKey = '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk';
|
const identityKey = '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk';
|
||||||
const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo';
|
const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo';
|
||||||
|
|
||||||
|
@ -134,24 +134,6 @@ void main() {
|
||||||
expect(matrix.directChats, matrix.accountData['m.direct'].content);
|
expect(matrix.directChats, matrix.accountData['m.direct'].content);
|
||||||
expect(matrix.presences.length, 1);
|
expect(matrix.presences.length, 1);
|
||||||
expect(matrix.rooms[1].ephemerals.length, 2);
|
expect(matrix.rooms[1].ephemerals.length, 2);
|
||||||
expect(matrix.rooms[1].inboundGroupSessions.length, 1);
|
|
||||||
expect(
|
|
||||||
matrix
|
|
||||||
.rooms[1]
|
|
||||||
.inboundGroupSessions[
|
|
||||||
'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
|
|
||||||
.content['session_key'],
|
|
||||||
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw');
|
|
||||||
if (olmEnabled) {
|
|
||||||
expect(
|
|
||||||
matrix
|
|
||||||
.rooms[1]
|
|
||||||
.inboundGroupSessions[
|
|
||||||
'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU']
|
|
||||||
.inboundGroupSession !=
|
|
||||||
null,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
expect(matrix.rooms[1].typingUsers.length, 1);
|
expect(matrix.rooms[1].typingUsers.length, 1);
|
||||||
expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com');
|
expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com');
|
||||||
expect(matrix.rooms[1].roomAccountData.length, 3);
|
expect(matrix.rooms[1].roomAccountData.length, 3);
|
||||||
|
@ -177,7 +159,7 @@ void main() {
|
||||||
expect(presenceCounter, 1);
|
expect(presenceCounter, 1);
|
||||||
expect(accountDataCounter, 3);
|
expect(accountDataCounter, 3);
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
expect(matrix.userDeviceKeys.length, 3);
|
expect(matrix.userDeviceKeys.length, 4);
|
||||||
expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false);
|
expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false);
|
||||||
expect(matrix.userDeviceKeys['@alice:example.com'].deviceKeys.length, 2);
|
expect(matrix.userDeviceKeys['@alice:example.com'].deviceKeys.length, 2);
|
||||||
expect(
|
expect(
|
||||||
|
@ -196,7 +178,7 @@ void main() {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
expect(matrix.userDeviceKeys.length, 2);
|
expect(matrix.userDeviceKeys.length, 3);
|
||||||
expect(matrix.userDeviceKeys['@alice:example.com'].outdated, true);
|
expect(matrix.userDeviceKeys['@alice:example.com'].outdated, true);
|
||||||
|
|
||||||
await matrix.handleSync(SyncUpdate.fromJson({
|
await matrix.handleSync(SyncUpdate.fromJson({
|
||||||
|
@ -335,7 +317,11 @@ void main() {
|
||||||
expect(eventUpdateList.length, 2);
|
expect(eventUpdateList.length, 2);
|
||||||
|
|
||||||
expect(eventUpdateList[0].type, 'm.new_device');
|
expect(eventUpdateList[0].type, 'm.new_device');
|
||||||
|
if (olmEnabled) {
|
||||||
expect(eventUpdateList[1].type, 'm.room_key');
|
expect(eventUpdateList[1].type, 'm.room_key');
|
||||||
|
} else {
|
||||||
|
expect(eventUpdateList[1].type, 'm.room.encrypted');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Login', () async {
|
test('Login', () async {
|
||||||
|
@ -388,115 +374,6 @@ void main() {
|
||||||
'mxc://example.org/SEsfnsuifSDFSSEF');
|
'mxc://example.org/SEsfnsuifSDFSSEF');
|
||||||
expect(aliceProfile.displayname, 'Alice Margatroid');
|
expect(aliceProfile.displayname, 'Alice Margatroid');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('signJson', () {
|
|
||||||
if (matrix.encryptionEnabled) {
|
|
||||||
expect(matrix.fingerprintKey.isNotEmpty, true);
|
|
||||||
expect(matrix.identityKey.isNotEmpty, true);
|
|
||||||
var payload = <String, dynamic>{
|
|
||||||
'unsigned': {
|
|
||||||
'foo': 'bar',
|
|
||||||
},
|
|
||||||
'auth': {
|
|
||||||
'success': true,
|
|
||||||
'mxid': '@john.doe:example.com',
|
|
||||||
'profile': {
|
|
||||||
'display_name': 'John Doe',
|
|
||||||
'three_pids': [
|
|
||||||
{'medium': 'email', 'address': 'john.doe@example.org'},
|
|
||||||
{'medium': 'msisdn', 'address': '123456789'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var payloadWithoutUnsigned = Map<String, dynamic>.from(payload);
|
|
||||||
payloadWithoutUnsigned.remove('unsigned');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
matrix.checkJsonSignature(
|
|
||||||
matrix.fingerprintKey, payload, matrix.userID, matrix.deviceID),
|
|
||||||
false);
|
|
||||||
expect(
|
|
||||||
matrix.checkJsonSignature(matrix.fingerprintKey,
|
|
||||||
payloadWithoutUnsigned, matrix.userID, matrix.deviceID),
|
|
||||||
false);
|
|
||||||
payload = matrix.signJson(payload);
|
|
||||||
payloadWithoutUnsigned = matrix.signJson(payloadWithoutUnsigned);
|
|
||||||
expect(payload['signatures'], payloadWithoutUnsigned['signatures']);
|
|
||||||
print(payload['signatures']);
|
|
||||||
expect(
|
|
||||||
matrix.checkJsonSignature(
|
|
||||||
matrix.fingerprintKey, payload, matrix.userID, matrix.deviceID),
|
|
||||||
true);
|
|
||||||
expect(
|
|
||||||
matrix.checkJsonSignature(matrix.fingerprintKey,
|
|
||||||
payloadWithoutUnsigned, matrix.userID, matrix.deviceID),
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test('Track oneTimeKeys', () async {
|
|
||||||
if (matrix.encryptionEnabled) {
|
|
||||||
var last = matrix.lastTimeKeysUploaded ?? DateTime.now();
|
|
||||||
await matrix.handleSync(SyncUpdate.fromJson({
|
|
||||||
'device_one_time_keys_count': {'signed_curve25519': 49}
|
|
||||||
}));
|
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
|
||||||
expect(
|
|
||||||
matrix.lastTimeKeysUploaded.millisecondsSinceEpoch >
|
|
||||||
last.millisecondsSinceEpoch,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test('Test invalidate outboundGroupSessions', () async {
|
|
||||||
if (matrix.encryptionEnabled) {
|
|
||||||
expect(matrix.rooms[1].outboundGroupSession == null, true);
|
|
||||||
await matrix.rooms[1].createOutboundGroupSession();
|
|
||||||
expect(matrix.rooms[1].outboundGroupSession != null, true);
|
|
||||||
await matrix.handleSync(SyncUpdate.fromJson({
|
|
||||||
'device_lists': {
|
|
||||||
'changed': [
|
|
||||||
'@alice:example.com',
|
|
||||||
],
|
|
||||||
'left': [
|
|
||||||
'@bob:example.com',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
|
||||||
expect(matrix.rooms[1].outboundGroupSession != null, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test('Test invalidate outboundGroupSessions', () async {
|
|
||||||
if (matrix.encryptionEnabled) {
|
|
||||||
await matrix.rooms[1].clearOutboundGroupSession(wipe: true);
|
|
||||||
expect(matrix.rooms[1].outboundGroupSession == null, true);
|
|
||||||
await matrix.rooms[1].createOutboundGroupSession();
|
|
||||||
expect(matrix.rooms[1].outboundGroupSession != null, true);
|
|
||||||
await matrix.handleSync(SyncUpdate.fromJson({
|
|
||||||
'rooms': {
|
|
||||||
'join': {
|
|
||||||
'!726s6s6q:example.com': {
|
|
||||||
'state': {
|
|
||||||
'events': [
|
|
||||||
{
|
|
||||||
'content': {'membership': 'leave'},
|
|
||||||
'event_id': '143273582443PhrSn:example.org',
|
|
||||||
'origin_server_ts': 1432735824653,
|
|
||||||
'room_id': '!726s6s6q:example.com',
|
|
||||||
'sender': '@alice:example.com',
|
|
||||||
'state_key': '@alice:example.com',
|
|
||||||
'type': 'm.room.member'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
|
||||||
expect(matrix.rooms[1].outboundGroupSession != null, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var deviceKeys = DeviceKeys.fromJson({
|
var deviceKeys = DeviceKeys.fromJson({
|
||||||
'user_id': '@alice:example.com',
|
'user_id': '@alice:example.com',
|
||||||
'device_id': 'JLAFKJWSCS',
|
'device_id': 'JLAFKJWSCS',
|
||||||
|
@ -512,16 +389,6 @@ void main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
test('startOutgoingOlmSessions', () async {
|
|
||||||
expect(matrix.olmSessions.length, 0);
|
|
||||||
if (olmEnabled) {
|
|
||||||
await matrix
|
|
||||||
.startOutgoingOlmSessions([deviceKeys], checkSignature: false);
|
|
||||||
expect(matrix.olmSessions.length, 1);
|
|
||||||
expect(matrix.olmSessions.entries.first.key,
|
|
||||||
'3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test('sendToDevice', () async {
|
test('sendToDevice', () async {
|
||||||
await matrix.sendToDevice(
|
await matrix.sendToDevice(
|
||||||
[deviceKeys],
|
[deviceKeys],
|
||||||
|
@ -547,13 +414,6 @@ void main() {
|
||||||
|
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
|
||||||
String sessionKey;
|
|
||||||
if (client1.encryptionEnabled) {
|
|
||||||
await client1.rooms[1].createOutboundGroupSession();
|
|
||||||
|
|
||||||
sessionKey = client1.rooms[1].outboundGroupSession.session_key();
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(client1.isLogged(), true);
|
expect(client1.isLogged(), true);
|
||||||
expect(client1.rooms.length, 2);
|
expect(client1.rooms.length, 2);
|
||||||
|
|
||||||
|
@ -571,12 +431,9 @@ void main() {
|
||||||
expect(client2.deviceID, client1.deviceID);
|
expect(client2.deviceID, client1.deviceID);
|
||||||
expect(client2.deviceName, client1.deviceName);
|
expect(client2.deviceName, client1.deviceName);
|
||||||
if (client2.encryptionEnabled) {
|
if (client2.encryptionEnabled) {
|
||||||
await client2.rooms[1].restoreOutboundGroupSession();
|
expect(client2.encryption.pickledOlmAccount,
|
||||||
expect(client2.pickledOlmAccount, client1.pickledOlmAccount);
|
client1.encryption.pickledOlmAccount);
|
||||||
expect(json.encode(client2.rooms[1].inboundGroupSessions[sessionKey]),
|
|
||||||
json.encode(client1.rooms[1].inboundGroupSessions[sessionKey]));
|
|
||||||
expect(client2.rooms[1].id, client1.rooms[1].id);
|
expect(client2.rooms[1].id, client1.rooms[1].id);
|
||||||
expect(client2.rooms[1].outboundGroupSession.session_key(), sessionKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await client1.logout();
|
await client1.logout();
|
||||||
|
|
99
test/encryption/encrypt_decrypt_room_message_test.dart
Normal file
99
test/encryption/encrypt_decrypt_room_message_test.dart
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import '../fake_client.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Encrypt/Decrypt room message', () {
|
||||||
|
var olmEnabled = true;
|
||||||
|
try {
|
||||||
|
olm.init();
|
||||||
|
olm.Account();
|
||||||
|
} catch (_) {
|
||||||
|
olmEnabled = false;
|
||||||
|
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||||
|
}
|
||||||
|
print('[LibOlm] Enabled: $olmEnabled');
|
||||||
|
|
||||||
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
Client client;
|
||||||
|
final roomId = '!726s6s6q:example.com';
|
||||||
|
Room room;
|
||||||
|
Map<String, dynamic> payload;
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
test('setupClient', () async {
|
||||||
|
client = await getClient();
|
||||||
|
room = client.getRoomById(roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('encrypt payload', () async {
|
||||||
|
payload = await client.encryption.encryptGroupMessagePayload(roomId, {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'text': 'Hello foxies!',
|
||||||
|
});
|
||||||
|
expect(payload['algorithm'], 'm.megolm.v1.aes-sha2');
|
||||||
|
expect(payload['ciphertext'] is String, true);
|
||||||
|
expect(payload['device_id'], client.deviceID);
|
||||||
|
expect(payload['sender_key'], client.identityKey);
|
||||||
|
expect(payload['session_id'] is String, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decrypt payload', () async {
|
||||||
|
final encryptedEvent = Event(
|
||||||
|
type: EventTypes.Encrypted,
|
||||||
|
content: payload,
|
||||||
|
roomId: roomId,
|
||||||
|
room: room,
|
||||||
|
originServerTs: now,
|
||||||
|
eventId: '\$event',
|
||||||
|
);
|
||||||
|
final decryptedEvent =
|
||||||
|
await client.encryption.decryptRoomEvent(roomId, encryptedEvent);
|
||||||
|
expect(decryptedEvent.type, 'm.room.message');
|
||||||
|
expect(decryptedEvent.content['msgtype'], 'm.text');
|
||||||
|
expect(decryptedEvent.content['text'], 'Hello foxies!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decrypt payload nocache', () async {
|
||||||
|
client.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
final encryptedEvent = Event(
|
||||||
|
type: EventTypes.Encrypted,
|
||||||
|
content: payload,
|
||||||
|
roomId: roomId,
|
||||||
|
room: room,
|
||||||
|
originServerTs: now,
|
||||||
|
eventId: '\$event',
|
||||||
|
);
|
||||||
|
final decryptedEvent =
|
||||||
|
await client.encryption.decryptRoomEvent(roomId, encryptedEvent);
|
||||||
|
expect(decryptedEvent.type, 'm.room.message');
|
||||||
|
expect(decryptedEvent.content['msgtype'], 'm.text');
|
||||||
|
expect(decryptedEvent.content['text'], 'Hello foxies!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dispose client', () async {
|
||||||
|
await client.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
120
test/encryption/encrypt_decrypt_to_device_test.dart
Normal file
120
test/encryption/encrypt_decrypt_to_device_test.dart
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import '../fake_client.dart';
|
||||||
|
import '../fake_matrix_api.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// key @othertest:fakeServer.notExisting
|
||||||
|
const otherPickledOlmAccount =
|
||||||
|
'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA';
|
||||||
|
|
||||||
|
group('Encrypt/Decrypt to-device messages', () {
|
||||||
|
var olmEnabled = true;
|
||||||
|
try {
|
||||||
|
olm.init();
|
||||||
|
olm.Account();
|
||||||
|
} catch (_) {
|
||||||
|
olmEnabled = false;
|
||||||
|
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||||
|
}
|
||||||
|
print('[LibOlm] Enabled: $olmEnabled');
|
||||||
|
|
||||||
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
Client client;
|
||||||
|
var otherClient =
|
||||||
|
Client('othertestclient', debug: true, httpClient: FakeMatrixApi());
|
||||||
|
DeviceKeys device;
|
||||||
|
Map<String, dynamic> payload;
|
||||||
|
|
||||||
|
test('setupClient', () async {
|
||||||
|
client = await getClient();
|
||||||
|
otherClient.database = client.database;
|
||||||
|
await otherClient.checkServer('https://fakeServer.notExisting');
|
||||||
|
otherClient.connect(
|
||||||
|
newToken: 'abc',
|
||||||
|
newUserID: '@othertest:fakeServer.notExisting',
|
||||||
|
newHomeserver: otherClient.api.homeserver,
|
||||||
|
newDeviceName: 'Text Matrix Client',
|
||||||
|
newDeviceID: 'FOXDEVICE',
|
||||||
|
newOlmAccount: otherPickledOlmAccount,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
|
device = DeviceKeys(
|
||||||
|
userId: client.userID,
|
||||||
|
deviceId: client.deviceID,
|
||||||
|
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||||
|
keys: {
|
||||||
|
'curve25519:${client.deviceID}': client.identityKey,
|
||||||
|
'ed25519:${client.deviceID}': client.fingerprintKey,
|
||||||
|
},
|
||||||
|
verified: true,
|
||||||
|
blocked: false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('encryptToDeviceMessage', () async {
|
||||||
|
payload = await otherClient.encryption
|
||||||
|
.encryptToDeviceMessage([device], 'm.to_device', {'hello': 'foxies'});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('encryptToDeviceMessagePayload', () async {
|
||||||
|
// just a hard test if nothing errors
|
||||||
|
await otherClient.encryption.encryptToDeviceMessagePayload(
|
||||||
|
device, 'm.to_device', {'hello': 'foxies'});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decryptToDeviceEvent', () async {
|
||||||
|
final encryptedEvent = ToDeviceEvent(
|
||||||
|
sender: '@othertest:fakeServer.notExisting',
|
||||||
|
type: EventTypes.Encrypted,
|
||||||
|
content: payload[client.userID][client.deviceID],
|
||||||
|
);
|
||||||
|
final decryptedEvent =
|
||||||
|
await client.encryption.decryptToDeviceEvent(encryptedEvent);
|
||||||
|
expect(decryptedEvent.type, 'm.to_device');
|
||||||
|
expect(decryptedEvent.content['hello'], 'foxies');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decryptToDeviceEvent nocache', () async {
|
||||||
|
client.encryption.olmManager.olmSessions.clear();
|
||||||
|
payload = await otherClient.encryption.encryptToDeviceMessage(
|
||||||
|
[device], 'm.to_device', {'hello': 'superfoxies'});
|
||||||
|
final encryptedEvent = ToDeviceEvent(
|
||||||
|
sender: '@othertest:fakeServer.notExisting',
|
||||||
|
type: EventTypes.Encrypted,
|
||||||
|
content: payload[client.userID][client.deviceID],
|
||||||
|
);
|
||||||
|
final decryptedEvent =
|
||||||
|
await client.encryption.decryptToDeviceEvent(encryptedEvent);
|
||||||
|
expect(decryptedEvent.type, 'm.to_device');
|
||||||
|
expect(decryptedEvent.content['hello'], 'superfoxies');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dispose client', () async {
|
||||||
|
await client.dispose(closeDatabase: true);
|
||||||
|
await otherClient.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
223
test/encryption/key_manager_test.dart
Normal file
223
test/encryption/key_manager_test.dart
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
/*
|
||||||
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import '../fake_client.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Key Manager', () {
|
||||||
|
var olmEnabled = true;
|
||||||
|
try {
|
||||||
|
olm.init();
|
||||||
|
olm.Account();
|
||||||
|
} catch (_) {
|
||||||
|
olmEnabled = false;
|
||||||
|
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||||
|
}
|
||||||
|
print('[LibOlm] Enabled: $olmEnabled');
|
||||||
|
|
||||||
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
Client client;
|
||||||
|
|
||||||
|
test('setupClient', () async {
|
||||||
|
client = await getClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle new m.room_key', () async {
|
||||||
|
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||||
|
final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
|
||||||
|
final sessionKey =
|
||||||
|
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw';
|
||||||
|
|
||||||
|
client.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
var event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.room_key',
|
||||||
|
content: {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'session_id': validSessionId,
|
||||||
|
'session_key': sessionKey,
|
||||||
|
},
|
||||||
|
encryptedContent: {
|
||||||
|
'sender_key': validSessionId,
|
||||||
|
});
|
||||||
|
await client.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getInboundGroupSession(
|
||||||
|
'!726s6s6q:example.com', validSessionId, validSenderKey) !=
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
// now test a few invalid scenarios
|
||||||
|
|
||||||
|
// not encrypted
|
||||||
|
client.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.room_key',
|
||||||
|
content: {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'session_id': validSessionId,
|
||||||
|
'session_key': sessionKey,
|
||||||
|
});
|
||||||
|
await client.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getInboundGroupSession(
|
||||||
|
'!726s6s6q:example.com', validSessionId, validSenderKey) !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('outbound group session', () async {
|
||||||
|
final roomId = '!726s6s6q:example.com';
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
|
||||||
|
false);
|
||||||
|
var sess =
|
||||||
|
await client.encryption.keyManager.createOutboundGroupSession(roomId);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
|
||||||
|
true);
|
||||||
|
await client.encryption.keyManager.clearOutboundGroupSession(roomId);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
|
||||||
|
true);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getInboundGroupSession(roomId,
|
||||||
|
sess.outboundGroupSession.session_id(), client.identityKey) !=
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
// rotate after too many messages
|
||||||
|
sess.sentMessages = 300;
|
||||||
|
await client.encryption.keyManager.clearOutboundGroupSession(roomId);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
|
||||||
|
false);
|
||||||
|
|
||||||
|
// rotate if devices in room change
|
||||||
|
sess =
|
||||||
|
await client.encryption.keyManager.createOutboundGroupSession(roomId);
|
||||||
|
client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS']
|
||||||
|
.blocked = true;
|
||||||
|
await client.encryption.keyManager.clearOutboundGroupSession(roomId);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
|
||||||
|
false);
|
||||||
|
client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS']
|
||||||
|
.blocked = false;
|
||||||
|
|
||||||
|
// rotate if too far in the past
|
||||||
|
sess =
|
||||||
|
await client.encryption.keyManager.createOutboundGroupSession(roomId);
|
||||||
|
sess.creationTime = DateTime.now().subtract(Duration(days: 30));
|
||||||
|
await client.encryption.keyManager.clearOutboundGroupSession(roomId);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
|
||||||
|
false);
|
||||||
|
|
||||||
|
// force wipe
|
||||||
|
sess =
|
||||||
|
await client.encryption.keyManager.createOutboundGroupSession(roomId);
|
||||||
|
await client.encryption.keyManager
|
||||||
|
.clearOutboundGroupSession(roomId, wipe: true);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
|
||||||
|
false);
|
||||||
|
|
||||||
|
// load from database
|
||||||
|
sess =
|
||||||
|
await client.encryption.keyManager.createOutboundGroupSession(roomId);
|
||||||
|
client.encryption.keyManager.clearOutboundGroupSessions();
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
|
||||||
|
false);
|
||||||
|
await client.encryption.keyManager.loadOutboundGroupSession(roomId);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager.getOutboundGroupSession(roomId) != null,
|
||||||
|
true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('inbound group session', () async {
|
||||||
|
final roomId = '!726s6s6q:example.com';
|
||||||
|
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||||
|
final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
|
||||||
|
final sessionContent = <String, dynamic>{
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU',
|
||||||
|
'session_key':
|
||||||
|
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'
|
||||||
|
};
|
||||||
|
client.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
client.encryption.keyManager
|
||||||
|
.setInboundGroupSession(roomId, sessionId, senderKey, sessionContent);
|
||||||
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession('otherroom', sessionId, senderKey) !=
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession('otherroom', 'invalid', senderKey) !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
|
||||||
|
client.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
await client.encryption.keyManager
|
||||||
|
.loadInboundGroupSession(roomId, sessionId, senderKey);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dispose client', () async {
|
||||||
|
await client.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
331
test/encryption/key_request_test.dart
Normal file
331
test/encryption/key_request_test.dart
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
/*
|
||||||
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import '../fake_client.dart';
|
||||||
|
import '../fake_matrix_api.dart';
|
||||||
|
|
||||||
|
Map<String, dynamic> jsonDecode(dynamic payload) {
|
||||||
|
if (payload is String) {
|
||||||
|
try {
|
||||||
|
return json.decode(payload);
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload is Map<String, dynamic>) return payload;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
/// All Tests related to device keys
|
||||||
|
group('Key Request', () {
|
||||||
|
var olmEnabled = true;
|
||||||
|
try {
|
||||||
|
olm.init();
|
||||||
|
olm.Account();
|
||||||
|
} catch (_) {
|
||||||
|
olmEnabled = false;
|
||||||
|
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||||
|
}
|
||||||
|
print('[LibOlm] Enabled: $olmEnabled');
|
||||||
|
|
||||||
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||||
|
final validSenderKey = '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI';
|
||||||
|
test('Create Request', () async {
|
||||||
|
var matrix = await getClient();
|
||||||
|
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
|
||||||
|
await matrix.encryption.keyManager
|
||||||
|
.request(requestRoom, 'sessionId', validSenderKey);
|
||||||
|
var foundEvent = false;
|
||||||
|
for (var entry in FakeMatrixApi.calledEndpoints.entries) {
|
||||||
|
final payload = jsonDecode(entry.value.first);
|
||||||
|
if (entry.key
|
||||||
|
.startsWith('/client/r0/sendToDevice/m.room_key_request') &&
|
||||||
|
(payload['messages'] is Map) &&
|
||||||
|
(payload['messages']['@alice:example.com'] is Map) &&
|
||||||
|
(payload['messages']['@alice:example.com']['*'] is Map)) {
|
||||||
|
final content = payload['messages']['@alice:example.com']['*'];
|
||||||
|
if (content['action'] == 'request' &&
|
||||||
|
content['body']['room_id'] == '!726s6s6q:example.com' &&
|
||||||
|
content['body']['sender_key'] == validSenderKey &&
|
||||||
|
content['body']['session_id'] == 'sessionId') {
|
||||||
|
foundEvent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundEvent, true);
|
||||||
|
await matrix.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
test('Reply To Request', () async {
|
||||||
|
var matrix = await getClient();
|
||||||
|
matrix.setUserId('@alice:example.com'); // we need to pretend to be alice
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await matrix
|
||||||
|
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
|
||||||
|
.setBlocked(false, matrix);
|
||||||
|
await matrix
|
||||||
|
.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
|
||||||
|
.setVerified(true, matrix);
|
||||||
|
// test a successful share
|
||||||
|
var event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.room_key_request',
|
||||||
|
content: {
|
||||||
|
'action': 'request',
|
||||||
|
'body': {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'sender_key': validSenderKey,
|
||||||
|
'session_id': validSessionId,
|
||||||
|
},
|
||||||
|
'request_id': 'request_1',
|
||||||
|
'requesting_device_id': 'OTHERDEVICE',
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
print(FakeMatrixApi.calledEndpoints.keys.toString());
|
||||||
|
expect(
|
||||||
|
FakeMatrixApi.calledEndpoints.keys.any(
|
||||||
|
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||||
|
true);
|
||||||
|
|
||||||
|
// test various fail scenarios
|
||||||
|
|
||||||
|
// no body
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.room_key_request',
|
||||||
|
content: {
|
||||||
|
'action': 'request',
|
||||||
|
'request_id': 'request_2',
|
||||||
|
'requesting_device_id': 'OTHERDEVICE',
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
FakeMatrixApi.calledEndpoints.keys.any(
|
||||||
|
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||||
|
false);
|
||||||
|
|
||||||
|
// request by ourself
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.room_key_request',
|
||||||
|
content: {
|
||||||
|
'action': 'request',
|
||||||
|
'body': {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'sender_key': validSenderKey,
|
||||||
|
'session_id': validSessionId,
|
||||||
|
},
|
||||||
|
'request_id': 'request_3',
|
||||||
|
'requesting_device_id': 'JLAFKJWSCS',
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
FakeMatrixApi.calledEndpoints.keys.any(
|
||||||
|
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||||
|
false);
|
||||||
|
|
||||||
|
// device not found
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.room_key_request',
|
||||||
|
content: {
|
||||||
|
'action': 'request',
|
||||||
|
'body': {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'sender_key': validSenderKey,
|
||||||
|
'session_id': validSessionId,
|
||||||
|
},
|
||||||
|
'request_id': 'request_4',
|
||||||
|
'requesting_device_id': 'blubb',
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
FakeMatrixApi.calledEndpoints.keys.any(
|
||||||
|
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||||
|
false);
|
||||||
|
|
||||||
|
// unknown room
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.room_key_request',
|
||||||
|
content: {
|
||||||
|
'action': 'request',
|
||||||
|
'body': {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!invalid:example.com',
|
||||||
|
'sender_key': validSenderKey,
|
||||||
|
'session_id': validSessionId,
|
||||||
|
},
|
||||||
|
'request_id': 'request_5',
|
||||||
|
'requesting_device_id': 'OTHERDEVICE',
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
FakeMatrixApi.calledEndpoints.keys.any(
|
||||||
|
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||||
|
false);
|
||||||
|
|
||||||
|
// unknwon session
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.room_key_request',
|
||||||
|
content: {
|
||||||
|
'action': 'request',
|
||||||
|
'body': {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'sender_key': validSenderKey,
|
||||||
|
'session_id': 'invalid',
|
||||||
|
},
|
||||||
|
'request_id': 'request_6',
|
||||||
|
'requesting_device_id': 'OTHERDEVICE',
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
FakeMatrixApi.calledEndpoints.keys.any(
|
||||||
|
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||||
|
false);
|
||||||
|
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await matrix.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
test('Receive shared keys', () async {
|
||||||
|
var matrix = await getClient();
|
||||||
|
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
|
||||||
|
await matrix.encryption.keyManager
|
||||||
|
.request(requestRoom, validSessionId, validSenderKey);
|
||||||
|
|
||||||
|
final session = await matrix.encryption.keyManager
|
||||||
|
.loadInboundGroupSession(
|
||||||
|
requestRoom.id, validSessionId, validSenderKey);
|
||||||
|
final sessionKey = session.inboundGroupSession
|
||||||
|
.export_session(session.inboundGroupSession.first_known_index());
|
||||||
|
matrix.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
var event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.forwarded_room_key',
|
||||||
|
content: {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'session_id': validSessionId,
|
||||||
|
'session_key': sessionKey,
|
||||||
|
'sender_key': validSenderKey,
|
||||||
|
'forwarding_curve25519_key_chain': [],
|
||||||
|
},
|
||||||
|
encryptedContent: {
|
||||||
|
'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
matrix.encryption.keyManager.getInboundGroupSession(
|
||||||
|
requestRoom.id, validSessionId, validSenderKey) !=
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
// now test a few invalid scenarios
|
||||||
|
|
||||||
|
// request not found
|
||||||
|
matrix.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.forwarded_room_key',
|
||||||
|
content: {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'session_id': validSessionId,
|
||||||
|
'session_key': sessionKey,
|
||||||
|
'sender_key': validSenderKey,
|
||||||
|
'forwarding_curve25519_key_chain': [],
|
||||||
|
},
|
||||||
|
encryptedContent: {
|
||||||
|
'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
matrix.encryption.keyManager.getInboundGroupSession(
|
||||||
|
requestRoom.id, validSessionId, validSenderKey) !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
|
||||||
|
// unknown device
|
||||||
|
await matrix.encryption.keyManager
|
||||||
|
.request(requestRoom, validSessionId, validSenderKey);
|
||||||
|
matrix.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.forwarded_room_key',
|
||||||
|
content: {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'session_id': validSessionId,
|
||||||
|
'session_key': sessionKey,
|
||||||
|
'sender_key': validSenderKey,
|
||||||
|
'forwarding_curve25519_key_chain': [],
|
||||||
|
},
|
||||||
|
encryptedContent: {
|
||||||
|
'sender_key': 'invalid',
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
matrix.encryption.keyManager.getInboundGroupSession(
|
||||||
|
requestRoom.id, validSessionId, validSenderKey) !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
|
||||||
|
// no encrypted content
|
||||||
|
await matrix.encryption.keyManager
|
||||||
|
.request(requestRoom, validSessionId, validSenderKey);
|
||||||
|
matrix.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
event = ToDeviceEvent(
|
||||||
|
sender: '@alice:example.com',
|
||||||
|
type: 'm.forwarded_room_key',
|
||||||
|
content: {
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': '!726s6s6q:example.com',
|
||||||
|
'session_id': validSessionId,
|
||||||
|
'session_key': sessionKey,
|
||||||
|
'sender_key': validSenderKey,
|
||||||
|
'forwarding_curve25519_key_chain': [],
|
||||||
|
});
|
||||||
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
|
expect(
|
||||||
|
matrix.encryption.keyManager.getInboundGroupSession(
|
||||||
|
requestRoom.id, validSessionId, validSenderKey) !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
|
||||||
|
await matrix.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
* Copyright (C) 2019, 2020 Famedly GmbH
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
@ -17,10 +17,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/encryption.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
import 'fake_matrix_api.dart';
|
import '../fake_client.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
/// All Tests related to the ChatTime
|
/// All Tests related to the ChatTime
|
||||||
|
@ -35,19 +36,24 @@ void main() {
|
||||||
}
|
}
|
||||||
print('[LibOlm] Enabled: $olmEnabled');
|
print('[LibOlm] Enabled: $olmEnabled');
|
||||||
|
|
||||||
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
if (!olmEnabled) return;
|
||||||
client.api.homeserver = Uri.parse('https://fakeserver.notexisting');
|
|
||||||
var room = Room(id: '!localpart:server.abc', client: client);
|
Client client;
|
||||||
|
Room room;
|
||||||
var updateCounter = 0;
|
var updateCounter = 0;
|
||||||
final keyVerification = KeyVerification(
|
KeyVerification keyVerification;
|
||||||
client: client,
|
|
||||||
|
test('setupClient', () async {
|
||||||
|
client = await getClient();
|
||||||
|
room = Room(id: '!localpart:server.abc', client: client);
|
||||||
|
keyVerification = KeyVerification(
|
||||||
|
encryption: client.encryption,
|
||||||
room: room,
|
room: room,
|
||||||
userId: '@alice:example.com',
|
userId: '@alice:example.com',
|
||||||
deviceId: 'ABCD',
|
deviceId: 'ABCD',
|
||||||
onUpdate: () => updateCounter++,
|
onUpdate: () => updateCounter++,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
if (!olmEnabled) return;
|
|
||||||
|
|
||||||
test('acceptSas', () async {
|
test('acceptSas', () async {
|
||||||
await keyVerification.acceptSas();
|
await keyVerification.acceptSas();
|
||||||
|
@ -91,7 +97,11 @@ void main() {
|
||||||
test('verifyActivity', () async {
|
test('verifyActivity', () async {
|
||||||
final verified = await keyVerification.verifyActivity();
|
final verified = await keyVerification.verifyActivity();
|
||||||
expect(verified, true);
|
expect(verified, true);
|
||||||
|
keyVerification?.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dispose client', () async {
|
||||||
|
await client.dispose(closeDatabase: true);
|
||||||
});
|
});
|
||||||
keyVerification.dispose();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
116
test/encryption/olm_manager_test.dart
Normal file
116
test/encryption/olm_manager_test.dart
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import '../fake_client.dart';
|
||||||
|
import '../fake_matrix_api.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Olm Manager', () {
|
||||||
|
var olmEnabled = true;
|
||||||
|
try {
|
||||||
|
olm.init();
|
||||||
|
olm.Account();
|
||||||
|
} catch (_) {
|
||||||
|
olmEnabled = false;
|
||||||
|
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||||
|
}
|
||||||
|
print('[LibOlm] Enabled: $olmEnabled');
|
||||||
|
|
||||||
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
Client client;
|
||||||
|
|
||||||
|
test('setupClient', () async {
|
||||||
|
client = await getClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('signatures', () async {
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'fox': 'floof',
|
||||||
|
};
|
||||||
|
final signedPayload = client.encryption.olmManager.signJson(payload);
|
||||||
|
expect(
|
||||||
|
client.encryption.olmManager.checkJsonSignature(client.fingerprintKey,
|
||||||
|
signedPayload, client.userID, client.deviceID),
|
||||||
|
true);
|
||||||
|
expect(
|
||||||
|
client.encryption.olmManager.checkJsonSignature(
|
||||||
|
client.fingerprintKey, payload, client.userID, client.deviceID),
|
||||||
|
false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uploadKeys', () async {
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
final res =
|
||||||
|
await client.encryption.olmManager.uploadKeys(uploadDeviceKeys: true);
|
||||||
|
expect(res, true);
|
||||||
|
var sent = json.decode(
|
||||||
|
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first);
|
||||||
|
expect(sent['device_keys'] != null, true);
|
||||||
|
expect(sent['one_time_keys'] != null, true);
|
||||||
|
expect(sent['one_time_keys'].keys.length, 66);
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await client.encryption.olmManager.uploadKeys();
|
||||||
|
sent = json.decode(
|
||||||
|
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first);
|
||||||
|
expect(sent['device_keys'] != null, false);
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await client.encryption.olmManager.uploadKeys(oldKeyCount: 20);
|
||||||
|
sent = json.decode(
|
||||||
|
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first);
|
||||||
|
expect(sent['one_time_keys'].keys.length, 46);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleDeviceOneTimeKeysCount', () async {
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
client.encryption.olmManager
|
||||||
|
.handleDeviceOneTimeKeysCount({'signed_curve25519': 20});
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(
|
||||||
|
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
|
||||||
|
true);
|
||||||
|
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
client.encryption.olmManager
|
||||||
|
.handleDeviceOneTimeKeysCount({'signed_curve25519': 70});
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(
|
||||||
|
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
|
||||||
|
false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startOutgoingOlmSessions', () async {
|
||||||
|
// start an olm session.....with ourself!
|
||||||
|
await client.encryption.olmManager.startOutgoingOlmSessions(
|
||||||
|
[client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]]);
|
||||||
|
expect(
|
||||||
|
client.encryption.olmManager.olmSessions
|
||||||
|
.containsKey(client.identityKey),
|
||||||
|
true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dispose client', () async {
|
||||||
|
await client.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
|
import 'package:famedlysdk/encryption.dart';
|
||||||
import 'package:famedlysdk/src/event.dart';
|
import 'package:famedlysdk/src/event.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
@ -219,7 +220,7 @@ void main() {
|
||||||
event.status = -1;
|
event.status = -1;
|
||||||
final resp2 = await event.sendAgain(txid: '1234');
|
final resp2 = await event.sendAgain(txid: '1234');
|
||||||
expect(resp1, null);
|
expect(resp1, null);
|
||||||
expect(resp2, '42');
|
expect(resp2.startsWith('\$event'), true);
|
||||||
|
|
||||||
await matrix.dispose(closeDatabase: true);
|
await matrix.dispose(closeDatabase: true);
|
||||||
});
|
});
|
||||||
|
|
48
test/fake_client.dart
Normal file
48
test/fake_client.dart
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
|
||||||
|
import 'fake_matrix_api.dart';
|
||||||
|
import 'fake_database.dart';
|
||||||
|
|
||||||
|
// key @test:fakeServer.notExisting
|
||||||
|
const pickledOlmAccount =
|
||||||
|
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
|
||||||
|
|
||||||
|
Future<Client> getClient() async {
|
||||||
|
final client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||||
|
client.database = getDatabase();
|
||||||
|
await client.checkServer('https://fakeServer.notExisting');
|
||||||
|
final resp = await client.api.login(
|
||||||
|
type: 'm.login.password',
|
||||||
|
user: 'test',
|
||||||
|
password: '1234',
|
||||||
|
initialDeviceDisplayName: 'Fluffy Matrix Client',
|
||||||
|
);
|
||||||
|
client.connect(
|
||||||
|
newToken: resp.accessToken,
|
||||||
|
newUserID: resp.userId,
|
||||||
|
newHomeserver: client.api.homeserver,
|
||||||
|
newDeviceName: 'Text Matrix Client',
|
||||||
|
newDeviceID: resp.deviceId,
|
||||||
|
newOlmAccount: pickledOlmAccount,
|
||||||
|
);
|
||||||
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
|
return client;
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import 'package:http/testing.dart';
|
||||||
|
|
||||||
class FakeMatrixApi extends MockClient {
|
class FakeMatrixApi extends MockClient {
|
||||||
static final calledEndpoints = <String, List<dynamic>>{};
|
static final calledEndpoints = <String, List<dynamic>>{};
|
||||||
|
static int eventCounter = 0;
|
||||||
|
|
||||||
FakeMatrixApi()
|
FakeMatrixApi()
|
||||||
: super((request) async {
|
: super((request) async {
|
||||||
|
@ -527,16 +528,32 @@ class FakeMatrixApi extends MockClient {
|
||||||
'rooms': ['!726s6s6q:example.com']
|
'rooms': ['!726s6s6q:example.com']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// 'sender': '@othertest:fakeServer.notExisting',
|
||||||
|
// 'content': {
|
||||||
|
// 'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
// 'room_id': '!726s6s6q:example.com',
|
||||||
|
// 'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU',
|
||||||
|
// 'session_key':
|
||||||
|
// 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'
|
||||||
|
// },
|
||||||
|
// 'type': 'm.room_key'
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
'sender': '@alice:example.com',
|
// this is the commented out m.room_key event - only encrypted
|
||||||
|
'sender': '@othertest:fakeServer.notExisting',
|
||||||
'content': {
|
'content': {
|
||||||
'algorithm': 'm.megolm.v1.aes-sha2',
|
'algorithm': 'm.olm.v1.curve25519-aes-sha2',
|
||||||
'room_id': '!726s6s6q:example.com',
|
'sender_key': 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg',
|
||||||
'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU',
|
'ciphertext': {
|
||||||
'session_key':
|
'7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk': {
|
||||||
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'
|
'type': 0,
|
||||||
|
'body':
|
||||||
|
'Awogyh7K4iLUQjcOxIfi7q7LhBBqv9w0mQ6JI9+U9tv7iF4SIHC6xb5YFWf9voRnmDBbd+0vxD/xDlVNRDlPIKliLGkYGiAkEbtlo+fng4ELtO4gSLKVbcFn7tZwZCEUE8H2miBsCCKABgMKIFrKDJwB7gM3lXPt9yVoh6gQksafKt7VFCNRN5KLKqsDEAAi0AX5EfTV7jJ1ZWAbxftjoSN6kCVIxzGclbyg1HjchmNCX7nxNCHWl+q5ZgqHYZVu2n2mCVmIaKD0kvoEZeY3tV1Itb6zf67BLaU0qgW/QzHCHg5a44tNLjucvL2mumHjIG8k0BY2uh+52HeiMCvSOvtDwHg7nzCASGdqPVCj9Kzw6z7F6nL4e3mYim8zvJd7f+mD9z3ARrypUOLGkTGYbB2PQOovf0Do8WzcaRzfaUCnuu/YVZWKK7DPgG8uhw/TjR6XtraAKZysF+4DJYMG9SQWx558r6s7Z5EUOF5CU2M35w1t1Xxllb3vrS83dtf9LPCrBhLsEBeYEUBE2+bTBfl0BDKqLiB0Cc0N0ixOcHIt6e40wAvW622/gMgHlpNSx8xG12u0s6h6EMWdCXXLWd9fy2q6glFUHvA67A35q7O+M8DVml7Y9xG55Y3DHkMDc9cwgwFkBDCAYQe6pQF1nlKytcVCGREpBs/gq69gHAStMQ8WEg38Lf8u8eBr2DFexrN4U+QAk+S//P3fJgf0bQx/Eosx4fvWSz9En41iC+ADCsWQpMbwHn4JWvtAbn3oW0XmL/OgThTkJMLiCymduYAa1Hnt7a3tP0KTL2/x11F02ggQHL28cCjq5W4zUGjWjl5wo2PsKB6t8aAvMg2ujGD2rCjb4yrv5VIzAKMOZLyj7K0vSK9gwDLQ/4vq+QnKUBG5zrcOze0hX+kz2909/tmAdeCH61Ypw7gbPUJAKnmKYUiB/UgwkJvzMJSsk/SEs5SXosHDI+HsJHJp4Mp4iKD0xRMst+8f9aTjaWwh8ZvELE1ZOhhCbF3RXhxi3x2Nu8ORIz+vhEQ1NOlMc7UIo98Fk/96T36vL/fviowT4C/0AlaapZDJBmKwhmwqisMjY2n1vY29oM2p5BzY1iwP7q9BYdRFst6xwo57TNSuRwQw7IhFsf0k+ABuPEZy5xB5nPHyIRTf/pr3Hw',
|
||||||
},
|
},
|
||||||
'type': 'm.room_key'
|
},
|
||||||
|
},
|
||||||
|
'type': 'm.room.encrypted',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1567,7 +1584,20 @@ class FakeMatrixApi extends MockClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
'@test:fakeServer.notExisting': {
|
||||||
|
'GHTYAJCE': {
|
||||||
|
'signed_curve25519:AAAAAQ': {
|
||||||
|
'key': 'qc72ve94cA28iuE0fXa98QO3uls39DHWdQlYyvvhGh0',
|
||||||
|
'signatures': {
|
||||||
|
'@test:fakeServer.notExisting': {
|
||||||
|
'ed25519:GHTYAJCE':
|
||||||
|
'dFwffr5kTKefO7sjnWLMhTzw7oV31nkPIDRxFy5OQT2OP5++Ao0KRbaBZ6qfuT7lW1owKK0Xk3s7QTBvc/eNDA',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'/client/r0/rooms/!localpart%3Aexample.com/invite': (var req) => {},
|
'/client/r0/rooms/!localpart%3Aexample.com/invite': (var req) => {},
|
||||||
|
@ -1584,7 +1614,8 @@ class FakeMatrixApi extends MockClient {
|
||||||
'/client/r0/keys/upload': (var req) => {
|
'/client/r0/keys/upload': (var req) => {
|
||||||
'one_time_key_counts': {
|
'one_time_key_counts': {
|
||||||
'curve25519': 10,
|
'curve25519': 10,
|
||||||
'signed_curve25519': 100,
|
'signed_curve25519':
|
||||||
|
json.decode(req)['one_time_keys']?.keys?.length ?? 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'/client/r0/keys/query': (var req) => {
|
'/client/r0/keys/query': (var req) => {
|
||||||
|
@ -1625,8 +1656,42 @@ class FakeMatrixApi extends MockClient {
|
||||||
},
|
},
|
||||||
'signatures': {},
|
'signatures': {},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
'@test:fakeServer.notExisting': {
|
||||||
|
'GHTYAJCE': {
|
||||||
|
'user_id': '@test:fakeServer.notExisting',
|
||||||
|
'device_id': 'GHTYAJCE',
|
||||||
|
'algorithms': [
|
||||||
|
'm.olm.v1.curve25519-aes-sha2',
|
||||||
|
'm.megolm.v1.aes-sha2'
|
||||||
|
],
|
||||||
|
'keys': {
|
||||||
|
'curve25519:GHTYAJCE':
|
||||||
|
'7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk',
|
||||||
|
'ed25519:GHTYAJCE':
|
||||||
|
'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo'
|
||||||
|
},
|
||||||
|
'signatures': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'@othertest:fakeServer.notExisting': {
|
||||||
|
'FOXDEVICE': {
|
||||||
|
'user_id': '@othertest:fakeServer.notExisting',
|
||||||
|
'device_id': 'FOXDEVICE',
|
||||||
|
'algorithms': [
|
||||||
|
'm.olm.v1.curve25519-aes-sha2',
|
||||||
|
'm.megolm.v1.aes-sha2'
|
||||||
|
],
|
||||||
|
'keys': {
|
||||||
|
'curve25519:FOXDEVICE':
|
||||||
|
'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg',
|
||||||
|
'ed25519:FOXDEVICE':
|
||||||
|
'R5/p04tticvdlNIxiiBIP0j9OQWv8ep6eEU6/lWKDxw',
|
||||||
|
},
|
||||||
|
'signatures': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'/client/r0/register': (var req) => {
|
'/client/r0/register': (var req) => {
|
||||||
'user_id': '@testuser:example.com',
|
'user_id': '@testuser:example.com',
|
||||||
|
@ -1706,13 +1771,13 @@ class FakeMatrixApi extends MockClient {
|
||||||
'/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {},
|
'/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {},
|
||||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/testtxid':
|
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/testtxid':
|
||||||
(var reqI) => {
|
(var reqI) => {
|
||||||
'event_id': '42',
|
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||||
},
|
},
|
||||||
'/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com':
|
'/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/r0/rooms/%211234%3Aexample.com/send/m.room.message/1234':
|
'/client/r0/rooms/%211234%3Aexample.com/send/m.room.message/1234':
|
||||||
(var reqI) => {
|
(var reqI) => {
|
||||||
'event_id': '42',
|
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||||
},
|
},
|
||||||
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag':
|
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -26,7 +26,7 @@ import 'package:famedlysdk/src/database/database.dart'
|
||||||
show DbRoom, DbRoomState, DbRoomAccountData;
|
show DbRoom, DbRoomState, DbRoomAccountData;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import 'fake_matrix_api.dart';
|
import 'fake_client.dart';
|
||||||
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
@ -37,15 +37,7 @@ void main() {
|
||||||
/// All Tests related to the Event
|
/// All Tests related to the Event
|
||||||
group('Room', () {
|
group('Room', () {
|
||||||
test('Login', () async {
|
test('Login', () async {
|
||||||
matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
matrix = await getClient();
|
||||||
|
|
||||||
final checkResp =
|
|
||||||
await matrix.checkServer('https://fakeServer.notExisting');
|
|
||||||
|
|
||||||
final loginResp = await matrix.login('test', '1234');
|
|
||||||
|
|
||||||
expect(checkResp, true);
|
|
||||||
expect(loginResp, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Create from json', () async {
|
test('Create from json', () async {
|
||||||
|
@ -315,7 +307,7 @@ void main() {
|
||||||
|
|
||||||
test('getTimeline', () async {
|
test('getTimeline', () async {
|
||||||
final timeline = await room.getTimeline();
|
final timeline = await room.getTimeline();
|
||||||
expect(timeline.events, []);
|
expect(timeline.events.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getUserByMXID', () async {
|
test('getUserByMXID', () async {
|
||||||
|
@ -338,13 +330,13 @@ void main() {
|
||||||
final dynamic resp = await room.sendEvent(
|
final dynamic resp = await room.sendEvent(
|
||||||
{'msgtype': 'm.text', 'body': 'hello world'},
|
{'msgtype': 'm.text', 'body': 'hello world'},
|
||||||
txid: 'testtxid');
|
txid: 'testtxid');
|
||||||
expect(resp, '42');
|
expect(resp.startsWith('\$event'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sendEvent', () async {
|
test('sendEvent', () async {
|
||||||
final dynamic resp =
|
final dynamic resp =
|
||||||
await room.sendTextEvent('Hello world', txid: 'testtxid');
|
await room.sendTextEvent('Hello world', txid: 'testtxid');
|
||||||
expect(resp, '42');
|
expect(resp.startsWith('\$event'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Not working because there is no real file to test it...
|
// Not working because there is no real file to test it...
|
||||||
|
@ -388,60 +380,6 @@ void main() {
|
||||||
);
|
);
|
||||||
expect(room.encrypted, true);
|
expect(room.encrypted, true);
|
||||||
expect(room.encryptionAlgorithm, 'm.megolm.v1.aes-sha2');
|
expect(room.encryptionAlgorithm, 'm.megolm.v1.aes-sha2');
|
||||||
expect(room.outboundGroupSession, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('createOutboundGroupSession', () async {
|
|
||||||
if (!room.client.encryptionEnabled) return;
|
|
||||||
await room.createOutboundGroupSession();
|
|
||||||
expect(room.outboundGroupSession != null, true);
|
|
||||||
expect(room.outboundGroupSession.session_id().isNotEmpty, true);
|
|
||||||
expect(
|
|
||||||
room.inboundGroupSessions
|
|
||||||
.containsKey(room.outboundGroupSession.session_id()),
|
|
||||||
true);
|
|
||||||
expect(
|
|
||||||
room.inboundGroupSessions[room.outboundGroupSession.session_id()]
|
|
||||||
.content['session_key'],
|
|
||||||
room.outboundGroupSession.session_key());
|
|
||||||
expect(
|
|
||||||
room.inboundGroupSessions[room.outboundGroupSession.session_id()]
|
|
||||||
.indexes.length,
|
|
||||||
0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clearOutboundGroupSession', () async {
|
|
||||||
if (!room.client.encryptionEnabled) return;
|
|
||||||
await room.clearOutboundGroupSession(wipe: true);
|
|
||||||
expect(room.outboundGroupSession == null, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('encryptGroupMessagePayload and decryptGroupMessage', () async {
|
|
||||||
if (!room.client.encryptionEnabled) return;
|
|
||||||
final payload = {
|
|
||||||
'msgtype': 'm.text',
|
|
||||||
'body': 'Hello world',
|
|
||||||
};
|
|
||||||
final encryptedPayload = await room.encryptGroupMessagePayload(payload);
|
|
||||||
expect(encryptedPayload['algorithm'], 'm.megolm.v1.aes-sha2');
|
|
||||||
expect(encryptedPayload['ciphertext'].isNotEmpty, true);
|
|
||||||
expect(encryptedPayload['device_id'], room.client.deviceID);
|
|
||||||
expect(encryptedPayload['sender_key'], room.client.identityKey);
|
|
||||||
expect(encryptedPayload['session_id'],
|
|
||||||
room.outboundGroupSession.session_id());
|
|
||||||
|
|
||||||
var encryptedEvent = Event(
|
|
||||||
content: encryptedPayload,
|
|
||||||
type: 'm.room.encrypted',
|
|
||||||
senderId: room.client.userID,
|
|
||||||
eventId: '1234',
|
|
||||||
roomId: room.id,
|
|
||||||
room: room,
|
|
||||||
originServerTs: DateTime.now(),
|
|
||||||
);
|
|
||||||
var decryptedEvent = room.decryptGroupMessage(encryptedEvent);
|
|
||||||
expect(decryptedEvent.type, 'm.room.message');
|
|
||||||
expect(decryptedEvent.content, payload);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setPushRuleState', () async {
|
test('setPushRuleState', () async {
|
||||||
|
|
|
@ -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()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -143,7 +143,8 @@ void main() {
|
||||||
expect(updateCount, 5);
|
expect(updateCount, 5);
|
||||||
expect(insertList, [0, 0, 0]);
|
expect(insertList, [0, 0, 0]);
|
||||||
expect(insertList.length, timeline.events.length);
|
expect(insertList.length, timeline.events.length);
|
||||||
expect(timeline.events[0].eventId, '42');
|
final eventId = timeline.events[0].eventId;
|
||||||
|
expect(eventId.startsWith('\$event'), true);
|
||||||
expect(timeline.events[0].status, 1);
|
expect(timeline.events[0].status, 1);
|
||||||
|
|
||||||
client.onEvent.add(EventUpdate(
|
client.onEvent.add(EventUpdate(
|
||||||
|
@ -155,7 +156,7 @@ void main() {
|
||||||
'content': {'msgtype': 'm.text', 'body': 'test'},
|
'content': {'msgtype': 'm.text', 'body': 'test'},
|
||||||
'sender': '@alice:example.com',
|
'sender': '@alice:example.com',
|
||||||
'status': 2,
|
'status': 2,
|
||||||
'event_id': '42',
|
'event_id': eventId,
|
||||||
'unsigned': {'transaction_id': '1234'},
|
'unsigned': {'transaction_id': '1234'},
|
||||||
'origin_server_ts': DateTime.now().millisecondsSinceEpoch
|
'origin_server_ts': DateTime.now().millisecondsSinceEpoch
|
||||||
},
|
},
|
||||||
|
@ -166,7 +167,7 @@ void main() {
|
||||||
expect(updateCount, 6);
|
expect(updateCount, 6);
|
||||||
expect(insertList, [0, 0, 0]);
|
expect(insertList, [0, 0, 0]);
|
||||||
expect(insertList.length, timeline.events.length);
|
expect(insertList.length, timeline.events.length);
|
||||||
expect(timeline.events[0].eventId, '42');
|
expect(timeline.events[0].eventId, eventId);
|
||||||
expect(timeline.events[0].status, 2);
|
expect(timeline.events[0].status, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,8 @@ void test() async {
|
||||||
await room.enableEncryption();
|
await room.enableEncryption();
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await Future.delayed(Duration(seconds: 5));
|
||||||
assert(room.encrypted == true);
|
assert(room.encrypted == true);
|
||||||
assert(room.outboundGroupSession == null);
|
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) ==
|
||||||
|
null);
|
||||||
|
|
||||||
print('++++ ($testUserA) Check known olm devices ++++');
|
print('++++ ($testUserA) Check known olm devices ++++');
|
||||||
assert(testClientA.userDeviceKeys.containsKey(testUserB));
|
assert(testClientA.userDeviceKeys.containsKey(testUserB));
|
||||||
|
@ -123,16 +124,30 @@ void test() async {
|
||||||
print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++");
|
print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++");
|
||||||
await room.sendTextEvent(testMessage);
|
await room.sendTextEvent(testMessage);
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await Future.delayed(Duration(seconds: 5));
|
||||||
assert(room.outboundGroupSession != null);
|
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) !=
|
||||||
var currentSessionIdA = room.outboundGroupSession.session_id();
|
null);
|
||||||
assert(room.inboundGroupSessions
|
var currentSessionIdA = room.client.encryption.keyManager
|
||||||
.containsKey(room.outboundGroupSession.session_id()));
|
.getOutboundGroupSession(room.id)
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
.outboundGroupSession
|
||||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
.session_id();
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
assert(room.client.encryption.keyManager
|
||||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
||||||
assert(inviteRoom.inboundGroupSessions
|
null);
|
||||||
.containsKey(room.outboundGroupSession.session_id()));
|
assert(testClientA
|
||||||
|
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||||
|
1);
|
||||||
|
assert(testClientB
|
||||||
|
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||||
|
1);
|
||||||
|
assert(testClientA
|
||||||
|
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||||
|
.session_id() ==
|
||||||
|
testClientB
|
||||||
|
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||||
|
.session_id());
|
||||||
|
assert(inviteRoom.client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||||
|
null);
|
||||||
assert(room.lastMessage == testMessage);
|
assert(room.lastMessage == testMessage);
|
||||||
assert(inviteRoom.lastMessage == testMessage);
|
assert(inviteRoom.lastMessage == testMessage);
|
||||||
print(
|
print(
|
||||||
|
@ -141,14 +156,27 @@ void test() async {
|
||||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++");
|
print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++");
|
||||||
await room.sendTextEvent(testMessage2);
|
await room.sendTextEvent(testMessage2);
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await Future.delayed(Duration(seconds: 5));
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
assert(testClientA
|
||||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
1);
|
||||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
assert(testClientB
|
||||||
|
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||||
|
1);
|
||||||
|
assert(testClientA
|
||||||
|
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||||
|
.session_id() ==
|
||||||
|
testClientB
|
||||||
|
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||||
|
.session_id());
|
||||||
|
|
||||||
assert(room.outboundGroupSession.session_id() == currentSessionIdA);
|
assert(room.client.encryption.keyManager
|
||||||
assert(inviteRoom.inboundGroupSessions
|
.getOutboundGroupSession(room.id)
|
||||||
.containsKey(room.outboundGroupSession.session_id()));
|
.outboundGroupSession
|
||||||
|
.session_id() ==
|
||||||
|
currentSessionIdA);
|
||||||
|
assert(room.client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
||||||
|
null);
|
||||||
assert(room.lastMessage == testMessage2);
|
assert(room.lastMessage == testMessage2);
|
||||||
assert(inviteRoom.lastMessage == testMessage2);
|
assert(inviteRoom.lastMessage == testMessage2);
|
||||||
print(
|
print(
|
||||||
|
@ -157,14 +185,31 @@ void test() async {
|
||||||
print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++");
|
print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++");
|
||||||
await inviteRoom.sendTextEvent(testMessage3);
|
await inviteRoom.sendTextEvent(testMessage3);
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await Future.delayed(Duration(seconds: 5));
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
assert(testClientA
|
||||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||||
assert(room.outboundGroupSession.session_id() == currentSessionIdA);
|
1);
|
||||||
assert(inviteRoom.outboundGroupSession != null);
|
assert(testClientB
|
||||||
assert(inviteRoom.inboundGroupSessions
|
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||||
.containsKey(inviteRoom.outboundGroupSession.session_id()));
|
1);
|
||||||
assert(room.inboundGroupSessions
|
assert(room.client.encryption.keyManager
|
||||||
.containsKey(inviteRoom.outboundGroupSession.session_id()));
|
.getOutboundGroupSession(room.id)
|
||||||
|
.outboundGroupSession
|
||||||
|
.session_id() ==
|
||||||
|
currentSessionIdA);
|
||||||
|
var inviteRoomOutboundGroupSession = inviteRoom.client.encryption.keyManager
|
||||||
|
.getOutboundGroupSession(inviteRoom.id);
|
||||||
|
|
||||||
|
assert(inviteRoomOutboundGroupSession != null);
|
||||||
|
assert(inviteRoom.client.encryption.keyManager.getInboundGroupSession(
|
||||||
|
inviteRoom.id,
|
||||||
|
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
||||||
|
'') !=
|
||||||
|
null);
|
||||||
|
assert(room.client.encryption.keyManager.getInboundGroupSession(
|
||||||
|
room.id,
|
||||||
|
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
||||||
|
'') !=
|
||||||
|
null);
|
||||||
assert(inviteRoom.lastMessage == testMessage3);
|
assert(inviteRoom.lastMessage == testMessage3);
|
||||||
assert(room.lastMessage == testMessage3);
|
assert(room.lastMessage == testMessage3);
|
||||||
print(
|
print(
|
||||||
|
@ -180,18 +225,42 @@ void test() async {
|
||||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++");
|
print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++");
|
||||||
await room.sendTextEvent(testMessage4);
|
await room.sendTextEvent(testMessage4);
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await Future.delayed(Duration(seconds: 5));
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
assert(testClientA
|
||||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
1);
|
||||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
assert(testClientB
|
||||||
assert(testClientA.olmSessions[testClientC.identityKey].length == 1);
|
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||||
assert(testClientC.olmSessions[testClientA.identityKey].length == 1);
|
1);
|
||||||
assert(testClientA.olmSessions[testClientC.identityKey].first.session_id() ==
|
assert(testClientA
|
||||||
testClientC.olmSessions[testClientA.identityKey].first.session_id());
|
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||||
assert(room.outboundGroupSession.session_id() != currentSessionIdA);
|
.session_id() ==
|
||||||
currentSessionIdA = room.outboundGroupSession.session_id();
|
testClientB
|
||||||
assert(inviteRoom.inboundGroupSessions
|
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||||
.containsKey(room.outboundGroupSession.session_id()));
|
.session_id());
|
||||||
|
assert(testClientA
|
||||||
|
.encryption.olmManager.olmSessions[testClientC.identityKey].length ==
|
||||||
|
1);
|
||||||
|
assert(testClientC
|
||||||
|
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||||
|
1);
|
||||||
|
assert(testClientA
|
||||||
|
.encryption.olmManager.olmSessions[testClientC.identityKey].first
|
||||||
|
.session_id() ==
|
||||||
|
testClientC
|
||||||
|
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||||
|
.session_id());
|
||||||
|
assert(room.client.encryption.keyManager
|
||||||
|
.getOutboundGroupSession(room.id)
|
||||||
|
.outboundGroupSession
|
||||||
|
.session_id() !=
|
||||||
|
currentSessionIdA);
|
||||||
|
currentSessionIdA = room.client.encryption.keyManager
|
||||||
|
.getOutboundGroupSession(room.id)
|
||||||
|
.outboundGroupSession
|
||||||
|
.session_id();
|
||||||
|
assert(inviteRoom.client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||||
|
null);
|
||||||
assert(room.lastMessage == testMessage4);
|
assert(room.lastMessage == testMessage4);
|
||||||
assert(inviteRoom.lastMessage == testMessage4);
|
assert(inviteRoom.lastMessage == testMessage4);
|
||||||
print(
|
print(
|
||||||
|
@ -206,14 +275,30 @@ void test() async {
|
||||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++");
|
print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++");
|
||||||
await room.sendTextEvent(testMessage6);
|
await room.sendTextEvent(testMessage6);
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await Future.delayed(Duration(seconds: 5));
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
assert(testClientA
|
||||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
1);
|
||||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
assert(testClientB
|
||||||
assert(room.outboundGroupSession.session_id() != currentSessionIdA);
|
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||||
currentSessionIdA = room.outboundGroupSession.session_id();
|
1);
|
||||||
assert(inviteRoom.inboundGroupSessions
|
assert(testClientA
|
||||||
.containsKey(room.outboundGroupSession.session_id()));
|
.encryption.olmManager.olmSessions[testClientB.identityKey].first
|
||||||
|
.session_id() ==
|
||||||
|
testClientB
|
||||||
|
.encryption.olmManager.olmSessions[testClientA.identityKey].first
|
||||||
|
.session_id());
|
||||||
|
assert(room.client.encryption.keyManager
|
||||||
|
.getOutboundGroupSession(room.id)
|
||||||
|
.outboundGroupSession
|
||||||
|
.session_id() !=
|
||||||
|
currentSessionIdA);
|
||||||
|
currentSessionIdA = room.client.encryption.keyManager
|
||||||
|
.getOutboundGroupSession(room.id)
|
||||||
|
.outboundGroupSession
|
||||||
|
.session_id();
|
||||||
|
assert(inviteRoom.client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||||
|
null);
|
||||||
assert(room.lastMessage == testMessage6);
|
assert(room.lastMessage == testMessage6);
|
||||||
assert(inviteRoom.lastMessage == testMessage6);
|
assert(inviteRoom.lastMessage == testMessage6);
|
||||||
print(
|
print(
|
||||||
|
@ -241,18 +326,18 @@ void test() async {
|
||||||
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
|
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
|
||||||
room.inboundGroupSessions.keys.toList()[i]);
|
room.inboundGroupSessions.keys.toList()[i]);
|
||||||
}
|
}
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
|
||||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1);
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id());
|
||||||
|
|
||||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++");
|
print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++");
|
||||||
await restoredRoom.sendTextEvent(testMessage5);
|
await restoredRoom.sendTextEvent(testMessage5);
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await Future.delayed(Duration(seconds: 5));
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
|
||||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1);
|
||||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||||
testClientB.olmSessions[testClientA.identityKey].first.session_id());
|
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id());
|
||||||
assert(restoredRoom.lastMessage == testMessage5);
|
assert(restoredRoom.lastMessage == testMessage5);
|
||||||
assert(inviteRoom.lastMessage == testMessage5);
|
assert(inviteRoom.lastMessage == testMessage5);
|
||||||
assert(testClientB.getRoomById(roomId).lastMessage == testMessage5);
|
assert(testClientB.getRoomById(roomId).lastMessage == testMessage5);
|
||||||
|
|
Loading…
Reference in a new issue