I built a post-quantum encrypted messenger in the browser
Most web messengers either skip end-to-end encryption entirely, or bolt on a classical key exchange that a future quantum computer will retroactively break. I wanted to see how far you can push real, post-quantum E2EE inside a browser — so I built one.
Meet chat-app: a real-time Persian web messenger where every message is protected by PQ-MPRBA, an original browser-native protocol combining a hybrid post-quantum ratchet with blinded authentication. The server never sees plaintext — and never learns who is talking to whom.
🚀 Try it live: https://chatapp-server-1i2a.onrender.com — hit ⚡ Quick Test Login, no sign-up needed.
⭐ Source: https://github.com/indirect-b/chat-app
Why post-quantum, and why now?
Classical elliptic-curve cryptography (ECDH, RSA) is fine against today's computers. It is not fine against an adversary who records your encrypted traffic today and decrypts it later, once a sufficiently powerful quantum computer exists. This is the "store now, decrypt later" problem, and for a messenger, it's the whole ballgame: chat history is exactly the kind of data that stays sensitive for decades.
The fix is a hybrid approach: combine a post-quantum key encapsulation mechanism with a classical one, so breaking a session requires breaking both hard problems. That's what chat-app does.
The protocol: PQ-MPRBA
PQ-MPRBA stands for Post-Quantum Multi-Party Ratchet with Blinded Authentication. It runs entirely in the browser, on top of the Web Crypto API.
Layer 1 — Hybrid post-quantum ratchet
Each session combines ML-KEM-768 (NIST FIPS 203, lattice-based) with classical ECDH P-256. Both shared secrets feed HKDF-SHA256 into a symmetric ratchet chain, and every message gets a fresh one-time AES-256-GCM key. That gives forward secrecy and post-compromise recovery: even if a key leaks, past messages stay safe and future ones heal.
Two hard engineering problems hide inside that sentence:
Out-of-order messages. On unstable networks, message #3 can arrive before #2. A naive ratchet would lose #2 forever. chat-app's ratchet fast-forwards and caches skipped one-time keys in a bounded in-memory vault (100 keys per room, 10-minute TTL). Consumed keys and ML-KEM secrets are zeroized in RAM the moment they're used.
Group key rotation. When someone joins or leaves a group, the group key must rotate — without corrupting messages already in flight. A deterministic finite state machine (STABLE → ROTATING → STABLE) queues outgoing messages during rotation, and a 3-epoch grace buffer keeps messages sent milliseconds before the change decryptable.
Layer 2 — Blinded authentication (Ristretto255-VOPRF)
Standard E2EE hides message content, but the server still sees who authenticates. chat-app uses a Verifiable Oblivious Pseudorandom Function over Ristretto255 (RFC 9497): the client blinds its identity locally, the server evaluates it with its secret key and returns a DLEQ zero-knowledge proof, and the client unblinds the result into an unlinkable routing token. The server can authenticate users without ever seeing — or linking — their identities, using compact 32-byte group elements instead of heavy RSA blind signatures.
What the app actually does
Beyond the cryptography, it's a complete messenger:
- Real-time direct & group messaging (Socket.IO, WebSocket + long-polling fallback)
- Login with Bale ID (OTP delivered by a Bale bot) plus a 1-click quick test login
- Typing indicators, presence, reactions, message edit/delete/pin/forward, read receipts
- Voice notes & file attachments up to 50 MB, processed off the main thread in Web Workers
- PBKDF2 room passphrases (100,000 iterations)
- A stealth admin dashboard at an obfuscated, server-side-only path
- Dual-mode persistence: PostgreSQL when configured, otherwise an atomic, mutex-guarded JSON store — so it survives free-tier redeploys
The stack
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite, modular RTL CSS |
| Crypto | Web Crypto API, @noble/post-quantum, @noble/curves, Web Workers |
| Backend | Node.js, Express, Socket.IO, Helmet, CORS, rate limiting |
| Auth | Bale OTP bot + Ristretto255-VOPRF blinded tokens |
| Storage | PostgreSQL or atomic JSON fallback; hybrid media blobs + disk |
The server is deliberately a zero-knowledge forwarder: it routes ciphertext and blinded tokens, nothing more.
An honest disclaimer
PQ-MPRBA is an original, experimental protocol I designed and implemented for this project. It has not been independently audited. I built it to learn, to push what's possible in a browser, and to invite scrutiny — not to claim it's production-grade for high-risk use. If you're a cryptographer, I'd genuinely love your review: the core lives in src/utils/crypto.ts.
What's next
- Independent review of the protocol
- Better group UX and message search
- A proper audit of the server-side hardening
If this kind of thing interests you, try the demo, star the repo, and tell me what breaks. That's how it gets better.















