Merge branch 'room-feature-key-sharing' into 'master'
[Room] Implement key sharing See merge request famedly/famedlysdk!217
This commit is contained in:
commit
8a3547a1ee
|
@ -35,6 +35,7 @@ export 'package:famedlysdk/src/utils/open_id_credentials.dart';
|
|||
export 'package:famedlysdk/src/utils/profile.dart';
|
||||
export 'package:famedlysdk/src/utils/push_rules.dart';
|
||||
export 'package:famedlysdk/src/utils/receipt.dart';
|
||||
export 'package:famedlysdk/src/utils/room_key_request.dart';
|
||||
export 'package:famedlysdk/src/utils/states_map.dart';
|
||||
export 'package:famedlysdk/src/utils/to_device_event.dart';
|
||||
export 'package:famedlysdk/src/utils/turn_server_credentials.dart';
|
||||
|
|
|
@ -33,6 +33,7 @@ import 'package:famedlysdk/src/sync/user_update.dart';
|
|||
import 'package:famedlysdk/src/utils/device_keys_list.dart';
|
||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
import 'package:famedlysdk/src/utils/open_id_credentials.dart';
|
||||
import 'package:famedlysdk/src/utils/room_key_request.dart';
|
||||
import 'package:famedlysdk/src/utils/session_key.dart';
|
||||
import 'package:famedlysdk/src/utils/to_device_event.dart';
|
||||
import 'package:famedlysdk/src/utils/turn_server_credentials.dart';
|
||||
|
@ -620,6 +621,10 @@ class Client {
|
|||
/// Will be called on call answers.
|
||||
final StreamController<Event> onCallAnswer = StreamController.broadcast();
|
||||
|
||||
/// Will be called when another device is requesting session keys for a room.
|
||||
final StreamController<RoomKeyRequest> onRoomKeyRequest =
|
||||
StreamController.broadcast();
|
||||
|
||||
/// Matrix synchronisation is done with https long polling. This needs a
|
||||
/// timeout which is usually 30 seconds.
|
||||
int syncTimeoutSec = 30;
|
||||
|
@ -1320,17 +1325,67 @@ class Client {
|
|||
try {
|
||||
switch (toDeviceEvent.type) {
|
||||
case "m.room_key":
|
||||
case "m.forwarded_room_key":
|
||||
Room room = getRoomById(toDeviceEvent.content["room_id"]);
|
||||
if (room != null && toDeviceEvent.content["session_id"] is String) {
|
||||
if (room != null) {
|
||||
final String sessionId = toDeviceEvent.content["session_id"];
|
||||
if (room != null) {
|
||||
room.setSessionKey(sessionId, toDeviceEvent.content);
|
||||
if (toDeviceEvent.type == "m.room_key" &&
|
||||
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.setSessionKey(
|
||||
sessionId,
|
||||
toDeviceEvent.content,
|
||||
forwarded: toDeviceEvent.type == "m.forwarded_room_key",
|
||||
);
|
||||
if (toDeviceEvent.type == "m.forwarded_room_key") {
|
||||
sendToDevice(
|
||||
[],
|
||||
"m.room_key_request",
|
||||
{
|
||||
"action": "request_cancellation",
|
||||
"request_id": base64.encode(
|
||||
utf8.encode(toDeviceEvent.content["room_id"])),
|
||||
"requesting_device_id": room.client.deviceID,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "m.room_key_request":
|
||||
if (!toDeviceEvent.content.containsKey("body")) break;
|
||||
Room room = getRoomById(toDeviceEvent.content["body"]["room_id"]);
|
||||
DeviceKeys deviceKeys;
|
||||
final String sessionId = toDeviceEvent.content["body"]["session_id"];
|
||||
if (userDeviceKeys.containsKey(toDeviceEvent.sender) &&
|
||||
userDeviceKeys[toDeviceEvent.sender]
|
||||
.deviceKeys
|
||||
.containsKey(toDeviceEvent.content["requesting_device_id"])) {
|
||||
deviceKeys = userDeviceKeys[toDeviceEvent.sender]
|
||||
.deviceKeys[toDeviceEvent.content["requesting_device_id"]];
|
||||
if (room.sessionKeys.containsKey(sessionId)) {
|
||||
final RoomKeyRequest roomKeyRequest =
|
||||
RoomKeyRequest.fromToDeviceEvent(toDeviceEvent, this);
|
||||
if (deviceKeys.userId == userID &&
|
||||
deviceKeys.verified &&
|
||||
!deviceKeys.blocked) {
|
||||
roomKeyRequest.forwardKey();
|
||||
} else {
|
||||
onRoomKeyRequest.add(roomKeyRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
print("[Matrix] Error while processing to-device-event: " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1650,16 +1705,22 @@ class Client {
|
|||
storeAPI?.setItem("/clients/$userID/olm-sessions", json.encode(pickleMap));
|
||||
}
|
||||
|
||||
/// Sends an encrypted [message] of this [type] to these [deviceKeys].
|
||||
Future<void> sendToDevice(List<DeviceKeys> deviceKeys, String type,
|
||||
Map<String, dynamic> message) async {
|
||||
if (!encryptionEnabled) return;
|
||||
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send
|
||||
/// the request to all devices of the current user, pass an empty list to [deviceKeys].
|
||||
Future<void> sendToDevice(
|
||||
List<DeviceKeys> deviceKeys, String type, Map<String, dynamic> message,
|
||||
{bool encrypted = true}) async {
|
||||
if (encrypted && !encryptionEnabled) return;
|
||||
// Don't send this message to blocked devices.
|
||||
if (deviceKeys?.isEmpty ?? true) return;
|
||||
if (deviceKeys.isNotEmpty) {
|
||||
deviceKeys.removeWhere((DeviceKeys deviceKeys) =>
|
||||
deviceKeys.blocked || deviceKeys.deviceId == deviceID);
|
||||
if (deviceKeys?.isEmpty ?? true) return;
|
||||
if (deviceKeys.isEmpty) return;
|
||||
}
|
||||
|
||||
Map<String, dynamic> sendToDeviceMessage = message;
|
||||
|
||||
if (encrypted) {
|
||||
// Create new sessions with devices if there is no existing session yet.
|
||||
List<DeviceKeys> deviceKeysWithoutSession =
|
||||
List<DeviceKeys>.from(deviceKeys);
|
||||
|
@ -1668,7 +1729,7 @@ class Client {
|
|||
if (deviceKeysWithoutSession.isNotEmpty) {
|
||||
await startOutgoingOlmSessions(deviceKeysWithoutSession);
|
||||
}
|
||||
Map<String, dynamic> encryptedMessage = {
|
||||
sendToDeviceMessage = {
|
||||
"algorithm": "m.olm.v1.curve25519-aes-sha2",
|
||||
"sender_key": identityKey,
|
||||
"ciphertext": Map<String, dynamic>(),
|
||||
|
@ -1676,7 +1737,8 @@ class Client {
|
|||
for (DeviceKeys device in deviceKeys) {
|
||||
List<olm.Session> existingSessions = olmSessions[device.curve25519Key];
|
||||
if (existingSessions == null || existingSessions.isEmpty) continue;
|
||||
existingSessions.sort((a, b) => a.session_id().compareTo(b.session_id()));
|
||||
existingSessions
|
||||
.sort((a, b) => a.session_id().compareTo(b.session_id()));
|
||||
|
||||
final Map<String, dynamic> payload = {
|
||||
"type": type,
|
||||
|
@ -1689,26 +1751,34 @@ class Client {
|
|||
final olm.EncryptResult encryptResult =
|
||||
existingSessions.first.encrypt(json.encode(payload));
|
||||
storeOlmSession(device.curve25519Key, existingSessions.first);
|
||||
encryptedMessage["ciphertext"][device.curve25519Key] = {
|
||||
sendToDeviceMessage["ciphertext"][device.curve25519Key] = {
|
||||
"type": encryptResult.type,
|
||||
"body": encryptResult.body,
|
||||
};
|
||||
}
|
||||
type = "m.room.encrypted";
|
||||
}
|
||||
|
||||
// Send with send-to-device messaging
|
||||
Map<String, dynamic> data = {
|
||||
"messages": Map<String, dynamic>(),
|
||||
};
|
||||
for (DeviceKeys device in deviceKeys) {
|
||||
if (deviceKeys.isEmpty) {
|
||||
data["messages"][this.userID] = Map<String, dynamic>();
|
||||
data["messages"][this.userID]["*"] = sendToDeviceMessage;
|
||||
} else {
|
||||
for (int i = 0; i < deviceKeys.length; i++) {
|
||||
DeviceKeys device = deviceKeys[i];
|
||||
if (!data["messages"].containsKey(device.userId)) {
|
||||
data["messages"][device.userId] = Map<String, dynamic>();
|
||||
}
|
||||
data["messages"][device.userId][device.deviceId] = encryptedMessage;
|
||||
data["messages"][device.userId][device.deviceId] = sendToDeviceMessage;
|
||||
}
|
||||
}
|
||||
final String messageID = "msg${DateTime.now().millisecondsSinceEpoch}";
|
||||
await jsonRequest(
|
||||
type: HTTPType.PUT,
|
||||
action: "/client/r0/sendToDevice/m.room.encrypted/$messageID",
|
||||
action: "/client/r0/sendToDevice/$type/$messageID",
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -388,6 +388,42 @@ class Event {
|
|||
/// 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);
|
||||
|
||||
/// If this event is encrypted and the decryption was not successful because
|
||||
/// the session is unknown, this requests the session key from other devices
|
||||
/// in the room. If the event is not encrypted or the decryption failed because
|
||||
/// of a different error, this throws an exception.
|
||||
Future<void> requestKey() async {
|
||||
if (this.type != EventTypes.Encrypted ||
|
||||
this.messageType != MessageTypes.BadEncrypted ||
|
||||
this.content["body"] != DecryptError.UNKNOWN_SESSION) {
|
||||
throw ("Session key not unknown");
|
||||
}
|
||||
await room.client.sendToDevice(
|
||||
[],
|
||||
"m.room_key_request",
|
||||
{
|
||||
"action": "request_cancellation",
|
||||
"request_id": base64.encode(utf8.encode(content["session_id"])),
|
||||
"requesting_device_id": room.client.deviceID,
|
||||
});
|
||||
await room.client.sendToDevice(
|
||||
[],
|
||||
"m.room_key_request",
|
||||
{
|
||||
"action": "request",
|
||||
"body": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": roomId,
|
||||
"sender_key": content["sender_key"],
|
||||
"session_id": content["session_id"],
|
||||
},
|
||||
"request_id": base64.encode(utf8.encode(content["session_id"])),
|
||||
"requesting_device_id": room.client.deviceID,
|
||||
},
|
||||
encrypted: false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
enum MessageTypes {
|
||||
|
|
|
@ -152,13 +152,18 @@ class Room {
|
|||
Map<String, SessionKey> _sessionKeys = {};
|
||||
|
||||
/// Add a new session key to the [sessionKeys].
|
||||
void setSessionKey(String sessionId, Map<String, dynamic> content) {
|
||||
void setSessionKey(String sessionId, Map<String, dynamic> content,
|
||||
{bool forwarded = false}) {
|
||||
if (sessionKeys.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: " +
|
||||
|
@ -176,6 +181,7 @@ class Room {
|
|||
"/clients/${client.deviceID}/rooms/${this.id}/session_keys",
|
||||
json.encode(sessionKeys));
|
||||
}
|
||||
onSessionKeyReceived.add(sessionId);
|
||||
}
|
||||
|
||||
/// Returns the [Event] for the given [typeKey] and optional [stateKey].
|
||||
|
@ -219,6 +225,11 @@ class Room {
|
|||
/// room id.
|
||||
final StreamController<String> onUpdate = StreamController.broadcast();
|
||||
|
||||
/// If there is a new session key received, this will be triggered with
|
||||
/// the session ID.
|
||||
final StreamController<String> onSessionKeyReceived =
|
||||
StreamController.broadcast();
|
||||
|
||||
/// The name of the room if set by a participant.
|
||||
String get name => states["m.room.name"] != null
|
||||
? states["m.room.name"].content["name"]
|
||||
|
|
|
@ -42,6 +42,7 @@ class Timeline {
|
|||
final onTimelineInsertCallback onInsert;
|
||||
|
||||
StreamSubscription<EventUpdate> sub;
|
||||
StreamSubscription<String> sessionIdReceivedSub;
|
||||
bool _requestingHistoryLock = false;
|
||||
|
||||
Map<String, Event> _eventCache = {};
|
||||
|
@ -77,6 +78,30 @@ class Timeline {
|
|||
|
||||
Timeline({this.room, this.events, this.onUpdate, this.onInsert}) {
|
||||
sub ??= room.client.onEvent.stream.listen(_handleEventUpdate);
|
||||
sessionIdReceivedSub ??=
|
||||
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
||||
}
|
||||
|
||||
/// Don't forget to call this before you dismiss this object!
|
||||
void cancelSubscriptions() {
|
||||
sub?.cancel();
|
||||
sessionIdReceivedSub?.cancel();
|
||||
}
|
||||
|
||||
void _sessionKeyReceived(String sessionId) {
|
||||
bool decryptAtLeastOneEvent = false;
|
||||
for (int i = 0; i < events.length; i++) {
|
||||
if (events[i].type == EventTypes.Encrypted &&
|
||||
events[i].messageType == MessageTypes.BadEncrypted &&
|
||||
events[i].content["body"] == DecryptError.UNKNOWN_SESSION &&
|
||||
events[i].content["session_id"] == sessionId) {
|
||||
events[i] = events[i].decrypted;
|
||||
if (events[i].type != EventTypes.Encrypted) {
|
||||
decryptAtLeastOneEvent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (decryptAtLeastOneEvent) onUpdate();
|
||||
}
|
||||
|
||||
int _findEvent({String event_id, String unsigned_txid}) {
|
||||
|
|
36
lib/src/utils/room_key_request.dart
Normal file
36
lib/src/utils/room_key_request.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/session_key.dart';
|
||||
|
||||
class RoomKeyRequest extends ToDeviceEvent {
|
||||
Client client;
|
||||
RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent, Client client) {
|
||||
this.client = client;
|
||||
this.sender = toDeviceEvent.sender;
|
||||
this.content = toDeviceEvent.content;
|
||||
this.type = toDeviceEvent.type;
|
||||
}
|
||||
|
||||
Room get room => client.getRoomById(this.content["body"]["room_id"]);
|
||||
|
||||
DeviceKeys get requestingDevice =>
|
||||
client.userDeviceKeys[sender].deviceKeys[content["requesting_device_id"]];
|
||||
|
||||
Future<void> forwardKey() async {
|
||||
Room room = this.room;
|
||||
final SessionKey session =
|
||||
room.sessionKeys[this.content["body"]["session_id"]];
|
||||
List<dynamic> forwardedKeys = [client.identityKey];
|
||||
for (final key in session.forwardingCurve25519KeyChain) {
|
||||
forwardedKeys.add(key);
|
||||
}
|
||||
await requestingDevice.setVerified(true, client);
|
||||
Map<String, dynamic> message = session.content;
|
||||
message["forwarding_curve25519_key_chain"] = forwardedKeys;
|
||||
message["session_key"] = session.inboundGroupSession.export_session(0);
|
||||
await client.sendToDevice(
|
||||
[requestingDevice],
|
||||
"m.forwarded_room_key",
|
||||
message,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,10 @@ class SessionKey {
|
|||
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});
|
||||
|
||||
|
|
|
@ -209,5 +209,42 @@ void main() {
|
|||
expect(resp1, null);
|
||||
expect(resp2, "42");
|
||||
});
|
||||
|
||||
test("requestKey", () async {
|
||||
Client matrix = Client("testclient", debug: true);
|
||||
matrix.httpClient = FakeMatrixApi();
|
||||
await matrix.checkServer("https://fakeServer.notExisting");
|
||||
await matrix.login("test", "1234");
|
||||
|
||||
Event event = Event.fromJson(
|
||||
jsonObj, Room(id: "!1234:example.com", client: matrix));
|
||||
String exception;
|
||||
try {
|
||||
await event.requestKey();
|
||||
} catch (e) {
|
||||
exception = e;
|
||||
}
|
||||
expect(exception, "Session key not unknown");
|
||||
|
||||
event = Event.fromJson({
|
||||
"event_id": id,
|
||||
"sender": senderID,
|
||||
"origin_server_ts": timestamp,
|
||||
"type": "m.room.encrypted",
|
||||
"room_id": "1234",
|
||||
"status": 2,
|
||||
"content": json.encode({
|
||||
"msgtype": "m.bad.encrypted",
|
||||
"body": DecryptError.UNKNOWN_SESSION,
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "AwgAEnACgAkLmt6qF84IK++J7UDH2Za1YVchHyprqTqsg...",
|
||||
"device_id": "RJYKSTBOIE",
|
||||
"sender_key": "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
|
||||
"session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ"
|
||||
}),
|
||||
}, Room(id: "!1234:example.com", client: matrix));
|
||||
|
||||
await event.requestKey();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ class FakeMatrixApi extends MockClient {
|
|||
return Response(json.encode(res), 405);
|
||||
}
|
||||
} else if (method == "PUT" &&
|
||||
action.contains("/client/r0/sendToDevice/m.room.encrypted/")) {
|
||||
action.contains("/client/r0/sendToDevice/")) {
|
||||
return Response(json.encode({}), 200);
|
||||
} else if (method == "GET" &&
|
||||
action.contains("/client/r0/rooms/") &&
|
||||
|
|
70
test/room_key_request_test.dart
Normal file
70
test/room_key_request_test.dart
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Zender & Kurtz GbR.
|
||||
*
|
||||
* Authors:
|
||||
* Christian Pauly <krille@famedly.com>
|
||||
* Marcel Radzio <mtrnord@famedly.com>
|
||||
*
|
||||
* This file is part of famedlysdk.
|
||||
*
|
||||
* famedlysdk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* famedlysdk 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'fake_matrix_api.dart';
|
||||
import 'fake_store.dart';
|
||||
|
||||
void main() {
|
||||
/// All Tests related to device keys
|
||||
group("Room Key Request", () {
|
||||
test("fromJson", () async {
|
||||
Map<String, dynamic> rawJson = {
|
||||
"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"
|
||||
};
|
||||
ToDeviceEvent toDeviceEvent = ToDeviceEvent.fromJson(rawJson);
|
||||
expect(toDeviceEvent.content, rawJson["content"]);
|
||||
expect(toDeviceEvent.sender, rawJson["sender"]);
|
||||
expect(toDeviceEvent.type, rawJson["type"]);
|
||||
|
||||
Client matrix = Client("testclient", debug: true);
|
||||
matrix.httpClient = FakeMatrixApi();
|
||||
matrix.storeAPI = FakeStore(matrix, {});
|
||||
await matrix.checkServer("https://fakeServer.notExisting");
|
||||
await matrix.login("test", "1234");
|
||||
Room room = matrix.getRoomById("!726s6s6q:example.com");
|
||||
if (matrix.encryptionEnabled) {
|
||||
await room.createOutboundGroupSession();
|
||||
rawJson["content"]["body"]["session_id"] = room.sessionKeys.keys.first;
|
||||
|
||||
RoomKeyRequest roomKeyRequest = RoomKeyRequest.fromToDeviceEvent(
|
||||
ToDeviceEvent.fromJson(rawJson), matrix);
|
||||
await roomKeyRequest.forwardKey();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue