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