Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Web Worker key isolation

Run Ownfold cryptographic operations behind a dedicated worker while keeping only opaque root-key handles on the UI thread.

@ownfold/browser can run its complete CryptoEngine behind a dedicated Web Worker. Root-key bytes are created, opened, compared, used, and destroyed inside the worker. The UI thread receives only an opaque RootKeyHandle; it cannot export key bytes through Ownfold’s public API.

Create a worker entry owned and bundled by the host application:

import { exposeOwnfoldCryptoWorker } from "@ownfold/browser/worker"

exposeOwnfoldCryptoWorker()

Pass that worker to the browser client:

import { createVaultClient, createWorkerCryptoEngine } from "@ownfold/browser"

const worker = new Worker(new URL("./ownfold.worker.ts", import.meta.url), {
  name: "ownfold-crypto",
  type: "module",
})

const cryptoEngine = createWorkerCryptoEngine({
  worker,
  operationTimeoutMs: 60_000,
})

export const vault = createVaultClient({
  transport,
  cryptoEngine,
})

Vite and other worker-aware bundlers compile the new URL(..., import.meta.url) entry as a separate asset. Confirm equivalent behavior in the host build tool instead of copying a generated worker filename into application code. Serve the worker from the application origin and allow it with an explicit worker-src 'self' Content Security Policy directive.

The operation timeout defaults to 60 seconds and must be a positive safe integer. Size it above the slowest tested Recovery Kit KDF operation on supported devices. A timeout is terminal for that engine instance because Ownfold cannot prove the state of an unresponsive worker.

Lifecycle and failures

Locking, local-state destruction, device revocation, and VaultClient.dispose() send root-key destruction to the worker. At final application teardown, call cryptoEngine.dispose() after disposing every client that uses it; the default behavior terminates the worker and fails pending operations with CRYPTO_WORKER_DISPOSED. A worker belongs to one engine and is not shared.

Malformed messages are rejected at the worker boundary. Worker crashes, message-cloning failures, invalid responses, and operation timeouts become typed CryptoEngineError values. Each of these is terminal for that engine: Ownfold terminates the worker, settles every pending operation, rejects later operations without posting them, and never falls back to main-thread cryptography. Create a new worker and unlock again before retrying. The adversarial suite verifies crash fan-out, timeout, malformed-response, post-failure, disposal, and late-event behavior through the exported narrow CryptoWorkerPort contract. Previously persisted ciphertext and local encrypted key material remain unchanged.

Ownfold copies caller-owned plaintext and recovery-secret buffers before transferring those copies, so application buffers are not detached. Decrypted plaintext necessarily returns to the UI thread for rendering. Non-extractable device-key material is structured-cloned between IndexedDB, the UI thread, and the worker when opening a device envelope.

Security boundary

Worker isolation reduces accidental root-key exposure from UI state, debugging tools, and ordinary main-thread code. It does not protect against malicious same-origin JavaScript: such code can invoke the unlocked client, intercept plaintext before encryption or after decryption, replace the worker bundle, or alter the application deployment. It also does not protect against a compromised device, browser extension, or operating system.

Use worker isolation together with automatic locking, a strict CSP, Trusted Types where supported, minimal third-party JavaScript, dependency pinning, and plaintext-free observability. Review the dedicated-worker ADR, threat model, and host security guidance before deployment.

Last updated on August 4, 2026

Was this page helpful?