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