// 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);
}
})();
});