TUI:
- Split 1,756-line app.rs monolith into 7 modules (types, draw, commands, input, file_transfer, network, mod)
- Message timestamps [HH:MM], scrolling (PageUp/Down/arrows), connection status dot, unread badge
- /help command, terminal bell on incoming DM, /devices + /kick commands
- 44 unit tests (types, input, draw with TestBackend)
Server — WZP Call Infrastructure (FC-2/3/5/6/7/10):
- Call state management (CallState, CallStatus, active_calls, calls + missed_calls sled trees)
- WS call signal awareness (Offer/Answer/Hangup update state, missed call on offline)
- Group call endpoint (POST /groups/:name/call with SHA-256 room ID, fan-out)
- Presence API (GET /presence/:fp, POST /presence/batch)
- Missed call flush on WS reconnect
- WZP relay config + CORS
Server — Security (FC-P1):
- Auth enforcement middleware (AuthFingerprint extractor on 13 write handlers)
- Session auto-recovery (delete corrupted ratchet, show [session reset])
- WS connection cap (5/fingerprint) + global concurrency limit (200)
- Device management (GET /devices, POST /devices/:id/kick, POST /devices/revoke-all)
Server — Federation:
- Two-server federation via JSON config (--federation flag)
- Periodic presence sync (every 5s, full-state, self-healing)
- Message forwarding via HTTP POST with SHA-256(secret||body) auth
- Graceful degradation (peer down = queue locally)
- deliver_or_queue() replaces push-or-queue in ws.rs + messages.rs
Client — Group Messaging:
- SenderKeyDistribution storage + GroupSenderKey decryption in TUI
- sender_keys sled tree in LocalDb
WASM:
- All 8 WireMessage variants handled (no more "unsupported")
- decrypt_group_message() + create_sender_key_from_distribution() exports
- CallSignal parsing with signal_type mapping
Docs:
- ARCHITECTURE.md rewritten with Mermaid diagrams
- README.md created
- TASK_PLAN.md with FC-P{phase}-T{task} naming
- PROGRESS.md updated to v0.0.21
WZP submodule updated to 6f4e8eb (IAX2 trunking, adaptive quality, metrics, all S-tasks done)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
22 KiB
Warzone Messenger (featherChat) — Architecture
Version: 0.0.21 Status: Phase 1 + Phase 2 + WZP Integration + Federation
High-Level Architecture
graph TB
subgraph Clients
CLI["CLI Client<br/>(warzone)"]
TUI["TUI Client<br/>(ratatui)"]
WEB["Web Client<br/>(WASM)"]
end
subgraph Protocol["warzone-protocol (shared library)"]
ID["Identity<br/>Ed25519 + X25519"]
X3DH["X3DH<br/>Key Agreement"]
DR["Double Ratchet<br/>Forward Secrecy"]
SK["Sender Keys<br/>Group Encryption"]
WIRE["WireMessage<br/>8 variants"]
end
subgraph ServerA["warzone-server (Alpha)"]
API_A["REST API<br/>(axum)"]
WS_A["WebSocket<br/>Relay"]
AUTH_A["Auth<br/>Middleware"]
CALLS_A["Call State<br/>Manager"]
FED_A["Federation<br/>Module"]
DB_A["sled DB<br/>7 trees"]
end
subgraph ServerB["warzone-server (Bravo)"]
API_B["REST API"]
WS_B["WebSocket Relay"]
FED_B["Federation Module"]
DB_B["sled DB"]
end
subgraph WZP["WarzonePhone"]
RELAY["WZP Relay<br/>(QUIC SFU)"]
BRIDGE["Web Bridge<br/>(audio)"]
end
CLI --> Protocol
TUI --> Protocol
WEB --> Protocol
Protocol -->|"HTTP / WS"| ServerA
Protocol -->|"HTTP / WS"| ServerB
FED_A <-->|"HTTP REST<br/>HMAC-SHA256"| FED_B
ServerA -->|"Call Signaling<br/>Token Validation"| WZP
ServerB -->|"Call Signaling"| WZP
Crate Structure
graph LR
subgraph Workspace
PROTO["warzone-protocol<br/>(library, no I/O)"]
SERVER["warzone-server<br/>(axum binary)"]
CLIENT["warzone-client<br/>(CLI/TUI binary)"]
WASM["warzone-wasm<br/>(wasm-bindgen)"]
MULE["warzone-mule<br/>(future)"]
end
SERVER --> PROTO
CLIENT --> PROTO
WASM --> PROTO
MULE --> PROTO
subgraph External["WarzonePhone (submodule)"]
WZP_PROTO["wzp-proto"]
WZP_CRYPTO["wzp-crypto"]
WZP_RELAY["wzp-relay"]
WZP_WEB["wzp-web"]
end
warzone/
├── Cargo.toml # Workspace root (v0.0.21)
├── federation.example.json # Federation config template
├── crates/
│ ├── warzone-protocol/ # Core crypto & message types
│ ├── warzone-server/ # Server binary (axum + sled)
│ ├── warzone-client/ # CLI/TUI client binary
│ ├── warzone-wasm/ # WASM bridge for web client
│ └── warzone-mule/ # Mule binary (future)
├── warzone-phone/ # WZP submodule (voice/video)
└── docs/
Protocol Modules
warzone-protocol
| Module | Purpose |
|---|---|
identity |
Seed, IdentityKeyPair, PublicIdentity, Fingerprint |
mnemonic |
BIP39 mnemonic encode/decode (24 words) |
crypto |
HKDF-SHA256, ChaCha20-Poly1305 AEAD |
prekey |
SignedPreKey, OneTimePreKey, PreKeyBundle |
x3dh |
X3DH key agreement (initiate + respond) |
ratchet |
Double Ratchet state machine (MAX_SKIP=1000) |
message |
WireMessage enum (8 variants), CallSignalType |
sender_keys |
Sender Key protocol for group encryption |
history |
Encrypted backup/restore |
ethereum |
secp256k1, Keccak-256, Ethereum address derivation |
types |
Fingerprint, DeviceId, SessionId, MessageId |
warzone-server
| Module | Purpose |
|---|---|
main |
CLI args, startup, federation init |
state |
AppState, Connections, CallState, DedupTracker |
db |
7 sled trees: keys, messages, groups, aliases, tokens, calls, missed_calls |
federation |
Peer config, presence sync, message forwarding |
auth_middleware |
Bearer token extractor (401 on protected routes) |
routes/auth |
Challenge-response authentication |
routes/ws |
WebSocket relay + call signaling awareness |
routes/messages |
Send, poll (fetch-and-delete), ack |
routes/groups |
Create, join, leave, kick, members, send |
routes/calls |
Call CRUD, group call initiation |
routes/devices |
Device listing, kick, revoke-all |
routes/presence |
Online status (single + batch) |
routes/federation |
Peer presence sync + message forwarding |
routes/wzp |
WZP relay config + service token |
routes/aliases |
Alias CRUD with TTL + recovery keys |
routes/keys |
Pre-key bundle registration & retrieval |
warzone-client (TUI)
| Module | Purpose |
|---|---|
tui/mod |
Event loop, run_tui() entry point |
tui/types |
App, ChatLine, scroll/connection state |
tui/draw |
Rendering: timestamps, scroll, status dot, badge |
tui/input |
Keyboard: text editing, scroll keys |
tui/commands |
/help, /call, /devices, /kick, 20+ commands |
tui/file_transfer |
Chunked file send (DM + group) |
tui/network |
WS/HTTP polling, group decrypt, session recovery |
storage |
LocalDb: sessions, pre_keys, contacts, history, sender_keys |
warzone-wasm
| Export | Purpose |
|---|---|
WasmIdentity |
Seed generation, fingerprint, bundle |
WasmSession |
Encrypt/decrypt with Double Ratchet |
decrypt_wire_message |
Full message pipeline (all 8 variants) |
create_receipt |
Build receipt WireMessages |
decrypt_group_message |
Sender Key group decryption |
create_sender_key_from_distribution |
Build SenderKey from distribution |
self_test |
End-to-end crypto verification in WASM |
Cryptographic Stack
graph TB
PLAIN["Plaintext Message"] --> DR["Double Ratchet<br/>(per-message keys)"]
DR --> X3DH_INIT["X3DH Session Init<br/>(3-4 DH operations)"]
X3DH_INIT --> AEAD["ChaCha20-Poly1305<br/>(AEAD encryption)"]
AEAD --> SIGN["Ed25519 Signature<br/>(pre-key signing)"]
SIGN --> WIRE["WireMessage<br/>(bincode serialization)"]
WIRE --> TRANSPORT["HTTP POST / WS Binary"]
style DR fill:#2d5016,color:#fff
style AEAD fill:#1a3a5c,color:#fff
style X3DH_INIT fill:#4a1a5c,color:#fff
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 with domain separation |
| SHA-256 | sha2 |
Fingerprints, file integrity, room hashing |
| Argon2id | argon2 |
Passphrase-based seed encryption at rest |
| secp256k1 ECDSA | k256 |
Ethereum-compatible signing |
| Keccak-256 | tiny-keccak |
Ethereum address derivation |
Identity Derivation
graph LR
SEED["BIP39 Seed<br/>(32 bytes, 24 words)"]
SEED -->|"HKDF(info='warzone-ed25519')"| ED["Ed25519 Signing Key"]
SEED -->|"HKDF(info='warzone-x25519')"| X25519["X25519 Encryption Key"]
SEED -->|"HKDF(info='warzone-secp256k1')"| SECP["secp256k1 Key"]
SEED -->|"HKDF(info='warzone-history')"| HIST["History Encryption Key"]
ED -->|"SHA-256[:16]"| FP["Fingerprint<br/>xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx"]
SECP -->|"Keccak-256[-20:]"| ETH["Ethereum Address<br/>0x..."]
A single mnemonic controls: messaging identity (Ed25519 + X25519), Ethereum wallet (secp256k1), and backup encryption. WarzonePhone uses the same seed with identical HKDF parameters for shared identity (verified by 15 cross-project tests).
Wire Protocol
WireMessage Variants
graph TB
WM["WireMessage (bincode)"]
WM --> KE["KeyExchange<br/>X3DH + first ratchet msg"]
WM --> MSG["Message<br/>Double Ratchet encrypted"]
WM --> REC["Receipt<br/>Sent/Delivered/Read"]
WM --> FH["FileHeader<br/>filename, size, SHA-256"]
WM --> FC["FileChunk<br/>64KB encrypted chunks"]
WM --> GSK["GroupSenderKey<br/>Sender Key encrypted"]
WM --> SKD["SenderKeyDistribution<br/>Share key via 1:1 channel"]
WM --> CS["CallSignal<br/>Offer/Answer/Hangup/..."]
CallSignalType
Offer | Answer | IceCandidate | Hangup | Reject | Ringing | Busy
Transport Encoding
| Client | Path | Format |
|---|---|---|
| CLI/TUI | WS binary | 64 hex chars (recipient fp) + raw bincode |
| CLI/TUI | HTTP POST | JSON envelope with bincode as byte array |
| Web | WS JSON | {"to": "fingerprint", "message": [bytes]} |
| Server↔Server | HTTP POST | JSON with base64 message + HMAC auth header |
Server Architecture
Route Map
Auth-Protected (bearer token required):
POST /v1/messages/send Send encrypted message
POST /v1/groups/create|join|send|leave|kick
POST /v1/alias/register|unregister|recover|renew|admin-remove
POST /v1/keys/register|replenish
POST /v1/calls/initiate|:id/end
POST /v1/groups/:name/call Group call initiation
POST /v1/devices/:id/kick Kick a device
POST /v1/devices/revoke-all Panic button
POST /v1/presence/batch Bulk online check
Public (no auth):
GET /v1/keys/:fp Fetch pre-key bundle
GET /v1/messages/poll/:fp Fetch queued messages
GET /v1/groups/:name|list|members
GET /v1/alias/resolve/:name|list|whois/:fp
GET /v1/calls/:id|active|missed
GET /v1/presence/:fp Online status
GET /v1/devices List own devices (auth)
GET /v1/wzp/relay-config WZP relay address + token
GET /v1/federation/status Federation health
GET /v1/ws/:fp WebSocket upgrade
POST /v1/auth/challenge|verify|validate
Federation (HMAC-authenticated, server-to-server):
POST /v1/federation/presence Presence sync
POST /v1/federation/forward Message forwarding
Message Routing
flowchart TD
MSG["Incoming Message<br/>for fingerprint X"] --> DEDUP{"Dedup Check<br/>(10K FIFO)"}
DEDUP -->|Duplicate| DROP["Drop"]
DEDUP -->|New| LOCAL{"push_to_client(X)<br/>Local WS?"}
LOCAL -->|Delivered| DONE["Done"]
LOCAL -->|Not local| FED{"Federation<br/>enabled?"}
FED -->|No| QUEUE["Queue in<br/>sled DB"]
FED -->|Yes| REMOTE{"X in remote<br/>presence?"}
REMOTE -->|No| QUEUE
REMOTE -->|Yes| FORWARD["HTTP POST to peer<br/>/v1/federation/forward"]
FORWARD -->|Success| DONE
FORWARD -->|Peer down| QUEUE
style DONE fill:#2d5016,color:#fff
style DROP fill:#5c1a1a,color:#fff
style QUEUE fill:#4a3a1a,color:#fff
WebSocket Lifecycle
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /v1/ws/:fingerprint
S->>S: Check connection cap (max 5)
S->>C: WS Upgrade
Note over S: Flush queued messages
S->>C: Binary(queued_msg_1)
S->>C: Binary(queued_msg_2)
Note over S: Flush missed calls
S->>C: Text({"type":"missed_call",...})
Note over S: Register push channel
loop Real-time
C->>S: Binary(64-hex-fp + bincode)
S->>S: Dedup + Call signal awareness
S->>S: deliver_or_queue(recipient)
end
C->>S: Close
S->>S: Cleanup stale senders
Federation
graph LR
subgraph ServerAlpha["Server Alpha"]
CA["Client A<br/>Client B"]
FHA["Federation Handle"]
end
subgraph ServerBravo["Server Bravo"]
CC["Client C<br/>Client D"]
FHB["Federation Handle"]
end
FHA <-->|"Presence sync<br/>(every 5s)"| FHB
FHA -->|"Forward message<br/>(HTTP POST)"| FHB
FHB -->|"Forward message<br/>(HTTP POST)"| FHA
Configuration
Each server has a federation.json:
{
"server_id": "alpha",
"shared_secret": "long-random-string-shared-between-both",
"peer": {
"id": "bravo",
"url": "http://10.0.0.2:7700"
},
"presence_interval_secs": 5
}
Start with: warzone-server --federation federation.json
Presence Sync
Every 5 seconds, each server POSTs its connected fingerprint list to the peer:
POST /v1/federation/presence
X-Federation-Token: SHA-256(secret || body)
{ "server_id": "alpha", "fingerprints": ["aabb...", "ccdd..."], "timestamp": ... }
The receiving server replaces its remote presence set entirely. If 3 intervals pass without a sync, the remote set is cleared (peer assumed down).
Message Forwarding
sequenceDiagram
participant A as Client A (Alpha)
participant SA as Server Alpha
participant SB as Server Bravo
participant C as Client C (Bravo)
A->>SA: Send message to C
SA->>SA: push_to_client(C) — not local
SA->>SA: remote_presence.contains(C) — yes
SA->>SB: POST /v1/federation/forward<br/>X-Federation-Token: HMAC
SB->>SB: Verify HMAC
SB->>C: push_to_client(C) via WS
SB->>SA: { "delivered": true }
Degradation
| Scenario | Behavior |
|---|---|
| Peer unreachable | Message queued locally, retried on next connection |
| Presence stale (>15s) | Remote fingerprints cleared, treated as offline |
| Peer restarts | Presence repopulates within 5 seconds |
| HMAC mismatch | Request rejected with 401 |
Call Infrastructure (WZP Integration)
sequenceDiagram
participant Caller as Caller (TUI)
participant FC as featherChat Server
participant WZP as WZP Relay
Caller->>FC: WireMessage::CallSignal(Offer)
FC->>FC: Create CallState(Ringing)
FC->>FC: push_to_client(callee)
alt Callee online
FC-->>Callee: CallSignal(Offer) via WS
Callee->>FC: CallSignal(Answer)
FC->>FC: Update CallState(Active)
Note over Caller,WZP: Both connect to WZP Relay with bearer token
Caller->>WZP: QUIC + AuthToken + Handshake
Callee->>WZP: QUIC + AuthToken + Handshake
Note over WZP: Encrypted media flows (ChaCha20-Poly1305)
else Callee offline
FC->>FC: Record missed call in sled
Note over FC: Flushed on callee's next WS connect
end
Caller->>FC: CallSignal(Hangup)
FC->>FC: Update CallState(Ended)
Server Endpoints
| Endpoint | Purpose |
|---|---|
POST /v1/calls/initiate |
Create call (returns call_id) |
GET /v1/calls/:id |
Get call state |
POST /v1/calls/:id/end |
End a call |
GET /v1/calls/active |
List active calls |
POST /v1/calls/missed |
Get & clear missed calls |
POST /v1/groups/:name/call |
Group call (fan-out to members) |
GET /v1/presence/:fp |
Check if peer is online |
GET /v1/wzp/relay-config |
Get relay address + service token |
Group Call Room ID
room_id = hex(SHA-256("featherchat-group:" + group_name)[:16])
Deterministic, 32 hex chars. Prevents leaking group name to relay via QUIC SNI.
Device Management
flowchart LR
USER["User with<br/>3 devices"] --> LIST["GET /v1/devices<br/>(lists all sessions)"]
USER --> KICK["POST /v1/devices/:id/kick<br/>(force-close one)"]
USER --> REVOKE["POST /v1/devices/revoke-all<br/>(nuke all except current)"]
KICK --> CLOSE["WS channel closed<br/>+ token invalidated"]
REVOKE --> NUKE["All WS closed<br/>+ all tokens cleared"]
- Max 5 WS connections per fingerprint
- Stale connections auto-cleaned on new registrations
/devicesand/kick <id>available as TUI commands
Security Model
What's Protected
| Layer | Protection | Mechanism |
|---|---|---|
| Message content | E2E encrypted | ChaCha20-Poly1305 via Double Ratchet |
| Forward secrecy | Per-message keys | DH ratchet step on direction change |
| Session establishment | Authenticated | X3DH with signed pre-keys |
| Identity | Deterministic from seed | HKDF with domain separation |
| Seed at rest | Encrypted | Argon2id passphrase KDF |
| API writes | Auth-gated | Bearer token middleware (401) |
| Inter-server | Authenticated | SHA-256(secret || body) token |
| WS connections | Rate-limited | 5 per fingerprint, 200 global |
| WZP relay | Token-gated | featherChat bearer token validation |
What's NOT Protected (Phase 1 scope)
| Data | Exposure |
|---|---|
| Sender/recipient metadata | Server sees routing info |
| Message timing | Server sees timestamps |
| Online/offline status | Server tracks WS connections |
| Group membership | Server stores plaintext member list |
| IP addresses | Server logs (standard for HTTP) |
Planned mitigations: sealed sender (Phase 6), onion routing, metadata encryption.
Trust Boundaries
graph TB
subgraph TRUSTED["Trusted: Your Device"]
SEED["Seed in memory"]
LDB["Local sled DB"]
end
subgraph SEMI["Semi-Trusted: Server"]
SRVR["Sees metadata<br/>Can't read messages"]
end
subgraph UNTRUSTED["Untrusted: Network"]
NET["TLS protects transport"]
end
TRUSTED -->|"E2E encrypted + TLS"| SEMI
SEMI -->|"TLS"| UNTRUSTED
Storage Model
Server sled Trees (7)
| Tree | Key Format | Value |
|---|---|---|
keys |
<fingerprint> |
bincode PreKeyBundle |
messages |
queue:<fp>:<uuid> |
raw bincode WireMessage |
groups |
<group_name> |
JSON GroupInfo |
aliases |
a:<alias>, fp:<fp>, rec:<alias> |
Various |
tokens |
<token_hex> |
JSON {fp, expires_at} |
calls |
<call_id> |
JSON CallState |
missed_calls |
missed:<fp>:<call_id> |
JSON {caller, timestamp} |
Client sled Trees (5)
| Tree | Key Format | Value |
|---|---|---|
sessions |
<peer_fp_hex> |
bincode RatchetState |
pre_keys |
spk:<id>, otpk:<id> |
32-byte StaticSecret |
contacts |
<fingerprint> |
JSON contact record |
history |
hist:<fp>:<ts>:<uuid> |
JSON message record |
sender_keys |
sk:<fp>:<group> |
bincode SenderKey |
Test Coverage
| Crate | Tests | Coverage |
|---|---|---|
| warzone-protocol | 28 | X3DH, Double Ratchet, Sender Keys, AEAD, HKDF, identity, ethereum, prekeys, mnemonic |
| warzone-client (types) | 10 | App init, scroll, connected, timestamps, normfp |
| warzone-client (input) | 25 | Text editing, cursor movement, scroll keys, quit |
| warzone-client (draw) | 9 | Rendering, timestamps, connection dot, scroll, unread badge |
| Total | 72 | All passing |
WZP side: 15 cross-project identity tests + 17 integration tests (separate repo).
Data Flow Diagrams
1:1 Direct Message (First Contact)
sequenceDiagram
participant A as Alice
participant S as Server
participant B as Bob
A->>S: GET /v1/keys/:bob_fp
S->>A: PreKeyBundle (bincode)
Note over A: X3DH initiate(bundle)<br/>Double Ratchet init_alice()<br/>ratchet.encrypt("hello")
A->>S: WireMessage::KeyExchange
S->>B: Push via WS (or queue)
Note over B: X3DH respond(spk_secret)<br/>init_bob()<br/>ratchet.decrypt() = "hello"
B->>S: WireMessage::Receipt(Delivered)
S->>A: Push receipt
Group Message (Sender Keys)
sequenceDiagram
participant A as Alice
participant S as Server
participant B as Bob
participant C as Carol
Note over A: SenderKey::generate("ops")
A->>S: SenderKeyDistribution (via 1:1 to Bob)
S->>B: Push distribution
A->>S: SenderKeyDistribution (via 1:1 to Carol)
S->>C: Push distribution
Note over A: sender_key.encrypt("attack")
A->>S: POST /groups/ops/send (GroupSenderKey)
S->>B: Fan-out
S->>C: Fan-out
Note over B,C: sender_key.decrypt() = "attack"
Federated Message
sequenceDiagram
participant A as Client A (Alpha)
participant SA as Server Alpha
participant SB as Server Bravo
participant C as Client C (Bravo)
Note over SA,SB: Presence sync (every 5s)
SA->>SB: POST /federation/presence [A, B]
SB->>SA: POST /federation/presence [C, D]
A->>SA: Message for C
SA->>SA: Not local, C in remote presence
SA->>SB: POST /federation/forward (HMAC auth)
SB->>C: Push via local WS
SB->>SA: { "delivered": true }
Extensibility
Adding New WireMessage Variants
- Add variant to
WireMessageinwarzone-protocol/src/message.rs - Update
extract_message_id()inroutes/messages.rsandroutes/ws.rs - Handle in
tui/network.rs(process_wire_message) - Handle in
warzone-wasm/src/lib.rs(decrypt_wire_message) - bincode serialization is automatic
Adding New Server Routes
- Create module in
routes/ - Implement
pub fn routes() -> Router<AppState> - Merge in
routes/mod.rs - Add
_auth: AuthFingerprintfor write endpoints
Adding Federation Peers (Future)
Current: 1 peer via JSON config. Future: N peers via config array or DNS discovery. The deliver_or_queue() method would iterate over peers checking remote presence.