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