Line data Source code
1 : import 'dart:async';
2 : import 'dart:convert';
3 : import 'dart:typed_data';
4 :
5 : import 'package:matrix/matrix.dart';
6 : import 'package:matrix/src/utils/crypto/crypto.dart';
7 : import 'package:matrix/src/voip/models/call_membership.dart';
8 :
9 : class LiveKitBackend extends CallBackend {
10 : final String livekitServiceUrl;
11 : final String livekitAlias;
12 :
13 : @override
14 : final bool e2eeEnabled;
15 :
16 0 : LiveKitBackend({
17 : required this.livekitServiceUrl,
18 : required this.livekitAlias,
19 : super.type = 'livekit',
20 : this.e2eeEnabled = true,
21 : });
22 :
23 : Timer? _memberLeaveEncKeyRotateDebounceTimer;
24 :
25 : /// participant:keyIndex:keyBin
26 : final Map<CallParticipant, Map<int, Uint8List>> _encryptionKeysMap = {};
27 :
28 : final List<Future> _setNewKeyTimeouts = [];
29 :
30 : int _indexCounter = 0;
31 :
32 : /// used to send the key again incase someone `onCallEncryptionKeyRequest` but don't just send
33 : /// the last one because you also cycle back in your window which means you
34 : /// could potentially end up sharing a past key
35 0 : int get latestLocalKeyIndex => _latestLocalKeyIndex;
36 : int _latestLocalKeyIndex = 0;
37 :
38 : /// the key currently being used by the local cryptor, can possibly not be the latest
39 : /// key, check `latestLocalKeyIndex` for latest key
40 0 : int get currentLocalKeyIndex => _currentLocalKeyIndex;
41 : int _currentLocalKeyIndex = 0;
42 :
43 0 : Map<int, Uint8List>? _getKeysForParticipant(CallParticipant participant) {
44 0 : return _encryptionKeysMap[participant];
45 : }
46 :
47 : /// always chooses the next possible index, we cycle after 16 because
48 : /// no real adv with infinite list
49 0 : int _getNewEncryptionKeyIndex() {
50 0 : final newIndex = _indexCounter % 16;
51 0 : _indexCounter++;
52 : return newIndex;
53 : }
54 :
55 : /// makes a new e2ee key for local user and sets it with a delay if specified
56 : /// used on first join and when someone leaves
57 : ///
58 : /// also does the sending for you
59 0 : Future<void> _makeNewSenderKey(
60 : GroupCallSession groupCall, bool delayBeforeUsingKeyOurself) async {
61 0 : final key = secureRandomBytes(32);
62 0 : final keyIndex = _getNewEncryptionKeyIndex();
63 0 : Logs().i('[VOIP E2EE] Generated new key $key at index $keyIndex');
64 :
65 0 : await _setEncryptionKey(
66 : groupCall,
67 0 : groupCall.localParticipant!,
68 : keyIndex,
69 : key,
70 : delayBeforeUsingKeyOurself: delayBeforeUsingKeyOurself,
71 : send: true,
72 : );
73 : }
74 :
75 : /// also does the sending for you
76 0 : Future<void> _ratchetLocalParticipantKey(
77 : GroupCallSession groupCall,
78 : List<CallParticipant> sendTo,
79 : ) async {
80 0 : final keyProvider = groupCall.voip.delegate.keyProvider;
81 :
82 : if (keyProvider == null) {
83 0 : throw Exception('[VOIP] _ratchetKey called but KeyProvider was null');
84 : }
85 :
86 0 : final myKeys = _encryptionKeysMap[groupCall.localParticipant];
87 :
88 0 : if (myKeys == null || myKeys.isEmpty) {
89 0 : await _makeNewSenderKey(groupCall, false);
90 : return;
91 : }
92 :
93 : Uint8List? ratchetedKey;
94 :
95 0 : while (ratchetedKey == null || ratchetedKey.isEmpty) {
96 0 : Logs().i('[VOIP E2EE] Ignoring empty ratcheted key');
97 0 : ratchetedKey = await keyProvider.onRatchetKey(
98 0 : groupCall.localParticipant!,
99 0 : latestLocalKeyIndex,
100 : );
101 : }
102 :
103 0 : Logs().i(
104 0 : '[VOIP E2EE] Ratched latest key to $ratchetedKey at idx $latestLocalKeyIndex');
105 :
106 0 : await _setEncryptionKey(
107 : groupCall,
108 0 : groupCall.localParticipant!,
109 0 : latestLocalKeyIndex,
110 : ratchetedKey,
111 : delayBeforeUsingKeyOurself: false,
112 : send: true,
113 : sendTo: sendTo,
114 : );
115 : }
116 :
117 : /// sets incoming keys and also sends the key if it was for the local user
118 : /// if sendTo is null, its sent to all _participants, see `_sendEncryptionKeysEvent`
119 0 : Future<void> _setEncryptionKey(
120 : GroupCallSession groupCall,
121 : CallParticipant participant,
122 : int encryptionKeyIndex,
123 : Uint8List encryptionKeyBin, {
124 : bool delayBeforeUsingKeyOurself = false,
125 : bool send = false,
126 : List<CallParticipant>? sendTo,
127 : }) async {
128 : final encryptionKeys =
129 0 : _encryptionKeysMap[participant] ?? <int, Uint8List>{};
130 :
131 0 : encryptionKeys[encryptionKeyIndex] = encryptionKeyBin;
132 0 : _encryptionKeysMap[participant] = encryptionKeys;
133 0 : if (participant.isLocal) {
134 0 : _latestLocalKeyIndex = encryptionKeyIndex;
135 : }
136 :
137 : if (send) {
138 0 : await _sendEncryptionKeysEvent(
139 : groupCall,
140 : encryptionKeyIndex,
141 : sendTo: sendTo,
142 : );
143 : }
144 :
145 : if (delayBeforeUsingKeyOurself) {
146 : // now wait for the key to propogate and then set it, hopefully users can
147 : // stil decrypt everything
148 0 : final useKeyTimeout = Future.delayed(CallTimeouts.useKeyDelay, () async {
149 0 : Logs().i(
150 0 : '[VOIP E2EE] setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin');
151 0 : await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
152 : participant, encryptionKeyBin, encryptionKeyIndex);
153 0 : if (participant.isLocal) {
154 0 : _currentLocalKeyIndex = encryptionKeyIndex;
155 : }
156 : });
157 0 : _setNewKeyTimeouts.add(useKeyTimeout);
158 : } else {
159 0 : Logs().i(
160 0 : '[VOIP E2EE] setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin');
161 0 : await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
162 : participant, encryptionKeyBin, encryptionKeyIndex);
163 0 : if (participant.isLocal) {
164 0 : _currentLocalKeyIndex = encryptionKeyIndex;
165 : }
166 : }
167 : }
168 :
169 : /// sends the enc key to the devices using todevice, passing a list of
170 : /// sendTo only sends events to them
171 : /// setting keyIndex to null will send the latestKey
172 0 : Future<void> _sendEncryptionKeysEvent(
173 : GroupCallSession groupCall,
174 : int keyIndex, {
175 : List<CallParticipant>? sendTo,
176 : }) async {
177 0 : Logs().i('Sending encryption keys event');
178 :
179 0 : final myKeys = _getKeysForParticipant(groupCall.localParticipant!);
180 0 : final myLatestKey = myKeys?[keyIndex];
181 :
182 : final sendKeysTo =
183 0 : sendTo ?? groupCall.participants.where((p) => !p.isLocal);
184 :
185 : if (myKeys == null || myLatestKey == null) {
186 0 : Logs().w(
187 : '[VOIP E2EE] _sendEncryptionKeysEvent Tried to send encryption keys event but no keys found!');
188 0 : await _makeNewSenderKey(groupCall, false);
189 0 : await _sendEncryptionKeysEvent(
190 : groupCall,
191 : keyIndex,
192 : sendTo: sendTo,
193 : );
194 : return;
195 : }
196 :
197 : try {
198 0 : final keyContent = EncryptionKeysEventContent(
199 0 : [EncryptionKeyEntry(keyIndex, base64Encode(myLatestKey))],
200 0 : groupCall.groupCallId,
201 : );
202 0 : final Map<String, Object> data = {
203 0 : ...keyContent.toJson(),
204 : // used to find group call in groupCalls when ToDeviceEvent happens,
205 : // plays nicely with backwards compatibility for mesh calls
206 0 : 'conf_id': groupCall.groupCallId,
207 0 : 'device_id': groupCall.client.deviceID!,
208 0 : 'room_id': groupCall.room.id,
209 : };
210 0 : await _sendToDeviceEvent(
211 : groupCall,
212 0 : sendTo ?? sendKeysTo.toList(),
213 : data,
214 : EventTypes.GroupCallMemberEncryptionKeys,
215 : );
216 : } catch (e, s) {
217 0 : Logs().e('[VOIP] Failed to send e2ee keys, retrying', e, s);
218 0 : await _sendEncryptionKeysEvent(
219 : groupCall,
220 : keyIndex,
221 : sendTo: sendTo,
222 : );
223 : }
224 : }
225 :
226 0 : Future<void> _sendToDeviceEvent(
227 : GroupCallSession groupCall,
228 : List<CallParticipant> remoteParticipants,
229 : Map<String, Object> data,
230 : String eventType,
231 : ) async {
232 0 : Logs().v(
233 0 : '[VOIP] _sendToDeviceEvent: sending ${data.toString()} to ${remoteParticipants.map((e) => e.id)} ');
234 : final txid =
235 0 : VoIP.customTxid ?? groupCall.client.generateUniqueTransactionId();
236 : final mustEncrypt =
237 0 : groupCall.room.encrypted && groupCall.client.encryptionEnabled;
238 :
239 : // could just combine the two but do not want to rewrite the enc thingy
240 : // wrappers here again.
241 0 : final List<DeviceKeys> mustEncryptkeysToSendTo = [];
242 : final Map<String, Map<String, Map<String, Object>>> unencryptedDataToSend =
243 0 : {};
244 :
245 0 : for (final participant in remoteParticipants) {
246 0 : if (participant.deviceId == null) continue;
247 : if (mustEncrypt) {
248 0 : await groupCall.client.userDeviceKeysLoading;
249 0 : final deviceKey = groupCall.client.userDeviceKeys[participant.userId]
250 0 : ?.deviceKeys[participant.deviceId];
251 : if (deviceKey != null) {
252 0 : mustEncryptkeysToSendTo.add(deviceKey);
253 : }
254 : } else {
255 0 : unencryptedDataToSend.addAll({
256 0 : participant.userId: {participant.deviceId!: data}
257 : });
258 : }
259 : }
260 :
261 : // prepped data, now we send
262 : if (mustEncrypt) {
263 0 : await groupCall.client.sendToDeviceEncrypted(
264 : mustEncryptkeysToSendTo,
265 : eventType,
266 : data,
267 : );
268 : } else {
269 0 : await groupCall.client.sendToDevice(
270 : eventType,
271 : txid,
272 : unencryptedDataToSend,
273 : );
274 : }
275 : }
276 :
277 0 : @override
278 : Map<String, Object?> toJson() {
279 0 : return {
280 0 : 'type': type,
281 0 : 'livekit_service_url': livekitServiceUrl,
282 0 : 'livekit_alias': livekitAlias,
283 : };
284 : }
285 :
286 0 : @override
287 : Future<void> requestEncrytionKey(
288 : GroupCallSession groupCall,
289 : List<CallParticipant> remoteParticipants,
290 : ) async {
291 0 : final Map<String, Object> data = {
292 0 : 'conf_id': groupCall.groupCallId,
293 0 : 'device_id': groupCall.client.deviceID!,
294 0 : 'room_id': groupCall.room.id,
295 : };
296 :
297 0 : await _sendToDeviceEvent(
298 : groupCall,
299 : remoteParticipants,
300 : data,
301 : EventTypes.GroupCallMemberEncryptionKeysRequest,
302 : );
303 : }
304 :
305 0 : @override
306 : Future<void> onCallEncryption(
307 : GroupCallSession groupCall,
308 : String userId,
309 : String deviceId,
310 : Map<String, dynamic> content,
311 : ) async {
312 0 : if (!e2eeEnabled) {
313 0 : Logs().w('[VOIP] got sframe key but we do not support e2ee');
314 : return;
315 : }
316 0 : final keyContent = EncryptionKeysEventContent.fromJson(content);
317 :
318 0 : final callId = keyContent.callId;
319 :
320 0 : if (keyContent.keys.isEmpty) {
321 0 : Logs().w(
322 0 : '[VOIP E2EE] Received m.call.encryption_keys where keys is empty: callId=$callId');
323 : return;
324 : } else {
325 0 : Logs().i(
326 0 : '[VOIP E2EE]: onCallEncryption, got keys from $userId:$deviceId ${keyContent.toJson()}');
327 : }
328 :
329 0 : for (final key in keyContent.keys) {
330 0 : final encryptionKey = key.key;
331 0 : final encryptionKeyIndex = key.index;
332 0 : await _setEncryptionKey(
333 : groupCall,
334 0 : CallParticipant(groupCall.voip, userId: userId, deviceId: deviceId),
335 : encryptionKeyIndex,
336 : // base64Decode here because we receive base64Encoded version
337 0 : base64Decode(encryptionKey),
338 : delayBeforeUsingKeyOurself: false,
339 : send: false,
340 : );
341 : }
342 : }
343 :
344 0 : @override
345 : Future<void> onCallEncryptionKeyRequest(
346 : GroupCallSession groupCall,
347 : String userId,
348 : String deviceId,
349 : Map<String, dynamic> content,
350 : ) async {
351 0 : if (!e2eeEnabled) {
352 0 : Logs().w('[VOIP] got sframe key request but we do not support e2ee');
353 : return;
354 : }
355 0 : final mems = groupCall.room.getCallMembershipsForUser(userId);
356 : if (mems
357 0 : .where(
358 0 : (mem) =>
359 0 : mem.callId == groupCall.groupCallId &&
360 0 : mem.userId == userId &&
361 0 : mem.deviceId == deviceId &&
362 0 : !mem.isExpired &&
363 : // sanity checks
364 0 : mem.backend.type == groupCall.backend.type &&
365 0 : mem.roomId == groupCall.room.id &&
366 0 : mem.application == groupCall.application,
367 : )
368 0 : .isNotEmpty) {
369 0 : Logs().d(
370 0 : '[VOIP] onCallEncryptionKeyRequest: request checks out, sending key on index: $latestLocalKeyIndex to $userId:$deviceId');
371 0 : await _sendEncryptionKeysEvent(
372 : groupCall,
373 0 : _latestLocalKeyIndex,
374 0 : sendTo: [
375 0 : CallParticipant(
376 0 : groupCall.voip,
377 : userId: userId,
378 : deviceId: deviceId,
379 : )
380 : ],
381 : );
382 : }
383 : }
384 :
385 0 : @override
386 : Future<void> onNewParticipant(
387 : GroupCallSession groupCall,
388 : List<CallParticipant> anyJoined,
389 : ) async {
390 0 : if (!e2eeEnabled) return;
391 0 : if (groupCall.voip.enableSFUE2EEKeyRatcheting) {
392 0 : await _ratchetLocalParticipantKey(groupCall, anyJoined);
393 : } else {
394 0 : await _makeNewSenderKey(groupCall, true);
395 : }
396 : }
397 :
398 0 : @override
399 : Future<void> onLeftParticipant(
400 : GroupCallSession groupCall,
401 : List<CallParticipant> anyLeft,
402 : ) async {
403 0 : _encryptionKeysMap.removeWhere((key, value) => anyLeft.contains(key));
404 :
405 : // debounce it because people leave at the same time
406 0 : if (_memberLeaveEncKeyRotateDebounceTimer != null) {
407 0 : _memberLeaveEncKeyRotateDebounceTimer!.cancel();
408 : }
409 0 : _memberLeaveEncKeyRotateDebounceTimer =
410 0 : Timer(CallTimeouts.makeKeyDelay, () async {
411 0 : await _makeNewSenderKey(groupCall, true);
412 : });
413 : }
414 :
415 0 : @override
416 : Future<void> dispose(GroupCallSession groupCall) async {
417 : // only remove our own, to save requesting if we join again, yes the other side
418 : // will send it anyway but welp
419 0 : _encryptionKeysMap.remove(groupCall.localParticipant!);
420 0 : _currentLocalKeyIndex = 0;
421 0 : _latestLocalKeyIndex = 0;
422 0 : _memberLeaveEncKeyRotateDebounceTimer?.cancel();
423 : }
424 :
425 0 : @override
426 : List<Map<String, String>>? getCurrentFeeds() {
427 : return null;
428 : }
429 :
430 0 : @override
431 : bool operator ==(Object other) =>
432 : identical(this, other) ||
433 0 : other is LiveKitBackend &&
434 0 : type == other.type &&
435 0 : livekitServiceUrl == other.livekitServiceUrl &&
436 0 : livekitAlias == other.livekitAlias;
437 :
438 0 : @override
439 : int get hashCode =>
440 0 : type.hashCode ^ livekitServiceUrl.hashCode ^ livekitAlias.hashCode;
441 :
442 : /// get everything else from your livekit sdk in your client
443 0 : @override
444 : Future<WrappedMediaStream?> initLocalStream(GroupCallSession groupCall,
445 : {WrappedMediaStream? stream}) async {
446 : return null;
447 : }
448 :
449 0 : @override
450 : CallParticipant? get activeSpeaker => null;
451 :
452 : /// these are unimplemented on purpose so that you know you have
453 : /// used the wrong method
454 0 : @override
455 : bool get isLocalVideoMuted =>
456 0 : throw UnimplementedError('Use livekit sdk for this');
457 :
458 0 : @override
459 : bool get isMicrophoneMuted =>
460 0 : throw UnimplementedError('Use livekit sdk for this');
461 :
462 0 : @override
463 : WrappedMediaStream? get localScreenshareStream =>
464 0 : throw UnimplementedError('Use livekit sdk for this');
465 :
466 0 : @override
467 : WrappedMediaStream? get localUserMediaStream =>
468 0 : throw UnimplementedError('Use livekit sdk for this');
469 :
470 0 : @override
471 : List<WrappedMediaStream> get screenShareStreams =>
472 0 : throw UnimplementedError('Use livekit sdk for this');
473 :
474 0 : @override
475 : List<WrappedMediaStream> get userMediaStreams =>
476 0 : throw UnimplementedError('Use livekit sdk for this');
477 :
478 0 : @override
479 : Future<void> setDeviceMuted(
480 : GroupCallSession groupCall, bool muted, MediaInputKind kind) async {
481 : return;
482 : }
483 :
484 0 : @override
485 : Future<void> setScreensharingEnabled(GroupCallSession groupCall, bool enabled,
486 : String desktopCapturerSourceId) async {
487 : return;
488 : }
489 :
490 0 : @override
491 : Future<void> setupP2PCallWithNewMember(GroupCallSession groupCall,
492 : CallParticipant rp, CallMembership mem) async {
493 : return;
494 : }
495 :
496 0 : @override
497 : Future<void> setupP2PCallsWithExistingMembers(
498 : GroupCallSession groupCall) async {
499 : return;
500 : }
501 :
502 0 : @override
503 : Future<void> updateMediaDeviceForCalls() async {
504 : return;
505 : }
506 : }
|