LCOV - code coverage report
Current view: top level - lib/src/voip/backend - livekit_backend.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 0.0 % 185 0
Test Date: 2025-01-14 13:39:53 Functions: - 0 0

            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              : }
        

Generated by: LCOV version 2.0-1