Repository navigation
fix: start new E2EE frame cryptors at the shared key index - #1246
hiroshihorie wants to merge 1 commit into
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.
There was a problem hiding this comment.
Devin Review found 2 potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| if (options.sharedKey) { | ||
| return _latestSharedIndex; |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| _latestSharedIndex = keyIndex ?? 0; | ||
| return _keyProvider.setSharedKey(key: _sharedKey!, index: _latestSharedIndex); |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
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.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.Testing
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 formatandimport_sorterall pass.Not covered yet: