Add e2ee option (#106)

* Add checkbox for e2ee

* add also to custom tab

* fix SSR
This commit is contained in:
lukasIO 2023-08-30 11:25:22 +02:00 committed by GitHub
parent 9a4fb5ddfd
commit 041d41a5fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 130 additions and 10 deletions

16
lib/DummyKeyProvider.ts Normal file
View file

@ -0,0 +1,16 @@
// just for demonstration purposes, extremely insecure
import { BaseKeyProvider, createKeyMaterialFromString } from 'livekit-client';
export class DummyKeyProvider extends BaseKeyProvider {
readonly participantKeys = new Map([
['dev1', 'dev1key'],
['dev2', 'dev2key'],
]);
async setKey(participantId: string) {
// @ts-ignore
const cryptoKey = await createKeyMaterialFromString(this.participantKeys.get(participantId));
this.onSetEncryptionKey(cryptoKey, participantId);
}
}

View file

@ -19,3 +19,14 @@ export function useServerUrl(region?: string) {
}); });
return serverUrl; return serverUrl;
} }
export function encodePassphrase(bytes: Uint8Array) {
const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join('');
return btoa(binString);
}
export function decodePassphrase(base64String: string) {
const binString = atob(base64String);
// @ts-ignore
return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

View file

@ -1,11 +1,43 @@
import { formatChatMessageLinks, LiveKitRoom, VideoConference } from '@livekit/components-react'; import { formatChatMessageLinks, LiveKitRoom, VideoConference } from '@livekit/components-react';
import { LogLevel } from 'livekit-client'; import { ExternalE2EEKeyProvider, LogLevel, Room, RoomOptions, VideoPresets } from 'livekit-client';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { DebugMode } from '../../lib/Debug'; import { DebugMode } from '../../lib/Debug';
import { decodePassphrase } from '../../lib/client-utils';
import { useMemo } from 'react';
export default function CustomRoomConnection() { export default function CustomRoomConnection() {
const router = useRouter(); const router = useRouter();
const { liveKitUrl, token } = router.query; const { liveKitUrl, token } = router.query;
const hash = typeof window !== 'undefined' && window.location.hash;
const keyProvider = new ExternalE2EEKeyProvider();
if (hash) {
keyProvider.setKey(decodePassphrase(hash.substring(1)));
}
const worker =
typeof window !== 'undefined' &&
new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
const roomOptions = useMemo((): RoomOptions => {
return {
publishDefaults: {
videoSimulcastLayers: [VideoPresets.h540, VideoPresets.h216],
},
adaptiveStream: { pixelDensity: 'screen' },
dynacast: true,
e2ee:
hash && worker
? {
keyProvider,
worker,
}
: undefined,
};
}, []);
const room = useMemo(() => new Room(roomOptions), []);
if (typeof liveKitUrl !== 'string') { if (typeof liveKitUrl !== 'string') {
return <h2>Missing LiveKit URL</h2>; return <h2>Missing LiveKit URL</h2>;
} }
@ -16,7 +48,7 @@ export default function CustomRoomConnection() {
return ( return (
<main data-lk-theme="default"> <main data-lk-theme="default">
{liveKitUrl && ( {liveKitUrl && (
<LiveKitRoom token={token} serverUrl={liveKitUrl} audio={true} video={true}> <LiveKitRoom room={room} token={token} serverUrl={liveKitUrl} audio={true} video={true}>
<VideoConference chatMessageFormatter={formatChatMessageLinks} /> <VideoConference chatMessageFormatter={formatChatMessageLinks} />
<DebugMode logLevel={LogLevel.info} /> <DebugMode logLevel={LogLevel.info} />
</LiveKitRoom> </LiveKitRoom>

View file

@ -1,7 +1,8 @@
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { ReactElement } from 'react'; import React, { ReactElement, useState } from 'react';
import styles from '../styles/Home.module.css'; import styles from '../styles/Home.module.css';
import { encodePassphrase } from '../lib/client-utils';
interface TabsProps { interface TabsProps {
children: ReactElement[]; children: ReactElement[];
@ -38,13 +39,28 @@ function Tabs(props: TabsProps) {
function DemoMeetingTab({ label }: { label: string }) { function DemoMeetingTab({ label }: { label: string }) {
const router = useRouter(); const router = useRouter();
const [e2ee, setE2ee] = useState(false);
const startMeeting = () => { const startMeeting = () => {
if (e2ee) {
const phrase = encodePassphrase(crypto.getRandomValues(new Uint8Array(256)));
router.push(`/rooms/${generateRoomId()}#${phrase}`);
} else {
router.push(`/rooms/${generateRoomId()}`); router.push(`/rooms/${generateRoomId()}`);
}
}; };
return ( return (
<div className={styles.tabContent}> <div className={styles.tabContent}>
<p style={{ marginTop: 0 }}>Try LiveKit Meet for free with our live demo project.</p> <p style={{ margin: 0 }}>Try LiveKit Meet for free with our live demo project.</p>
<button className="lk-button" onClick={startMeeting}> <div style={{ display: 'flex', gap: '1rem' }}>
<input
id="use-e2ee"
type="checkbox"
checked={e2ee}
onChange={(ev) => setE2ee(ev.target.checked)}
></input>
<label htmlFor="use-e2ee">Enable end-to-end encryption</label>
</div>
<button style={{ marginTop: '1rem' }} className="lk-button" onClick={startMeeting}>
Start Meeting Start Meeting
</button> </button>
</div> </div>
@ -53,12 +69,19 @@ function DemoMeetingTab({ label }: { label: string }) {
function CustomConnectionTab({ label }: { label: string }) { function CustomConnectionTab({ label }: { label: string }) {
const router = useRouter(); const router = useRouter();
const [e2ee, setE2ee] = useState(false);
const onSubmit: React.FormEventHandler<HTMLFormElement> = (event) => { const onSubmit: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault(); event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement); const formData = new FormData(event.target as HTMLFormElement);
const serverUrl = formData.get('serverUrl'); const serverUrl = formData.get('serverUrl');
const token = formData.get('token'); const token = formData.get('token');
if (e2ee) {
const passphrase = encodePassphrase(crypto.getRandomValues(new Uint8Array(256)));
router.push(`/custom/?liveKitUrl=${serverUrl}&token=${token}#${passphrase}`);
} else {
router.push(`/custom/?liveKitUrl=${serverUrl}&token=${token}`); router.push(`/custom/?liveKitUrl=${serverUrl}&token=${token}`);
}
}; };
return ( return (
<form className={styles.tabContent} onSubmit={onSubmit}> <form className={styles.tabContent} onSubmit={onSubmit}>
@ -80,6 +103,15 @@ function CustomConnectionTab({ label }: { label: string }) {
rows={9} rows={9}
style={{ padding: '1px 2px', fontSize: 'inherit', lineHeight: 'inherit' }} style={{ padding: '1px 2px', fontSize: 'inherit', lineHeight: 'inherit' }}
/> />
<div style={{ display: 'flex', gap: '1rem' }}>
<input
id="use-e2ee"
type="checkbox"
checked={e2ee}
onChange={(ev) => setE2ee(ev.target.checked)}
></input>
<label htmlFor="use-e2ee">Enable end-to-end encryption</label>
</div>
<hr <hr
style={{ width: '100%', borderColor: 'rgba(255, 255, 255, 0.15)', marginBlock: '1rem' }} style={{ width: '100%', borderColor: 'rgba(255, 255, 255, 0.15)', marginBlock: '1rem' }}
/> />

View file

@ -6,14 +6,22 @@ import {
VideoConference, VideoConference,
formatChatMessageLinks, formatChatMessageLinks,
} from '@livekit/components-react'; } from '@livekit/components-react';
import { LogLevel, RoomConnectOptions, RoomOptions, VideoPresets } from 'livekit-client'; import {
ExternalE2EEKeyProvider,
LogLevel,
Room,
RoomConnectOptions,
RoomOptions,
VideoPresets,
} from 'livekit-client';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { DebugMode } from '../../lib/Debug'; import { DebugMode } from '../../lib/Debug';
import { useServerUrl } from '../../lib/client-utils'; import { decodePassphrase, useServerUrl } from '../../lib/client-utils';
import { DummyKeyProvider } from '../../lib/DummyKeyProvider';
const Home: NextPage = () => { const Home: NextPage = () => {
const router = useRouter(); const router = useRouter();
@ -78,6 +86,16 @@ const ActiveRoom = ({ roomName, userChoices, onLeave }: ActiveRoomProps) => {
const liveKitUrl = useServerUrl(region as string | undefined); const liveKitUrl = useServerUrl(region as string | undefined);
const hash = typeof window !== 'undefined' && window.location.hash;
const keyProvider = new ExternalE2EEKeyProvider();
if (hash) {
keyProvider.setKey(decodePassphrase(hash.substring(1)));
}
const worker =
typeof window !== 'undefined' &&
new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
const roomOptions = useMemo((): RoomOptions => { const roomOptions = useMemo((): RoomOptions => {
return { return {
videoCaptureDefaults: { videoCaptureDefaults: {
@ -95,9 +113,20 @@ const ActiveRoom = ({ roomName, userChoices, onLeave }: ActiveRoomProps) => {
}, },
adaptiveStream: { pixelDensity: 'screen' }, adaptiveStream: { pixelDensity: 'screen' },
dynacast: true, dynacast: true,
e2ee:
hash && worker
? {
keyProvider,
worker,
}
: undefined,
}; };
}, [userChoices, hq]); }, [userChoices, hq]);
const room = useMemo(() => new Room(roomOptions), []);
room.setE2EEEnabled(true);
const connectOptions = useMemo((): RoomConnectOptions => { const connectOptions = useMemo((): RoomConnectOptions => {
return { return {
autoSubscribe: false, autoSubscribe: false,
@ -108,9 +137,9 @@ const ActiveRoom = ({ roomName, userChoices, onLeave }: ActiveRoomProps) => {
<> <>
{liveKitUrl && ( {liveKitUrl && (
<LiveKitRoom <LiveKitRoom
room={room}
token={token} token={token}
serverUrl={liveKitUrl} serverUrl={liveKitUrl}
options={roomOptions}
connectOptions={connectOptions} connectOptions={connectOptions}
video={userChoices.videoEnabled} video={userChoices.videoEnabled}
audio={userChoices.audioEnabled} audio={userChoices.audioEnabled}