DEV Community

Cover image for I built a post-quantum encrypted messenger in the browser — here's how it works
Mohammad Taha Azizi Ahad
Mohammad Taha Azizi Ahad

Posted on

I built a post-quantum encrypted messenger in the browser — here's how it works

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

Top comments (3)

Collapse
 
preetest profile image
Preetest Team •

Read the write-up and opened the live demo as a plain visitor. Two things worth knowing for a messenger whose whole point is security:

  1. The server sends the page with no HSTS and no Content-Security-Policy (the other standard headers are absent too). The post-quantum layer protects the message bodies, but without a CSP any script injected into the page would see the keys before they are used, and without HSTS a first visit over plain HTTP can be intercepted. Both are a few lines of config on Render.
  2. The page declares lang="fa" with a Persian title while the article is in English. Fine if the UI is Persian by design, but screen readers and search engines take that attribute literally.

Disclosure: we make Preetest, an AI website-testing platform. If you want the full check (security headers, accessibility, mobile, broken links) on the demo, say yes here and I will run it and post the report link back in this thread. We never scan a site without the owner's okay.

Collapse
 
officialmailkr profile image
오피셜메일 •

순서가 뒤바뀐 메시지용 키를 100개·10분으로 제한한 부분이 흥미롭네요. 장시간 오프라인인 사용자가 한도를 넘긴 메시지를 받으면 어떤 복구 안내가 나오는지도 궁금합니다. 독립 감사 전의 실험적 프로토콜임을 명시한 점도 중요하다고 봅니다.

Collapse
 
indirectb profile image
Mohammad Taha Azizi Ahad •

Great question, thank you for reading so closely! You're right about the trade-off: the vault is deliberately bounded (100 keys, 10-minute TTL) so memory stays small and old keys are zeroized quickly. If a user comes back after a long time offline and the gap is beyond that window, those messages can't be recovered from skipped keys — the ratchet can't rewind that far. Right now that's an accepted limitation: the session would need a fresh key exchange to continue securely. A proper in-app recovery flow — detecting the gap and prompting a re-key instead of silently dropping messages — is exactly the kind of thing on my list, and your question pushes it up. And yes, the experimental/audit disclaimer matters to me: I'd rather be honest about the limits than oversell it. Thanks again!