Skip to content

Commit d7c135a

Browse files
feat: implement retry encryption key request mechanism for livekit calls
1 parent 6f67282 commit d7c135a

File tree

3 files changed

+297
-46
lines changed

3 files changed

+297
-46
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'dart:async';
2+
3+
/// Retries the `retryFunction` after a set `timeInterval` until `dispose` is called
4+
class RetryEventModel {
5+
final Duration timeInterval;
6+
final void Function(Timer? timer) retryFunction;
7+
8+
final Timer? _timer;
9+
10+
RetryEventModel({required this.timeInterval, required this.retryFunction})
11+
: _timer = Timer.periodic(timeInterval, retryFunction);
12+
13+
void dispose() => _timer?.cancel();
14+
}

lib/src/voip/backend/livekit_backend.dart

Lines changed: 90 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:convert';
33
import 'dart:typed_data';
44

55
import 'package:matrix/matrix.dart';
6+
import 'package:matrix/src/models/retry_event_model.dart';
67
import 'package:matrix/src/utils/crypto/crypto.dart';
78

89
class LiveKitBackend extends CallBackend {
@@ -24,6 +25,18 @@ class LiveKitBackend extends CallBackend {
2425
/// participant:keyIndex:keyBin
2526
final Map<CallParticipant, Map<int, Uint8List>> _encryptionKeysMap = {};
2627

28+
/// Tracks pending encryption key request retries per participant
29+
final Map<CallParticipant, RetryEventModel> _requestEncryptionKeyPending = {};
30+
31+
/// Tracks retry count per participant to prevent infinite retries
32+
final Map<CallParticipant, int> _keyRequestRetryCount = {};
33+
34+
/// Maximum number of retry attempts for key requests
35+
static const int _maxKeyRequestRetries = 5;
36+
37+
/// Retry interval for key requests
38+
static const Duration _keyRequestRetryInterval = Duration(seconds: 2);
39+
2740
final List<Future> _setNewKeyTimeouts = [];
2841

2942
int _indexCounter = 0;
@@ -79,10 +92,7 @@ class LiveKitBackend extends CallBackend {
7992
'_makeNewSenderKey using previous key because last created at ${_lastNewKeyTime.toString()}',
8093
);
8194
// still a fairly new key, just send that
82-
await _sendEncryptionKeysEvent(
83-
groupCall,
84-
_latestLocalKeyIndex,
85-
);
95+
await _sendEncryptionKeysEvent(groupCall, _latestLocalKeyIndex);
8696
return;
8797
}
8898

@@ -222,20 +232,22 @@ class LiveKitBackend extends CallBackend {
222232
);
223233
// now wait for the key to propogate and then set it, hopefully users can
224234
// stil decrypt everything
225-
final useKeyTimeout =
226-
Future.delayed(groupCall.voip.timeouts!.useKeyDelay, () async {
227-
Logs().i(
228-
'[VOIP E2EE] delayed setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin',
229-
);
230-
await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
231-
participant,
232-
encryptionKeyBin,
233-
encryptionKeyIndex,
234-
);
235-
if (participant.isLocal) {
236-
_currentLocalKeyIndex = encryptionKeyIndex;
237-
}
238-
});
235+
final useKeyTimeout = Future.delayed(
236+
groupCall.voip.timeouts!.useKeyDelay,
237+
() async {
238+
Logs().i(
239+
'[VOIP E2EE] delayed setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin',
240+
);
241+
await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
242+
participant,
243+
encryptionKeyBin,
244+
encryptionKeyIndex,
245+
);
246+
if (participant.isLocal) {
247+
_currentLocalKeyIndex = encryptionKeyIndex;
248+
}
249+
},
250+
);
239251
_setNewKeyTimeouts.add(useKeyTimeout);
240252
} else {
241253
Logs().i(
@@ -271,17 +283,15 @@ class LiveKitBackend extends CallBackend {
271283
'[VOIP E2EE] _sendEncryptionKeysEvent Tried to send encryption keys event but no keys found!',
272284
);
273285
await _makeNewSenderKey(groupCall, false);
274-
await _sendEncryptionKeysEvent(
275-
groupCall,
276-
keyIndex,
277-
sendTo: sendTo,
278-
);
286+
await _sendEncryptionKeysEvent(groupCall, keyIndex, sendTo: sendTo);
279287
return;
280288
}
281289

282290
try {
283291
final keyContent = EncryptionKeysEventContent(
284-
[EncryptionKeyEntry(keyIndex, base64Encode(myLatestKey))],
292+
[
293+
EncryptionKeyEntry(keyIndex, base64Encode(myLatestKey)),
294+
],
285295
groupCall.groupCallId,
286296
);
287297
final Map<String, Object> data = {
@@ -300,11 +310,7 @@ class LiveKitBackend extends CallBackend {
300310
);
301311
} catch (e, s) {
302312
Logs().e('[VOIP E2EE] Failed to send e2ee keys, retrying', e, s);
303-
await _sendEncryptionKeysEvent(
304-
groupCall,
305-
keyIndex,
306-
sendTo: sendTo,
307-
);
313+
await _sendEncryptionKeysEvent(groupCall, keyIndex, sendTo: sendTo);
308314
}
309315
}
310316

@@ -375,6 +381,10 @@ class LiveKitBackend extends CallBackend {
375381
GroupCallSession groupCall,
376382
List<CallParticipant> remoteParticipants,
377383
) async {
384+
Logs().v(
385+
'[VOIP E2EE] requesting stream encryption keys from ${remoteParticipants.map((e) => e.id)}',
386+
);
387+
378388
final Map<String, Object> data = {
379389
'conf_id': groupCall.groupCallId,
380390
'device_id': groupCall.client.deviceID!,
@@ -387,6 +397,32 @@ class LiveKitBackend extends CallBackend {
387397
data,
388398
EventTypes.GroupCallMemberEncryptionKeysRequest,
389399
);
400+
401+
// Set up retry timers for each participant
402+
for (final rp in remoteParticipants) {
403+
// Skip if a retry is already pending for this participant
404+
if (_requestEncryptionKeyPending.containsKey(rp)) continue;
405+
406+
// Check retry count to prevent infinite retries
407+
final currentCount = _keyRequestRetryCount[rp] ?? 0;
408+
if (currentCount >= _maxKeyRequestRetries) {
409+
Logs().w(
410+
'[VOIP E2EE] Max retries ($_maxKeyRequestRetries) reached for ${rp.id}, giving up key request',
411+
);
412+
_keyRequestRetryCount.remove(rp);
413+
continue;
414+
}
415+
_keyRequestRetryCount[rp] = currentCount + 1;
416+
417+
_requestEncryptionKeyPending[rp] = RetryEventModel(
418+
timeInterval: _keyRequestRetryInterval,
419+
retryFunction: (_) {
420+
// Remove the pending entry before retrying to allow new retry to be scheduled
421+
_requestEncryptionKeyPending.remove(rp)?.dispose();
422+
unawaited(requestEncrytionKey(groupCall, [rp]));
423+
},
424+
);
425+
}
390426
}
391427

392428
@override
@@ -403,8 +439,15 @@ class LiveKitBackend extends CallBackend {
403439
final keyContent = EncryptionKeysEventContent.fromJson(content);
404440

405441
final callId = keyContent.callId;
406-
final p =
407-
CallParticipant(groupCall.voip, userId: userId, deviceId: deviceId);
442+
final p = CallParticipant(
443+
groupCall.voip,
444+
userId: userId,
445+
deviceId: deviceId,
446+
);
447+
448+
// Cancel any pending retry for this participant since we received keys
449+
_requestEncryptionKeyPending.remove(p)?.dispose();
450+
_keyRequestRetryCount.remove(p);
408451

409452
if (keyContent.keys.isEmpty) {
410453
Logs().w(
@@ -471,11 +514,7 @@ class LiveKitBackend extends CallBackend {
471514
groupCall,
472515
_latestLocalKeyIndex,
473516
sendTo: [
474-
CallParticipant(
475-
groupCall.voip,
476-
userId: userId,
477-
deviceId: deviceId,
478-
),
517+
CallParticipant(groupCall.voip, userId: userId, deviceId: deviceId),
479518
],
480519
);
481520
return true;
@@ -525,16 +564,14 @@ class LiveKitBackend extends CallBackend {
525564
if (_memberLeaveEncKeyRotateDebounceTimer != null) {
526565
_memberLeaveEncKeyRotateDebounceTimer!.cancel();
527566
}
528-
_memberLeaveEncKeyRotateDebounceTimer =
529-
Timer(groupCall.voip.timeouts!.makeKeyOnLeaveDelay, () async {
530-
// we skipJoinDebounce here because we want to make sure a new key is generated
531-
// and that the join debounce does not block us from making a new key
532-
await _makeNewSenderKey(
533-
groupCall,
534-
true,
535-
skipJoinDebounce: true,
536-
);
537-
});
567+
_memberLeaveEncKeyRotateDebounceTimer = Timer(
568+
groupCall.voip.timeouts!.makeKeyOnLeaveDelay,
569+
() async {
570+
// we skipJoinDebounce here because we want to make sure a new key is generated
571+
// and that the join debounce does not block us from making a new key
572+
await _makeNewSenderKey(groupCall, true, skipJoinDebounce: true);
573+
},
574+
);
538575
}
539576

540577
@override
@@ -545,6 +582,13 @@ class LiveKitBackend extends CallBackend {
545582
_currentLocalKeyIndex = 0;
546583
_latestLocalKeyIndex = 0;
547584
_memberLeaveEncKeyRotateDebounceTimer?.cancel();
585+
586+
// Clean up all pending encryption key request retries
587+
for (final retry in _requestEncryptionKeyPending.values) {
588+
retry.dispose();
589+
}
590+
_requestEncryptionKeyPending.clear();
591+
_keyRequestRetryCount.clear();
548592
}
549593

550594
@override

0 commit comments

Comments
 (0)