A few months ago I got annoyed by the same problem every "anonymous feedback" tool has: the server can read everything. The admin promises not to look, but the database is sitting right there in plaintext. If it leaks, gets subpoenaed, or an employee with DB access gets curious, the "anonymous" part was never really true.

So I built SecretGate - a self-hosted inbox where encryption happens entirely in the visitor's browser before anything touches the server. The server only ever stores ciphertext.

How it actually works

  • Each inbox owner generates an RSA-OAEP keypair in their browser. The private key never leaves the device unencrypted.
  • Senders' browsers generate a one-time AES-256-GCM key, encrypt the message with it, then wrap that key with the owner's RSA public key.
  • The server stores: encrypted_content, the owner's public key, and a password hash (Argon2id) for the owner's auth token. Nothing else.
  • To read messages, the owner derives a key from their Secret Code (PBKDF2, 600,000 iterations) to unwrap their private key locally, then decrypts.

Stack is boring on purpose: PHP 8.1+, MySQL, Redis for rate limiting. No framework magic, because I wanted every request path to be auditable by hand.

What I learned pressure-testing this on Reddit

I posted an earlier version of this on r/selfhosted and got genuinely good pushback that changed how I talk about it:

"Browser-side encryption doesn't protect against a malicious server" - true, and important. If I control the server, I control the JS it serves. Client-side encryption protects data at rest (a stolen DB dump, a leaked backup) - it does not protect against a compromised or dishonest operator serving modified code. Every web-based E2E tool (Signal web, ProtonMail webmail) has this same trust-on-first-use limitation unless you're verifying the served code yourself.

"This solves confidentiality, not anonymity, and those are different problems" - also true. Most "anonymous feedback" tools focus on hiding who sent something; I'm solving for protecting what was written, regardless of who's identified. They're not substitutes for each other, they're complementary.

I'd rather state those boundaries plainly than oversell "zero-knowledge" as something stronger than it is.

Why I actually kept going with this

I'm one person. No team, no funding, just a few months of nights spent reading RFCs and rewriting the same crypto flow after finding holes in it. There were a lot of points where it would've been easier to just store plaintext and call it a day. I didn't, because the whole point disappears the moment you cut that corner.

Full disclosure

I built a Business/Agency edition on top of this - a license plus the ability to white-label it for client work (agencies, HR/whistleblowing intake, legal consultancies - anywhere someone needs a confidential channel they don't have to build themselves):

👉 https://sasirumindaka.gumroad.com/l/secretgate-business

There's also a live demo if you just want to poke at the architecture, no purchase needed: https://business.secretgate.site (please don't put anything real in it).

Genuinely curious: if you've built or evaluated E2E-encrypted web tools, how do you handle the "trust the served JS" problem? Subresource Integrity doesn't really cover dynamically-rendered pages, and I haven't found a clean answer yet. Would love to hear how others have thought about it.