Files
featherChat/warzone/docs/ARCHITECTURE.md
Siavash Sameni f8eaf30bb4 refactor: federation uses persistent WS instead of HTTP polling
- Server-to-server communication via WebSocket at /v1/federation/ws
- Auth as first WS frame (shared secret), presence + forwards over same connection
- Auto-reconnect every 3s on disconnect, instant presence push on connect
- Replaces HTTP REST polling (no more 5s intervals, lower latency)
- Removed dead HMAC helpers (auth is now direct secret comparison over WS)
- Simplified ARCHITECTURE.md mermaid diagrams for Gitea rendering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:56:13 +04:00

21 KiB

Warzone Messenger (featherChat) — Architecture

Version: 0.0.21 Status: Phase 1 + Phase 2 + WZP Integration + Federation


High-Level Architecture

graph TB
    CLI[CLI Client] --> PROTO[warzone-protocol]
    TUI[TUI Client] --> PROTO
    WEB[Web Client WASM] --> PROTO
    PROTO -->|HTTP / WS| SRVA[Server Alpha]
    PROTO -->|HTTP / WS| SRVB[Server Bravo]
    SRVA <-->|Federation WS| SRVB
    SRVA -->|Call Signaling| WZP[WarzonePhone Relay]
    SRVB -->|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 WS JSON JSON frames over persistent federation WS

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 Alpha[Server Alpha]
        CA[Client A + B]
    end
    subgraph Bravo[Server Bravo]
        CC[Client C + D]
    end
    Alpha <-->|Persistent WS\nPresence + Forward| Bravo

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"
  }
}

Start with: warzone-server --federation federation.json

Presence Sync

On startup each server opens a persistent WebSocket to its peer and authenticates with the shared secret. Presence updates and message forwards flow over this single connection:

WS /v1/federation/ws
Auth: {"type":"auth","secret":"HMAC(shared_secret)"}
Presence: {"type":"presence","fingerprints":["aabb...","ccdd..."]}
Forward:  {"type":"forward","to":"<fp>","message":"<base64>"}

The receiving server replaces its remote presence set on each presence frame. If the WebSocket drops, the server auto-reconnects every 3 seconds and re-sends its full presence list.

Message Forwarding

sequenceDiagram
    participant SA as Server Alpha
    participant SB as Server Bravo

    Note over SA,SB: Persistent WS connection
    SA->>SB: {"type":"auth","secret":"..."}
    SA->>SB: {"type":"presence","fingerprints":["A","B"]}
    SB->>SA: {"type":"presence","fingerprints":["C","D"]}

    Note over SA: Client A sends message to C
    SA->>SB: {"type":"forward","to":"C","message":"base64..."}
    Note over SB: Deliver to Client C via local WS

Degradation

Scenario Behavior
WS disconnected Auto-reconnect every 3s, messages queue locally
Peer restarts Presence repopulates on WS reconnect
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
  • /devices and /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: Persistent WS between servers
    SA->>SB: presence ["A","B"]
    SB->>SA: presence ["C","D"]

    A->>SA: Message for C
    SA->>SA: Not local, C in remote presence
    SA->>SB: forward to C via federation WS
    SB->>C: Push via local WS

Extensibility

Adding New WireMessage Variants

  1. Add variant to WireMessage in warzone-protocol/src/message.rs
  2. Update extract_message_id() in routes/messages.rs and routes/ws.rs
  3. Handle in tui/network.rs (process_wire_message)
  4. Handle in warzone-wasm/src/lib.rs (decrypt_wire_message)
  5. bincode serialization is automatic

Adding New Server Routes

  1. Create module in routes/
  2. Implement pub fn routes() -> Router<AppState>
  3. Merge in routes/mod.rs
  4. Add _auth: AuthFingerprint for 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.