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.
The BYOK promise
Section titled “The BYOK promise”“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:
- Keys live in an encrypted vault in your browser (
synthhires-credentials+ mirror), AES-256-GCM. - For each request, the key is wrapped in a single-use, expiring E2EE handoff envelope and sent only for that request.
- The server unwraps, uses, and discards — the plaintext key is never stored or logged server-side.
The concepts behind security
Section titled “The concepts behind security”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.sendor 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 concepts → Function/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)”Key derivation
Section titled “Key derivation”- 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 thev2:prefix.
Resilience
Section titled “Resilience”- 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.
Cloud sync (explicit opt-in only)
Section titled “Cloud sync (explicit opt-in only)”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.
Handoff envelopes
Section titled “Handoff envelopes”buildVaultHandoff(provider) (credentials.ts):
- Fetches the server’s E2EE public JWK (
/api/handoff-pubkey). - Wraps the plaintext key with the public key, stamps
issuedAt. - Returns
{ provider, ephemeralPublicJwk, encryptedBlob, issuedAt }— the envelope travels with the chat request. - 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).
Authentication
Section titled “Authentication”- Magic link + OTP codes (
src/lib/auth.ts): email-based sign-in with verification codes (verification_codestable: expiry + attempt counter). - Sessions (
sessionstable): server-side session tokens with expiry;authStorerestores fromsessionStoragesynchronously and reconciles with/api/auth/mein 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.
Request hardening
Section titled “Request hardening”- CORS / rate limits / redaction live in
src/lib/security.ts. - Log redaction: all sensitive logging goes through the single
src/lib/log-redaction.tssink — keys, tokens, and PII never reach logs in plaintext. - Error sanitization: stream errors are passed through
safeErrorLogbefore surfacing. - Bounded retries: the retry handler applies max 3 attempts with explicit 429/5xx backoff — no infinite loops.
Bridge trust model
Section titled “Bridge trust model”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.
Do-not-regress checklist
Section titled “Do-not-regress checklist”- 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
requireResolvedAuthin a new API route. - Never log raw keys/tokens — route through
log-redaction.ts.