initial
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { ExternalE2EEKeyProvider } from 'livekit-client';
|
||||
// Vite-native Worker import. `?worker` triggers a dedicated build chunk
|
||||
// shipped as a classic/module worker. The default export is the Worker
|
||||
// constructor; we instantiate once per tab and reuse.
|
||||
import LivekitE2EEWorker from 'livekit-client/e2ee-worker?worker';
|
||||
|
||||
const STORAGE_KEY = 'chatapp.e2ee';
|
||||
|
||||
export interface CallE2EESettings {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// Default ON — the whole point of this project is zero-knowledge, so honour
|
||||
// that for the SFU path too. Can be turned off for debugging or if a user's
|
||||
// browser lacks RTCRtpScriptTransform / Insertable Streams support.
|
||||
const DEFAULTS: CallE2EESettings = { enabled: true };
|
||||
|
||||
type Listener = (s: CallE2EESettings) => void;
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
let cached: CallE2EESettings | null = null;
|
||||
|
||||
function read(): CallE2EESettings {
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
cached = DEFAULTS;
|
||||
return cached;
|
||||
}
|
||||
const parsed = JSON.parse(raw) as Partial<CallE2EESettings>;
|
||||
cached = {
|
||||
enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : DEFAULTS.enabled,
|
||||
};
|
||||
return cached;
|
||||
} catch {
|
||||
cached = DEFAULTS;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
function write(s: CallE2EESettings): void {
|
||||
cached = s;
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
|
||||
} catch {
|
||||
/* quota / private mode */
|
||||
}
|
||||
for (const l of listeners) l(s);
|
||||
}
|
||||
|
||||
export function getCallE2EESettings(): CallE2EESettings {
|
||||
return read();
|
||||
}
|
||||
|
||||
export function updateCallE2EESettings(patch: Partial<CallE2EESettings>): CallE2EESettings {
|
||||
const next = { ...read(), ...patch };
|
||||
write(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function subscribeCallE2EESettings(listener: Listener): () => void {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
// Shared single Worker instance — LiveKit supports reusing it across rooms.
|
||||
let workerInstance: Worker | null = null;
|
||||
function getWorker(): Worker {
|
||||
if (!workerInstance) {
|
||||
workerInstance = new LivekitE2EEWorker();
|
||||
}
|
||||
return workerInstance;
|
||||
}
|
||||
|
||||
// Feature detection — Insertable Streams (RTCRtpScriptTransform or the older
|
||||
// encodedStreams API) is required for LiveKit E2EE. Returns false on browsers
|
||||
// that can't encrypt the media path (the caller then skips the `e2ee` option).
|
||||
export function isE2EESupported(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const hasScriptTransform =
|
||||
typeof (window as unknown as { RTCRtpScriptTransform?: unknown }).RTCRtpScriptTransform !==
|
||||
'undefined';
|
||||
const sender = (window as unknown as { RTCRtpSender?: { prototype?: unknown } }).RTCRtpSender;
|
||||
const hasEncodedStreams =
|
||||
!!sender?.prototype &&
|
||||
'createEncodedStreams' in (sender.prototype as Record<string, unknown>);
|
||||
return hasScriptTransform || hasEncodedStreams;
|
||||
}
|
||||
|
||||
interface E2EEBundle {
|
||||
keyProvider: ExternalE2EEKeyProvider;
|
||||
worker: Worker;
|
||||
}
|
||||
|
||||
// Derives a stable per-conversation passphrase entirely client-side. The
|
||||
// passphrase is never transmitted anywhere; each member computes it locally
|
||||
// from the conversation id they already hold via RLS-protected Supabase data.
|
||||
// A stronger variant would ship a random per-conversation secret through the
|
||||
// existing E2E envelope system — deferred to M3.
|
||||
export async function createCallE2EE(conversationId: string): Promise<E2EEBundle> {
|
||||
const keyProvider = new ExternalE2EEKeyProvider();
|
||||
const salt = 'chat-app-voice-e2ee-v1';
|
||||
const enc = new TextEncoder();
|
||||
const buf = await crypto.subtle.digest('SHA-256', enc.encode(salt + ':' + conversationId));
|
||||
const hex = Array.from(new Uint8Array(buf))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
await keyProvider.setKey(hex);
|
||||
return { keyProvider, worker: getWorker() };
|
||||
}
|
||||
Reference in New Issue
Block a user