2020-06-04 11:39:51 +00:00
|
|
|
/*
|
|
|
|
* 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;
|
2020-08-13 08:40:39 +00:00
|
|
|
import 'package:pedantic/pedantic.dart';
|
2020-06-04 11:39:51 +00:00
|
|
|
|
|
|
|
import './encryption.dart';
|
|
|
|
import './utils/outbound_group_session.dart';
|
2020-08-13 08:40:39 +00:00
|
|
|
import './utils/session_key.dart';
|
|
|
|
import '../famedlysdk.dart';
|
|
|
|
import '../matrix_api.dart';
|
|
|
|
import '../src/utils/logs.dart';
|
2020-06-04 11:39:51 +00:00
|
|
|
|
2020-06-05 20:03:28 +00:00
|
|
|
const MEGOLM_KEY = 'm.megolm_backup.v1';
|
|
|
|
|
2020-06-04 11:39:51 +00:00
|
|
|
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>{};
|
|
|
|
|
2020-06-05 20:03:28 +00:00
|
|
|
KeyManager(this.encryption) {
|
|
|
|
encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async {
|
|
|
|
final keyObj = olm.PkDecryption();
|
|
|
|
try {
|
2020-08-11 16:11:51 +00:00
|
|
|
final info = await client.getRoomKeysBackup();
|
2020-06-23 06:30:50 +00:00
|
|
|
if (info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2) {
|
2020-06-12 14:17:00 +00:00
|
|
|
return false;
|
|
|
|
}
|
2020-06-12 15:15:26 +00:00
|
|
|
if (keyObj.init_with_private_key(base64.decode(secret)) ==
|
2020-06-23 06:30:50 +00:00
|
|
|
info.authData['public_key']) {
|
2020-06-12 15:15:26 +00:00
|
|
|
_requestedSessionIds.clear();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
2020-06-05 20:03:28 +00:00
|
|
|
} catch (_) {
|
|
|
|
return false;
|
|
|
|
} finally {
|
|
|
|
keyObj.free();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool get enabled => client.accountData[MEGOLM_KEY] != null;
|
2020-06-04 11:39:51 +00:00
|
|
|
|
|
|
|
/// 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 (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']);
|
|
|
|
}
|
2020-08-06 09:35:02 +00:00
|
|
|
} catch (e, s) {
|
2020-06-04 11:39:51 +00:00
|
|
|
inboundGroupSession.free();
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.error(
|
|
|
|
'[LibOlm] Could not create new InboundGroupSession: ' + e.toString(),
|
|
|
|
s);
|
2020-06-04 11:39:51 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-06-10 08:44:22 +00:00
|
|
|
final newSession = SessionKey(
|
2020-06-04 11:39:51 +00:00
|
|
|
content: content,
|
|
|
|
inboundGroupSession: inboundGroupSession,
|
|
|
|
indexes: {},
|
|
|
|
key: client.userID,
|
|
|
|
);
|
2020-06-10 08:44:22 +00:00
|
|
|
final oldFirstIndex =
|
|
|
|
oldSession?.inboundGroupSession?.first_known_index() ?? 0;
|
|
|
|
final newFirstIndex = newSession.inboundGroupSession.first_known_index();
|
|
|
|
if (oldSession == null ||
|
|
|
|
newFirstIndex < oldFirstIndex ||
|
|
|
|
(oldFirstIndex == newFirstIndex &&
|
|
|
|
newSession.forwardingCurve25519KeyChain.length <
|
|
|
|
oldSession.forwardingCurve25519KeyChain.length)) {
|
|
|
|
// use new session
|
|
|
|
oldSession?.dispose();
|
|
|
|
} else {
|
|
|
|
// we are gonna keep our old session
|
|
|
|
newSession.dispose();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!_inboundGroupSessions.containsKey(roomId)) {
|
|
|
|
_inboundGroupSessions[roomId] = <String, SessionKey>{};
|
|
|
|
}
|
|
|
|
_inboundGroupSessions[roomId][sessionId] = newSession;
|
2020-06-04 11:39:51 +00:00
|
|
|
client.database?.storeInboundGroupSession(
|
|
|
|
client.id,
|
|
|
|
roomId,
|
|
|
|
sessionId,
|
|
|
|
inboundGroupSession.pickle(client.userID),
|
|
|
|
json.encode(content),
|
|
|
|
json.encode({}),
|
|
|
|
);
|
2020-07-26 05:54:03 +00:00
|
|
|
// Note to self: When adding key-backup that needs to be unawaited(), else
|
|
|
|
// we might accidentally end up with http requests inside of the sync loop
|
2020-06-04 11:39:51 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2020-06-05 08:51:11 +00:00
|
|
|
/// clear all cached inbound group sessions. useful for testing
|
|
|
|
void clearOutboundGroupSessions() {
|
|
|
|
_outboundGroupSessions.clear();
|
|
|
|
}
|
|
|
|
|
2020-06-04 11:39:51 +00:00
|
|
|
/// 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();
|
2020-08-06 09:35:02 +00:00
|
|
|
} catch (e, s) {
|
2020-06-04 11:39:51 +00:00
|
|
|
outboundGroupSession.free();
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.error(
|
|
|
|
'[LibOlm] Unable to create new outboundGroupSession: ' + e.toString(),
|
|
|
|
s);
|
2020-06-04 11:39:51 +00:00
|
|
|
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 {
|
2020-08-11 16:11:51 +00:00
|
|
|
await client.sendToDeviceEncrypted(deviceKeys, 'm.room_key', rawSession);
|
2020-06-04 11:39:51 +00:00
|
|
|
await storeOutboundGroupSession(roomId, sess);
|
|
|
|
_outboundGroupSessions[roomId] = sess;
|
|
|
|
} catch (e, s) {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.error(
|
2020-06-04 11:39:51 +00:00
|
|
|
'[LibOlm] Unable to send the session key to the participating devices: ' +
|
2020-08-06 09:35:02 +00:00
|
|
|
e.toString(),
|
|
|
|
s);
|
2020-06-04 11:39:51 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-06-05 20:03:28 +00:00
|
|
|
Future<bool> isCached() async {
|
|
|
|
if (!enabled) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return (await encryption.ssss.getCached(MEGOLM_KEY)) != null;
|
|
|
|
}
|
|
|
|
|
2020-06-12 14:17:00 +00:00
|
|
|
Future<void> loadFromResponse(RoomKeys keys) async {
|
2020-06-05 20:03:28 +00:00
|
|
|
if (!(await isCached())) {
|
|
|
|
return;
|
|
|
|
}
|
2020-06-06 11:47:37 +00:00
|
|
|
final privateKey =
|
|
|
|
base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
|
2020-06-05 20:03:28 +00:00
|
|
|
final decryption = olm.PkDecryption();
|
2020-08-11 16:11:51 +00:00
|
|
|
final info = await client.getRoomKeysBackup();
|
2020-06-05 20:03:28 +00:00
|
|
|
String backupPubKey;
|
|
|
|
try {
|
|
|
|
backupPubKey = decryption.init_with_private_key(privateKey);
|
|
|
|
|
|
|
|
if (backupPubKey == null ||
|
2020-06-23 06:30:50 +00:00
|
|
|
info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2 ||
|
|
|
|
info.authData['public_key'] != backupPubKey) {
|
2020-06-05 20:03:28 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-06-12 14:17:00 +00:00
|
|
|
for (final roomEntry in keys.rooms.entries) {
|
|
|
|
final roomId = roomEntry.key;
|
|
|
|
for (final sessionEntry in roomEntry.value.sessions.entries) {
|
|
|
|
final sessionId = sessionEntry.key;
|
|
|
|
final session = sessionEntry.value;
|
|
|
|
final firstMessageIndex = session.firstMessageIndex;
|
|
|
|
final forwardedCount = session.forwardedCount;
|
|
|
|
final isVerified = session.isVerified;
|
|
|
|
final sessionData = session.sessionData;
|
2020-06-05 20:03:28 +00:00
|
|
|
if (firstMessageIndex == null ||
|
|
|
|
forwardedCount == null ||
|
|
|
|
isVerified == null ||
|
|
|
|
!(sessionData is Map)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
Map<String, dynamic> decrypted;
|
|
|
|
try {
|
|
|
|
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
|
|
|
|
sessionData['mac'], sessionData['ciphertext']));
|
2020-08-06 09:35:02 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logs.error(
|
|
|
|
'[LibOlm] Error decrypting room key: ' + e.toString(), s);
|
2020-06-05 20:03:28 +00:00
|
|
|
}
|
|
|
|
if (decrypted != null) {
|
|
|
|
decrypted['session_id'] = sessionId;
|
|
|
|
decrypted['room_id'] = roomId;
|
2020-06-06 11:47:37 +00:00
|
|
|
setInboundGroupSession(
|
|
|
|
roomId, sessionId, decrypted['sender_key'], decrypted,
|
|
|
|
forwarded: true);
|
2020-06-05 20:03:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
decryption.free();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> loadSingleKey(String roomId, String sessionId) async {
|
2020-08-11 16:11:51 +00:00
|
|
|
final info = await client.getRoomKeysBackup();
|
2020-06-12 14:17:28 +00:00
|
|
|
final ret =
|
2020-08-11 16:11:51 +00:00
|
|
|
await client.getRoomKeysSingleKey(roomId, sessionId, info.version);
|
2020-06-12 14:17:00 +00:00
|
|
|
final keys = RoomKeys.fromJson({
|
2020-06-05 20:03:28 +00:00
|
|
|
'rooms': {
|
|
|
|
roomId: {
|
|
|
|
'sessions': {
|
2020-06-12 14:17:00 +00:00
|
|
|
sessionId: ret.toJson(),
|
2020-06-05 20:03:28 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
2020-06-12 14:17:00 +00:00
|
|
|
await loadFromResponse(keys);
|
2020-06-05 20:03:28 +00:00
|
|
|
}
|
|
|
|
|
2020-06-04 11:39:51 +00:00
|
|
|
/// Request a certain key from another device
|
2020-06-13 17:48:38 +00:00
|
|
|
Future<void> request(Room room, String sessionId, String senderKey,
|
|
|
|
{bool tryOnlineBackup = true}) async {
|
|
|
|
if (tryOnlineBackup) {
|
|
|
|
// let's first check our online key backup store thingy...
|
|
|
|
var hadPreviously =
|
|
|
|
getInboundGroupSession(room.id, sessionId, senderKey) != null;
|
|
|
|
try {
|
|
|
|
await loadSingleKey(room.id, sessionId);
|
|
|
|
} catch (err, stacktrace) {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.error(
|
|
|
|
'[KeyManager] Failed to access online key backup: ' +
|
|
|
|
err.toString(),
|
|
|
|
stacktrace);
|
2020-06-13 17:48:38 +00:00
|
|
|
}
|
|
|
|
if (!hadPreviously &&
|
|
|
|
getInboundGroupSession(room.id, sessionId, senderKey) != null) {
|
|
|
|
return; // we managed to load the session from online backup, no need to care about it now
|
|
|
|
}
|
2020-06-05 20:03:28 +00:00
|
|
|
}
|
2020-08-01 07:06:39 +00:00
|
|
|
try {
|
|
|
|
// 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,
|
|
|
|
);
|
2020-08-11 16:11:51 +00:00
|
|
|
final userList = await room.requestParticipants();
|
|
|
|
await client.sendToDevicesOfUserIds(
|
|
|
|
userList.map<String>((u) => u.id).toSet(),
|
|
|
|
'm.room_key_request',
|
|
|
|
{
|
|
|
|
'action': 'request',
|
|
|
|
'body': {
|
|
|
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
|
|
|
'room_id': room.id,
|
|
|
|
'sender_key': senderKey,
|
|
|
|
'session_id': sessionId,
|
2020-06-04 11:39:51 +00:00
|
|
|
},
|
2020-08-11 16:11:51 +00:00
|
|
|
'request_id': requestId,
|
|
|
|
'requesting_device_id': client.deviceID,
|
|
|
|
},
|
|
|
|
);
|
2020-08-01 07:06:39 +00:00
|
|
|
outgoingShareRequests[request.requestId] = request;
|
2020-08-06 09:35:02 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logs.error(
|
|
|
|
'[Key Manager] Sending key verification request failed: ' +
|
|
|
|
e.toString(),
|
|
|
|
s);
|
2020-08-01 07:06:39 +00:00
|
|
|
}
|
2020-06-04 11:39:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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') {
|
2020-06-29 12:02:18 +00:00
|
|
|
if (!(event.content['request_id'] is String)) {
|
2020-06-04 11:39:51 +00:00
|
|
|
return; // invalid event
|
|
|
|
}
|
|
|
|
if (event.content['action'] == 'request') {
|
|
|
|
// we are *receiving* a request
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.info('[KeyManager] Received key sharing request...');
|
2020-06-04 11:39:51 +00:00
|
|
|
if (!event.content.containsKey('body')) {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.info('[KeyManager] No body, doing nothing');
|
2020-06-04 11:39:51 +00:00
|
|
|
return; // no body
|
|
|
|
}
|
|
|
|
if (!client.userDeviceKeys.containsKey(event.sender) ||
|
|
|
|
!client.userDeviceKeys[event.sender].deviceKeys
|
|
|
|
.containsKey(event.content['requesting_device_id'])) {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.info('[KeyManager] Device not found, doing nothing');
|
2020-06-04 11:39:51 +00:00
|
|
|
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) {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.info('[KeyManager] Request is by ourself, ignoring');
|
2020-06-04 11:39:51 +00:00
|
|
|
return; // ignore requests by ourself
|
|
|
|
}
|
|
|
|
final room = client.getRoomById(event.content['body']['room_id']);
|
|
|
|
if (room == null) {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.info('[KeyManager] Unknown room, ignoring');
|
2020-06-04 11:39:51 +00:00
|
|
|
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) {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.info('[KeyManager] Unknown session, ignoring');
|
2020-06-04 11:39:51 +00:00
|
|
|
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)) {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.info('[KeyManager] Already processed this request, ignoring');
|
2020-06-04 11:39:51 +00:00
|
|
|
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) {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.info('[KeyManager] All checks out, forwarding key...');
|
2020-06-04 11:39:51 +00:00
|
|
|
// alright, we can forward the key
|
|
|
|
await roomKeyRequest.forwardKey();
|
|
|
|
} else {
|
2020-08-06 09:35:02 +00:00
|
|
|
Logs.info(
|
|
|
|
'[KeyManager] Asking client, if the key should be forwarded');
|
2020-06-04 11:39:51 +00:00
|
|
|
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
|
|
|
|
}
|
2020-08-11 16:11:51 +00:00
|
|
|
// Send with send-to-device messaging
|
|
|
|
final sendToDeviceMessage = {
|
|
|
|
'action': 'request_cancellation',
|
|
|
|
'request_id': request.requestId,
|
|
|
|
'requesting_device_id': client.deviceID,
|
|
|
|
};
|
|
|
|
var data = <String, Map<String, Map<String, dynamic>>>{};
|
|
|
|
for (final device in request.devices) {
|
|
|
|
if (!data.containsKey(device.userId)) {
|
|
|
|
data[device.userId] = {};
|
|
|
|
}
|
|
|
|
data[device.userId][device.deviceId] = sendToDeviceMessage;
|
|
|
|
}
|
2020-06-04 11:39:51 +00:00
|
|
|
await client.sendToDevice(
|
2020-08-11 16:11:51 +00:00
|
|
|
'm.room_key_request',
|
|
|
|
client.generateUniqueTransactionId(),
|
|
|
|
data,
|
|
|
|
);
|
2020-06-04 11:39:51 +00:00
|
|
|
} else if (event.type == 'm.room_key') {
|
2020-06-05 08:15:36 +00:00
|
|
|
if (event.encryptedContent == null) {
|
|
|
|
return; // the event wasn't encrypted, this is a security risk;
|
|
|
|
}
|
2020-06-04 11:39:51 +00:00
|
|
|
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;
|
|
|
|
}
|
2020-06-05 08:56:51 +00:00
|
|
|
setInboundGroupSession(roomId, sessionId,
|
|
|
|
event.encryptedContent['sender_key'], event.content,
|
2020-06-04 11:39:51 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
var message = session.content;
|
|
|
|
message['forwarding_curve25519_key_chain'] = forwardedKeys;
|
|
|
|
|
2020-06-07 13:09:11 +00:00
|
|
|
message['sender_key'] = request.senderKey;
|
2020-06-12 12:32:42 +00:00
|
|
|
message['sender_claimed_ed25519_key'] =
|
|
|
|
forwardedKeys.isEmpty ? keyManager.encryption.fingerprintKey : null;
|
2020-06-07 13:09:11 +00:00
|
|
|
if (message['sender_claimed_ed25519_key'] == null) {
|
|
|
|
for (final value in keyManager.client.userDeviceKeys.values) {
|
|
|
|
for (final key in value.deviceKeys.values) {
|
|
|
|
if (key.curve25519Key == forwardedKeys.first) {
|
|
|
|
message['sender_claimed_ed25519_key'] = key.ed25519Key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (message['sender_claimed_ed25519_key'] != null) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-06-04 11:39:51 +00:00
|
|
|
message['session_key'] = session.inboundGroupSession
|
|
|
|
.export_session(session.inboundGroupSession.first_known_index());
|
|
|
|
// send the actual reply of the key back to the requester
|
2020-08-11 16:11:51 +00:00
|
|
|
await keyManager.client.sendToDeviceEncrypted(
|
2020-06-04 11:39:51 +00:00
|
|
|
[requestingDevice],
|
|
|
|
'm.forwarded_room_key',
|
|
|
|
message,
|
|
|
|
);
|
|
|
|
keyManager.incomingShareRequests.remove(request.requestId);
|
|
|
|
}
|
|
|
|
}
|