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

Generated by: LCOV version 1.14