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> |
||
|---|---|---|
| migrations | ||
| public | ||
| src | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| wrangler.jsonc | ||
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.