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>
14 KiB
Warzone Messenger (featherChat) — Integration & Extensibility Guide
Version: 0.0.20
Items marked with (future) are designed but not yet implemented.
WarzonePhone Integration (future)
WarzonePhone is envisioned as a separate project for encrypted voice/video calls, sharing infrastructure with the messenger.
Shared Components
- Identity: Same BIP39 seed and fingerprint. One identity for messaging + calls.
- Server infrastructure: Same server hosts both message relay and SRTP/VoIP signaling.
- Pre-key bundles: Reuse X3DH bundles for call setup (SRTP key exchange).
- Contact list: Shared aliases and contact metadata.
Voice Messages
Before VoIP is built, voice messages can be sent as file attachments:
/file voice-message.opus
The /file command already supports arbitrary file transfer up to 10 MB. An Opus audio file at 32 kbps allows ~40 minutes per message.
Integration Pattern
warzone-protocol (shared)
│
┌─────┴──────┐
│ │
warzone-client warzone-phone
(messaging) (VoIP, future)
Both binaries link against warzone-protocol for identity, key exchange, and encryption.
Ethereum / Web3 Integration
Current Implementation (v0.0.20)
The ethereum module in warzone-protocol provides:
- secp256k1 keypair derived from the BIP39 seed via
HKDF(seed, info="warzone-secp256k1") - Ethereum address computation:
Keccak-256(uncompressed_pubkey[1:])[-20:] - EIP-55 checksummed addresses
- ECDSA signing and verification (secp256k1)
- CLI command:
warzone eth - TUI command:
/eth
MetaMask / Wallet Connect (future)
Planned integration flow:
1. User clicks "Connect Wallet" in web client
2. Web client requests eth_sign(challenge) from MetaMask
3. Server verifies secp256k1 signature
4. Server maps Ethereum address → Warzone fingerprint
5. Session established
Challenge: MetaMask signs with secp256k1, but Warzone messaging
uses Ed25519/X25519. The wallet connect only proves ownership of
the Ethereum address — a separate X3DH session is still needed
for E2E encryption.
ENS Resolution (future)
Planned: resolve ENS names to Warzone fingerprints.
@vitalik.eth → resolve ENS → 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
→ server lookup → Warzone fingerprint
→ /peer @vitalik.eth
Implementation would use alloy or ethers-rs for ENS resolution.
Hardware Wallet Support (future)
Ledger and Trezor natively support secp256k1. Integration plan:
- Seed lives on the hardware wallet, never exported
- Ed25519 signing delegated to device (BIP44 path
m/44'/1234'/0') - X25519 derived from Ed25519 or separate derivation path
- Session key delegation: sign once per 30 days, client uses delegated key for daily operations
Session Delegation (future)
For hardware wallets that cannot be used for every message:
Hardware wallet signs: "I delegate signing to ephemeral key X for 30 days"
Client stores ephemeral key in memory
All messages signed with ephemeral key
Contacts verify delegation chain: HW_pubkey → delegation_cert → ephemeral_sig
OIDC Integration (future)
For organizational deployments, an OIDC provider can gate registration and associate corporate identities.
Concept
1. User authenticates with corporate IdP (Okta, Azure AD, etc.)
2. IdP issues OIDC token containing email/groups
3. User presents OIDC token to Warzone server during registration
4. Server verifies token, associates fingerprint with corporate identity
5. Optional: server restricts messaging to verified users only
Benefits:
- Gated registration (only org members can register)
- Corporate directory integration (resolve by email)
- Audit trail (fingerprint ↔ corporate identity mapping)
- Seed recovery via corporate identity (re-register)
Implementation Pattern
// Future: auth middleware
async fn register_with_oidc(
State(state): State<AppState>,
bearer: TypedHeader<Authorization<Bearer>>,
Json(req): Json<RegisterRequest>,
) -> AppResult<Json<Value>> {
let claims = verify_oidc_token(&bearer.token())?;
// Associate claims.email with req.fingerprint
// Only allow registration if claims are valid
}
DNS Federation (future)
Server Discovery
Each Warzone server publishes a DNS TXT record:
_warzone._tcp.example.com TXT "v=wz1; endpoint=https://wz.example.com; pubkey=base64..."
Other servers discover peers by querying DNS:
1. User sends message to user@example.com
2. Local server: DNS TXT lookup → _warzone._tcp.example.com
3. Parse endpoint URL and server pubkey
4. TLS connection, mutual authentication
5. Deliver encrypted message blob
Key Transparency
Users publish their public keys in DNS to prevent server MITM:
_wz._id.<SHA256(fingerprint)[:16]>.example.com TXT "v=wz1; fp=a3f8...; pubkey=base64...; sig=base64..."
The sig field is a self-signature — even the DNS admin cannot forge it without the user's private key.
Alias Resolution via DNS (future)
_wz._alias.alice.example.com TXT "fp=a3f8c912..."
Transport Abstraction
The protocol is transport-agnostic. The WireMessage format is identical regardless of how it travels.
Current Transports (v0.0.20)
| Transport | Client→Server | Server→Client | Status |
|---|---|---|---|
| HTTPS | POST JSON | GET poll | Implemented |
| WebSocket | Binary/JSON | Binary push | Implemented |
Planned Transports (future)
| Transport | Range | Bandwidth | Use Case |
|---|---|---|---|
| Bluetooth | 10-100m | ~2 Mbps | Mule sync, nearby devices |
| LoRa | 2-15 km | 0.3-50 kbps | Emergency text, receipts |
| Wi-Fi Direct | ~200m | ~250 Mbps | Local group mesh |
| USB/File | Physical | Unlimited | Sneakernet, mule export |
LoRa Compact Format (future)
For LoRa's ~250 byte payload limit:
[1] version
[1] type (text=0x01, receipt=0x02, beacon=0x03)
[8] sender fingerprint (truncated)
[8] recipient fingerprint (truncated)
[4] timestamp (unix 32-bit)
[12] nonce
[~216] ciphertext (~200 chars of text)
USB / Sneakernet (future)
warzone export --since 24h --to /mnt/usb/messages.wz
# Carry USB drive to destination
warzone import /mnt/usb/messages.wz
Implementing a New Transport
Define a type that implements the transport interface (conceptual — trait not yet formalized):
// Future trait
trait Transport: Send + Sync {
async fn send(&self, endpoint: &str, blob: &[u8]) -> Result<()>;
async fn recv(&self) -> Result<Vec<u8>>;
fn name(&self) -> &str;
}
The message blob is always a bincode-serialized WireMessage. The transport only needs to deliver bytes.
Multi-Server Mode (future)
Federation
Servers communicate using mutual TLS and server-to-server protocol:
Server A Server B
│ │
│ DNS lookup: _warzone._tcp.B │
│ TLS connect + mutual auth │
│ ─── deliver encrypted blob ────────→│
│ ←── delivery receipt ───────────────│
Server-to-Server Relay
When direct connectivity is not available:
Server A → Server C (relay) → Server B
Server C is configured as a relay for B.
C queues messages for B until B reconnects.
Gossip Discovery (future)
Servers share their known peer lists:
{
"peers": [
{"domain": "wz.example.com", "pubkey": "base64...", "last_seen": 1711443600},
{"domain": "chat.org", "pubkey": "base64...", "last_seen": 1711440000}
]
}
Mule Protocol (future)
Physical message relay between disconnected networks:
- Mule authenticates with source server
- Mule picks up queued outbound messages (encrypted blobs)
- Mule physically travels to destination
- Mule delivers blobs to destination server
- Mule carries back delivery receipts
- Receipt enforcement: no receipts = no new pickup
Custom Client Development
Using warzone-protocol as a Library
Add to your Cargo.toml:
[dependencies]
warzone-protocol = { path = "../warzone/crates/warzone-protocol" }
Core operations:
use warzone_protocol::identity::Seed;
use warzone_protocol::prekey::{generate_signed_pre_key, generate_one_time_pre_keys};
use warzone_protocol::x3dh;
use warzone_protocol::ratchet::RatchetState;
use warzone_protocol::message::WireMessage;
// Generate identity
let seed = Seed::generate();
let identity = seed.derive_identity();
let pub_id = identity.public_identity();
println!("Fingerprint: {}", pub_id.fingerprint);
// Generate pre-key bundle
let (spk_secret, spk) = generate_signed_pre_key(&identity, 1);
let otpks = generate_one_time_pre_keys(1, 10);
// Initiate session (Alice side)
let x3dh_result = x3dh::initiate(&identity, &their_bundle)?;
let mut ratchet = RatchetState::init_alice(
x3dh_result.shared_secret,
x25519_dalek::PublicKey::from(their_bundle.signed_pre_key.public_key),
);
// Encrypt
let encrypted = ratchet.encrypt(b"hello")?;
// Build wire message
let wire = WireMessage::Message {
id: uuid::Uuid::new_v4().to_string(),
sender_fingerprint: pub_id.fingerprint.to_string(),
ratchet_message: encrypted,
};
let bytes = bincode::serialize(&wire)?;
WASM for Browsers
The warzone-wasm crate exposes the protocol to JavaScript:
import init, { WasmIdentity, WasmSession, decrypt_wire_message } from './warzone_wasm.js';
await init();
// Create identity
const identity = new WasmIdentity();
console.log("Fingerprint:", identity.fingerprint());
console.log("Seed:", identity.seed_hex());
// Register bundle with server
const bundleBytes = identity.bundle_bytes();
await fetch('/v1/keys/register', {
method: 'POST',
body: JSON.stringify({
fingerprint: identity.fingerprint_hex(),
bundle: Array.from(bundleBytes),
}),
});
// Create session and encrypt
const session = WasmSession.initiate(identity, theirBundleBytes);
const encrypted = session.encrypt_key_exchange(identity, theirBundleBytes, "hello");
// Decrypt incoming
const result = decrypt_wire_message(
identity.seed_hex(),
identity.spk_secret_hex(),
messageBytes,
existingSessionBase64, // null for first message
);
const parsed = JSON.parse(result);
// parsed.sender, parsed.text, parsed.session_data, parsed.message_id
Native Mobile (future)
The warzone-protocol crate compiles to any Rust target:
- iOS: via
cargo-lipoor Swift package with C FFI - Android: via
cargo-ndkwith JNI bindings - Same crypto, same wire format, full interop
Notification Integration (future)
ntfy Concept
ntfy provides push notifications without Google Play Services:
User registers topic: wz_<fingerprint_prefix>
Server pushes on new message:
POST https://ntfy.example.com/wz_a3f8c912
Body: "New message" (NO content — E2E encrypted)
User receives push → opens Warzone to read
Self-hostable alongside the Warzone server. ntfy handles Android/iOS/desktop notifications.
Metadata Consideration
ntfy sees that someone messaged a topic (user). Mitigation: self-host ntfy on the same infrastructure as the Warzone server.
How to Add New Message Types
Step 1: Extend WireMessage
In warzone-protocol/src/message.rs:
pub enum WireMessage {
// ... existing variants ...
/// Your new message type
MyNewType {
id: String,
sender_fingerprint: String,
// your fields here
},
}
bincode serialization is automatic — the variant gets a new enum tag.
Step 2: Update Server Dedup
In warzone-server/src/routes/messages.rs and routes/ws.rs, update extract_message_id():
WireMessage::MyNewType { id, .. } => Some(id),
Step 3: Handle in Clients
TUI client (warzone-client/src/tui/app.rs): Handle the new variant in the message receive/poll loop.
Web client (warzone-wasm/src/lib.rs): Add a match arm in decrypt_wire_message():
WireMessage::MyNewType { id, sender_fingerprint, .. } => {
Ok(serde_json::json!({
"type": "my_new_type",
"id": id,
"sender": sender_fingerprint,
}).to_string())
}
Step 4: Add Tests
In the protocol crate, add serialization and round-trip tests.
How to Add New Commands
TUI Commands
In warzone-client/src/tui/app.rs, inside handle_send():
if text.starts_with("/mycommand ") {
let arg = text[11..].trim();
self.add_message(ChatLine {
sender: "system".into(),
text: format!("My command: {}", arg),
is_system: true,
is_self: false,
message_id: None,
});
return;
}
Pattern: parse the command text, perform the action, add a system message for feedback.
Web Commands
In the web client JavaScript, add to the command dispatcher:
if (text.startsWith('/mycommand ')) {
const arg = text.slice(11).trim();
addSystemMessage(`My command: ${arg}`);
return;
}
How to Add New Storage Backends
Current Pattern
Both server (db.rs) and client (storage.rs) use sled directly with method wrappers:
pub struct LocalDb {
sessions: sled::Tree,
// ...
}
impl LocalDb {
pub fn save_session(&self, peer: &Fingerprint, state: &RatchetState) -> Result<()> {
let data = bincode::serialize(state)?;
self.sessions.insert(key, data)?;
Ok(())
}
}
Abstracting to Traits (future)
trait SessionStore {
fn save_session(&self, peer: &Fingerprint, state: &RatchetState) -> Result<()>;
fn load_session(&self, peer: &Fingerprint) -> Result<Option<RatchetState>>;
}
trait MessageStore {
fn queue_message(&self, to: &str, message: &[u8]) -> Result<()>;
fn poll_messages(&self, fingerprint: &str) -> Result<Vec<Vec<u8>>>;
}
// Implementations:
struct SledStore { /* ... */ }
struct SqliteStore { /* ... */ }
struct IndexedDbStore { /* ... */ } // for WASM
The key insight: all storage is key-value with prefix scanning. Any ordered KV store (sled, RocksDB, SQLite, IndexedDB, LevelDB) can serve as a backend.