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

Chat thread with the PQ-KEM badge

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

Login screen — Bale ID + OTP, E2EE notice

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.

⭐ https://github.com/indirect-b/chat-app