/selftest — runs full Alice→Bob encrypt/decrypt cycle within WASM
(tests X3DH + Double Ratchet + bincode serialize/deserialize)
/bundleinfo — dumps bundle contents, verifies SPK secret matches
SPK public key in the registered bundle
These help isolate whether the bug is in WASM crypto (self-test fails)
or in CLI↔WASM interop (self-test passes but cross-client fails).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/reset — clears all localStorage (identity, sessions, SPK)
/sessions — shows active session peers and SPK secret prefix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
<script type="module"> doesn't expose functions to onclick attributes.
Replaced all onclick="fn()" with document.getElementById().onclick = fn
so buttons work from module scope.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: WASM was regenerating random pre-keys on every call to
decrypt_wire_message, instead of using the SPK that was registered
with the server. CLI sender encrypts to the registered SPK, but
WASM was trying to decrypt with a different random key.
Fix:
- WasmIdentity now stores spk_secret_bytes internally
- SPK secret persisted to localStorage as 'wz-spk'
- On load: restored from localStorage, not regenerated
- bundle_bytes() uses stored SPK secret (cached, deterministic)
- decrypt_wire_message() takes spk_secret_hex parameter
- Web UI passes stored SPK to all decrypt calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 updated with all completed items (16 done, 1 remaining).
WebSocket real-time push added as the last Phase 1 task.
Phase 2 cleaned up (removed items already done in Phase 1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
warzone-wasm crate:
- Compiles warzone-protocol to WebAssembly via wasm-pack
- Exposes WasmIdentity, WasmSession, decrypt_wire_message to JS
- Same X25519 + ChaCha20-Poly1305 + X3DH + Double Ratchet as CLI
- 344KB WASM binary (optimized with wasm-opt)
WireMessage moved to warzone-protocol:
- Shared type used by CLI client, WASM bridge, and TUI
- Guarantees identical bincode serialization across all clients
Web client rewritten:
- Loads WASM module on startup (/wasm/warzone_wasm.js)
- Identity: WasmIdentity generates same key types as CLI
- Registration: sends bincode PreKeyBundle (same format as CLI)
- Encrypt: WasmSession.encrypt/encrypt_key_exchange
- Decrypt: decrypt_wire_message (handles KeyExchange + Message)
- Sessions persisted in localStorage (base64 ratchet state)
- Groups: per-member WASM encryption (interop with CLI members)
Server routes:
- GET /wasm/warzone_wasm.js — serves WASM JS glue
- GET /wasm/warzone_wasm_bg.wasm — serves WASM binary
- Both embedded at compile time via include_str!/include_bytes!
Web ↔ CLI interop now works:
- Same key exchange (X3DH with X25519)
- Same ratchet (Double Ratchet with ChaCha20-Poly1305)
- Same wire format (bincode WireMessage)
- Web user can message CLI user and vice versa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Storage:
- Detects sled lock contention, shows actionable error:
"Database locked by another warzone process"
with ps command to find the process and rm command to force unlock
TUI:
- Poll loop no longer calls load_seed() (was re-prompting passphrase)
- Seed passed from main.rs to run_tui to poll_loop
- Single passphrase prompt per app launch
Warnings fixed:
- Removed unused `Context` import in tui/app.rs
- Added #[allow(dead_code)] on validate_token (used when auth middleware wired)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sending a message includes `from` fingerprint
- Server renews alias TTL on send (proves identity: you encrypted it)
- Polling/receiving does NOT renew (anyone can spam messages to you)
- Key registration does NOT renew (separate concern)
This prevents alias keepalive attacks where someone spams a user
just to keep their alias from expiring.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aliases now have a lifecycle:
- 365-day TTL from last activity (send/receive/renew)
- 30-day grace period after expiry (only recovery key can reclaim)
- After grace: anyone can register the alias
- Recovery key generated on first registration, rotated on recovery
- Auto-renew on activity via POST /v1/alias/renew
New endpoints:
- POST /v1/alias/recover {alias, recovery_key, new_fingerprint}
Reclaim alias with recovery key, even if expired. Works across
identity changes (new seed → new fingerprint, same alias).
Recovery key is rotated on each recovery.
- POST /v1/alias/renew {fingerprint}
Heartbeat — resets TTL. Returns days until expiry.
Resolve now returns expiry info:
- GET /v1/alias/resolve/:name → includes expires_in_days, expired flag
- GET /v1/alias/list → includes expiry status per alias
Phase 2: DNS automation — separate DNS authority manages parent zone,
servers update delegated records via API. Recovery key maps to DNS
record ownership for out-of-band reclamation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- POST /v1/alias/register — claim an alias (one per fingerprint)
- GET /v1/alias/resolve/:name — alias → fingerprint
- GET /v1/alias/whois/:fingerprint — fingerprint → alias (reverse)
- GET /v1/alias/list — list all aliases
- Bidirectional mapping in sled (a:name→fp, fp:fp→name)
- One alias per person, re-registering replaces old alias
Web client:
- /alias <name> — register your alias
- /aliases — list all registered aliases
- /info — now shows alias alongside fingerprint
- Peer input accepts @alias (resolved before sending)
- Received messages show @alias instead of fingerprint
- DM: paste @alias or fingerprint in peer input
CLI TUI:
- /alias <name> — register alias
- /aliases — list all aliases
- /peer @alias — resolves alias to fingerprint
- Alias resolution displayed in system messages
Addressing model:
- @manwe (local) → server resolves → fingerprint
- @manwe.b1.example.com (federated) → DNS resolve (Phase 3)
- Raw fingerprint → always works, no resolution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- fetchPeerKey: catch JSON parse error for CLI bincode bundles,
show clear "CLI client — needs WASM bridge" message
- Group send: silently skip CLI members instead of showing
error per member (mixed groups work, web members get messages,
CLI members are skipped without noise)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Server: /join endpoint creates the group if it doesn't exist
- CLI TUI: /g <name> auto-joins before switching
- Web: /g <name> auto-joins before switching
- No more "group not found" errors — just /g ops and go
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- POST /v1/groups/create — create named group
- POST /v1/groups/:name/join — join group
- GET /v1/groups/:name — get group info + member list
- GET /v1/groups — list all groups
- POST /v1/groups/:name/send — fan-out encrypted messages to members
- Groups stored in sled, members tracked by fingerprint
Web client:
- /gcreate <name> — create group
- /gjoin <name> — join group
- /g <name> — switch to group chat mode
- /glist — list all groups
- /dm — switch back to DM mode
- Group messages encrypted per-member (ECDH + AES-GCM for each)
- Group tag shown on received messages: "sender [groupname]"
CLI TUI client:
- Same commands: /gcreate, /gjoin, /g, /glist, /dm
- Group messages encrypted per-member (X3DH + Double Ratchet for each)
- Automatic X3DH key exchange with new group members on first message
- Sessions established and persisted per-member
Architecture:
- Client-side fan-out encryption: message encrypted N times (once per member)
- Server stores one copy per recipient in their message queue
- Reuses existing 1:1 encryption — no new crypto primitives needed
- Works for groups ≤ 50 members (per DESIGN.md)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`warzone chat [peer-fp] -s <server>` launches an interactive terminal UI:
- Header: your fingerprint, peer fingerprint, server URL
- Message area: color-coded (green=you, yellow=peer, cyan=system)
- Input bar with cursor at bottom
- Background polling every 2s for incoming messages
- Full X3DH + Double Ratchet on send/receive
- Session persistence across messages
Commands in TUI:
- /peer <fingerprint> — set who you're chatting with
- /info — show your fingerprint
- /quit or /q or Esc or Ctrl+C — exit
Usage:
warzone chat "6baf:6d0b:4541:9cae:f06b:83da:69bc:05ee" -s http://localhost:7700
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
poll_messages now collects all queued messages, returns them,
then deletes them from sled. No more duplicate delivery.
This is correct for store-and-forward: once the client receives
the messages, the server's job is done. If the client crashes
before processing, the messages are lost — acceptable for Phase 1.
Phase 2 can add explicit ack-based delivery if needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Axum 0.7 uses :param for path parameters. {param} is axum 0.8+ syntax.
Routes were silently not matching, causing 404 on all key lookups.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Client: strip colons before putting fingerprints in URL paths
(colons in URLs confuse axum path matching).
Server: normalize fingerprints in message routes too.
All fingerprint storage and lookup is now hex-only, case-insensitive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server: normalize fingerprints by stripping colons and lowercasing
before storing/looking up in sled. Adds tracing for register/lookup.
Client: check HTTP status before parsing JSON response body.
Shows clear error when user is not registered instead of parse error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was showing xxxx:xxxx:xxxx:xxxx (8 bytes) but from_hex expected
16 bytes, causing parse failure. Now displays all 16 bytes:
xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
Users need to re-init to see the full fingerprint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All data paths now use keystore::data_dir() which checks
WARZONE_HOME first, falls back to ~/.warzone.
This avoids the HOME override hack that breaks rustup/cargo.
Usage: WARZONE_HOME=/tmp/bob warzone init
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
X3DH fix:
- Added identity_encryption_key (X25519) to PreKeyBundle
- initiate() and respond() now use correct DH operations per Signal spec:
DH1=IK_a*SPK_b, DH2=EK_a*IK_b, DH3=EK_a*SPK_b, DH4=EK_a*OPK_b
- All 17 tests pass including x3dh_shared_secret_matches
Web client (served at /):
- Identity generation with seed (stored in localStorage)
- Recovery from hex-encoded seed
- Auto-load saved identity on page load
- Fingerprint display (same format as CLI: xxxx:xxxx:xxxx:xxxx)
- Key registration with server via /v1/keys/register
- Chat UI with message polling (5s interval)
- Commands: /help, /info, /seed
- Dark theme matching warzone aesthetic
Both clients (CLI + Web) now exist:
- CLI: warzone init, warzone info, warzone recover
- Web: http://localhost:7700/ (served by warzone-server)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Key transparency via DNS TXT records with self-signatures
(server can't MITM because it can't forge user's signature)
- Per-device ratchet sessions (Signal model), cross-device sync via seed
- LoRa deferred to later phases, not Phase 1
- Sealed sender before onion routing
- Phase 3 updated to include key transparency alongside federation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Decisions: Sender Keys for groups, optional onion routing, deniability
by default, Bluetooth + LoRa transports, no tokenization.
New sections: transport abstraction (HTTPS/WS/BT/LoRa/Wi-Fi Direct/USB),
LoRa compact binary format, sealed sender vs onion routing discussion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- /reply <msg> or /r <msg> sends encrypted DM to last person
- lastDmPeer set when sending a DM or receiving one
- Shows error if no prior DM conversation exists
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browser:
- ECDH key pair saved to localStorage (chat-key-priv, chat-key-pub)
- Loaded on reconnect, only generated once
- Re-registers public key with server on every connect
- Corrupted keys auto-regenerate
Server:
- Keys saved to keys.json on disk after each registration
- Loaded on startup, survives restarts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- /keys POST: register ECDH public key (JWK) for a username
- /keys GET: list users with registered keys
- /keys/<user> GET: get user's public key
- /dm POST: relay encrypted DM blob to recipient
- SSE streams now register for DM delivery via name param
- Server never sees plaintext - only ciphertext passes through
Web UI:
- Auto-generates ECDH P-256 key pair on load (no setup needed)
- /dm @username message - sends E2E encrypted DM
- /users - list users with registered keys
- DMs shown with lock icon, pink color, direction arrows
- Decryption happens entirely in browser
- Key re-registered on name change
- Derived AES keys cached per peer
Protocol:
- ECDH key exchange: each client exports JWK public key
- Shared secret derived via ECDH P-256
- Messages encrypted with AES-256-GCM + random 12-byte nonce
- Ciphertext + nonce sent as base64 through server
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- /group/<name> URL creates/joins a group (auto-created on first visit)
- / and /chat redirect to /group/lobby (default group)
- Each group has isolated history, clients, and SSE streams
- /setpass <password> sets a password for the current group
- /clearpass removes the password
- Password prompt modal in web UI, stored in sessionStorage
- SSE sends auth-fail event if wrong password, triggers re-prompt
- Group name shown as tag in header
- TCP clients use lobby group by default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Web manifest with standalone display mode
- SVG chat bubble icon (no external assets needed)
- Service worker for install + offline page
- iOS meta tags: apple-mobile-web-app-capable, status bar style
- Mobile-optimized layout: safe-area insets, dvh units, rounded inputs
- Name input moved to header, file button + send in bottom bar
- 16px font on input (prevents iOS zoom)
- Name persisted to localStorage on mobile
- Keyboard-aware scroll (visualViewport resize listener)
- Install banner with prompt for Android Chrome
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- /tunnel/<dest> routes: parspack (185.208.174.152:22),
mequ (188.213.68.133:2022), alipi (10.66.66.2:22)
- /tunnel without dest defaults to parspack
Client (tunnel.py):
- --destination / -d flag to pick target
- Lists available destinations in --help
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Web UI:
- Requests browser notification permission on load
- Shows desktop notification for messages from others when tab unfocused
- Tab title shows unread count: "(3) Chat"
- Resets on focus
Terminal client:
- Bell (\a) on messages from others
- Terminal title updates to show sender and preview
- Title resets when user types
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Web UI:
- Textarea replaces input: Shift+Enter for newline, Enter to send
- Pasted text preserves newlines, tabs, whitespace
- Markdown: ```code blocks```, `inline code`, **bold**, *italic*, auto-links
- File upload button (paperclip icon), files stored in memory with download links
Python CLI client:
- Colored usernames: green for self, cyan for system, unique color per other user
- /file <path> command to upload files
- Multiline messages displayed with continuation indent
- JSON protocol for multiline + file support (backwards compatible)
Server:
- POST /chat/upload for multipart file uploads
- GET /files/<id>/<name> for file downloads
- TCP protocol accepts JSON packets for multiline text and file transfers
- Falls back to plain text for old clients
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>