banner
BGIcon

Overview

Quietwire is an end-to-end encrypted chat app built on Signal's design — PQXDH, a double ratchet with encrypted headers, sender keys for groups, sealed sender and multiple devices per account — with post-quantum cryptography at every step. Sessions start with X25519 + ML-KEM-768, the ratchet keeps mixing in fresh ML-KEM secrets as people chat, and every identity signature is Ed25519 + ML-DSA-65 with both halves required to verify. All cryptography runs in the browser; the server only routes and stores ciphertext.

The whole backend is one Cloudflare Worker with a Durable Object per device as its mailbox and a second Durable Object holding an RFC 9162 Merkle log for key transparency. It is live at chat.devme.online.

Technologies

  • - TypeScript
  • - React + IndexedDB
  • - Cloudflare Workers + Durable Objects
  • - D1, KV, R2, Queues
  • - ML-KEM-768 / ML-DSA-65
  • - WebRTC
  • - Vitest + Playwright
Onboarding: pick a username, keys are generated on the device
A post-quantum session (PQ badge on the contact)
Safety number and QR code to verify a contact in person
Group chat with polls, pins and mentions
Voice messages and file transfer, end-to-end encrypted
Dark theme
BG
Icon
  • Year

    2026

  • Type

    Full-Stack Security Project

  • Role

    Solo Developer

Highlights

Implemented a post-quantum handshake, ratchet, sealed sender and device linking, all hybrid X25519 + ML-KEM-768.

Built key transparency as an append-only, signed Merkle log that clients check with inclusion and consistency proofs.

Shipped a full messenger — groups, files, voice notes, polls, view-once, 1:1 and mesh group calls up to six people, push, app lock and an offline shell — deployed and live.

Every claim in the docs carries a receipt (code, test, recorded run or spec), and a check script fails if any of the 382 receipts stops holding.

BG
Icon
  • Server sees

    Ciphertext only

  • Tests

    54 unit · 17 live · 34 browser

Technical Implementation

Per-device mailboxes: Each device gets its own Durable Object with SQLite storage that holds envelopes until they are acknowledged, so offline delivery works. Sockets use WebSocket Hibernation, which keeps idle connections open at no cost and answers pings without waking the object. One send fans out over DO RPC to every recipient device.

Multi-device by address: Accounts are addressed as user.device, with per-device prekeys and sessions, and every device is signed by the account identity. The server refuses a send that misses one of the recipient's current devices and names the current list, so the client refreshes and re-encrypts instead of silently leaving a device behind.

Sealed sender: When a contact has shared an access key, each envelope is wrapped in an X25519 + ML-KEM outer layer under a server certificate and posted over an unauthenticated endpoint, so the server delivers a message it cannot attribute.

Cloudflare services, each with a job: D1 holds the directory, devices, prekeys and hashed tokens — a one-time prekey is read and deleted in one RETURNING statement so it can never be handed out twice. KV holds short-lived challenges and linking channels, R2 stores attachment ciphertext, Queues send a content-free Web Push to offline devices, and Turnstile, the Rate Limiting binding and a daily cron cover abuse and cleanup.

Calls: 1:1 and group calls run over WebRTC as a full mesh of up to six people, with call signalling carried inside the end-to-end encrypted channel and optional short-lived TURN credentials for strict NATs.

On the device: Messages live in IndexedDB, optionally encrypted at rest behind an app lock with idle auto-lock. A Web Lock allows one tab per account, because two tabs advancing the same ratchet would desync it.

BG
Icon
  • Status

    Live, not audited

  • Verified against

    RFC 8291 · RFC 9162 vectors

Design Decisions & Challenges

The hardest bug was crossed first messages: when two strangers both sent first at the same moment, each side replaced its session with the other's and decryption failed. The fix follows Signal — older sessions per address are kept and tried — and a test now drives both sides into that race and checks they converge.

The client saves new session state before anything is sent. A crash between encrypting and saving would otherwise reuse ratchet keys, which is exactly the failure an end-to-end protocol cannot afford.

Self-contained pieces are checked against published vectors: Web Push encryption reproduces RFC 8291 Appendix A byte for byte, and the Merkle log matches the published root for every tree size from 0 to 8.

Testing went past unit tests into the real stack: live tests run against the Worker with D1, Durable Objects, KV, R2 and Queues, and browser tests drive the actual UI in Chrome — including scanning a safety-number QR code through a fake camera. Those suites caught real bugs, from missing read receipts after accepting a message request to a race when switching on the app lock.

It is a learning and portfolio project and has not been audited, and the threat model says plainly what the server can and cannot see.