Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Security & BYOK

This page documents the security model end-to-end: the BYOK promise, the local vault, handoff envelopes, authentication, request hardening and the bridge trust model.

New to the AI side? Read AI concepts from scratch first — agents act through tools on your machine/services, which is exactly why all of this consent, encryption and auditing exists.

“Your API keys are encrypted locally with AES-GCM and never travel to our servers.”

This is a contracted claim (visible in the product’s trust copy: “Tus API keys se cifran localmente con AES-GCM y jamás viajan a nuestros servidores”). The implementation is designed so the claim holds by construction:

  1. Keys live in an encrypted vault in your browser (synthhires-credentials + mirror), AES-256-GCM.
  2. For each request, the key is wrapped in a single-use, expiring E2EE handoff envelope and sent only for that request.
  3. The server unwraps, uses, and discards — the plaintext key is never stored or logged server-side.

Because agents have tools that act (see Runtimes) and because the platform is BYOK (the model calls run against your keys), the security model guards two distinct things:

  • Your keys never leave your device. The model interacts through a proxy that only ever receives a short-lived, single-use E2EE handoff envelope — your key is unwrapped for one request and dropped. This is not a bolt-on; it’s the contract the whole platform depends on.
  • Agents act under consent. An autonomous loop calling desktop.shell.execute, mobile.sms.send or an external API is genuinely powerful, so every capability is scoped, consent-gated, audited and revocable. The trust boundary is drawn before the model gets the tool, not after.

Both ideas — we never hold your keys plaintext and tools only run with explicit consent — are what make giving a model hands acceptable. See AI conceptsFunction/tool calling for why tools exist at all.

The local vault (src/lib/credentials.ts + kdf.ts)

Section titled “The local vault (src/lib/credentials.ts + kdf.ts)”
  • v1 (legacy): deriveLegacyV1Key() — device-local derivation. Still readable for backward compatibility.
  • v2 (hardened): deriveVaultKey(salt, VAULT_KDF_SECRET) — combines a device salt (getOrCreateVaultSalt()) with a server-provided secret fetched from the vault-secret endpoint. v2 blobs carry the v2: prefix.
  • Read-before-write: v2 writes only happen when the server secret is reachable this session; reads fall back to v1 so a transient secret-outage never “disconnects” BYOK.
  • Self-healing: if a blob fails with the active key, the client tries known historical/dev secrets; on success it flags migration and re-encrypts with the current key on the next save.
  • No silent wipes: unreadable blobs are preserved as raw entries rather than deleted, so a corrupt read can’t destroy data.

Per-provider opt-in (synthhires:vault:cloud-sync-opt-in) can mirror a key to /api/vault (server-side vault_entries). Without that explicit consent, no key is ever uploaded. migrateToServer only touches opted-in providers.

buildVaultHandoff(provider) (credentials.ts):

  1. Fetches the server’s E2EE public JWK (/api/handoff-pubkey).
  2. Wraps the plaintext key with the public key, stamps issuedAt.
  3. Returns { provider, ephemeralPublicJwk, encryptedBlob, issuedAt } — the envelope travels with the chat request.
  4. The server decrypts with its private key, uses the key for the provider call, and drops it. The envelope is single-use by design (ephemeral key per envelope).
  • Magic link + OTP codes (src/lib/auth.ts): email-based sign-in with verification codes (verification_codes table: expiry + attempt counter).
  • Sessions (sessions table): server-side session tokens with expiry; authStore restores from sessionStorage synchronously and reconciles with /api/auth/me in the background.
  • The auth gate: every API route begins with await requireResolvedAuth(cookies, request) (src/lib/auth-server.ts) — the single gate; one wrong line there breaks every route (it is on the do-not-edit list).
  • Guest/local mode: unauthenticated users can still use the platform locally; sessions are saved in the browser only.
  • CORS / rate limits / redaction live in src/lib/security.ts.
  • Log redaction: all sensitive logging goes through the single src/lib/log-redaction.ts sink — keys, tokens, and PII never reach logs in plaintext.
  • Error sanitization: stream errors are passed through safeErrorLog before surfacing.
  • Bounded retries: the retry handler applies max 3 attempts with explicit 429/5xx backoff — no infinite loops.

The desktop/mobile bridge (external Rust repo) connects outbound-only:

  • No inbound ports: 100% outbound WebSocket, TLS 1.3.
  • Scoped capabilities: desktop.shell.execute, desktop.fs.read, mobile.sms.send, … — each requires consent (consent_prompt/consent_response frames) and is audit-logged (device_action_log).
  • Destructive-command guard: the daemon filters destructive commands before execution.
  • Revocation: revoking a device kills its token (device_tokens) and closes the connection.
  • Pairing: pairing codes (pairing_codes) are single-use; the device proves possession of the code before receiving a token.
  • Never weaken the BYOK trust copy or the encryption flow.
  • Never store provider keys server-side outside vault_entries (encrypted at rest, opt-in only).
  • Never bypass requireResolvedAuth in a new API route.
  • Never log raw keys/tokens — route through log-redaction.ts.