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>
22 KiB
Warzone Messenger (featherChat) — Security Model & Threat Analysis
Version: 0.0.20 Last Updated: 2026-03-28
Threat Model
What Is Protected
| Asset | Protection |
|---|---|
| Message content | E2E encrypted (ChaCha20-Poly1305 via Double Ratchet) |
| Message integrity | AEAD authentication + Ed25519 signatures |
| Past messages | Forward secrecy (Double Ratchet DH ratchet) |
| Future messages | Future secrecy (recovery after DH ratchet step) |
| Identity seed at rest | Argon2id + ChaCha20-Poly1305 passphrase encryption |
| Identity portability | BIP39 mnemonic (24 words) |
| Session state | Encrypted backup (HKDF + ChaCha20-Poly1305) |
| Pre-key authenticity | Ed25519 signature on signed pre-keys |
| Key exchange integrity | X3DH with 3-4 DH operations |
What Is NOT Protected (Current)
| Asset | Exposure |
|---|---|
| Sender/recipient metadata | Server sees who talks to whom |
| Message timing | Server sees when messages are sent/received |
| Group membership | Server stores group member lists in plaintext |
| Alias ↔ fingerprint mapping | Server stores this mapping |
| Message sizes | Server sees encrypted message sizes |
| Online/offline status | Server knows when clients connect via WebSocket |
| IP addresses | Server sees client IP addresses |
Trust Boundaries
┌─────────────────────────────────────────────────────┐
│ TRUSTED: Client device │
│ - Seed in memory (after unlock) │
│ - Ratchet state │
│ - Plaintext messages (in memory and local DB) │
├─────────────────────────────────────────────────────┤
│ SEMI-TRUSTED: Server │
│ - Sees metadata (who, when, how much) │
│ - Cannot read message content │
│ - Cannot forge messages (no signing keys) │
│ - Could drop, delay, or reorder messages │
│ - Could serve wrong pre-key bundles (MITM) │
│ → Mitigated by fingerprint verification │
│ → Future: DNS key transparency │
├─────────────────────────────────────────────────────┤
│ UNTRUSTED: Network │
│ - All traffic encrypted (TLS + E2E) │
│ - Passive observer sees TLS-encrypted WebSocket │
│ - Active attacker cannot read or forge messages │
├─────────────────────────────────────────────────────┤
│ UNTRUSTED: Other users │
│ - Cannot read messages not addressed to them │
│ - Cannot impersonate others (no access to seed) │
│ - Trust established via TOFU or fingerprint verify │
└─────────────────────────────────────────────────────┘
Cryptographic Primitives
Why Each Primitive Was Chosen
| Primitive | Used For | Why This One |
|---|---|---|
| Ed25519 | Signing, identity | Fast, compact (32-byte keys), no side-channel concerns, widely audited. Deterministic signatures (no nonce reuse risk). |
| X25519 | Key exchange (DH) | Paired with Ed25519, same curve family. Constant-time implementations. Used by Signal, WireGuard, Noise. |
| ChaCha20-Poly1305 | AEAD encryption | Stream cipher — no padding oracle attacks. Faster than AES on platforms without AES-NI (ARM, WASM). Used by TLS 1.3, WireGuard, Signal. |
| HKDF-SHA256 | Key derivation | Standard (RFC 5869). Clean domain separation via info strings. Used throughout Signal protocol. |
| SHA-256 | Fingerprints | Ubiquitous, well-understood. 128-bit truncation for fingerprints provides sufficient collision resistance for this use case. |
| Argon2id | Passphrase KDF | Winner of the Password Hashing Competition. Memory-hard (resists GPU/ASIC attacks). id variant provides resistance to both side-channel and GPU attacks. |
| secp256k1 ECDSA | Ethereum compatibility | Required for Ethereum address derivation and wallet interop. Not used for messaging crypto. |
| Keccak-256 | Ethereum addresses | Required by Ethereum spec. Only used for address derivation, not for messaging. |
Crate Security
All crypto crates are widely audited Rust implementations:
| Crate | Notes |
|---|---|
ed25519-dalek 2.x |
Maintained by dalek-cryptography team |
x25519-dalek 2.x |
Same team, constant-time DH |
chacha20poly1305 0.10 |
RustCrypto project, audited |
hkdf 0.12 |
RustCrypto project |
sha2 0.10 |
RustCrypto project |
argon2 0.5 |
RustCrypto project, Argon2id support |
k256 0.13 |
RustCrypto secp256k1 implementation |
rand 0.8 |
OS-provided randomness (OsRng) |
zeroize 1.x |
Secure memory zeroing (Seed, keys) |
Key Derivation Paths
All keys are derived from a single 32-byte seed using HKDF-SHA256 with distinct info strings for domain separation:
Seed (32 bytes)
│
├─ HKDF(ikm=seed, salt="", info="warzone-ed25519") → Ed25519 signing key
│
├─ HKDF(ikm=seed, salt="", info="warzone-x25519") → X25519 encryption key
│
├─ HKDF(ikm=seed, salt="", info="warzone-secp256k1") → secp256k1 key (Ethereum)
│
└─ HKDF(ikm=seed, salt="", info="warzone-history") → History encryption key
X3DH Key Derivation
DH1 = our_identity_x25519 * their_signed_pre_key
DH2 = our_ephemeral * their_identity_x25519
DH3 = our_ephemeral * their_signed_pre_key
DH4 = our_ephemeral * their_one_time_pre_key (optional)
shared_secret = HKDF(ikm=DH1||DH2||DH3[||DH4], salt="", info="warzone-x3dh")
Double Ratchet Key Derivation
Root KDF: HKDF(ikm=DH_output, salt=root_key, info="warzone-rk")
→ (new_root_key, chain_key)
Chain KDF: HKDF(ikm=chain_key, salt="", info="warzone-ck")
→ (new_chain_key, message_key)
Sender Key Derivation
Message key: HKDF(ikm=chain_key, salt="", info="wz-sk-msg-{generation}-{counter}")
Chain step: HKDF(ikm=chain_key, salt="", info="wz-sk-chain")
Seed Encryption
salt = random(16 bytes)
key = Argon2id(passphrase, salt) → 32 bytes
nonce = random(12 bytes)
ciphertext = ChaCha20-Poly1305(key, nonce, seed)
File: WZS1(4) || salt(16) || nonce(12) || ciphertext(48)
Forward Secrecy
What Forward Secrecy Means
If an attacker compromises your current keys, they cannot decrypt past messages. Each message uses a unique key derived from the ratchet state, and old keys are deleted after use.
How the Double Ratchet Provides It
Message 1: chain_key_0 → message_key_0 (used, then deleted)
Message 2: chain_key_1 → message_key_1 (used, then deleted)
...
DH Ratchet step: new DH exchange → new root_key → new chain
Message N: chain_key_N → message_key_N (used, then deleted)
Symmetric ratchet: Each message key is derived from the previous chain key, then the old chain key is overwritten. Compromise of the current chain key reveals future messages in this chain, but not past ones.
DH ratchet: Periodically, a new Diffie-Hellman exchange occurs (new ephemeral keypair). This "heals" from compromise — even if the current chain key is stolen, after the next DH ratchet step, the attacker is locked out again.
Forward Secrecy Properties
| Scenario | Past Messages | Future Messages |
|---|---|---|
| Current message key compromised | Safe | Safe |
| Current chain key compromised | Safe | At risk until next DH ratchet |
| DH private key compromised | Safe | At risk until next DH ratchet |
| Seed compromised | At risk (all) | At risk (all) |
Key insight: Compromising the seed compromises everything because all keys derive from it. Protect the seed above all else.
Skipped Message Keys
The Double Ratchet caches up to 1,000 skipped message keys to handle out-of-order delivery. These cached keys are a temporary window — they allow decryption of delayed messages but represent stored key material.
Sender Keys: Trade-offs for Groups
Efficiency vs Forward Secrecy
Double Ratchet (1:1): O(1) encrypt, O(1) decrypt. Perfect forward secrecy per message.
Sender Keys (groups): O(1) encrypt (one ciphertext for all members), O(1) decrypt per member. Forward secrecy per sender key chain, but NOT per-message.
What Sender Keys Provide
- Forward ratcheting: each message advances the chain. Once a message key is derived and used, the chain key moves forward irreversibly.
- Compromise of the current chain key reveals future messages in this generation, but not past messages.
- Key rotation on member join/leave: all members generate new sender keys.
What Sender Keys Do NOT Provide
- No per-message forward secrecy: Unlike Double Ratchet, there is no DH ratchet step per message. The symmetric chain ratchets forward, but a compromised chain key reveals all future messages until rotation.
- No future secrecy from member removal: When a member is kicked, all remaining members must rotate their sender keys. If the kicked member cached the old chain key before rotation, they can read messages encrypted before the rotation completes.
When This Matters
For groups under 50 members (the target), Sender Keys are a reasonable trade-off. For larger or higher-security groups, MLS (Message Layer Security, RFC 9420) would be more appropriate. MLS is not implemented and is not planned for the near term.
Seed Security
Encryption at Rest
The seed file uses defense-in-depth:
- Argon2id with default parameters for memory-hard key derivation
- ChaCha20-Poly1305 for authenticated encryption
- Random salt (16 bytes) prevents rainbow tables
- Random nonce (12 bytes) prevents nonce reuse
- File permissions set to 0600 on Unix
Passphrase Strength
Argon2id makes brute-force expensive, but the passphrase is still the weak link. Users should choose strong passphrases. An empty passphrase stores the seed in plaintext (for testing only).
Memory Safety
The Seed struct implements Zeroize and ZeroizeOnDrop from the zeroize crate. When the seed goes out of scope, its memory is securely overwritten with zeros.
Hardware Wallet Concept (future)
The ideal: seed never leaves a hardware device. Ed25519/X25519 operations delegated to Ledger/Trezor. Session key delegation allows daily use without touching the hardware wallet for every message.
Server Trust Model
What the Server Can Do
- See metadata: sender fingerprint, recipient fingerprint, timestamp, message size
- Drop messages: silently discard messages (denial of service)
- Delay messages: hold messages before delivery
- Reorder messages: deliver messages in a different order
- Serve wrong pre-key bundles: MITM attack on key exchange (mitigated by fingerprint verification)
- See group membership: group member lists are stored in plaintext
- See online status: WebSocket connections reveal when users are online
- See alias mappings: alias ↔ fingerprint relationships
What the Server CANNOT Do
- Read message content: all messages are E2E encrypted
- Forge messages: the server does not have users' signing keys
- Derive keys: the server never has seeds, private keys, or session keys
- Decrypt past messages: even if the server is later compromised, stored ciphertext remains unreadable
- Modify messages undetected: AEAD authentication detects tampering
MITM via Pre-Key Substitution
The most serious server attack: serve a malicious pre-key bundle (the server's own) instead of the recipient's real bundle. The sender would unknowingly encrypt to the server.
Current mitigation: Users verify fingerprints out-of-band. If the fingerprint matches, the pre-key bundle is authentic (because the signed pre-key is signed by the identity key corresponding to the fingerprint).
Future mitigation: DNS key transparency — users publish their public keys in DNS TXT records with self-signatures. The server cannot forge these records without the user's private key.
Alias System Security
TTL and Reclamation
- Aliases expire after 365 days of inactivity
- 30-day grace period after expiry: only the recovery key holder can reclaim
- After grace period: anyone can register the alias
- Activity auto-renews: sending messages, WebSocket activity
This prevents:
- Squatting: unused aliases are reclaimed
- Immediate takeover: grace period gives the original owner time to recover
Recovery Keys
- Generated server-side (16 random bytes, displayed as 32 hex chars)
- Rotated on each recovery operation
- Stored alongside the alias record
- The only way to reclaim an alias after losing access to the associated fingerprint
Admin Capabilities
The server admin (via WARZONE_ADMIN_PASSWORD) can:
- Remove any alias (
/alias/admin-remove) - Access is password-protected but not cryptographically authenticated
The admin cannot:
- Read messages
- Impersonate users
- Modify pre-key bundles without detection
WASM Security
Same Crypto as Native
The web client uses the exact same Rust code compiled to WebAssembly:
warzone-protocolcompiled viawasm-pack- X3DH, Double Ratchet, ChaCha20-Poly1305 — identical implementations
- CLI-to-web and web-to-CLI messages are fully interoperable
Randomness
WASM uses OsRng from the rand crate, which maps to crypto.getRandomValues() in the browser. This is a cryptographically secure random number generator provided by the Web Crypto API.
Key Storage Limitations
| Concern | Native CLI | Web Client |
|---|---|---|
| Seed storage | Encrypted file (Argon2id) | localStorage (plaintext hex) |
| Session persistence | sled DB on disk | localStorage (base64) |
| Memory protection | Zeroize on drop | WASM linear memory (no zeroize guarantee) |
| Process isolation | OS process isolation | Browser sandbox |
| Side channels | Constant-time crypto | Same (WASM), but JS interop may leak timing |
Key risk: The web client stores the seed as plaintext hex in localStorage. Any XSS vulnerability could steal the seed. The native client encrypts the seed with a passphrase.
No OTPKs in Web Client
The web client does not generate one-time pre-keys because localStorage cannot reliably store secrets that must be used exactly once. X3DH works without DH4 (the one-time pre-key DH). This means:
- The key exchange still produces a shared secret from 3 DH operations
- Anti-replay protection is slightly weaker (an attacker who captures the initial key exchange message could replay it if the server does not enforce OTP key deletion)
- The server-side dedup tracker provides partial replay protection
Known Weaknesses and Mitigations Planned
1. No Sealed Sender
Weakness: The server sees sender and recipient fingerprints.
Mitigation (Phase 6): Sealed sender — the server only sees the recipient, not the sender. The sender's identity is encrypted inside the E2E envelope.
2. No Traffic Analysis Protection
Weakness: Message timing and sizes reveal communication patterns.
Mitigation (Phase 6): Traffic padding and shaping. Optional onion routing between federated servers.
3. Web Client Seed Exposure
Weakness: Seed stored as plaintext hex in localStorage.
Mitigation (planned): Use Web Crypto API's CryptoKey with non-extractable flag. Derive encryption keys inside a Service Worker. Prompt for passphrase on load.
4. No Key Transparency
Weakness: Server could serve malicious pre-key bundles (MITM).
Mitigation (Phase 3): DNS TXT records with self-signed public keys. Cross-check server response against DNS.
5. Server Metadata Exposure
Weakness: Group membership, alias mappings, online status visible to server.
Mitigation (partial, Phase 6): Sealed sender hides sender identity. Group membership could be encrypted to group members only.
6. No Post-Quantum Crypto
Weakness: All key exchanges use classical DH (X25519). A future quantum computer could break stored ciphertext.
Mitigation (future): Hybrid key exchange (X25519 + ML-KEM/Kyber). The HKDF construction supports this naturally — concatenate classical and post-quantum shared secrets as HKDF input.
7. Auth Token Storage
Weakness: Challenge nonces stored in memory (not persisted). Server restart clears pending challenges.
Mitigation (planned): Store challenges in sled DB with TTL.
Comparison with Other Messengers
vs Signal
| Aspect | Warzone | Signal |
|---|---|---|
| Protocol | X3DH + Double Ratchet (same) | X3DH + Double Ratchet |
| Groups | Sender Keys | Sender Keys (+ considering MLS) |
| Identity | Seed-based (BIP39 mnemonic) | Phone number based |
| Server | Self-hosted, open source | Centralized |
| Federation | Planned (DNS-based) | No federation |
| Offline delivery | Mule protocol (planned) | Push notification only |
| Metadata protection | Not yet (planned) | Sealed sender, SGX enclaves |
| Audit | Not audited | Extensively audited |
| Mobile | Not yet (planned) | Native iOS/Android apps |
| Phone requirement | No phone needed | Requires phone number |
vs Matrix (Element)
| Aspect | Warzone | Matrix/Element |
|---|---|---|
| Protocol | Signal protocol (X3DH + DR) | Olm/Megolm (Double Ratchet variant) |
| Groups | Sender Keys | Megolm (similar concept) |
| Federation | Planned (DNS TXT) | Built-in (homeserver federation) |
| Complexity | Minimal (5 crates) | Complex (homeserver, synapse) |
| Encryption by default | Always E2E | Optional (not all rooms) |
| Binary size | Single static binary | Python/Node server + Electron client |
| Offline/mule | Designed for it | Not designed for offline |
vs SimpleX
| Aspect | Warzone | SimpleX |
|---|---|---|
| Identity | Seed-based fingerprint | No user identity (contact-level) |
| Metadata | Server sees sender/recipient | Server sees neither (relays) |
| Groups | Sender Keys | Per-member encryption |
| Federation | Planned (DNS) | Relay-based |
| Offline delivery | Mule protocol (planned) | Queue-based |
| Simplicity | Single binary, sled DB | Haskell server, complex setup |
| Ethereum integration | Built-in | None |
Key Differentiators
- Seed-based identity with BIP39 mnemonic — no phone numbers, no accounts, portable across devices
- Dual-curve identity — same seed produces both messaging keys and Ethereum address
- Designed for warzone conditions — mule protocol, transport abstraction, offline-first
- Single static binary — no runtime dependencies, easy deployment
- WASM web client with identical crypto — no JS crypto, same Rust code
- Self-hosted by design — no dependency on centralized infrastructure