A real-time encrypted messaging application powered by fips-crypto, demonstrating post-quantum cryptography in the browser.
- ML-KEM-768 Key Exchange (FIPS 203) — Quantum-resistant key encapsulation to establish shared secrets between peers
- ML-DSA-65 Signatures (FIPS 204) — Every message and file is signed for authenticity
- XChaCha20-Poly1305 Encryption — Authenticated encryption for messages and files using the ML-KEM shared secret
- End-to-End Encrypted Chat — Text messages protected by encrypt-then-sign
- Encrypted File & Image Sharing — Files up to 5 MB, encrypted with the same pipeline as text
- Performance Benchmark — Compare fips-crypto vs pure JavaScript side-by-side in the browser
- Educational "How It Works" Tab — Step-by-step explanation of key generation, key exchange, encryption, and signing
- Crypto Detail on Every Message — Expandable view showing ciphertext size, signature size, and operation timings
- Dark / Light Theme — Toggle with persistent preference
- WebSocket Relay Server — Stateless message relay; the server never sees plaintext
# Install dependencies and start everything
make devThis starts:
- Relay server on
ws://localhost:3001 - Web app on
http://localhost:5173
Open two browser tabs to http://localhost:5173, pick different nicknames, and start chatting.
| Command | Description |
|---|---|
make dev |
Install deps + start relay server + dev server |
make build |
Type-check and build for production |
make test |
Run all tests |
make server |
Start relay server only |
make clean |
Remove build artifacts |
When you enter a nickname, two keypairs are generated in your browser via fips-crypto:
| Algorithm | Standard | Public Key | Secret Key |
|---|---|---|---|
| ML-KEM-768 | FIPS 203 | 1,184 bytes | 2,400 bytes |
| ML-DSA-65 | FIPS 204 | 1,952 bytes | 4,032 bytes |
Alice Bob
───── ───
1. Generate ML-KEM-768 keypair
2. Send KEM public key to Bob ────────>
3. Encapsulate → ciphertext + shared secret
<──── 4. Send ciphertext back
5. Decapsulate → same shared secret
Both now share a 32-byte symmetric key
Sending:
- Encrypt plaintext (or file bytes) with XChaCha20-Poly1305 using the shared key
- Sign the ciphertext with ML-DSA-65 using the sender's secret key
Receiving:
- Verify signature with ML-DSA-65 using the sender's public key
- Decrypt ciphertext with XChaCha20-Poly1305 using the shared key
The relay server only sees encrypted ciphertext and signatures — it cannot read messages or files.
src/
├── crypto/ # Crypto primitives (identity, KEM, encryption, signing)
├── protocol/ # WebSocket message types, client, session state machine
├── store/ # Zustand state (identity, peers, sessions, messages, etc.)
├── hooks/ # React hooks (useWebSocket, useKeyExchange, useBenchmark)
├── benchmark/ # Benchmark runner comparing fips-crypto vs pure JS
├── components/ # React UI components
└── App.tsx # Root component (login screen + main app)
server/
└── server.ts # WebSocket relay server (Node.js + ws)
tests/
├── crypto/ # Unit tests for all crypto modules
├── protocol/ # Session state machine tests
├── stores/ # Zustand store tests
├── server/ # WebSocket server integration tests
└── integration/ # End-to-end crypto flow tests
| Layer | Technology |
|---|---|
| Post-Quantum Crypto | fips-crypto 1.0+ (ML-KEM-768, ML-DSA-65) |
| Symmetric Encryption | XChaCha20-Poly1305 via @noble/ciphers |
| Frontend | React 19 + TypeScript + Vite |
| State Management | Zustand |
| Styling | Tailwind CSS (dark/light mode) |
| WebSocket Server | Node.js 20+ + ws |
| Testing | Vitest |
make test200 tests across 21 test files:
| Suite | Tests | Coverage |
|---|---|---|
crypto/encoding |
8 | Base64 round-trips, UTF-8, fingerprints |
crypto/encoding-extended |
19 | All byte values, crypto key sizes, invalid base64, unicode, CJK, empty edge cases |
crypto/identity |
2 | Key generation, correct sizes, uniqueness |
crypto/identity-extended |
7 | Unicode nicknames, empty nicknames, key independence, non-zero keys, round-trip with fips-crypto |
crypto/kem |
4 | Encapsulate/decapsulate match, different secrets per session |
crypto/kem-extended |
8 | Wrong key decapsulation, multiple sessions, bidirectional KEM, invalid key/ciphertext lengths |
crypto/symmetric |
8 | Round-trips, nonce uniqueness, tamper detection, wrong-key rejection, 1MB file |
crypto/symmetric-extended |
18 | Nonce structure, tag authentication, ciphertext overhead, key sensitivity, minimum ciphertext, long strings |
crypto/signing |
5 | Sign/verify, tamper rejection, wrong-key rejection |
crypto/signing-extended |
10 | Empty/large messages, signature size, bit-flip detection, truncated/empty signatures, invalid key lengths |
crypto/envelope |
7 | Deterministic output, metadata binding (timestamp, filename, MIME type) |
crypto/envelope-extended |
11 | Envelope structure, file metadata sizes, timestamp encoding, 1MB ciphertext, unicode filenames |
protocol/session |
5 | State machine transitions |
protocol/session-extended |
9 | All state × event combinations, full initiator/responder flows, re-initiation, unknown events |
stores |
10 | All Zustand store CRUD operations |
stores-extended |
23 | Duplicate peers, empty operations, message ordering, file attachments, crypto detail, stable empty arrays |
server |
7 | Registration, peer discovery, message relay, error handling |
server-extended |
12 | Invalid JSON, missing fields, duplicate registration, list-peers, 3-client topology, relay isolation, bidirectional relay |
integration/e2e |
7 | Full text + file exchange, impersonation detection, relay-tampered metadata, tampered ciphertext, multi-message |
integration/e2e-extended |
10 | Three-party sessions, empty/5MB files, MIME/type tampering, bidirectional chat, session re-establishment, unicode messages, base64 transport fidelity |
benchmark/runner |
8 | Result structure, progress callbacks, math correctness, ML-KEM + ML-DSA operation coverage |
This demo assumes an honest-but-curious relay that faithfully delivers messages but may attempt to read content. The relay server never sees plaintext — all messages and files are encrypted client-side before transmission.
- Secret keys never leave the browser — only public keys are transmitted
- End-to-end encryption — the relay server cannot decrypt messages or files
- Authenticated signed envelope — signatures cover the full message envelope (ciphertext + timestamp + message type + file metadata), preventing the relay from rewriting metadata without breaking the signature
- Signature-gated decryption — messages with invalid signatures are rejected before decryption; they appear as
[Message rejected: invalid signature]and the ciphertext is never decrypted or rendered - Peer identity pinning — during key exchange, the peer's ML-DSA public key is verified against the key announced during peer discovery; mismatched keys are rejected
- Per-session keys — each key exchange produces a fresh shared secret via ML-KEM
- Random nonces — XChaCha20-Poly1305 uses 24-byte random nonces (safe for random generation)
- Payload size limits — the server enforces an 8 MB
maxPayloadat the WebSocket frame level, and the receiver rejects oversized base64 payloads before decoding
- No out-of-band fingerprint verification — peer identity is pinned from the relay's
peer-list/peer-joinedmessages. A malicious relay could substitute a different public key during discovery (MITM). Production systems should add out-of-band fingerprint comparison (e.g., QR code scanning, safety numbers). - No forward secrecy — compromising a KEM secret key retroactively decrypts all messages in that session. Production systems should implement ratcheting (e.g., Double Ratchet with PQC KEM).
- No message ordering or replay protection — the protocol does not enforce message sequence numbers. A relay could replay or reorder messages.
- Ephemeral keys — keys exist only in browser memory and are lost on page reload. This is by design for a demo.
Note: This is a demo application for educational purposes. It is not audited for production use.
MIT