Skip to content

Security model

What we protect, and what we cannot

This page is a threat model, not a security pitch. It lists what is mitigated and what stays exposed. Nothing here is a guarantee we have not verified.

At a glance

  • Server

    None — no backend accepts keys

  • API keys

    Optional AES-256-GCM encryption + auto-lock

  • Chat history

    Local (IndexedDB) · optional encryption

  • Third-party scripts

    AdSense — content pages only

What we protect

Only protections that actually ship

Every item below exists in this repository's code or deployment config, with the setting that turns it on.

  • Key encryption + auto-lock

    With a passphrase, API keys are encrypted using PBKDF2-SHA256 (600,000 iterations) → AES-256-GCM. The ciphertext is stored under keychat.apiKeys.v2 and the plaintext key exists in memory only while unlocked. The same passphrase can also encrypt chat history, and locking removes the decrypted chats from memory. Auto-lock can use 5/15/30/60 minutes, lock on tab switch, or never; you can also lock manually from the sidebar or Security tab.

  • No server, no account

    There is no server that accepts keys. No signup, no login, no server-side chat logs and no user database. Breaching KeyChat yields no keys, because there is nothing server-side to breach — just static files and ad slots.

  • Browser straight to the provider

    Requests go from this browser directly to the provider API. Gemini keys are sent in the x-goog-api-key header, not in the URL. Keys never appear in a URL, query string, Referer or browser history.

  • Rendering and import hardening

    Markdown renders without raw HTML, and links inside answers get noopener noreferrer nofollow. Imported JSON is schema-validated and size-capped, so a crafted file cannot freeze the tab.

  • Pinned dependencies and input limits

    There are only 7 runtime dependencies, all pinned to exact versions (no ^ or ~). npm audit is checked regularly. Input is capped at 32,000 characters per message, 8,000 for the system prompt, and 5 MB / 100 conversations for imports — one paste cannot fill browser storage.

  • Deployment hardening

    CSP (meta + host headers), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, a strict Permissions-Policy (camera, microphone, geolocation, payment off), COOP/CORP and HSTS ship with the deployment config. The site cannot be framed, and object/form submissions are blocked.

Input limits (as defined in code)

  • 32,000 chars per message
  • 8,000 chars system prompt
  • Import 5 MB · 100 conversations
  • Up to 200 saved chats

What we cannot protect

Knowing the limits is part of safety

These are structural limits, not missing features. If one applies to you, combine KeyChat with an appropriate control.

  • An unlocked key lives in memory

    Encryption protects keys at rest. While unlocked, the key is plain text in page memory and a successful XSS can read it. CSP is defence-in-depth only: 'unsafe-inline' is required by Next.js and AdSense, so it is not a complete XSS defence.

  • Malicious browser extensions

    Extensions can read the DOM, network requests and the clipboard. A web page cannot stop them. Keep extensions minimal and use a clean browser profile when handling keys.

  • Chat history is plain text by default

    Conversations stay on this device's IndexedDB, but they are plain text by default. After enabling the security lock, turn on 'Encrypt chat history' to encrypt them with the same passphrase and reduce exposure through backups or a shared PC. If you forget the passphrase, both keys and chats are unrecoverable.

  • Local malware and OS accounts

    Anyone, or anything, with access to your OS account can read browser storage. OS account locks, disk encryption and updates matter more here than any web app control.

  • Provider-side logs and policies

    What happens after a request arrives is up to the provider. Logging, retention and training policies at Anthropic, OpenAI, Google and OpenRouter are outside KeyChat's control. Check them before sending sensitive content.

  • Shared devices raise the risk

    Browser data is visible to the same OS user. On a shared PC, use 'Until this tab closes' or 'Forget on reload', lock immediately after use and delete saved keys.

A forgotten passphrase is unrecoverable

Because the passphrase is never stored, neither you nor KeyChat can recover the encrypted keys. If it is lost, delete the saved keys and re-enter your API keys. That is by design, not a bug.

Threat model

Threat, mitigation and residual risk

For each threat: what mitigates it and what still remains. No row has an empty residual risk.

  • Lost or stolen device

    Mitigation
    Optional key and history encryption (AES-256-GCM), auto-lock, storage mode
    Residual risk
    A device recovered while unlocked exposes the keys, and history encryption is opt-in.
  • Cloud backup or browser sync leak

    Mitigation
    With encryption, only ciphertext reaches localStorage. The passphrase is not stored (sessionStorage only if you opt in)
    Residual risk
    Weak passphrases can be brute-forced offline. 600,000 PBKDF2 iterations raise the cost; they do not guarantee safety.
  • Shared computer

    Mitigation
    Three storage modes (browser / tab session / memory), manual lock, auto-lock
    Residual risk
    The next user on the same OS account can see an unlocked vault, and chat history too unless history encryption is enabled.
  • Malicious browser extension

    Mitigation
    Nothing a web page can do
    Residual risk
    It can read unlocked keys, conversations, the clipboard and requests.
  • XSS (markdown, imports, third-party scripts)

    Mitigation
    Markdown without raw HTML, link rel restrictions, import validation, CSP, no scripts beyond AdSense
    Residual risk
    A successful injection can read keys and chats from memory. CSP needs 'unsafe-inline', and connect-src is broad to allow custom endpoints.
  • Supply chain (dependency compromise)

    Mitigation
    Exact version pins, minimal runtime dependencies, npm audit
    Residual risk
    A compromised package or native binary running at build time cannot be stopped.
  • Man-in-the-middle (MITM)

    Mitigation
    HTTPS-only deployment plus HSTS. Keys travel in headers, not URLs
    Residual risk
    A network with a trusted certificate can still intercept. Provider logs are outside our control.
  • Provider-side logging and retention

    Mitigation
    Requests go only to the provider you selected
    Residual risk
    The provider's logs, retention and policy are outside KeyChat's control.
  • Lost passphrase

    Mitigation
    Explicit warning in the UI. The only recovery is deleting keys and re-entering them
    Residual risk
    The ciphertext cannot be recovered. This is intentional.

This table reflects the implementation in this repository. If the implementation changes, this table must change with it.

Recommended setup

You do not need all of these. If your environment is higher risk, start from the top.

  • A passphrase of 12+ characters

    The app requires 12+ characters; mixing case, digits and symbols is better. Length raises the cost of offline guessing.

  • Auto-lock at 15 minutes

    15 minutes is a good default. Drop to 5 if you leave your desk often or work on the move.

  • Shared PC: session or memory mode

    Pick 'Until this tab closes' or 'Forget on reload', lock immediately after use, then delete saved keys.

  • No scripts beyond AdSense

    Even if you fork the site, do not add analytics, chat widgets or font CDNs. Every script runs in the same origin as the unlocked key.

  • Pin the HTTPS domain

    Do not allow HTTP, and enable HSTS. It is already in the provided _headers and vercel.json.

  • Back up chats yourself

    There is no cross-device sync. Export important chats as JSON and delete history on shared machines.

Apply these in chat settings

Report a vulnerability

Tell us if you find something

For security issues, email is faster than a public issue. Include reproduction steps, impact and the browser you used.

Scope

The static app, its build output and the deployment configuration (headers, CSP, redirects). Issues in the Anthropic, OpenAI, Google or OpenRouter APIs belong with those providers.

No paid bounty

This is an ad-funded personal project, so there is no bounty. We will credit you in the fix notes if you want, using the name you prefer.

Disclosure

Please do not disclose before a fix. For reproducible reports we ask for a 90-day fix window; after that we agree on a publication date together.

Report a vulnerability

Contact
Email (button below)
Response target
Acknowledge within 72 hours
Fix target
Valid reports fixed or mitigated within 90 days
Machine readable
/.well-known/security.txt
Send a report/.well-known/security.txt

[email protected]

Read next

The data policy, the key setup process and where the security settings actually live.

Ad

KeyChat

AI character chat on your own API key. No chat fee — funded by ads and optional purchases.