perf(P6B.T6): offload Argon2 + userKey unseal to Web Worker
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>
This commit is contained in:
@@ -4,10 +4,11 @@ import {
|
||||
} from '@chat-app/shared/auth';
|
||||
import {
|
||||
generateRecoveryCode, generateUserKeyPair, getCryptoBackend, normalizeRecoveryCode,
|
||||
openUserKey, sealUserKey,
|
||||
sealUserKey,
|
||||
} from '@chat-app/shared/crypto';
|
||||
import { migrateOwnLegacyBundles } from '@chat-app/shared/chat';
|
||||
|
||||
import { openUserKeyMaybeWorker } from './cryptoWorker';
|
||||
import { devLocalSecretStore } from './secretStore';
|
||||
import { supabase } from './supabase';
|
||||
|
||||
@@ -71,7 +72,7 @@ export async function loadOrUnlockUserKey(p: UnlockParams): Promise<UnlockOutcom
|
||||
if (!sealed || !salt) throw new Error('no recovery blob configured');
|
||||
let priv: Uint8Array;
|
||||
try {
|
||||
priv = await openUserKey({ sealed, pin: secret, salt, kdfParams: remote.kdfParams });
|
||||
priv = await openUserKeyMaybeWorker({ sealed, pin: secret, salt, kdfParams: remote.kdfParams });
|
||||
} catch (err) {
|
||||
await recordPinAttempt(supabase, p.userId, false, p.isRecoveryCode === true).catch(() => {});
|
||||
throw err;
|
||||
|
||||
Reference in New Issue
Block a user