docs/ARCHITECTURE.md (531 lines): System design, ASCII diagrams, crypto stack, dual-curve identity, wire protocol (7 WireMessage variants), server/client architecture, data flow diagrams, storage model, extensibility points docs/USAGE.md (550 lines): Complete user guide: installation, all CLI commands (10), all TUI commands (20+), all web commands, file transfer, identity management, aliases, groups, multi-device, backup, keyboard shortcuts docs/INTEGRATION.md (542 lines): WarzonePhone concept, Ethereum/Web3, OIDC, DNS federation, transport abstraction, multi-server mode, custom clients, ntfy, how-to guides for extending message types/commands/storage docs/PROGRESS.md (234 lines): Timeline, Phase 1 (16 features), Phase 2 (16 features), v0.0.20, 28 tests, bugs fixed, known limitations, Phase 3-7 roadmap docs/SECURITY.md (438 lines): Threat model, 8 crypto primitives, key derivation paths, forward secrecy, Sender Keys trade-offs, seed security, server trust, WASM security, known weaknesses, comparison with Signal/Matrix/SimpleX Total: 3,751 lines across 8 doc files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
26 KiB
Warzone Messenger (featherChat) — Architecture
Version: 0.0.20 Status: Phase 1 complete, Phase 2 complete
High-Level Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Clients │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ CLI Client │ │ TUI Client │ │ Web Client (WASM) │ │
│ │ (warzone) │ │ (ratatui) │ │ (wasm-bindgen) │ │
│ └──────┬───────┘ └──────┬───────┘ └───────────┬───────────┘ │
│ │ │ │ │
│ ┌──────┴─────────────────┴───────────────────────┴──────────┐ │
│ │ warzone-protocol │ │
│ │ Identity · X3DH · Double Ratchet · Sender Keys · History │ │
│ └───────────────────────────┬───────────────────────────────┘ │
└──────────────────────────────┼──────────────────────────────────┘
│ HTTP / WebSocket
▼
┌──────────────────────────────────────────────────────────────────┐
│ warzone-server │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ HTTP API │ │ WebSocket│ │ Auth │ │ Message Router │ │
│ │ (axum) │ │ Relay │ │Challenge │ │ + Dedup │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────────┬────────┘ │
│ │ │ │ │ │
│ ┌────┴─────────────┴─────────────┴──────────────────┴────────┐ │
│ │ sled Database │ │
│ │ keys · messages · groups · aliases · tokens │ │
│ └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Crate Structure
The project is a Cargo workspace with five crates:
warzone/
├── Cargo.toml # Workspace root (v0.0.20)
├── crates/
│ ├── warzone-protocol/ # Core crypto & message types (library)
│ ├── warzone-server/ # Server binary (axum + sled)
│ ├── warzone-client/ # CLI/TUI client binary
│ ├── warzone-wasm/ # WASM bridge for web client
│ └── warzone-mule/ # Mule binary (future)
└── docs/
warzone-protocol
The protocol crate is the heart of the system. It is a pure library with zero I/O dependencies, used by all other crates (including WASM).
| Module | Purpose |
|---|---|
identity |
Seed, IdentityKeyPair, PublicIdentity, Fingerprint |
mnemonic |
BIP39 mnemonic encode/decode |
crypto |
HKDF-SHA256, ChaCha20-Poly1305 AEAD |
prekey |
SignedPreKey, OneTimePreKey, PreKeyBundle |
x3dh |
X3DH key agreement (initiate + respond) |
ratchet |
Double Ratchet state machine |
message |
WireMessage enum, MessageContent, ReceiptType |
session |
Session management types |
store |
Storage trait definitions |
sender_keys |
Sender Key protocol for groups |
history |
Encrypted backup/restore (HKDF → ChaCha20) |
ethereum |
secp256k1 identity, Ethereum address derivation |
types |
Fingerprint, DeviceId, SessionId, MessageId |
errors |
ProtocolError enum |
warzone-server
An axum HTTP + WebSocket server with sled embedded database.
| Module | Purpose |
|---|---|
main |
CLI args, server startup (default :7700) |
state |
AppState, Connections map, DedupTracker |
db |
Database struct (5 sled trees) |
routes/keys |
Pre-key bundle registration & retrieval |
routes/messages |
Send, poll (fetch-and-delete), ack |
routes/groups |
Create, join, leave, kick, members, list |
routes/aliases |
Register, resolve, recover, renew, admin |
routes/auth |
Challenge-response authentication |
routes/ws |
WebSocket real-time message delivery |
routes/web |
Static file serving for web client |
routes/health |
Health check endpoint |
warzone-client
CLI and TUI client with local sled database for sessions, contacts, and history.
| Module | Purpose |
|---|---|
main |
CLI parser (clap): init, recover, info, etc. |
keystore |
Seed encryption at rest (Argon2id + ChaCha20) |
storage |
LocalDb: sessions, pre_keys, contacts, history |
net |
HTTP + WebSocket client |
tui/app |
TUI application (ratatui + crossterm) |
cli/* |
CLI subcommand handlers |
warzone-wasm
WASM bridge exposing protocol functions to JavaScript via wasm-bindgen.
| Export | Purpose |
|---|---|
WasmIdentity |
Seed generation, fingerprint, bundle |
WasmSession |
Encrypt/decrypt with Double Ratchet |
create_receipt |
Build receipt WireMessages |
decrypt_wire_message |
Full message decryption pipeline |
self_test |
End-to-end crypto verification in WASM |
debug_bundle_info |
Bundle introspection for debugging |
warzone-mule (future)
Placeholder for the mule binary — physical message relay for disconnected networks.
Cryptographic Stack
Primitives
| Primitive | Crate | Purpose |
|---|---|---|
| Ed25519 | ed25519-dalek |
Signing, identity verification |
| X25519 | x25519-dalek |
Diffie-Hellman key exchange |
| ChaCha20-Poly1305 | chacha20poly1305 |
Authenticated encryption (AEAD) |
| HKDF-SHA256 | hkdf + sha2 |
Key derivation |
| SHA-256 | sha2 |
Fingerprint computation |
| Argon2id | argon2 |
Passphrase-based key derivation |
| secp256k1 ECDSA | k256 |
Ethereum-compatible signing |
| Keccak-256 | tiny-keccak |
Ethereum address derivation |
Protocol Stack
Application (plaintext)
│
▼
Double Ratchet (per-message keys, forward secrecy)
│
▼
X3DH (session establishment, 3-4 DH operations)
│
▼
ChaCha20-Poly1305 (authenticated encryption)
│
▼
Ed25519 (message signing + pre-key signing)
│
▼
WireMessage (bincode serialization)
│
▼
Transport (HTTP POST / WebSocket binary frame)
Dual-Curve Identity Model
Warzone derives two separate cryptographic identities from a single BIP39 seed:
BIP39 Seed (32 bytes, 24 words)
│
├─── HKDF(info="warzone-ed25519") ──→ Ed25519 signing keypair
│ │
│ └─→ SHA-256[:16] = Fingerprint
│
├─── HKDF(info="warzone-x25519") ──→ X25519 encryption keypair
│ (used in X3DH + Double Ratchet)
│
└─── HKDF(info="warzone-secp256k1") ──→ secp256k1 keypair
│
└─→ Keccak-256[-20:] = Ethereum Address
Messaging identity: Ed25519 (signing) + X25519 (encryption). The fingerprint is SHA-256(Ed25519_pubkey)[:16], displayed as xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx.
Ethereum identity: secp256k1 keypair derived from the same seed with domain-separated HKDF. The Ethereum address is derived using standard Keccak-256(uncompressed_pubkey[1:])[-20:]. EIP-55 checksummed display is supported.
This allows a single mnemonic to control both a Warzone messaging identity and an Ethereum wallet address.
Wire Protocol
All messages between clients use the WireMessage enum, serialized with bincode:
enum WireMessage {
// Session establishment (X3DH + first ratchet message)
KeyExchange {
id, sender_fingerprint, sender_identity_encryption_key,
ephemeral_public, used_one_time_pre_key_id, ratchet_message,
},
// Subsequent DM messages (Double Ratchet encrypted)
Message { id, sender_fingerprint, ratchet_message },
// Delivery / read receipts (plaintext metadata)
Receipt { sender_fingerprint, message_id, receipt_type },
// File transfer: header announces the file
FileHeader { id, sender_fingerprint, filename, file_size, total_chunks, sha256 },
// File transfer: individual chunk (encrypted data)
FileChunk { id, sender_fingerprint, filename, chunk_index, total_chunks, data },
// Group message encrypted with Sender Key (O(1) encryption)
GroupSenderKey { id, sender_fingerprint, group_name, generation, counter, ciphertext },
// Sender Key distribution (sent via 1:1 encrypted channel)
SenderKeyDistribution { sender_fingerprint, group_name, chain_key, generation },
}
Transport Encoding
- CLI ↔ Server (HTTP): JSON envelope with bincode message as byte array
- CLI ↔ Server (WebSocket binary): 64 hex chars (recipient fingerprint) + raw bincode bytes
- Web ↔ Server (WebSocket JSON):
{"to": "fingerprint", "message": [byte_array]}
Server Architecture
Framework
- axum 0.7 with tokio async runtime
- sled 0.34 embedded database (zero-config, no external DB)
- tower-http for CORS and tracing middleware
Route Structure
All API endpoints live under /v1:
POST /v1/keys/register Register pre-key bundle
GET /v1/keys/:fingerprint Fetch pre-key bundle
POST /v1/keys/replenish Upload additional OTPKs
GET /v1/keys/:fp/otpk-count Check remaining OTPKs
GET /v1/keys/:fp/devices List registered devices
GET /v1/keys/list List all registered fingerprints
POST /v1/messages/send Send encrypted message
GET /v1/messages/poll/:fp Fetch-and-delete queued messages
DELETE /v1/messages/:id/ack Explicit message acknowledgment
POST /v1/groups/create Create a group
GET /v1/groups List all groups
GET /v1/groups/:name Get group info
POST /v1/groups/:name/join Join a group
POST /v1/groups/:name/send Fan-out group message
POST /v1/groups/:name/leave Leave a group
POST /v1/groups/:name/kick Kick a member (creator only)
GET /v1/groups/:name/members List members with aliases
POST /v1/alias/register Register an alias
POST /v1/alias/recover Recover alias with recovery key
POST /v1/alias/renew Heartbeat / renew TTL
GET /v1/alias/resolve/:name Resolve alias → fingerprint
GET /v1/alias/whois/:fp Reverse lookup fingerprint → alias
GET /v1/alias/list List all aliases
POST /v1/alias/unregister Remove your own alias
POST /v1/alias/admin-remove Admin: remove any alias
POST /v1/auth/challenge Request a challenge
POST /v1/auth/verify Verify signature, get bearer token
GET /v1/ws/:fingerprint WebSocket upgrade for real-time push
GET / Web client static files
GET /health Health check
Message Routing
Incoming message
│
├─→ Dedup check (bounded FIFO, 10,000 IDs)
│ │
│ ├─→ Duplicate? → silently drop
│ └─→ New? → continue
│
├─→ Try WebSocket push to recipient
│ │
│ ├─→ Connected? → instant delivery
│ └─→ Offline? → queue in sled DB
│
└─→ Renew sender's alias TTL
WebSocket Protocol
- Client connects to
/v1/ws/:fingerprint - Server flushes any queued messages from DB
- Server registers a push channel for this connection
- Incoming WS messages are routed to recipient's WS or queued
- Multiple devices per fingerprint are supported (all receive a copy)
- On disconnect, stale senders are cleaned up
Dedup Tracker
The DedupTracker prevents message duplication across HTTP and WebSocket delivery paths:
- Bounded to 10,000 message IDs
- FIFO eviction when capacity is exceeded
- Uses
HashSetfor O(1) lookup +VecDequefor ordering - Thread-safe via
std::sync::Mutex
Web Client Architecture (WASM)
The web client runs the exact same crypto as the CLI through the warzone-wasm crate:
┌─────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ JavaScript UI │ │ warzone-wasm (WASM) │ │
│ │ (chat.html) │──│ WasmIdentity │ │
│ │ │ │ WasmSession │ │
│ │ WebSocket ◄─────┤ │ decrypt_wire_message() │ │
│ │ localStorage │ │ create_receipt() │ │
│ │ │ │ self_test() │ │
│ └──────────────────┘ └──────────────────────────┘ │
│ │
│ Storage: │
│ localStorage: seed_hex, spk_secret_hex │
│ localStorage: session:<fp> (base64 ratchet state) │
└─────────────────────────────────────────────────────┘
Key design decisions:
- Web Crypto provides cryptographic randomness (
OsRngin WASM maps tocrypto.getRandomValues) - No OTPKs in the web client — cannot reliably store secrets. X3DH works without DH4 (OTPKs are an anti-replay optimization, not a security requirement)
- SPK secret stored in localStorage as hex, restored on page load
- Session state serialized as base64 bincode, stored per-peer in localStorage
- bincode wire format ensures CLI ↔ Web interoperability
Data Flow Diagrams
1:1 Direct Message (First Message)
Alice Server Bob
│ │ │
│ GET /v1/keys/:bob_fp │ │
│─────────────────────────────→│ │
│ ← PreKeyBundle (bincode) │ │
│ │ │
│ X3DH initiate(bundle) │ │
│ → shared_secret │ │
│ Double Ratchet init_alice() │ │
│ ratchet.encrypt("hello") │ │
│ │ │
│ WireMessage::KeyExchange │ │
│ POST /v1/messages/send │ │
│─────────────────────────────→│ push via WS (or queue) │
│ │─────────────────────────────→│
│ │ │
│ │ X3DH respond(spk_secret) │
│ │ → shared_secret │
│ │ init_bob() │
│ │ ratchet.decrypt() │
│ │ → "hello" │
1:1 Direct Message (Subsequent)
Alice Server Bob
│ │ │
│ ratchet.encrypt("hi again") │ │
│ WireMessage::Message │ │
│ ─── WS binary ─────────────→│ ─── WS push ───────────────→│
│ │ │
│ │ ratchet.decrypt()│
│ │ → "hi again" │
│ │ │
│ │ WireMessage::Receipt │
│ │←──────────────────────────── │
│←─────────────────────────────│ │
│ ✓✓ delivered │ │
Group Message (Sender Keys)
Alice Server Bob, Carol
│ │ │
│ SenderKey::generate("ops") │ │
│ Distribute via 1:1 channels: │ │
│ encrypt(SK_dist, Bob) │ │
│ encrypt(SK_dist, Carol) │ │
│ ─────────────────────────────→│──── push to Bob,Carol ──→│
│ │ │
│ sender_key.encrypt("attack") │ │
│ WireMessage::GroupSenderKey │ │
│ POST /groups/ops/send │ │
│ ─────────────────────────────→│──── fan-out ────────────→│
│ │ │
│ │ bob_copy.decrypt() │
│ │ carol_copy.decrypt() │
│ │ → "attack" │
File Transfer
Sender Server Recipient
│ │ │
│ Read file, compute SHA-256 │ │
│ Split into 64KB chunks │ │
│ │ │
│ WireMessage::FileHeader │ │
│ ─────────────────────────────→│──── push ────────────────→│
│ │ │
│ WireMessage::FileChunk[0] │ │
│ ─────────────────────────────→│──── push ────────────────→│
│ WireMessage::FileChunk[1] │ │
│ ─────────────────────────────→│──── push ────────────────→│
│ ... │ │
│ WireMessage::FileChunk[N-1] │ │
│ ─────────────────────────────→│──── push ────────────────→│
│ │ │
│ │ Reassemble chunks │
│ │ Verify SHA-256 │
│ │ Save to downloads/ │
File constraints: max 10 MB, 64 KB chunks.
Storage Model
Server sled Trees
| Tree | Key Format | Value | Purpose |
|---|---|---|---|
keys |
<fingerprint> |
bincode PreKeyBundle | Identity + pre-key storage |
keys |
device:<fp>:<device_id> |
bincode PreKeyBundle | Per-device bundles |
keys |
otpk:<fp>:<id> |
hex pubkey | One-time pre-keys |
messages |
queue:<fp>:<uuid> |
raw bincode WireMessage | Offline message queue |
groups |
<group_name> |
JSON GroupInfo | Group membership |
aliases |
a:<alias> |
fingerprint string | Forward lookup |
aliases |
fp:<fingerprint> |
alias string | Reverse lookup |
aliases |
rec:<alias> |
JSON AliasRecord | Full record (TTL, recovery) |
tokens |
<token_hex> |
JSON {fp, expires_at} | Auth bearer tokens |
Client sled Trees
| Tree | Key Format | Value | Purpose |
|---|---|---|---|
sessions |
<peer_fp_hex> |
bincode RatchetState | Double Ratchet sessions |
pre_keys |
spk:<id> |
32-byte StaticSecret | Signed pre-key secrets |
pre_keys |
otpk:<id> |
32-byte StaticSecret | One-time pre-key secrets |
contacts |
<fingerprint> |
JSON contact record | Contact list |
history |
hist:<fp>:<ts>:<uuid> |
JSON message record | Message history |
Client Filesystem
~/.warzone/
├── identity.seed # WZS1 magic + salt(16) + nonce(12) + ciphertext(48)
│ # or plain 32 bytes (legacy/testing)
└── db/ # sled database directory
├── conf
├── db
└── ...
The WARZONE_HOME environment variable overrides ~/.warzone.
Extensibility Points
Adding New WireMessage Variants
- Add a new variant to
WireMessageinwarzone-protocol/src/message.rs - Update
extract_message_id()in bothroutes/messages.rsandroutes/ws.rs - Handle in the TUI poll loop (
tui/app.rs) - Handle in
decrypt_wire_message()inwarzone-wasm/src/lib.rs - bincode serialization is automatic (enum tag + fields)
Adding New Transport Traits (future)
The design document specifies a Transport trait:
trait Transport {
async fn send(&self, endpoint: &str, blob: &[u8]) -> Result<()>;
async fn recv(&self) -> Result<Vec<u8>>;
}
Current transports: HTTPS (reqwest) and WebSocket (axum/tungstenite). Planned: Bluetooth, LoRa, Wi-Fi Direct, USB/file.
Adding New Storage Backends
Client storage currently uses sled directly. The pattern for abstraction:
LocalDbinstorage.rsalready provides a clean API- Replace sled calls with trait methods for alternative backends (SQLite, IndexedDB, etc.)
- Server's
Databaseindb.rsfollows the same pattern
Adding New Server Routes
- Create a module in
routes/ - Implement
pub fn routes() -> Router<AppState> - Merge into the router in
routes/mod.rs - Access shared state via
State(state): State<AppState>