Skip to content

fix(e2ee): decrypt data packets with the ratcheted key when retrying - #2188

Merged
lukasIO merged 1 commit into
livekit:mainfrom
ninglinLiu:fix/data-cryptor-ratchet
Oct 27, 2026
Merged

lukasIO merged 1 commit into
livekit:mainfrom
ninglinLiu:fix/data-cryptor-ratchet

Conversation

@ninglinLiu

Copy link
Copy Markdown
Contributor

Fixes #2187

When a data packet fails to decrypt and ratcheting is enabled, DataCryptor.decrypt() derives the ratcheted key and passes it to the retry in ratchetOpts.encryptionKey, but every attempt decrypted with the stored keySet.encryptionKey, so the retry could never succeed. A receiver that only gets data from a sender (no media to ratchet it through FrameCryptor) never decrypted that sender's packets again after the sender ratcheted.

This mirrors FrameCryptor.decryptFrame():

  • decrypt with ratchetOpts.encryptionKey ?? keySet.encryptionKey
  • pass initialMaterial ?? keySet to the recursive call, so the "stored key unchanged" checks compare against the key the packet was first tried with

Tests

  • The existing handles ratcheting when enabled test ratcheted the sender with ratchetKey(1, false), which doesn't apply the ratcheted key, so the sender kept encrypting with the initial key and the test never exercised the retry. It now calls ratchetKey(1).
  • New test: after a one-step ratchet the receiver decrypts the packet, stores the ratcheted key, and decrypts the next packet without ratcheting again.

Both tests fail on main with DataCryptor: valid key missing for participant receiver and pass with this change. npx vitest run src/e2ee (97 passed), npx tsc --noEmit, eslint and prettier pass. Includes a patch changeset.

This PR was prepared with AI assistance and reviewed/tested by the author.

@changeset-bot

changeset-bot Bot commented Oct 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 404ac32

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
livekit-client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@RaphaelFakhri RaphaelFakhri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I ran this branch locally. The two ratchet tests fail on main with valid key missing for participant receiver and pass with the change. vitest run src passes (995 passed, 1 skipped) and tsc --noEmit is clean.

I also ran some edge cases in a throwaway test against the PR's DataCryptor, with the receiver one ratchet step behind:

  • Five packets decrypted concurrently all succeed. ratchetKey deduplicates them into one derivation, KeyRatcheted fires once, and the next packet decrypts without another ratchet.
  • A late packet encrypted with the pre-ratchet key rejects with valid key missing (window 2) and leaves the stored key set unchanged, so later packets still decrypt.
  • If setKey() replaces the key while a retry is in flight, the retry still decrypts and doesn't overwrite the new key.
  • Key index 0 and keyringSize - 1 both recover.

One non-blocking note: a gap of two or more ratchet steps still fails, even with ratchetWindowSize: 4. When a sender calls ratchetKey(1) twice, the receiver throws valid key missing. Each retry calls keys.ratchetKey(keyIndex, false), which derives from the stored material, and that material stays the same until a decryption succeeds. As a result, every attempt tries the same one-step key. FrameCryptor.decryptFrame() has the same loop, so this is consistent with media and not a regression. A follow-up that ratchets from the previous attempt's material would make the window size meaningful for both.

@lukasIO lukasIO left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this makes sense, thank you!

@lukasIO
lukasIO merged commit 20f4a10 into livekit:main Oct 27, 2026
6 checks passed
@github-actions github-actions Bot mentioned this pull request Oct 27, 2026
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.

E2EE: data packets can't be decrypted after the sender ratchets, because DataCryptor's ratchet retry reuses the old key

3 participants