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

Generated by: LCOV version 1.14