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 185 0.0 %
Date: 2024-11-12 07:37:08 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 1.14