Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/e2ee-required-fields.md
Original file line number Diff line number Diff line change
@@ -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
83 changes: 83 additions & 0 deletions packages/livekit-rtc/src/e2ee.test.ts
Original file line number Diff line number Diff line change
@@ -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<FfiRequest>[] = [];
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);
});
});
16 changes: 14 additions & 2 deletions packages/livekit-rtc/src/e2ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,15 @@ 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: {
case: 'setKey',
value: new SetKeyRequest({
keyIndex: keyIndex,
participantIdentity: participantIdentity,
key: key,
}),
},
});
Expand Down Expand Up @@ -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;
}
Expand All @@ -216,6 +225,7 @@ export class FrameCryptor {
case: 'cryptorSetEnabled',
value: new FrameCryptorSetEnabledRequest({
participantIdentity: this.participantIdentity,
trackSid: this.trackSid,
enabled: this.enabled,
}),
},
Expand All @@ -237,6 +247,7 @@ export class FrameCryptor {
case: 'cryptorSetKeyIndex',
value: new FrameCryptorSetKeyIndexRequest({
participantIdentity: this.participantIdentity,
trackSid: this.trackSid,
keyIndex: this.keyIndex,
}),
},
Expand Down Expand Up @@ -312,6 +323,7 @@ export class E2EEManager {
new FrameCryptor(
this.roomHandle,
cryptor.participantIdentity!,
cryptor.trackSid!,
cryptor.keyIndex!,
cryptor.enabled!,
),
Expand Down
Loading