Most "anonymous feedback" tools say the same thing: trust us, it's anonymous. I wanted to build one where you don't have to trust me, and where you can actually check. This is how I did it with plain PHP and the browser's built-in WebCrypto API.
The goal
The server should store messages it cannot read, even if someone gets full access to the database.
That means all the cryptography has to happen in the browser. PHP only ever sees ciphertext.
The design
1. The inbox owner generates a key pair in the browser.
const keyPair = await crypto.subtle.generateKey( { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, true, ["encrypt", "decrypt"] ); Only the public key is sent to the server. The private key stays with the owner.
2. The private key is wrapped with the owner's Secret Code.
The Secret Code is stretched with PBKDF2 (600,000 iterations) into an AES-GCM key, which encrypts the private key:
const wrappingKey = await crypto.subtle.deriveKey( { name: "PBKDF2", salt, iterations: 600000, hash: "SHA-256" }, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); 3. The Secret Code never reaches the server.
To prove ownership to the server, I derive a separate auth key from the Secret Code (different salt), and the server stores only an Argon2id hash of that:
$hash = password_hash($auth_key, PASSWORD_ARGON2ID, [ /* cost options */ ]); So even the login credential on the server isn't the Secret Code itself.
4. Senders encrypt hybrid-style.
RSA can only encrypt small payloads, so the sender's browser generates a random AES-256-GCM key, encrypts the message with it, then encrypts that AES key with the owner's public key. The request body is one opaque blob: RSA-wrapped key + IV + ciphertext.
You can verify it in a minute
This is the part I care about most. Open the demo, press F12, go to the Network tab, send a message, and look at the request body. You won't find your plaintext, because it was never sent.
Demo (please keep test messages short and don't type anything real): https://business.secretgate.site/
Honest limits
- It's a web app, so like any browser-based E2EE you're trusting the JavaScript the site serves. Self-hosting means you control that code, and the Network tab lets anyone verify what gets sent.
- I ran 383 automated checks and a manual staging checklist, but it is not a professional pen-test certification.
- Metadata (like timing and message size) isn't hidden.
- It needs Redis (for rate limiting, which fails closed), so it needs a VPS, not basic shared hosting.
About the product
I turned this into a product called SecretGate (Business Agency Edition), aimed at agencies who want to offer clients a private feedback or tip-line portal under their own brand. It's closed-source, $299, one license per live project. For the launch, the first 50 buyers get $50 off (the link applies it automatically): https://sasirumindaka.gumroad.com/l/secretgate-business/LAUNCH50
Full disclosure: I built this and I'm selling it. I also used AI (Claude) to help with code review, tests and writing; I designed it, I run it, and I stand behind it.
I'd love honest feedback, especially on the crypto design. What would you change? And what would stop you from using something like this?
