Skip to content

Commit

Permalink
feat: encryption key provider
Browse files Browse the repository at this point in the history
ported over from #1551
  • Loading branch information
td-famedly committed Jan 4, 2024
1 parent 756b801 commit 4137dc4
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 75 deletions.
1 change: 1 addition & 0 deletions lib/matrix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export 'src/voip/models/call_events.dart';
export 'src/voip/models/group_call_events.dart';
export 'src/voip/models/webrtc_delegate.dart';
export 'src/voip/models/participant.dart';
export 'src/voip/models/call_backend.dart';
export 'src/voip/utils/conn_tester.dart';
export 'src/voip/utils/constants.dart';
export 'src/voip/utils/rtc_candidate_extension.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ class Client extends MatrixApi {
if (groupCall) {
powerLevelContentOverride ??= {};
powerLevelContentOverride['events'] = <String, dynamic>{
famedlyCallMemberEventType: 0,
VoIPEventTypes.FamedlyCallMemberEvent: 0,
};
}
final roomId = await createRoom(
Expand Down
8 changes: 5 additions & 3 deletions lib/src/room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1858,15 +1858,16 @@ class Room {
50;
}

bool get canJoinGroupCall => canChangeStateEvent(famedlyCallMemberEventType);
bool get canJoinGroupCall =>
canChangeStateEvent(VoIPEventTypes.FamedlyCallMemberEvent);

/// if returned value is not null `org.matrix.msc3401.call.member` is present
/// and group calls can be used
@Deprecated('User canJoinGroupCall')
bool get groupCallsEnabled {
final powerLevelMap = getState(EventTypes.RoomPowerLevels)?.content;
if (powerLevelMap == null) return false;
return powerForChangingStateEvent(famedlyCallMemberEventType) <=
return powerForChangingStateEvent(VoIPEventTypes.FamedlyCallMemberEvent) <=
getDefaultPowerLevel(powerLevelMap);
}

Expand All @@ -1880,7 +1881,8 @@ class Room {
final eventsMap = newPowerLevelMap.tryGetMap<String, Object?>('events') ??
<String, Object?>{};
eventsMap.addAll({
famedlyCallMemberEventType: getDefaultPowerLevel(currentPowerLevelsMap)
VoIPEventTypes.FamedlyCallMemberEvent:
getDefaultPowerLevel(currentPowerLevelsMap)
});
newPowerLevelMap.addAll({'events': eventsMap});
await client.setRoomStateWithKey(
Expand Down
169 changes: 159 additions & 10 deletions lib/src/voip/group_call_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@
*/

import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:webrtc_interface/webrtc_interface.dart';

import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/cached_stream_controller.dart';
import 'package:matrix/src/voip/models/call_backend.dart';
import 'package:matrix/src/utils/crypto/crypto.dart';
import 'package:matrix/src/voip/models/call_membership.dart';
import 'package:matrix/src/voip/models/call_options.dart';
import 'package:matrix/src/voip/models/key_provider.dart';
import 'package:matrix/src/voip/utils/stream_helper.dart';

/// Holds methods for managing a group call. This class is also responsible for
Expand All @@ -40,6 +43,7 @@ class GroupCallSession {
final Client client;
final VoIP voip;
final Room room;
final CallBackend backend;
final String? application;
final String? scope;

Expand Down Expand Up @@ -86,11 +90,14 @@ class GroupCallSession {
final CachedStreamController<WrappedMediaStream> onStreamRemoved =
CachedStreamController();

bool get isLivekitCall => backend is LiveKitBackend;

GroupCallSession({
String? groupCallId,
required this.client,
required this.room,
required this.voip,
required this.backend,
this.application = 'm.call',
this.scope = 'm.room',
}) {
Expand Down Expand Up @@ -182,8 +189,12 @@ class GroupCallSession {
/// you can pass that `stream` on to this function.
/// This allows you to configure the camera before joining the call without
/// having to reopen the stream and possibly losing settings.
Future<WrappedMediaStream> initLocalStream(
Future<WrappedMediaStream?> initLocalStream(
{WrappedMediaStream? stream}) async {
if (isLivekitCall) {
Logs().i('Livekit group call: not starting local call feed.');
return null;
}
if (state != GroupCallState.LocalCallFeedUninitialized) {
throw Exception('Cannot initialize local call feed in the $state state.');
}
Expand Down Expand Up @@ -277,8 +288,12 @@ class GroupCallSession {
await onMemberStateChanged(memberState);
}

onActiveSpeakerLoop();

if (isLivekitCall) {
await makeNewSenderKey(false);
await sendEncryptionKeysEvent();
} else {
onActiveSpeakerLoop();
}
voip.currentGroupCID = groupCallId;

await voip.delegate.handleNewGroupCall(this);
Expand Down Expand Up @@ -469,6 +484,12 @@ class GroupCallSession {
return;
}

if (isLivekitCall) {
Logs()
.i('Received incoming call whilst in signaling-only mode! Ignoring.');
return;
}

final existingCall = getCallForParticipant(newCall.remoteParticipant!);

if (existingCall != null && existingCall.callId == newCall.callId) {
Expand Down Expand Up @@ -496,7 +517,7 @@ class GroupCallSession {
callId: groupCallId,
application: application,
scope: scope,
backend: CallBackend.fromJson({'type': 'mesh'}),
backend: backend,
deviceId: client.deviceID!,
expiresTs: DateTime.now()
.add(CallTimeouts.expireTsBumpDuration)
Expand Down Expand Up @@ -595,11 +616,6 @@ class GroupCallSession {
element.roomId == room.id; // sanity checks
}).toList();

if (state != GroupCallState.Entered) {
Logs().d('[VOIP] group call state is currently $state');
return;
}

for (final mem in memsForCurrentGroupCall) {
Logs().e(
'[VOIP] onMemberStateChanged, handling mem ${mem.userId + mem.deviceId}');
Expand Down Expand Up @@ -655,6 +671,10 @@ class GroupCallSession {
continue;
}

if (state != GroupCallState.Entered || isLivekitCall) {
Logs().d('[VOIP] group call state is currently $state');
return;
}
// Only initiate a call with a participant who has a id that is lexicographically
// less than your own. Otherwise, that user will call you.
if (localParticipant.id.compareTo(rp.id) > 0) {
Expand Down Expand Up @@ -1101,4 +1121,133 @@ class GroupCallSession {
Logs().d(
'[VOIP] participant removed, current list: ${participants.map((e) => e.id).toString()}');
}

/// participant:keyIndex:keyBin
Map<Participant, Map<int, Uint8List>> encryptionKeysMap = {};

List<Timer> setNewKeyTimeouts = [];

Map<int, Uint8List>? getKeysForParticipant(Participant participant) {
return encryptionKeysMap[participant];
}

int getNewEncryptionKeyIndex() {
return (getKeysForParticipant(localParticipant)?.length ?? 0) % 16;
}

Future<void> makeNewSenderKey(bool delayBeforeUse) async {
final encryptionKey = secureRandomBytes(16).toString();
final encryptionKeyIndex = getNewEncryptionKeyIndex();
Logs().i('Generated new key at index $encryptionKeyIndex');

await setEncryptionKey(
localParticipant,
encryptionKeyIndex,
encryptionKey,
delayBeforeuse: delayBeforeUse,
);
}

Future<void> setEncryptionKey(Participant participant, int encryptionKeyIndex,
String encryptionKeyString,
{bool delayBeforeuse = false}) async {
final keyBin = base64Decode(encryptionKeyString);

final encryptionKeys = encryptionKeysMap[participant] ?? <int, Uint8List>{};

if (encryptionKeys[encryptionKeyIndex] != null &&
listEquals(encryptionKeys[encryptionKeyIndex]!, keyBin)) {
Logs().i('Ignoring duplicate key');
return;
}

encryptionKeys[encryptionKeyIndex] = keyBin;

encryptionKeysMap[participant] = encryptionKeys;

if (delayBeforeuse) {
final useKeyTimeout =
Timer.periodic(CallTimeouts.useKeyDelay, (Timer timer) async {
setNewKeyTimeouts.remove(timer);
timer.cancel();
Logs().i(
'Delayed-emitting key changed event for ${participant.id} idx $encryptionKeyIndex');
await voip.delegate.keyProvider?.onSetEncryptionKey(
participant, encryptionKeyString, encryptionKeyIndex);
});
setNewKeyTimeouts.add(useKeyTimeout);
} else {
await voip.delegate.keyProvider?.onSetEncryptionKey(
participant, encryptionKeyString, encryptionKeyIndex);
}
}

Future<void> sendEncryptionKeysEvent() async {
Logs().i('Sending encryption keys event');

final myKeys = getKeysForParticipant(localParticipant);

if (myKeys == null) {
Logs().w('Tried to send encryption keys event but no keys found!');
return;
}

try {
final List<EncryptionKeyEntry> keys = [];
for (int i = 0; i < myKeys.length; i++) {
if (myKeys[i] != null) {
keys.add(EncryptionKeyEntry(i, base64UrlEncode(myKeys[i]!)));
}
}
final keyContent = EncryptionKeysEventContent(
keys,
groupCallId,
);
final txid = VoIP.customTxid ?? client.generateUniqueTransactionId();
final mustEncrypt = room.encrypted && client.encryptionEnabled;

for (final participant in participants) {
if (mustEncrypt) {
await client.sendToDeviceEncrypted([
client.userDeviceKeys[participant.userId]!
.deviceKeys[participant.deviceId]!
], VoIPEventTypes.EncryptionKeysEvent, keyContent.toJson());
} else {
await client.sendToDevice(
VoIPEventTypes.EncryptionKeysEvent,
txid,
{
participant.userId: {participant.deviceId: keyContent.toJson()}
},
);
}
Logs().d(
'E2EE: updateEncryptionKeyEvent participantId=${participant.id} numSent=${myKeys.length}');
}
} catch (error) {
// TODO: resend keys.
}
}

Future<void> onCallEncryption(String roomId, Participant remoteParticipant,
Map<String, dynamic> content) async {
final keyContent = EncryptionKeysEventContent.fromJson(content);

final callId = keyContent.callId;

if (keyContent.keys.isEmpty) {
Logs().w(
'Received m.call.encryption_keys where keys is empty: callId=$callId');
return;
}

for (final key in keyContent.keys) {
final encryptionKey = key.key;
final encryptionKeyIndex = key.index;
Logs().d(
'E2EE: onCallEncryption ${remoteParticipant.id} encryptionKeyIndex=$encryptionKeyIndex');
await setEncryptionKey(
remoteParticipant, encryptionKeyIndex, encryptionKey);
}
}
}
32 changes: 16 additions & 16 deletions lib/src/voip/models/call_backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ abstract class CallBackend {
factory CallBackend.fromJson(Map<String, Object?> json) {
final String type = json['type'] as String;
if (type == 'mesh') {
return MeshBackend.fromJson(json);
return MeshBackend(type: type);
} else if (type == 'livekit') {
return LiveKit.fromJson(json);
return LiveKitBackend(
livekitAlias: json['livekit_alias'] as String,
livekitServiceUrl: json['livekit_service_url'] as String,
type: type,
);
} else {
throw ArgumentError('Invalid type: $type');
}
Expand All @@ -17,12 +21,8 @@ abstract class CallBackend {
Map<String, Object?> toJson();
}

class MeshBackend implements CallBackend {
@override
String type = 'mesh';

MeshBackend.fromJson(Map<String, Object?> json)
: type = json['type'] as String;
class MeshBackend extends CallBackend {
MeshBackend({super.type = 'mesh'});

@override
Map<String, Object?> toJson() {
Expand All @@ -32,22 +32,22 @@ class MeshBackend implements CallBackend {
}
}

class LiveKit implements CallBackend {
@override
String type = 'livekit';

class LiveKitBackend extends CallBackend {
final String livekitServiceUrl;
final String livekitAlias;

LiveKit.fromJson(Map<String, Object?> json)
: type = json['type'] as String,
livekitServiceUrl = json['livekit_service_url'] as String,
livekitAlias = json['livekit_alias'] as String;
LiveKitBackend({
required this.livekitServiceUrl,
required this.livekitAlias,
super.type = 'livekit',
});

@override
Map<String, Object?> toJson() {
return {
'type': type,
'livekit_service_url': livekitServiceUrl,
'livekit_alias': livekitAlias,
};
}
}
1 change: 0 additions & 1 deletion lib/src/voip/models/call_membership.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:matrix/matrix.dart';
import 'package:matrix/src/voip/models/call_backend.dart';

class FamedlyCallMemberEvent {
final List<CallMembership> memberships;
Expand Down
Loading

0 comments on commit 4137dc4

Please sign in to comment.