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.v2and 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-keyheader, 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 auditis 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 strictPermissions-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
Optional key and history encryption (AES-256-GCM), auto-lock, storage mode
A device recovered while unlocked exposes the keys, and history encryption is opt-in.
Cloud backup or browser sync leak
With encryption, only ciphertext reaches localStorage. The passphrase is not stored (sessionStorage only if you opt in)
Weak passphrases can be brute-forced offline. 600,000 PBKDF2 iterations raise the cost; they do not guarantee safety.
Shared computer
Three storage modes (browser / tab session / memory), manual lock, auto-lock
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
Nothing a web page can do
It can read unlocked keys, conversations, the clipboard and requests.
XSS (markdown, imports, third-party scripts)
Markdown without raw HTML, link rel restrictions, import validation, CSP, no scripts beyond AdSense
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)
Exact version pins, minimal runtime dependencies, npm audit
A compromised package or native binary running at build time cannot be stopped.
Man-in-the-middle (MITM)
HTTPS-only deployment plus HSTS. Keys travel in headers, not URLs
A network with a trusted certificate can still intercept. Provider logs are outside our control.
Provider-side logging and retention
Requests go only to the provider you selected
The provider's logs, retention and policy are outside KeyChat's control.
Lost passphrase
Explicit warning in the UI. The only recovery is deleting keys and re-entering them
The ciphertext cannot be recovered. This is intentional.
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
Security setup worth five minutes
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
_headersandvercel.json.Back up chats yourself
There is no cross-device sync. Export important chats as JSON and delete history on shared machines.
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
Read next
Docs that pair with this page
The data policy, the key setup process and where the security settings actually live.
KeyChat
AI character chat on your own API key. No chat fee — funded by ads and optional purchases.