From 593b012fb5f51604467ef0afbf5ce49944a3b447 Mon Sep 17 00:00:00 2001 From: ninglinLiu Date: Tue, 27 Oct 2026 18:39:48 +0800 Subject: [PATCH] fix(rtc-node): send the required key and track SID in E2EE requests --- .changeset/e2ee-required-fields.md | 5 ++ packages/livekit-rtc/src/e2ee.test.ts | 83 +++++++++++++++++++++++++++ packages/livekit-rtc/src/e2ee.ts | 16 +++++- 3 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 .changeset/e2ee-required-fields.md create mode 100644 packages/livekit-rtc/src/e2ee.test.ts diff --git a/.changeset/e2ee-required-fields.md b/.changeset/e2ee-required-fields.md new file mode 100644 index 00000000..186abbd1 --- /dev/null +++ b/.changeset/e2ee-required-fields.md @@ -0,0 +1,5 @@ +--- +'@livekit/rtc-node': patch +--- + +Fix E2EE requests that could not be encoded: `KeyProvider.setKey` now takes the key to install, and `FrameCryptor` keeps its track SID so `setEnabled` and `setKeyIndex` work diff --git a/packages/livekit-rtc/src/e2ee.test.ts b/packages/livekit-rtc/src/e2ee.test.ts new file mode 100644 index 00000000..8a66db0d --- /dev/null +++ b/packages/livekit-rtc/src/e2ee.test.ts @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import type { PartialMessage } from '@bufbuild/protobuf'; +import { + E2eeManagerGetFrameCryptorsResponse, + FfiRequest, + FrameCryptor as FrameCryptorInfo, +} from '@livekit/rtc-ffi-bindings'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { E2EEManager, FrameCryptor, KeyProvider, defaultKeyProviderOptions } from './e2ee.js'; +import { FfiClient } from './ffi_client.js'; + +const roomHandle = BigInt(1); + +function captureRequests() { + const requests: PartialMessage[] = []; + vi.spyOn(FfiClient.instance, 'request').mockImplementation((req) => { + requests.push(req); + return {} as never; + }); + return requests; +} + +describe('E2EE FFI requests', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('KeyProvider.setKey sends the key so the request can be encoded', () => { + const requests = captureRequests(); + const key = new Uint8Array([1, 2, 3]); + + new KeyProvider(roomHandle, defaultKeyProviderOptions).setKey('alice', key, 0); + + const req = new FfiRequest(requests[0]); + expect(() => req.toBinary()).not.toThrow(); + const e2ee = req.message.value as { message: { case: string; value: { key?: Uint8Array } } }; + expect(e2ee.message.case).toBe('setKey'); + expect(e2ee.message.value.key).toEqual(key); + }); + + it('FrameCryptor requests carry the track SID so they can be encoded', () => { + const requests = captureRequests(); + const cryptor = new FrameCryptor(roomHandle, 'alice', 'TR_audio', 0, true); + + cryptor.setEnabled(false); + cryptor.setKeyIndex(2); + + expect(requests).toHaveLength(2); + for (const r of requests) { + const req = new FfiRequest(r); + expect(() => req.toBinary()).not.toThrow(); + const e2ee = req.message.value as { message: { value: { trackSid?: string } } }; + expect(e2ee.message.value.trackSid).toBe('TR_audio'); + } + }); + + it('E2EEManager.frameCryptors keeps the track SID of each cryptor', () => { + vi.spyOn(FfiClient.instance, 'request').mockReturnValue({ + message: { + case: 'managerGetFrameCryptors', + value: new E2eeManagerGetFrameCryptorsResponse({ + frameCryptors: [ + new FrameCryptorInfo({ + participantIdentity: 'alice', + trackSid: 'TR_video', + keyIndex: 1, + enabled: true, + }), + ], + }), + }, + }); + + const [cryptor] = new E2EEManager(roomHandle).frameCryptors(); + + expect(cryptor!.participantIdentity).toBe('alice'); + expect(cryptor!.trackSid).toBe('TR_video'); + expect(cryptor!.keyIndex).toBe(1); + expect(cryptor!.enabled).toBe(true); + }); +}); diff --git a/packages/livekit-rtc/src/e2ee.ts b/packages/livekit-rtc/src/e2ee.ts index 6d60d4af..0e9464e3 100644 --- a/packages/livekit-rtc/src/e2ee.ts +++ b/packages/livekit-rtc/src/e2ee.ts @@ -130,7 +130,7 @@ export class KeyProvider { return (res.message.value as RatchetSharedKeyResponse).newKey!; } - setKey(participantIdentity: string, keyIndex: number) { + setKey(participantIdentity: string, key: Uint8Array, keyIndex: number) { const req = new E2eeRequest({ roomHandle: this.roomHandle, message: { @@ -138,6 +138,7 @@ export class KeyProvider { value: new SetKeyRequest({ keyIndex: keyIndex, participantIdentity: participantIdentity, + key: key, }), }, }); @@ -198,12 +199,20 @@ export class KeyProvider { export class FrameCryptor { private roomHandle = BigInt(0); participantIdentity: string; + trackSid: string; keyIndex: number; enabled: boolean; - constructor(roomHandle: bigint, participantIdentity: string, keyIndex: number, enabled: boolean) { + constructor( + roomHandle: bigint, + participantIdentity: string, + trackSid: string, + keyIndex: number, + enabled: boolean, + ) { this.roomHandle = roomHandle; this.participantIdentity = participantIdentity; + this.trackSid = trackSid; this.keyIndex = keyIndex; this.enabled = enabled; } @@ -216,6 +225,7 @@ export class FrameCryptor { case: 'cryptorSetEnabled', value: new FrameCryptorSetEnabledRequest({ participantIdentity: this.participantIdentity, + trackSid: this.trackSid, enabled: this.enabled, }), }, @@ -237,6 +247,7 @@ export class FrameCryptor { case: 'cryptorSetKeyIndex', value: new FrameCryptorSetKeyIndexRequest({ participantIdentity: this.participantIdentity, + trackSid: this.trackSid, keyIndex: this.keyIndex, }), }, @@ -312,6 +323,7 @@ export class E2EEManager { new FrameCryptor( this.roomHandle, cryptor.participantIdentity!, + cryptor.trackSid!, cryptor.keyIndex!, cryptor.enabled!, ),