Cipher — E2E encrypted messenger (Signal Protocol) on Cloudflare
Find a file
maverick 34c7ef08eb Add multi-device key sync: QR device linking + encrypted recovery code
Answers 'how do you get keys to login elsewhere' — both mechanisms, Signal/WhatsApp style:

Server:
- migration 0002: key_backups (recovery blobs) + provisions (QR relay), ciphertext only
- routes/backup.js: POST/GET /api/backup, /backup/exists; provision new/get/complete
- routes/keys.js: POST /api/keys/reset — atomically wipe stale prekeys + install fresh
  (fixes Bad MAC: restore regenerates prekey IDs that INSERT-OR-IGNORE kept stale)

Crypto (crypto.js):
- exportIdentityBlob/importIdentityBlob (identity travels only encrypted)
- generateRecoveryCode (120-bit Crockford base32) + encrypt/decryptWithCode (PBKDF2 250k)
- ephemeral ECDH (P-256) encrypt/decrypt for QR transfer

Client (app.js): account-menu 'Set up recovery code' + 'Link a device' (camera scan
via BarcodeDetector or paste); auth-screen 'Restore with recovery code' + 'Link from
another device' (shows QR + code, polls, restores). Vendored qrcode-generator.

All verified live end-to-end (3-process isolated-store integration tests):
restore-from-code and QR-link both let a fresh device receive + decrypt messages.
SW cache v3->v4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:04:46 +10:00
migrations Add multi-device key sync: QR device linking + encrypted recovery code 2026-07-22 13:04:46 +10:00
public Add multi-device key sync: QR device linking + encrypted recovery code 2026-07-22 13:04:46 +10:00
src Add multi-device key sync: QR device linking + encrypted recovery code 2026-07-22 13:04:46 +10:00
.gitignore Cipher: E2E encrypted messenger scaffold (Signal Protocol + Cloudflare) 2026-07-22 01:56:59 +10:00
package-lock.json Add multi-device key sync: QR device linking + encrypted recovery code 2026-07-22 13:04:46 +10:00
package.json Add multi-device key sync: QR device linking + encrypted recovery code 2026-07-22 13:04:46 +10:00
README.md Add README with architecture overview 2026-07-22 02:06:59 +10:00
wrangler.jsonc Fix PBKDF2 iteration cap (Workers max 100k), add custom domain + error handler 2026-07-22 02:04:40 +10:00

Cipher

End-to-end encrypted messenger — a WhatsApp/Signal-style app running entirely on Cloudflare.

Live: https://cipher.theradicalparty.com

Design

The server is a dumb key directory + encrypted relay. It never sees message plaintext or private keys — only public keys and ciphertext. Real-time delivery plus offline store-and-forward, so messages arrive even when the recipient is away.

Concern How
Encryption Signal Protocol (X3DH + Double Ratchet) via libsignal, in the browser
Real-time + offline queue Per-user Mailbox Durable Object (WebSocket + SQLite queue)
Key directory D1 — users, devices, prekey bundles (public keys only)
Media Client-side AES-GCM, ciphertext stored in R2; key travels inside the E2E message
API Hono on Workers
Client Installable PWA, IndexedDB key store, Web Push wake-up
Identity Username + password; QR device-linking planned

Private keys are generated on the device and stored only in IndexedDB. They are never transmitted. See docs/ notes inline in each source file.

Architecture

 Alice (PWA)                    Cloudflare Worker                 Bob (PWA)
 ┌─────────────┐   publish pubkeys   ┌──────────────┐
 │ libsignal   │ ──────────────────► │ D1 key dir   │
 │ IndexedDB   │ ◄── prekey bundle ─ │              │
 └─────┬───────┘                     └──────────────┘
       │ encrypt (X3DH/ratchet)
       │ POST ciphertext             ┌──────────────┐   WS push    ┌──────────┐
       └───────────────────────────► │ Mailbox DO   │ ───────────► │ decrypt  │
                                      │ (queue+WS)   │   or queue   │ locally  │
       encrypt media (AES-GCM) ─► R2 └──────────────┘              └──────────┘

Develop

npm install
npm run dev                 # local Worker + assets
npm run db:migrate:local    # apply schema to local D1

Deploy

Push to main → Forgejo post-receive hook runs wrangler deploy.

git pushall main            # pushes to Forgejo (origin) + GitHub (backup)

Vendored crypto

public/js/vendor/libsignal.js is a self-contained browser bundle of @privacyresearch/libsignal-protocol-typescript (built with esbuild + a Buffer shim). Rebuild with the esbuild command noted in the commit history if bumping the library — loading crypto from a CDN at runtime is intentionally avoided.

Status

Working: registration, login, key directory, E2E text + media, real-time and offline delivery (both verified live). Planned: QR multi-device linking, group chats (sender keys / MLS), WebRTC voice/video, Web Push fan-out, delivery/read receipts, disappearing messages.