π‘οΈ Security
How it works
π clientkeys encrypts your secret in your browser before it ever leaves your device. The encryption key is placed in the URL fragment (the part after #), which browsers never send to servers. This means the server β and we, the operators β cannot read your secret.
What we protect
- Encryption in transit: All communication uses HTTPS (TLS). The secret is encrypted with AES-256-GCM before it leaves your browser.
- One-time access: Each encrypted secret is atomically deleted from our database the moment it is retrieved. It cannot be read twice.
- Zero server knowledge: We never see the plaintext or the encryption key. We see only encrypted ciphertext and metadata (creation time, view time).
- No tracking: This service contains no analytics, no tracking scripts, and no third-party code on pages that handle secrets.
What we do not protect
- Compromised endpoints: If your device or the recipient's device has malware (keyloggers, malicious browser extensions), the secret can be intercepted before encryption or after decryption.
- URL interception: After the recipient opens the link, the plaintext is displayed in their browser. They can screenshot, copy, or forward it. The URL itself (if intercepted in transit or from the recipient's browser history) can be used to retrieve the secret β once.
- Metadata: We can see that a secret was created, when, and when it was viewed. We do not log IP addresses, but our infrastructure provider may retain access logs briefly.
- Ongoing communication: clientkeys is a tool for one-shot credential handoffs. It is not a replacement for end-to-end encrypted messaging for ongoing conversations.
Technical details
- Encryption: AES-256-GCM via the browser Web Crypto API
- Key transport: URL fragment (never transmitted to server)
- Ciphertext storage: Upstash Redis with automatic TTL expiration
- Hosting: Vercel with strict Content Security Policy
- CSP disclosure: Our
script-srcuses per-request nonces to block inline and third-party JavaScript. Because Tailwind CSS relies on inlinestyleattributes that CSP nonces cannot protect, ourstyle-srcincludes'unsafe-inline'. This is a pragmatic tradeoff: CSS-based attacks are far more limited than script-based attacks, require a separate injection vector that our script policy already blocks, and cannot execute code or steal cookies. We disclose this because security claims should be precise, not absolute. - No analytics scripts on secret-handling pages
Registered accounts
Sending and receiving secrets is anonymous β no account is required and never will be. Accounts exist solely for registered users who want a dashboard to manage their receipts.
- Authentication: We use Google OAuth (βSign in with Googleβ) exclusively for account management. Google confirms your identity and provides your name, email address, and profile picture. We do not receive access to your Google data, and we never share anything back to Google.
- What we store: Your name, email, and avatar URL from your Google profile. A unique identifier from Google, used only to recognize you on return visits. This data is stored in a Neon Postgres database and is never shared with third parties. If you upload a logo, it is stored on Vercel Blob with a public URL β anyone with the URL can view the image, so use a non-sensitive company logo or profile picture.
- What stays separate: Your account identity is stored separately from the encrypted secrets you create. The encryption key still lives only in the URL fragment and is never transmitted to us or to Google.
Paid accounts
Paid accounts unlock branded secret sharing, custom time-to-live, and the ability to request secrets from others. A free trial is coming soon.
Secret requests
A secret request is the inverse of sending. Instead of encrypting and giving someone the key, you give someone a public key so they can encrypt and only you can decrypt.
Creating a request generates an ECDH P-256 keypair in your browser. The public key rides in the URL fragment, unreachable by the server. The private key stays in IndexedDB, local to that device.
When the recipient responds, their browser generates an ephemeral keypair, derives a 256-bit shared secret (ECDH), and encrypts with AES-256-GCM. The ciphertext and ephemeral public key go to Redis β the server stores opaque bytes it cannot read.
When you click decrypt, your browser retrieves the private key from IndexedDB, fetches the ciphertext and ephemeral public key, recovers the shared secret, and decrypts.
Each request accepts one response. After that, the endpoint returns 404, identical to an expired request. The private key is device-local. Clearing your browser or losing the device makes outstanding responses permanently unreadable.
Email delivery
Pro users will be able to send secret links and request links directly via email. To include the full URL in the email body, the server must briefly handle the fragment that contains the decryption key. The key is held in memory only β never written to disk, never logged, discarded immediately after the Mailgun API call completes.
Email delivery is optional. You can always copy and share the link manually if you prefer the server to never handle the key at all.
Report a vulnerability
Email security@clientkeys.me. PGP key available on request.