// Web Worker — runs Argon2id pwhash + sealed user-key open off the main // thread. PIN-unlock used to freeze the UI for ~1-2 s on mid-hardware while // the moderate-preset KDF ran; pushing it here keeps the unlock screen // responsive. // // The worker bundles its own libsodium-wrappers-sumo instance and registers // it as the shared CryptoBackend inside this worker realm — there is no // shared state with the main thread, so we initialise once per worker and // re-use it across messages (the client wrapper currently spawns one-shot, // but the worker is safe to keep alive too). // // Message protocol (one-shot RPC): // request: { op: 'openUserKey', input: OpenUserKeyInput } // response: { ok: true, result: { privateKey: Uint8Array } } // | { ok: false, error: string } // // The private key bytes are transferred (zero-copy) back to the caller via // the structured-clone Transferable list; the worker's view of the buffer is // detached on transfer which also clears the only worker-side reference. /// import { setCryptoBackend, openUserKey } from '@chat-app/shared/crypto'; import type { KdfParams } from '@chat-app/shared/crypto'; import sodium from 'libsodium-wrappers-sumo'; import { createLibsodiumBackend } from '../lib/cryptoBackend'; export interface OpenUserKeyInput { sealed: Uint8Array; pin: string; salt: Uint8Array; kdfParams: KdfParams; } export interface OpenUserKeyResult { privateKey: Uint8Array; } type WorkerRequest = { op: 'openUserKey'; input: OpenUserKeyInput }; type WorkerResponse = | { ok: true; result: OpenUserKeyResult } | { ok: false; error: string }; let backendReady: Promise | null = null; async function ensureBackend(): Promise { if (!backendReady) { backendReady = (async () => { await sodium.ready; setCryptoBackend(await createLibsodiumBackend()); })(); } return backendReady; } self.addEventListener('message', (ev: MessageEvent) => { const msg = ev.data; void (async () => { try { if (!msg || msg.op !== 'openUserKey') { throw new Error('unknown op: ' + String((msg as { op?: unknown })?.op)); } await ensureBackend(); const privateKey = await openUserKey(msg.input); const response: WorkerResponse = { ok: true, result: { privateKey } }; const transfers: Transferable[] = []; if (privateKey?.buffer instanceof ArrayBuffer) { transfers.push(privateKey.buffer); } (self as unknown as Worker).postMessage(response, transfers); } catch (err) { const message = err instanceof Error ? err.message : String(err); const response: WorkerResponse = { ok: false, error: message }; (self as unknown as Worker).postMessage(response); } })(); });