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>
543 lines
14 KiB
Markdown
543 lines
14 KiB
Markdown
# 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
|
|
|
|
```rust
|
|
// 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)
|
|
|
|
```bash
|
|
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):
|
|
|
|
```rust
|
|
// 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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
1. Mule authenticates with source server
|
|
2. Mule picks up queued outbound messages (encrypted blobs)
|
|
3. Mule physically travels to destination
|
|
4. Mule delivers blobs to destination server
|
|
5. Mule carries back delivery receipts
|
|
6. Receipt enforcement: no receipts = no new pickup
|
|
|
|
---
|
|
|
|
## Custom Client Development
|
|
|
|
### Using warzone-protocol as a Library
|
|
|
|
Add to your `Cargo.toml`:
|
|
|
|
```toml
|
|
[dependencies]
|
|
warzone-protocol = { path = "../warzone/crates/warzone-protocol" }
|
|
```
|
|
|
|
Core operations:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```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-lipo` or Swift package with C FFI
|
|
- **Android:** via `cargo-ndk` with JNI bindings
|
|
- Same crypto, same wire format, full interop
|
|
|
|
---
|
|
|
|
## Notification Integration (future)
|
|
|
|
### ntfy Concept
|
|
|
|
[ntfy](https://ntfy.sh) 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`:
|
|
|
|
```rust
|
|
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()`:
|
|
|
|
```rust
|
|
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()`:
|
|
|
|
```rust
|
|
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()`:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```rust
|
|
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)
|
|
|
|
```rust
|
|
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.
|