Skip to content

Commit

Permalink
Experimental end-to-end encryption support (#557)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasIO authored Jun 29, 2023
1 parent 5c751eb commit e244d9c
Show file tree
Hide file tree
Showing 42 changed files with 2,190 additions and 57 deletions.
Binary file removed .DS_Store
Binary file not shown.
5 changes: 5 additions & 0 deletions .changeset/stupid-pans-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"livekit-client": minor
---

Experimental end-to-end encryption support
24 changes: 24 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ <h2>Livekit Sample App</h2>
<div>
<input type="text" class="form-control" id="token" />
</div>
<div>
<b>E2EE key</b>
</div>
<div>
<input type="text" class="form-control" id="crypto-key" />
</div>
</div>

<!-- connect options -->
Expand Down Expand Up @@ -114,6 +120,24 @@ <h2>Livekit Sample App</h2>
>
Share Screen
</button>
<button
id="toggle-e2ee-button"
class="btn btn-secondary mt-1"
disabled
type="button"
onclick="appActions.toggleE2EE()"
>
Enable E2EE
</button>
<button
id="e2ee-ratchet-button"
class="btn btn-secondary mt-1"
disabled
type="button"
onclick="appActions.ratchetE2EEKey()"
>
Ratchet Key
</button>
<select
id="simulate-scenario"
class="custom-select"
Expand Down
73 changes: 66 additions & 7 deletions example/sample.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//@ts-ignore
import E2EEWorker from '../src/e2ee/worker/e2ee.worker?worker';
import {
ConnectionQuality,
ConnectionState,
DataPacket_Kind,
DisconnectReason,
ExternalE2EEKeyProvider,
LocalAudioTrack,
LocalParticipant,
LogLevel,
Expand All @@ -27,7 +30,7 @@ import {
supportsAV1,
supportsVP9,
} from '../src/index';
import { SimulationScenario } from '../src/room/types';
import type { SimulationScenario } from '../src/room/types';

const $ = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;

Expand All @@ -37,6 +40,7 @@ const state = {
decoder: new TextDecoder(),
defaultDevices: new Map<MediaDeviceKind, string>(),
bitrateInterval: undefined as any,
e2eeKeyProvider: new ExternalE2EEKeyProvider(),
};
let currentRoom: Room | undefined;

Expand All @@ -45,11 +49,17 @@ let startTime: number;
const searchParams = new URLSearchParams(window.location.search);
const storedUrl = searchParams.get('url') ?? 'ws://localhost:7880';
const storedToken = searchParams.get('token') ?? '';
$<HTMLInputElement>('url').value = storedUrl;
$<HTMLInputElement>('token').value = storedToken;
(<HTMLInputElement>$('url')).value = storedUrl;
(<HTMLInputElement>$('token')).value = storedToken;
let storedKey = searchParams.get('key');
if (!storedKey) {
(<HTMLSelectElement>$('crypto-key')).value = 'password';
} else {
(<HTMLSelectElement>$('crypto-key')).value = storedKey;
}

function updateSearchParams(url: string, token: string) {
const params = new URLSearchParams({ url, token });
function updateSearchParams(url: string, token: string, key: string) {
const params = new URLSearchParams({ url, token, key });
window.history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`);
}

Expand All @@ -64,10 +74,11 @@ const appActions = {
const adaptiveStream = (<HTMLInputElement>$('adaptive-stream')).checked;
const shouldPublish = (<HTMLInputElement>$('publish-option')).checked;
const preferredCodec = (<HTMLSelectElement>$('preferred-codec')).value as VideoCodec;
const cryptoKey = (<HTMLSelectElement>$('crypto-key')).value;
const autoSubscribe = (<HTMLInputElement>$('auto-subscribe')).checked;

setLogLevel(LogLevel.debug);
updateSearchParams(url, token);
setLogLevel(LogLevel.info);
updateSearchParams(url, token, cryptoKey);

const roomOpts: RoomOptions = {
adaptiveStream,
Expand All @@ -76,10 +87,13 @@ const appActions = {
simulcast,
videoSimulcastLayers: [VideoPresets.h90, VideoPresets.h216],
videoCodec: preferredCodec || 'vp8',
dtx: true,
red: true,
},
videoCaptureDefaults: {
resolution: VideoPresets.h720.resolution,
},
e2ee: { keyProvider: state.e2eeKeyProvider, worker: new E2EEWorker() },
};

const connectOpts: RoomConnectOptions = {
Expand Down Expand Up @@ -181,9 +195,17 @@ const appActions = {
appendLog(`tracks published in ${Date.now() - startTime}ms`);
updateButtonsForPublishState();
}
})
.on(RoomEvent.ParticipantEncryptionStatusChanged, () => {
updateButtonsForPublishState();
});

try {
// read and set current key from input
const cryptoKey = (<HTMLSelectElement>$('crypto-key')).value;
state.e2eeKeyProvider.setKey(cryptoKey);
await room.setE2EEEnabled(true);

await room.connect(url, token, connectOptions);
const elapsed = Date.now() - startTime;
appendLog(
Expand All @@ -210,6 +232,23 @@ const appActions = {
return room;
},

toggleE2EE: async () => {
if (!currentRoom) return;

// read and set current key from input
const cryptoKey = (<HTMLSelectElement>$('crypto-key')).value;
state.e2eeKeyProvider.setKey(cryptoKey);

await currentRoom.setE2EEEnabled(!currentRoom.isE2EEEnabled);
},

ratchetE2EEKey: async () => {
if (!currentRoom) {
return;
}
await state.e2eeKeyProvider.ratchetKey();
},

toggleAudio: async () => {
if (!currentRoom) return;
const enabled = currentRoom.localParticipant.isMicrophoneEnabled;
Expand Down Expand Up @@ -387,6 +426,7 @@ function handleData(msg: Uint8Array, participant?: RemoteParticipant) {

function participantConnected(participant: Participant) {
appendLog('participant', participant.identity, 'connected', participant.metadata);
console.log('tracks', participant.tracks);
participant
.on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
appendLog('track was muted', pub.trackSid, participant.identity);
Expand Down Expand Up @@ -479,6 +519,7 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
<div class="right">
<span id="signal-${identity}"></span>
<span id="mic-${identity}" class="mic-on"></span>
<span id="e2ee-${identity}" class="e2ee-on"></span>
</div>
</div>
${
Expand Down Expand Up @@ -587,6 +628,15 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
micElm.innerHTML = '<i class="fas fa-microphone-slash"></i>';
}

const e2eeElm = $(`e2ee-${identity}`)!;
if (participant.isEncrypted) {
e2eeElm.className = 'e2ee-on';
e2eeElm.innerHTML = '<i class="fas fa-lock"></i>';
} else {
e2eeElm.className = 'e2ee-off';
e2eeElm.innerHTML = '<i class="fas fa-unlock"></i>';
}

switch (participant.connectionQuality) {
case ConnectionQuality.Excellent:
case ConnectionQuality.Good:
Expand Down Expand Up @@ -718,6 +768,8 @@ function setButtonsForState(connected: boolean) {
'disconnect-room-button',
'flip-video-button',
'send-button',
'toggle-e2ee-button',
'e2ee-ratchet-button',
];
const disconnectedSet = ['connect-button'];

Expand Down Expand Up @@ -792,6 +844,13 @@ function updateButtonsForPublishState() {
lp.isScreenShareEnabled ? 'Stop Screen Share' : 'Share Screen',
lp.isScreenShareEnabled,
);

// e2ee
setButtonState(
'toggle-e2ee-button',
`${currentRoom.isE2EEEnabled ? 'Disable' : 'Enable'} E2EE`,
currentRoom.isE2EEEnabled,
);
}

async function acquireDeviceList() {
Expand Down
2 changes: 2 additions & 0 deletions example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
Expand All @@ -14,5 +15,6 @@
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["../src/**/*", "sample.ts"],
"exclude": ["**/*.test.ts", "build/**/*"]
}
22 changes: 16 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@
"unpkg": "./dist/livekit-client.umd.js",
"module": "./dist/livekit-client.esm.mjs",
"exports": {
"types": "./dist/src/index.d.ts",
"import": "./dist/livekit-client.esm.mjs",
"require": "./dist/livekit-client.umd.js"
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/livekit-client.esm.mjs",
"require": "./dist/livekit-client.umd.js"
},
"./e2ee-worker": {
"types": "./dist/src/e2ee/worker/e2ee.worker.d.ts",
"import": "./dist/livekit-client.e2ee.worker.mjs",
"require": "./dist/livekit-client.e2ee.worker.js"
}
},
"files": [
"dist",
Expand All @@ -18,19 +25,22 @@
"typesVersions": {
"<4.8": {
"./dist/src/index.d.ts": [
"./dist/src/ts4.2/index.d.ts"
"./dist/ts4.2/src/index.d.ts"
],
"./dist/src/e2ee/worker/e2ee.worker.d.ts": [
"./dist/ts4.2//dist/src/e2ee/worker/e2ee.worker.d.ts"
]
}
},
"repository": "git@github.com:livekit/client-sdk-js.git",
"author": "David Zhao <david@davidzhao.com>",
"license": "Apache-2.0",
"scripts": {
"build": "rollup --config --bundleConfigAsCjs && yarn downlevel-dts",
"build": "rollup --config --bundleConfigAsCjs && rollup --config rollup.config.worker.js --bundleConfigAsCjs && yarn downlevel-dts",
"build:watch": "rollup --watch --config rollup.config.js",
"build-docs": "typedoc",
"proto": "protoc --plugin=node_modules/ts-proto/protoc-gen-ts_proto --ts_proto_opt=esModuleInterop=true --ts_proto_out=./src/proto --ts_proto_opt=outputClientImpl=false,useOptionals=messages,oneof=unions -I./protocol ./protocol/livekit_rtc.proto ./protocol/livekit_models.proto",
"sample": "vite serve example --port 8080 --open",
"sample": "vite example -c vite.config.js",
"lint": "eslint src",
"test": "jest",
"deploy": "gh-pages -d example/dist",
Expand Down
33 changes: 22 additions & 11 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,37 @@ import replace from 'rollup-plugin-re';
import typescript from 'rollup-plugin-typescript2';
import packageJson from './package.json';

function kebabCaseToPascalCase(string = '') {
export function kebabCaseToPascalCase(string = '') {
return string.replace(/(^\w|-\w)/g, (replaceString) =>
replaceString.replace(/-/, '').toUpperCase(),
);
}

/**
* @type {import('rollup').InputPluginOption}
*/
export const commonPlugins = [
nodeResolve({ browser: true, preferBuiltins: false }),
commonjs(),
json(),
babel({
babelHelpers: 'bundled',
plugins: ['@babel/plugin-transform-object-rest-spread'],
presets: ['@babel/preset-env'],
extensions: ['.js', '.ts', '.mjs'],
babelrc: false,
}),
];

/**
* @type {import('rollup').RollupOptions}
*/
export default {
input: 'src/index.ts',
output: [
{
file: `dist/${packageJson.name}.esm.mjs`,
format: 'esm',
format: 'es',
strict: true,
sourcemap: true,
},
Expand All @@ -35,16 +54,8 @@ export default {
],
plugins: [
del({ targets: 'dist/*' }),
nodeResolve({ browser: true, preferBuiltins: false }),
json(),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
babel({
babelHelpers: 'bundled',
plugins: ['@babel/plugin-transform-object-rest-spread'],
presets: ['@babel/preset-env'],
extensions: ['.js', '.ts', '.mjs'],
}),
...commonPlugins,
replace({
patterns: [
{
Expand Down
25 changes: 25 additions & 0 deletions rollup.config.worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import terser from '@rollup/plugin-terser';
import typescript from 'rollup-plugin-typescript2';
import packageJson from './package.json';
import { commonPlugins, kebabCaseToPascalCase } from './rollup.config';

export default {
input: 'src/e2ee/worker/e2ee.worker.ts',
output: [
{
file: `dist/${packageJson.name}.e2ee.worker.mjs`,
format: 'es',
strict: true,
sourcemap: true,
},
{
file: `dist/${packageJson.name}.e2ee.worker.js`,
format: 'umd',
strict: true,
sourcemap: true,
name: kebabCaseToPascalCase(packageJson.name) + '.e2ee.worker',
plugins: [terser()],
},
],
plugins: [typescript({ tsconfig: './src/e2ee/worker/tsconfig.json' }), ...commonPlugins],
};
3 changes: 2 additions & 1 deletion src/api/SignalClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface SignalOptions {
publishOnly?: string;
adaptiveStream?: boolean;
maxRetries: number;
e2eeEnabled: boolean;
}

type SignalMessage = SignalRequest['message'];
Expand Down Expand Up @@ -402,7 +403,7 @@ export class SignalClient {
sendAddTrack(req: AddTrackRequest) {
return this.sendRequest({
$case: 'addTrack',
addTrack: AddTrackRequest.fromPartial(req),
addTrack: req,
});
}

Expand Down
1 change: 1 addition & 0 deletions src/connectionHelper/checks/turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class TURNCheck extends Checker {
const joinRes = await signalClient.join(this.url, this.token, {
autoSubscribe: true,
maxRetries: 0,
e2eeEnabled: false,
});

let hasTLS = false;
Expand Down
1 change: 1 addition & 0 deletions src/connectionHelper/checks/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class WebSocketCheck extends Checker {
const joinRes = await signalClient.join(this.url, this.token, {
autoSubscribe: true,
maxRetries: 0,
e2eeEnabled: false,
});
this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
await signalClient.close();
Expand Down
Loading

0 comments on commit e244d9c

Please sign in to comment.