Files
featherChat/warzone/docs/SECURITY.md
Siavash Sameni c2be68ca20 docs: comprehensive update all docs to v0.0.46
11 files updated to reflect current state (v0.0.22 → v0.0.46):

ARCHITECTURE.md:
- Ring tones, group calls, read receipts, markdown rendering sections
- Bot API expanded (BotFather, numeric IDs, Telegram compat)
- Admin commands, known issues, 155 tests

TASK_PLAN.md:
- All P1-P4 marked DONE with version numbers
- Additional completed work section (bots, ETH, ring tones, group calls)
- New FC-P7 (Voice & Transport): cpal, Sender Keys, WebTransport
- FC-P6-T9/T10 added

PROGRESS.md:
- Full version history table v0.0.22 through v0.0.46
- Known issues section

README.md:
- Voice calls, ring tones, group calls, read receipts, markdown, 155 tests

SECURITY.md:
- Bot API security, voice call security, admin commands sections
- Updated protection tables

USAGE.md:
- Group calls, read receipts, markdown formatting, admin commands

CLIENT.md:
- Call commands, read receipts, markdown rendering

LLM_HELP.md + LLM_BOT_DEV.md:
- Call/group call/admin commands, ring tones, per-bot numeric IDs

TESTING_E2E.md:
- Tests 16-18: ring tones, group calls, admin commands

CLAUDE.md:
- Ring tone notes, group signal endpoint, MLS roadmap

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

26 KiB

Warzone Messenger (featherChat) — Security Model & Threat Analysis

Version: 0.0.46 Last Updated: 2026-03-30


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
Friend list E2E encrypted blob (ChaCha20 + HKDF-derived key)
API write operations Bearer token middleware on all POST routes
Device sessions Kick/revoke-all, max 5 WS per fingerprint
Bot aliases Reserved suffixes (Bot/bot/_bot) enforced
DM call signaling E2E encrypted via WireMessage::CallSignal
Call room names Hashed (not plaintext) on relay

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
Bot messages Plaintext (not E2E) in v1 — bots don't hold ratchet sessions
Group call media Transport-only (QUIC TLS), not E2E — MLS planned
Admin commands No role-based auth yet (TODO: admin role system)

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  │
└─────────────────────────────────────────────────────┘

Authentication & Authorization

  • Challenge-response: Ed25519 signature over random challenge
  • Bearer tokens: 7-day TTL, required on all write endpoints
  • Auth middleware: AuthFingerprint extractor returns 401 on invalid/missing token
  • Bot tokens: separate namespace (bot:<token>), validated per-request
  • Federation: shared secret compared on WS auth frame

Protected endpoints (require bearer token):

  • messages/send, groups/, aliases/, calls/, devices/, friends, presence/batch

Public endpoints (no auth):

  • keys/:fp, messages/poll, groups GET, alias/resolve, resolve/:address, bot/*

Rate Limiting & Abuse Prevention

  • Global: 200 concurrent requests (tower ConcurrencyLimitLayer)
  • Per-fingerprint: max 5 WebSocket connections
  • Stale connections auto-cleaned on new registrations
  • Federation: auto-reconnect with 3s backoff (no amplification)

Session Recovery

On ratchet decryption failure:

  1. Corrupted session deleted from local DB
  2. Warning shown: "[session reset]"
  3. Next KeyExchange re-establishes the session automatically

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:

  1. Argon2id with default parameters for memory-hard key derivation
  2. ChaCha20-Poly1305 for authenticated encryption
  3. Random salt (16 bytes) prevents rainbow tables
  4. Random nonce (12 bytes) prevents nonce reuse
  5. 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-protocol compiled via wasm-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

Bot API Security

Bot messages are plaintext in v1 — bots do not hold Double Ratchet sessions. This is a deliberate trade-off for simplicity.

  • Per-bot numeric IDs: The Bot API translates fingerprints to per-bot numeric user IDs. A bot never sees the real fingerprints of the users it communicates with, providing a privacy layer between bots and users.
  • BotFather token storage: Bot tokens are stored in the server's sled database as bot:<token> entries. Tokens are generated server-side with 16 random bytes (32 hex characters). Treat tokens as secrets.
  • Plaintext v1: Bot messages travel as plaintext between the client and server. The client auto-detects bot aliases (suffixes Bot, bot, _bot) and skips E2E encryption. Future versions may support bot-side ratchet sessions.

Voice Call Security

DM Calls

DM call signaling (offer, answer, ICE candidates) is transmitted via WireMessage::CallSignal, which travels through the existing E2E encrypted WebSocket channel. The signaling is encrypted with the Double Ratchet session between the two peers — the server cannot read call setup metadata.

Group Calls

Group calls use the WarzonePhone QUIC SFU relay for multi-party audio mixing. Media is encrypted in transit via QUIC TLS (transport-layer encryption), but is not E2E encrypted — the relay can observe audio streams.

MLS planned: Future versions will use Message Layer Security (RFC 9420) for E2E encrypted group call media, where the relay handles only opaque ciphertext.

Room Access Control

Call room names are hashed before being sent to the WZP relay, so the relay does not see human-readable room identifiers. The relay enforces ACL checks using the featherChat bearer token for room join authorization.


Admin Commands

Command Scope Auth
/admin-calls List active calls on the server None (TODO)
/admin-unalias Remove any user's alias WARZONE_ADMIN_PASSWORD

Current limitation: /admin-calls has no authentication protection. Any connected client can invoke it. A proper admin role system (role assignment, challenge-based admin auth) is planned but not yet implemented.

/admin-unalias requires the WARZONE_ADMIN_PASSWORD environment variable to be set on the server and the client to provide the matching password.


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

  1. Seed-based identity with BIP39 mnemonic — no phone numbers, no accounts, portable across devices
  2. Dual-curve identity — same seed produces both messaging keys and Ethereum address
  3. Designed for warzone conditions — mule protocol, transport abstraction, offline-first
  4. Single static binary — no runtime dependencies, easy deployment
  5. WASM web client with identical crypto — no JS crypto, same Rust code
  6. Self-hosted by design — no dependency on centralized infrastructure