db59e3f658
PIN-unlock used to freeze the renderer for ~1-2 s on mid-hardware while the moderate-preset Argon2id KDF + sealed-key secretbox open ran on the main thread. Push that work into a Vite-bundled ESM Web Worker so the unlock screen stays responsive. The worker (apps/desktop/src/workers/crypto.worker.ts) bundles its own libsodium-wrappers-sumo instance and registers a fresh CryptoBackend inside the worker realm. Client wrapper (apps/desktop/src/lib/cryptoWorker.ts) spawns a one-shot worker per unlock — workers are cheap, PIN-unlock is once-per-session, and one-shot avoids the request-id bookkeeping that the existing decrypt.worker needs for high-volume per-message decrypts. Falls back to inline main-thread openUserKey when the Worker constructor is unavailable (vitest's jsdom) or when worker spawn / round-trip fails (strict CSP). All 14 desktop + 71 shared tests still pass — the existing loadOrUnlockUserKey test exercises the inline-fallback branch. Private key bytes are transferred (zero-copy) back to the main thread, detaching the worker-side ArrayBuffer view on transfer. Build emits crypto.worker-<hash>.js (~2.5 MB, mostly libsodium WASM glue duplicated from the main bundle). Acceptable trade-off for the unblocked UI; a future change could lazy-load libsodium on the main thread to drop the duplication. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>