Repository navigation
fix: start new E2EE frame cryptors at the shared key index - #1246
Merged
Merged
Conversation
In shared-key mode setSharedKey passed its index to the native key ring but never recorded it, so getLatestIndex always returned 0. New sender frame cryptors and encrypted data packets read that value, so an app that keeps its key at another index (for example epoch based rotation) published tracks that encrypted at an empty or stale slot. The sender dropped every frame with a missing key error, or receivers could not decrypt. setSharedKey now records the index and getLatestIndex returns it for every participant in shared-key mode, which matches the JS SDK. Per-participant mode is unchanged.
hiroshihorie
marked this pull request as ready for review
October 20, 2026 10:25
hiroshihorie
requested review from
changt,
cloudwebrtc and
xianshijing-lk
as code owners
October 20, 2026 10:25
cloudwebrtc
approved these changes
Oct 20, 2026
The key ring stores a key at its index modulo the ring size, and the native ring clamps its size to 1 to 256. getLatestIndex returned the raw index, so a key set at an index past the ring size gave new frame cryptors an index outside the ring. Their sender dropped every frame with a missing key error even though the key was in the wrapped slot. The recorded index is now the slot the key lands in, in shared and per participant mode. A negative index replaces the key in the current slot, so it no longer moves the recorded index either.
The web worker read the key index sent with dataCryptorEncrypt but never applied it, so the data packet cryptor always encrypted with the key in slot 0. With the shared key installed at another index, sending encrypted data on web failed because slot 0 was empty. The worker now sets the requested index on the data packet cryptor before encrypting, which is what the native data packet cryptor does with its index argument.
Recompiled with `dart compile js web/e2ee.worker.dart -o example/web/e2ee.worker.dart.js -m` so the example app's bundled worker applies the requested key index to data packets. The rest of the diff comes from the newer Dart SDK (3.13.1).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In shared-key E2EE mode,
BaseKeyProvider.setSharedKey(key, keyIndex: n)passednto the native key ring but never recorded it.getLatestIndex()only read the per-participant map, so in shared-key mode it always returned 0. Two things read that value:E2EEManager._addRtpSender)encryptData)So an app that keeps its shared key at a non-zero index got tracks whose sender cryptor pointed at slot 0. Epoch-based rotation with
epoch % keyRingSizeis one example. What happened next depended on what slot 0 held:missingKey.Either way the publication looked healthy and the app saw no error. The only workaround was calling
e2eeManager.setKeyIndex()after every publish, and that races the cryptor setup.Change
setSharedKeynow records the index.getLatestIndex()returns that index for every participant.dataCryptorEncrypt. It used to ignore it and always encrypt data packets with the key in slot 0.This matches the JS SDK, where the sender reads the key handler's current index. It also matches livekit/client-sdk-swift#1157, which starts new Swift sender cryptors at the key provider's current index.
Existing cryptors are not moved when
setSharedKeychanges the index. Apps still move live tracks withE2EEManager.setKeyIndex()as before. So a rotation that installs the new key first and switches later keeps working.The branch also merges
main, which brings the flutter_webrtc 1.6.3 pin the e2e harness needs.Testing
test/e2ee/key_provider_test.dartcovers the shared index, the ring wrap (16 and 19 in a ring of 16, 260 in a ring of 300 clamped to 256), the negative index and the per-participant wrap. Each case fails without the change it covers.flutter test(487 passed, 1 skipped),flutter analyze,dart formatandimport_sorterall pass. The web worker compiles withdart compile js.E2EESharedKeyIndexTest(branchhiroshi/e2ee-shared-key-index-scenarioin livekit/e2e-flutter), installs a shared key at index 3 with nothing in slot 0, publishes the camera, and checks the sender's key index and that the receiver decrypts. It repeats with index 21 in a ring of 16. On macOS against a local server:e2eetag (3 of 3)main, the first phase fails with the sender at index 0, and the receiver never decrypts because the sender drops every frameNot covered yet: