Skip to content

fix: start new E2EE frame cryptors at the shared key index - #1246

Open
hiroshihorie wants to merge 1 commit into
mainfrom
hiroshi/e2ee-shared-key-index
Open

hiroshihorie wants to merge 1 commit into
mainfrom
hiroshi/e2ee-shared-key-index

Conversation

@hiroshihorie

Copy link
Copy Markdown
Member

Summary

In shared-key E2EE mode, BaseKeyProvider.setSharedKey(key, keyIndex: n) passed n to 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:

  • new sender frame cryptors (E2EEManager._addRtpSender)
  • encrypted data packets (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 % keyRingSize is one example. What happened next depended on what slot 0 held:

  • Empty: the sender dropped every frame with missingKey.
  • An older key: receivers could not decrypt.

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

  • setSharedKey now records the index.
  • In shared-key mode, getLatestIndex() returns that index for every participant.
  • Per-participant mode is unchanged.

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 setSharedKey changes the index. Apps still move live tracks with E2EEManager.setKeyIndex() as before. So a rotation that installs the new key first and switches later keeps working.

Testing

  • New test/e2ee/key_provider_test.dart. Its two shared-key cases fail on main (index 0 instead of 5 and 3) and pass with this change.
  • flutter test (462 passed, 1 skipped), flutter analyze, dart format and import_sorter all pass.

Not covered yet:

  • An E2EEManager-level test that a new sender cryptor receives the index. That needs a native RTP sender in tests.
  • A device or e2e run with a non-zero shared key index.

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
hiroshihorie marked this pull request as ready for review October 20, 2026 10:25

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +57 to +58
if (options.sharedKey) {
return _latestSharedIndex;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Shared-key data packets still fail on web

With a nonzero shared index, encryptData passes _latestSharedIndex, but the web worker leaves its data cryptor at zero. The data cryptor cannot find the key, so encrypted packets fail to send.

Learn more

A shared-key index is selected by the Dart E2EE manager for data encryption. On web, encryptData forwards it to the WebRTC data packet cryptor, but dataCryptorEncrypt reads that index without applying it to the worker's cryptor. E2EEDataPacketCryptor.encrypt therefore looks up index zero, where no key exists if only a nonzero shared key was installed. The web worker then reports an encryption error instead of returning an encrypted packet.

Example: Install only shared key 5, then send encrypted data on web. Dart passes index 5; the worker cryptor reads slot 0 and cannot encrypt.

Recommended fix: Have dataCryptorEncrypt apply the requested index to the data packet cryptor before encrypt, and validate the resulting key index. Add a web data encryption regression test with only a nonzero shared key installed.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +98 to +99
_latestSharedIndex = keyIndex ?? 0;
return _keyProvider.setSharedKey(key: _sharedKey!, index: _latestSharedIndex);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Wrapped shared indices break web tracks

When keyIndex exceeds the web key-ring size, _latestSharedIndex retains it while the key handler wraps it. New frame cryptors read outside the ring and drop encrypted frames.

Learn more

The web key handler stores a shared key at keyIndex % cryptoKeyRing.length. The new value returned by getLatestIndex is not reduced the same way. The sender cryptor receives that raw index, and its frame encoder accesses the ring at that index. For indices beyond the configured ring, the key exists in a wrapped slot but encryption cannot retrieve it.

Example: With a ring of 16 slots, setSharedKey('key', keyIndex: 16) stores the key in slot 0 on web. A new sender receives index 16 and fails to encrypt instead of using slot 0.

Recommended fix: Normalize the index used for new cryptors to the same ring slot as the web key handler, while preserving the original argument passed to setSharedKey. Cover an index equal to the ring size in a regression test.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants