From 60a7006ed913cf5e53bbebb7d61fec81d020baf1 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Thu, 26 Mar 2026 21:59:19 +0400 Subject: [PATCH] Add documentation: protocol spec, server admin, client guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/PROTOCOL.md (520 lines): - Identity model (seed → Ed25519 + X25519 via HKDF) - X3DH key exchange (4 DH operations, ASCII flow diagram) - Double Ratchet (chain/DH ratchet, skipped keys, state machine) - KDF chains with domain separation strings - AEAD (ChaCha20-Poly1305) - Wire format (WireMessage enum, bincode serialization) - Pre-key bundle format and lifecycle docs/SERVER.md (429 lines): - Build and run instructions - Full API reference with request/response examples - Database structure (sled trees) - Deployment (nginx reverse proxy, systemd unit) - Security considerations - Backup and recovery docs/CLIENT.md (507 lines): - Quick start guide - All CLI commands with examples - Identity management and mnemonic backup - Web client usage and limitations - Session and pre-key management - Threat model table - Troubleshooting guide Co-Authored-By: Claude Opus 4.6 (1M context) --- warzone/docs/CLIENT.md | 507 ++++++++++++++++++++++++++++++++++++++ warzone/docs/PROTOCOL.md | 520 +++++++++++++++++++++++++++++++++++++++ warzone/docs/SERVER.md | 429 ++++++++++++++++++++++++++++++++ 3 files changed, 1456 insertions(+) create mode 100644 warzone/docs/CLIENT.md create mode 100644 warzone/docs/PROTOCOL.md create mode 100644 warzone/docs/SERVER.md diff --git a/warzone/docs/CLIENT.md b/warzone/docs/CLIENT.md new file mode 100644 index 0000000..35d878e --- /dev/null +++ b/warzone/docs/CLIENT.md @@ -0,0 +1,507 @@ +# Warzone Client -- Operation Guide + +--- + +## 1. Installation + +### Build from Source + +Requires Rust 1.75+. + +```bash +cd warzone/ +cargo build -p warzone-client --release +``` + +The binary is at `target/release/warzone`. You can copy it anywhere or add +`target/release` to your `PATH`. + +```bash +# Optional: install to ~/.cargo/bin +cargo install --path crates/warzone-client +``` + +--- + +## 2. Quick Start + +```bash +# 1. Generate a new identity +warzone init + +# 2. Register your key bundle with a server +warzone register -s http://wz.example.com:7700 + +# 3. Send an encrypted message +warzone send a3f8:c912:44be:7d01 "Hello from Warzone" -s http://wz.example.com:7700 + +# 4. Poll for incoming messages +warzone recv -s http://wz.example.com:7700 +``` + +--- + +## 3. CLI Commands + +### warzone init + +Generate a new identity (seed, keypair, and pre-keys). + +```bash +$ warzone init +Identity generated! + +Fingerprint: b7d1:e845:0022:9f3a + +Recovery mnemonic (WRITE THIS DOWN): + + 1. abandon 2. ability 3. able 4. about + 5. above 6. absent 7. absorb 8. abstract + 9. absurd 10. abuse 11. access 12. accident +13. account 14. accuse 15. achieve 16. acid +17. acoustic 18. acquire 19. across 20. act +21. action 22. actor 23. actress 24. actual + +Seed saved to ~/.warzone/identity.seed +Generated 1 signed pre-key + 10 one-time pre-keys + +To register with a server, run: + warzone send -s http://server:7700 + +Or register your key bundle manually: + (bundle auto-registered on first send) +``` + +**What happens:** +1. Generates 32 random bytes (seed) from `OsRng`. +2. Derives Ed25519 signing key and X25519 encryption key from the seed. +3. Converts seed to a 24-word BIP39 mnemonic and displays it. +4. Saves the raw seed to `~/.warzone/identity.seed` (mode 0600 on Unix). +5. Generates 1 signed pre-key (id=1) and 10 one-time pre-keys (ids 0-9). +6. Stores pre-key secrets in the local sled database at `~/.warzone/db/`. +7. Saves the public pre-key bundle to `~/.warzone/bundle.bin`. + +--- + +### warzone recover \ + +Recover an identity from a BIP39 mnemonic. + +```bash +$ warzone recover abandon ability able about above absent absorb abstract \ + absurd abuse access accident account accuse achieve acid \ + acoustic acquire across act action actor actress actual +Identity recovered! +Fingerprint: b7d1:e845:0022:9f3a +Seed saved to ~/.warzone/identity.seed +``` + +**Note:** recovery restores the seed and keypair but does NOT restore +pre-keys or sessions. You will need to run `warzone init`-style pre-key +generation separately or your contacts will need to re-establish sessions. + +--- + +### warzone info + +Display your fingerprint and public keys. + +```bash +$ warzone info +Fingerprint: b7d1:e845:0022:9f3a +Signing key: 3a7c... (64 hex chars) +Encryption key: 9d2f... (64 hex chars) +``` + +Requires a saved identity (`~/.warzone/identity.seed`). + +--- + +### warzone register + +Register your pre-key bundle with a server. + +```bash +$ warzone register -s http://wz.example.com:7700 +Bundle registered with http://wz.example.com:7700 +``` + +**Flags:** + +| Flag | Short | Default | Description | +|------|-------|---------|-------------| +| `--server` | `-s` | `http://localhost:7700` | Server URL | + +This uploads `~/.warzone/bundle.bin` to the server. Registration is also +performed automatically on the first `send`. + +--- + +### warzone send + +Send an encrypted message to a recipient. + +```bash +$ warzone send a3f8:c912:44be:7d01 "Hello, are you safe?" -s http://wz.example.com:7700 +No existing session. Fetching key bundle for a3f8:c912:44be:7d01... +Bundle registered with http://wz.example.com:7700 +Message sent to a3f8:c912:44be:7d01 +``` + +**Arguments:** + +| Argument | Description | +|----------|-------------| +| `recipient` | Recipient fingerprint (e.g. `a3f8:c912:44be:7d01`) | +| `message` | Message text (quote if it contains spaces) | + +**Flags:** + +| Flag | Short | Default | Description | +|------|-------|---------|-------------| +| `--server` | `-s` | `http://localhost:7700` | Server URL | + +**Behavior:** +1. Auto-registers your bundle with the server (if not already done). +2. Checks for an existing Double Ratchet session with the recipient. +3. If no session exists: + - Fetches recipient's pre-key bundle from the server. + - Verifies the signed pre-key signature. + - Performs X3DH key exchange. + - Initializes the Double Ratchet as Alice (initiator). + - Sends a `WireMessage::KeyExchange` containing the X3DH parameters + and the first encrypted message. +4. If a session exists: + - Encrypts using the existing ratchet. + - Sends a `WireMessage::Message`. +5. Updates the local session state. + +--- + +### warzone recv + +Poll for and decrypt incoming messages. + +```bash +$ warzone recv -s http://wz.example.com:7700 +Polling for messages as b7d1:e845:0022:9f3a... +Received 2 message(s): + + [new session] a3f8:c912:44be:7d01: Hello, are you safe? + a3f8:c912:44be:7d01: I'm sending supplies tomorrow. +``` + +**Flags:** + +| Flag | Short | Default | Description | +|------|-------|---------|-------------| +| `--server` | `-s` | `http://localhost:7700` | Server URL | + +**Behavior:** +1. Polls `/v1/messages/poll/{our_fingerprint}`. +2. For each message: + - Deserializes the `WireMessage` from bincode. + - **KeyExchange:** loads signed pre-key secret and (if applicable) + one-time pre-key secret from local storage, performs X3DH respond, + initializes ratchet as Bob, decrypts the message, and saves the session. + - **Message:** loads existing session, decrypts with the ratchet, saves + updated session state. +3. Prints decrypted messages to stdout. + +**Note:** messages are currently NOT acknowledged after polling. They will +be returned again on the next poll. Acknowledgment is TODO. + +--- + +### warzone chat + +Launch the interactive TUI. + +```bash +$ warzone chat -s http://wz.example.com:7700 +TODO: launch TUI connected to http://wz.example.com:7700 +``` + +**Status:** not yet implemented. The TUI will use `ratatui` and `crossterm` +(dependencies are already in `Cargo.toml`). Planned for Phase 2. + +--- + +## 4. Identity Management + +### Storage Layout + +``` +~/.warzone/ + identity.seed # 32-byte raw seed (plaintext -- encryption is TODO) + bundle.bin # bincode-serialized PreKeyBundle (public data) + db/ # sled database directory + sessions/ # Double Ratchet state per peer + pre_keys/ # signed and one-time pre-key secrets +``` + +### File Permissions + +On Unix, `identity.seed` is created with mode `0600` (owner read/write only). +The sled database directory inherits default permissions. + +### Seed Security + +**Current state:** the seed is stored as **plaintext** 32 bytes. This is a +known Phase 1 limitation. + +**Planned (Phase 2):** encrypt the seed at rest using: +- Passphrase input at startup +- Argon2id key derivation from passphrase +- ChaCha20-Poly1305 encryption of the seed bytes + +### Mnemonic Backup + +The 24-word BIP39 mnemonic shown during `init` is the ONLY way to recover +your identity if you lose `~/.warzone/`. Write it down on paper and store it +securely. + +The mnemonic is displayed once at generation time and can be recovered from +the seed using the protocol library, but the CLI does not currently expose a +"show mnemonic" command. + +### Recovery + +```bash +warzone recover word1 word2 word3 ... word24 +``` + +This recreates `~/.warzone/identity.seed` with the same seed. The same +fingerprint and keypairs are derived deterministically. However: + +- Pre-keys are NOT regenerated. Run `warzone init` on a fresh directory to + generate new pre-keys (this will also generate a new seed, so you would need + to coordinate). +- Sessions are NOT recovered. All contacts will need to establish new sessions. + +**TODO:** a `recover` flow that also regenerates pre-keys without creating a +new seed. + +--- + +## 5. Web Client + +The web client is served by the server at `/`. Open it in a browser: + +``` +http://localhost:7700/ +``` + +### Features + +- **Generate New Identity:** creates a random 32-byte seed in the browser. +- **Recover from Mnemonic:** paste a hex-encoded seed (not BIP39 words; + hex encoding is used as a placeholder). +- **Chat interface:** dark-themed monospace UI with message display. +- **Commands:** + - `/help` -- show available commands + - `/info` -- show your fingerprint + - `/seed` -- display your seed (hex-encoded) + +### How It Works + +1. Seed is generated with `crypto.getRandomValues(32)`. +2. ECDH P-256 keypair is derived (not X25519 -- Web Crypto limitation). +3. Fingerprint is `SHA-256(ECDH_public_key)[0..16]` formatted as 4 hex + groups. +4. Seed is saved in `localStorage` under key `wz-seed`. +5. On page load, the client tries to auto-load a saved seed. +6. Public key is registered with the server via `POST /v1/keys/register`. +7. Messages are polled every 5 seconds from `/v1/messages/poll/{fingerprint}`. + +### Limitations + +- **No cross-client compatibility:** the web client uses P-256 while the CLI + uses X25519/Ed25519. Messages between the two cannot be decrypted. This + will be resolved in Phase 2 (WASM port of the protocol library). +- **No Double Ratchet:** message decryption is not implemented in JS. + Received messages display as `[encrypted message]`. +- **No BIP39:** seed is shown as hex bytes, not mnemonic words. +- **Unencrypted seed storage:** `localStorage` is accessible to any JS on + the same origin. + +--- + +## 6. Session Management + +### How Sessions Work + +A "session" is a Double Ratchet state between you and one peer, identified +by their fingerprint. + +1. **First message to a peer:** X3DH key exchange establishes a shared secret. + The ratchet is initialized. The session is saved in `~/.warzone/db/` + under the `sessions` tree, keyed by the peer's fingerprint (hex-encoded). + +2. **Subsequent messages:** the ratchet state is loaded, used to encrypt or + decrypt, then saved back. + +3. **Bidirectional:** both parties maintain the same session. When Bob + receives Alice's KeyExchange, he initializes his side of the ratchet. From + then on, both use `WireMessage::Message`. + +### Session Storage + +Sessions are serialized with `bincode` and stored in the `sessions` sled +tree. The key is the peer's 32-character hex fingerprint. + +### Session Reset + +There is currently no command to reset a session. If a session becomes +corrupted or out of sync: + +1. Delete the local database: `rm -rf ~/.warzone/db/` +2. Re-run `warzone init` to generate new pre-keys. +3. Re-register with the server. +4. Your contact must also reset their session with you. + +**TODO (Phase 2):** a `warzone reset-session ` command. + +--- + +## 7. Pre-Key Management + +### What Are Pre-Keys + +Pre-keys enable asynchronous session establishment. When Alice wants to +message Bob for the first time: + +1. Alice fetches Bob's **pre-key bundle** from the server. +2. The bundle contains Bob's public identity key, a signed pre-key, and + optionally a one-time pre-key. +3. Alice uses these to perform X3DH without Bob being online. + +### Pre-Key Types + +| Type | Quantity | Lifetime | Purpose | +|------|----------|----------|---------| +| Signed pre-key | 1 (id=1) | Long-term (no rotation yet) | Medium-term DH key, signed by identity | +| One-time pre-keys | 10 (ids 0-9) | Single use | Consumed during X3DH, then deleted | + +### When to Replenish + +One-time pre-keys are consumed when someone initiates a session with you. +After all 10 are used, X3DH falls back to using only the signed pre-key +(DH4 is skipped), which provides slightly weaker security properties. + +**Current state:** there is no automatic replenishment. You must manually +re-initialize if you expect many incoming new sessions. + +**TODO (Phase 2):** the server will notify the client when one-time pre-key +supply is low, and the client will upload fresh ones automatically. + +--- + +## 8. Security Model + +### What Is Encrypted + +- **Message body:** encrypted with ChaCha20-Poly1305 using per-message keys + from the Double Ratchet. Even the server cannot read it. + +### What Is NOT Encrypted + +- **Sender fingerprint:** visible to the server and anyone intercepting + traffic. +- **Recipient fingerprint:** visible to the server (needed for routing). +- **Message size:** visible to the server. +- **Timing:** when messages are sent and received. +- **IP addresses:** visible to the server and network observers. +- **Seed on disk:** stored as plaintext (encryption TODO). + +### Threat Model + +| Threat | Protected? | Notes | +|--------|-----------|-------| +| Server reads messages | Yes | E2E encryption; server sees only ciphertext | +| Network eavesdropper reads messages | Yes | E2E encryption | +| Server impersonates a user | Partially | Pre-key signatures prevent forgery of signed pre-keys, but the server could substitute a fake bundle (no key transparency yet) | +| Compromised past session key | Yes | Forward secrecy via chain ratchet; break-in recovery via DH ratchet | +| Stolen device (seed file) | No | Seed is plaintext on disk (encryption TODO) | +| Metadata analysis (who talks to whom) | No | Fingerprints visible to server | +| Active MITM on first contact | Partially | TOFU model; no out-of-band verification mechanism in the client yet | +| One-time pre-keys exhausted | Graceful degradation | X3DH works without OT pre-keys but with reduced replay protection | + +### Trust Model + +**Trust on first use (TOFU):** the first time you message someone, you trust +that the server returns their genuine pre-key bundle. There is no +verification step yet. + +**Planned (Phase 3):** DNS-based key transparency where users publish +self-signed public keys in DNS TXT records, allowing cross-verification +independent of the server. + +--- + +## 9. Troubleshooting + +### "No identity found. Run `warzone init` first." + +You haven't generated an identity, or `~/.warzone/identity.seed` is missing. + +```bash +warzone init +``` + +### "No bundle found. Run `warzone init` first." + +The pre-key bundle file `~/.warzone/bundle.bin` is missing. This happens if +you ran `recover` without a full `init`. + +Re-run `warzone init` (this will generate a NEW identity). To keep your +recovered identity, you would need to manually regenerate pre-keys (not yet +supported as a standalone command). + +### "failed to fetch recipient's bundle. Are they registered?" + +The recipient has not registered their pre-key bundle with the server, or +you are using the wrong server URL, or the fingerprint is incorrect. + +- Verify the fingerprint (ask the recipient for theirs via `warzone info`). +- Verify the server URL. +- Ask the recipient to run `warzone register -s `. + +### "X3DH respond failed" / "missing signed pre-key" + +Your signed pre-key secret is missing from the local database. This can +happen if: +- The database was deleted or corrupted. +- You recovered an identity but did not regenerate pre-keys. + +Fix: re-initialize with `warzone init` (generates a new identity) or restore +from backup. + +### "decrypt failed" / "no session" + +- **"no session"**: you received a `WireMessage::Message` from someone you + have no session with. This means you missed their initial `KeyExchange` + message, or your session database was lost. Ask them to re-send their first + message. +- **"decrypt failed"**: the ratchet state is out of sync. This can happen if + one side's state was lost or if messages were duplicated. Reset the session + on both sides. + +### Messages Keep Reappearing on recv + +Messages are not auto-acknowledged after polling. This is a known Phase 1 +limitation. The same messages will be returned on every `recv` call. + +**Workaround:** none currently. Acknowledgment will be added in Phase 2. + +### Corrupted Database + +If `~/.warzone/db/` is corrupted: + +```bash +rm -rf ~/.warzone/db/ +warzone init # regenerate pre-keys (NOTE: generates a new identity) +``` + +To keep your existing identity, manually copy `identity.seed` before +deleting, then use `warzone recover` after re-init. diff --git a/warzone/docs/PROTOCOL.md b/warzone/docs/PROTOCOL.md new file mode 100644 index 0000000..55799a9 --- /dev/null +++ b/warzone/docs/PROTOCOL.md @@ -0,0 +1,520 @@ +# Warzone Protocol Specification + +This document describes the cryptographic protocol used by Warzone messenger +as currently implemented in the `warzone-protocol` crate. + +--- + +## 1. Identity Model + +### Seed-Based Identity + +Every identity begins with a **seed**: 32 cryptographically random bytes +generated from `OsRng`. + +``` +seed (32 bytes, from OsRng) + | + +-- HKDF-SHA256(seed, info="warzone-ed25519") --> Ed25519 signing keypair + | + +-- HKDF-SHA256(seed, info="warzone-x25519") --> X25519 encryption keypair +``` + +The seed is the single root secret. Both key derivations use HKDF with an +empty salt and distinct `info` strings for domain separation. + +### Key Types + +| Key | Algorithm | Purpose | +|-----|-----------|---------| +| Signing keypair | Ed25519 (via `ed25519-dalek`) | Signs pre-keys, proves identity | +| Encryption keypair | X25519 (via `x25519-dalek`) | Diffie-Hellman key exchange | + +### Fingerprint + +The fingerprint is the primary user identifier. It is computed as: + +``` +fingerprint = SHA-256(Ed25519_public_key)[0..16] // first 16 bytes +``` + +Displayed as four colon-separated groups of 4 hex digits (8 bytes / 64 bits +of the 128-bit fingerprint): + +``` +a3f8:c912:44be:7d01 +``` + +Note: the `Display` implementation uses only the first 8 bytes (4 groups of +`u16`). The full 16 bytes are stored internally and used for session keying +and lookups. The `from_hex` parser strips colons and decodes all 16 bytes. + +### BIP39 Mnemonic + +The 32-byte seed is presented to users as a 24-word BIP39 mnemonic for +human-readable backup. Recovery works by converting the mnemonic back to 32 +bytes and re-deriving the same keypairs deterministically. + +### PublicIdentity + +The shareable portion of an identity: + +```rust +pub struct PublicIdentity { + pub signing: VerifyingKey, // Ed25519 public key (32 bytes) + pub encryption: PublicKey, // X25519 public key (32 bytes) + pub fingerprint: Fingerprint, // SHA-256(signing)[0..16] +} +``` + +Serialized with serde; the dalek types use raw-bytes serialization. + +--- + +## 2. Pre-Key Bundles + +Pre-key bundles enable asynchronous key exchange (the recipient does not need +to be online when the sender initiates a session). + +### Signed Pre-Key + +A medium-term X25519 keypair signed by the identity Ed25519 key: + +```rust +pub struct SignedPreKey { + pub id: u32, + pub public_key: [u8; 32], // X25519 public key + pub signature: Vec, // Ed25519 signature over public_key + pub timestamp: i64, // unix timestamp of generation +} +``` + +The signature covers `public_key` directly (the raw 32 bytes). Verification +uses the identity's Ed25519 verifying key. + +### One-Time Pre-Key + +A single-use X25519 keypair. Each key has a numeric `id`. After a key exchange +consumes it, the private half is deleted. + +```rust +pub struct OneTimePreKeyPublic { + pub id: u32, + pub public_key: [u8; 32], // X25519 public key +} +``` + +### Bundle Format + +The complete bundle uploaded to the server: + +```rust +pub struct PreKeyBundle { + pub identity_key: [u8; 32], // Ed25519 verifying key + pub identity_encryption_key: [u8; 32], // X25519 identity public key + pub signed_pre_key: SignedPreKey, + pub one_time_pre_key: Option, +} +``` + +Serialized with `bincode` for the wire and for local storage. + +### Lifecycle + +1. `warzone init` generates 1 signed pre-key (id=1) and 10 one-time pre-keys + (ids 0-9). +2. Private halves are stored in the local sled database under the `pre_keys` + tree (keys: `spk:`, `otpk:`). +3. The public bundle is saved to `~/.warzone/bundle.bin`. +4. On first `send` (or explicit `register`), the bundle is uploaded to the + server. +5. When a one-time pre-key is consumed during X3DH, it is atomically removed + from local storage (`take_one_time_pre_key`). + +**TODO (Phase 2):** automatic replenishment of one-time pre-keys when supply +runs low; signed pre-key rotation on a schedule. + +--- + +## 3. X3DH Key Exchange + +The implementation follows Signal's Extended Triple Diffie-Hellman (X3DH) +specification. + +### Initiator (Alice) + +Alice fetches Bob's `PreKeyBundle` from the server, then: + +``` +1. Verify signed_pre_key.signature against identity_key +2. Generate ephemeral X25519 keypair (ek) +3. Compute four DH values: + DH1 = X25519(Alice_identity_x25519, Bob_signed_pre_key) + DH2 = X25519(Alice_ephemeral, Bob_identity_x25519) + DH3 = X25519(Alice_ephemeral, Bob_signed_pre_key) + DH4 = X25519(Alice_ephemeral, Bob_one_time_pre_key) [if present] +4. Concatenate: DH1 || DH2 || DH3 [|| DH4] +5. shared_secret = HKDF-SHA256(concat, salt="", info="warzone-x3dh", len=32) +6. Zeroize the DH concatenation +``` + +The result includes: +- `shared_secret` (32 bytes) -- used to initialize the Double Ratchet +- `ephemeral_public` -- sent to Bob +- `used_one_time_pre_key_id` -- tells Bob which OT pre-key was consumed + +### Responder (Bob) + +Bob receives Alice's ephemeral public key plus her identity encryption key +and computes the same DH operations in the mirror order: + +``` +DH1 = X25519(Bob_signed_pre_key_secret, Alice_identity_x25519) +DH2 = X25519(Bob_identity_x25519, Alice_ephemeral) +DH3 = X25519(Bob_signed_pre_key_secret, Alice_ephemeral) +DH4 = X25519(Bob_one_time_pre_key, Alice_ephemeral) [if used] +``` + +The concatenation and HKDF produce the identical `shared_secret`. + +### ASCII Diagram + +``` + Alice Server Bob + | | | + |--- fetch Bob's bundle ------>| | + |<-- PreKeyBundle -------------| | + | | | + | [verify SPK signature] | | + | [generate ephemeral key] | | + | [DH1..DH4 -> HKDF] | | + | [init ratchet as Alice] | | + | | | + |--- WireMessage::KeyExchange -|---> queue for Bob | + | (ephemeral_pub, otpk_id, | | + | ratchet_message) | | + | | | + | | Bob polls ----------->| + | |<-- WireMessage::KeyExchange | + | | | + | | [load SPK secret, OT secret]| + | | [DH1..DH4 -> HKDF] | + | | [init ratchet as Bob] | + | | [decrypt first message] | +``` + +--- + +## 4. Double Ratchet + +The Double Ratchet provides forward secrecy and break-in recovery. The +implementation follows Signal's Double Ratchet specification. + +### State + +```rust +pub struct RatchetState { + dh_self: Vec, // our current X25519 secret (32 bytes) + dh_remote: Option<[u8; 32]>, // their current DH public key + root_key: [u8; 32], // root chain key + chain_key_send: Option<[u8; 32]>, // sending chain key + chain_key_recv: Option<[u8; 32]>, // receiving chain key + send_count: u32, // messages sent in current sending chain + recv_count: u32, // messages received in current receiving chain + prev_send_count: u32, // messages in previous sending chain + skipped: BTreeMap<([u8; 32], u32), [u8; 32]>, // cached keys for out-of-order messages +} +``` + +### Initialization + +**Alice (initiator):** +1. Receives `shared_secret` from X3DH and Bob's signed pre-key public as the + initial remote ratchet key. +2. Generates a fresh DH keypair. +3. Performs `kdf_rk(shared_secret, DH(new_key, bob_spk))` to produce the + first root key and sending chain key. +4. No receiving chain yet (waits for Bob's first message). + +**Bob (responder):** +1. Receives the same `shared_secret` from X3DH. +2. Uses his signed pre-key secret as the initial DH self key. +3. Root key = shared_secret. No chain keys yet (waits for Alice's first + message to trigger the first DH ratchet step). + +### Sending a Message + +``` +1. If no sending chain exists, perform a DH ratchet step first +2. (new_chain_key, message_key) = kdf_ck(chain_key_send) +3. chain_key_send = new_chain_key +4. header = RatchetHeader { dh_public, prev_chain_length, message_number } +5. aad = bincode::serialize(header) +6. ciphertext = AEAD_encrypt(message_key, plaintext, aad) +7. send_count += 1 +8. Return RatchetMessage { header, ciphertext } +``` + +### Receiving a Message + +``` +1. Check skipped message cache: if (dh_public, message_number) is cached, + use that message key to decrypt and return +2. If message.dh_public != dh_remote: + a. Skip any missed messages in the current receiving chain + b. DH ratchet step: + - New receiving chain: kdf_rk(root_key, DH(our_secret, their_new_pub)) + - New sending chain: kdf_rk(root_key, DH(new_secret, their_new_pub)) + - Reset counters +3. Skip messages up to message_number (cache skipped keys) +4. (new_chain_key, message_key) = kdf_ck(chain_key_recv) +5. aad = bincode::serialize(header) +6. plaintext = AEAD_decrypt(message_key, ciphertext, aad) +``` + +### Skipped Messages + +When messages arrive out of order, the ratchet fast-forwards the receiving +chain and caches the intermediate message keys in `skipped`. A maximum of +`MAX_SKIP = 1000` messages can be skipped in one step to prevent resource +exhaustion. + +Cached keys are indexed by `(dh_public_key, message_number)` and are consumed +(removed from the map) on first use. + +### Message Header + +```rust +pub struct RatchetHeader { + pub dh_public: [u8; 32], // sender's current DH ratchet public key + pub prev_chain_length: u32, // messages in previous sending chain + pub message_number: u32, // index in current sending chain +} +``` + +### DH Ratchet Diagram + +``` +Alice Bob + | | + | send_chain_0 (from X3DH) | + |------- msg 0 (dh_pub_A0) ---------------------->| + |------- msg 1 (dh_pub_A0) ---------------------->| + | | + | recv: new dh_pub_A0 | + | DH ratchet step | + | send_chain_1 | + |<------ msg 0 (dh_pub_B1) -----------------------| + | | + | recv: new dh_pub_B1 | + | DH ratchet step | + | send_chain_2 | + |------- msg 0 (dh_pub_A2) ---------------------->| + | | +``` + +Each direction change triggers a DH ratchet step, producing new chain keys +and providing forward secrecy and break-in recovery. + +--- + +## 5. KDF Chains + +All key derivation uses HKDF-SHA256 (via the `hkdf` crate with `sha2`). + +### hkdf_derive + +```rust +fn hkdf_derive(ikm: &[u8], salt: &[u8], info: &[u8], len: usize) -> Vec +``` + +- Empty salt is treated as `None` (HKDF uses a zero-filled salt internally). +- `info` provides domain separation. + +### Domain Separation Strings + +| Context | info string | salt | Input | +|---------|-------------|------|-------| +| Ed25519 key from seed | `warzone-ed25519` | (empty) | seed | +| X25519 key from seed | `warzone-x25519` | (empty) | seed | +| X3DH shared secret | `warzone-x3dh` | (empty) | DH1\|\|DH2\|\|DH3[\|\|DH4] | +| Root key ratchet | `warzone-ratchet-rk` | root_key | DH output | +| Chain key -> message key | `warzone-ratchet-mk` | (empty) | chain_key | +| Chain key -> next chain key | `warzone-ratchet-ck` | (empty) | chain_key | + +### Root Key KDF (kdf_rk) + +``` +derived = HKDF(ikm=dh_output, salt=root_key, info="warzone-ratchet-rk", len=64) +new_root_key = derived[0..32] +new_chain_key = derived[32..64] +``` + +### Chain Key KDF (kdf_ck) + +``` +message_key = HKDF(ikm=chain_key, salt="", info="warzone-ratchet-mk", len=32) +new_chain_key = HKDF(ikm=chain_key, salt="", info="warzone-ratchet-ck", len=32) +``` + +--- + +## 6. AEAD Encryption + +All symmetric encryption uses **ChaCha20-Poly1305** (via the +`chacha20poly1305` crate). + +### Encrypt + +``` +1. Generate 12-byte random nonce from OsRng +2. ciphertext = ChaCha20-Poly1305(key, nonce, plaintext, aad) +3. Output = nonce (12 bytes) || ciphertext (includes 16-byte Poly1305 tag) +``` + +### Decrypt + +``` +1. Split input: first 12 bytes = nonce, remainder = ciphertext+tag +2. plaintext = ChaCha20-Poly1305_decrypt(key, nonce, ciphertext, aad) +``` + +### Associated Data + +For ratchet messages, the AAD is the `bincode`-serialized `RatchetHeader`. +This binds the ciphertext to the specific ratchet position and prevents +header manipulation. + +--- + +## 7. Wire Format + +### WireMessage Enum + +The top-level wire format is a `bincode`-serialized enum: + +```rust +pub enum WireMessage { + KeyExchange { + sender_fingerprint: String, + sender_identity_encryption_key: [u8; 32], + ephemeral_public: [u8; 32], + used_one_time_pre_key_id: Option, + ratchet_message: RatchetMessage, + }, + Message { + sender_fingerprint: String, + ratchet_message: RatchetMessage, + }, +} +``` + +**KeyExchange** is sent as the first message in a new session. It carries the +X3DH parameters that the recipient needs to derive the shared secret and +establish the ratchet. + +**Message** is sent for all subsequent messages in an established session. + +### RatchetMessage + +```rust +pub struct RatchetMessage { + pub header: RatchetHeader, // DH public key, counters + pub ciphertext: Vec, // nonce || ChaCha20-Poly1305 ciphertext +} +``` + +### WarzoneMessage (Defined But Not Yet Used on Wire) + +The `message.rs` module defines a higher-level envelope: + +```rust +pub struct WarzoneMessage { + pub version: u8, + pub id: MessageId, + pub from: Fingerprint, + pub to: Fingerprint, + pub timestamp: i64, + pub msg_type: MessageType, // Text, File, KeyExchange, Receipt + pub session_id: SessionId, + pub ratchet_header: RatchetHeader, + pub ciphertext: Vec, + pub signature: Vec, // Ed25519 signature +} +``` + +**Status:** this struct is defined but the current send/recv flow uses the +simpler `WireMessage` enum directly. The `WarzoneMessage` envelope with +signatures, message IDs, and session tracking will be integrated in Phase 2. + +### MessageContent (Plaintext, Inside Envelope) + +```rust +pub enum MessageContent { + Text { body: String }, + File { filename: String, data: Vec }, + Receipt { message_id: MessageId }, +} +``` + +**Status:** not yet used. Currently, raw UTF-8 bytes are encrypted directly. +Structured content types will be used in Phase 2. + +### Serialization + +- **Wire (client <-> server):** `bincode` for `WireMessage` and + `PreKeyBundle`. The server stores raw bincode blobs. +- **Server API:** JSON for request/response wrappers. Binary payloads are + base64-encoded within JSON. +- **Local storage:** `bincode` for `RatchetState` and pre-key secrets in the + sled database. + +--- + +## 8. Transport + +### Current Transport + +HTTP POST/GET over TCP via `reqwest` (client) and `axum` (server). No TLS in +the current implementation; TLS is expected to be provided by a reverse proxy. + +Messages are delivered via polling: the client periodically GETs +`/v1/messages/poll/{fingerprint}`. + +### Future Transports (Phase 2+) + +- WebSocket for real-time push +- Server-to-server federation (Phase 3) +- Bluetooth, LoRa, Wi-Fi Direct, USB sneakernet (Phase 4-5) + +--- + +## 9. Security Properties + +### What Is Achieved (Phase 1) + +- **Confidentiality:** messages are encrypted with ChaCha20-Poly1305 using + per-message keys derived from the Double Ratchet. +- **Forward secrecy:** compromising the current ratchet state does not reveal + past message keys (chain ratchet is one-way). +- **Break-in recovery:** after a DH ratchet step, a compromised state becomes + useless for future messages. +- **Asynchronous key exchange:** X3DH allows session establishment without + both parties being online simultaneously. +- **Out-of-order tolerance:** skipped message keys are cached (up to 1000). +- **Server learns nothing:** the server stores and forwards opaque bincode + blobs. It never sees plaintext. + +### What Is NOT Yet Implemented + +- **Message signing:** `WarzoneMessage.signature` is defined but not populated. + Currently, messages are not authenticated by Ed25519 signature. (Phase 2) +- **Sealed sender:** the server can see sender and recipient fingerprints in + the clear. (Phase 6) +- **Key transparency:** no DNS-based verification of public keys. (Phase 3) +- **Seed encryption at rest:** the seed file is stored as plaintext 32 bytes. + Argon2 + ChaCha20-Poly1305 encryption is TODO. +- **Pre-key replenishment:** one-time pre-keys are not automatically + replenished after consumption. +- **Message deduplication:** no dedup on the server or client. +- **Group encryption:** Sender Keys not yet implemented. (Phase 2) diff --git a/warzone/docs/SERVER.md b/warzone/docs/SERVER.md new file mode 100644 index 0000000..36a6077 --- /dev/null +++ b/warzone/docs/SERVER.md @@ -0,0 +1,429 @@ +# Warzone Server -- Operation & Administration + +--- + +## 1. Building + +The server is part of the Cargo workspace. From the workspace root: + +```bash +# Debug build +cargo build -p warzone-server + +# Release build (recommended for deployment) +cargo build -p warzone-server --release +``` + +The resulting binary is at `target/release/warzone-server` (or +`target/debug/warzone-server`). It is a single statically-linked binary with +no runtime dependencies beyond libc. + +### Minimum Rust Version + +Rust 1.75 or later (set via `rust-version = "1.75"` in `Cargo.toml`). + +--- + +## 2. Running + +```bash +# Default: bind 0.0.0.0:7700, data in ./warzone-data +./warzone-server + +# Custom bind address and data directory +./warzone-server --bind 127.0.0.1:8080 --data-dir /var/lib/warzone +``` + +### CLI Flags + +| Flag | Short | Default | Description | +|------|-------|---------|-------------| +| `--bind` | `-b` | `0.0.0.0:7700` | Address and port to listen on | +| `--data-dir` | `-d` | `./warzone-data` | Directory for sled database files | + +### Logging + +The server uses `tracing-subscriber`. Control log level with the `RUST_LOG` +environment variable: + +```bash +RUST_LOG=info ./warzone-server +RUST_LOG=warzone_server=debug ./warzone-server +RUST_LOG=trace ./warzone-server # very verbose +``` + +--- + +## 3. API Reference + +All API endpoints are under the `/v1` prefix. The web UI is served at `/`. + +### Health Check + +``` +GET /v1/health +``` + +**Response:** +```json +{ + "status": "ok", + "version": "0.1.0" +} +``` + +Use this for monitoring, load balancer health probes, and uptime checks. + +--- + +### Register Key Bundle + +``` +POST /v1/keys/register +Content-Type: application/json +``` + +**Request body:** +```json +{ + "fingerprint": "a3f8:c912:44be:7d01", + "bundle": [/* bincode-serialized PreKeyBundle as byte array */] +} +``` + +The `bundle` field is a JSON array of unsigned bytes (the raw bincode +serialization of a `PreKeyBundle`). + +**Response:** +```json +{ + "ok": true +} +``` + +**Behavior:** stores the bundle in the `keys` sled tree, keyed by the +fingerprint string. Overwrites any existing bundle for the same fingerprint. + +--- + +### Fetch Key Bundle + +``` +GET /v1/keys/{fingerprint} +``` + +**Path parameter:** the fingerprint string, e.g. `a3f8:c912:44be:7d01`. + +**Response (200):** +```json +{ + "fingerprint": "a3f8:c912:44be:7d01", + "bundle": "base64-encoded-bincode-bytes..." +} +``` + +The `bundle` value is standard base64-encoded bincode. The client decodes +base64, then deserializes with bincode to recover the `PreKeyBundle`. + +**Response (404):** returned if no bundle is registered for the fingerprint. + +--- + +### Send Message + +``` +POST /v1/messages/send +Content-Type: application/json +``` + +**Request body:** +```json +{ + "to": "b7d1:e845:0022:9f3a", + "message": [/* bincode-serialized WireMessage as byte array */] +} +``` + +**Response:** +```json +{ + "ok": true +} +``` + +**Behavior:** the message bytes are stored in the `messages` sled tree under +the key `queue:{recipient_fingerprint}:{uuid}`. The UUID is generated +server-side to ensure unique keys. + +The server does NOT parse, validate, or inspect the message contents. It is an +opaque blob. + +--- + +### Poll Messages + +``` +GET /v1/messages/poll/{fingerprint} +``` + +**Response (200):** +```json +[ + "base64-encoded-message-1", + "base64-encoded-message-2" +] +``` + +Returns a JSON array of base64-encoded message blobs. Each blob is a +bincode-serialized `WireMessage`. An empty array means no messages. + +**Behavior:** scans the `messages` sled tree for all keys prefixed with +`queue:{fingerprint}`. Messages are NOT deleted by polling; they remain until +explicitly acknowledged. + +--- + +### Acknowledge Message + +``` +DELETE /v1/messages/{id}/ack +``` + +**Path parameter:** the message storage key (currently the full sled key +including the `queue:` prefix and UUID). + +**Response:** +```json +{ + "ok": true +} +``` + +**Behavior:** removes the message from the `messages` tree. + +**Note:** the current implementation requires knowing the exact sled key to +acknowledge. A proper message-ID-based index is planned for Phase 2. + +--- + +## 4. Web UI + +The server serves a single-page web client at the root path `/`. + +``` +GET / +``` + +Returns an HTML page with embedded CSS and JavaScript. The web client provides: + +- **Identity generation:** generates a random 32-byte seed in the browser + using `crypto.getRandomValues()`. +- **Identity recovery:** paste a hex-encoded seed to recover. +- **Fingerprint display:** shows the user's fingerprint in the header. +- **Key registration:** automatically registers a public key with the server + on entry. +- **Message polling:** polls `/v1/messages/poll/{fingerprint}` every 5 seconds. +- **Slash commands:** `/help`, `/info`, `/seed`. + +### Web Client Limitations + +- Uses ECDH P-256 (Web Crypto API) instead of X25519. Cross-client + compatibility with the CLI is not yet implemented. (Phase 2) +- Does not use BIP39 mnemonics; seed is displayed as hex. +- Message decryption is not yet wired (Double Ratchet in JS is TODO). +- The seed is stored in `localStorage` (unencrypted). + +--- + +## 5. Database + +The server uses **sled** (embedded key-value store). All data lives under the +directory specified by `--data-dir`. + +### Trees (Tables) + +| Tree | Key format | Value | Purpose | +|------|-----------|-------|---------| +| `keys` | fingerprint string (UTF-8 bytes) | bincode `PreKeyBundle` | Pre-key bundle storage | +| `messages` | `queue:{fingerprint}:{uuid}` (UTF-8 bytes) | bincode `WireMessage` | Message queue | +| `otpks` | (reserved) | (reserved) | One-time pre-key tracking (not yet used server-side) | + +### Data Directory Structure + +``` +warzone-data/ + db # sled database file + conf # sled config + blobs/ # sled blob storage (if any) + snap.*/ # sled snapshots +``` + +The exact file layout is managed by sled internally. The entire directory +should be treated as a unit for backup. + +### What the Server Stores + +- **Pre-key bundles:** public keys only. The server never holds private keys. +- **Encrypted message blobs:** opaque binary data. The server cannot read + message contents. +- **Metadata visible to server:** sender fingerprint, recipient fingerprint, + message size, timestamps (implicit from storage order). + +--- + +## 6. Deployment + +### Single Binary + +The recommended deployment is a single `warzone-server` binary behind a +reverse proxy for TLS termination. + +### Reverse Proxy (nginx) + +```nginx +server { + listen 443 ssl http2; + server_name wz.example.com; + + ssl_certificate /etc/letsencrypt/live/wz.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/wz.example.com/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:7700; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support (for future real-time push) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +When using a reverse proxy, bind the server to localhost only: + +```bash +./warzone-server --bind 127.0.0.1:7700 +``` + +### systemd Service + +```ini +[Unit] +Description=Warzone Messenger Server +After=network.target + +[Service] +Type=simple +User=warzone +ExecStart=/usr/local/bin/warzone-server --bind 127.0.0.1:7700 --data-dir /var/lib/warzone +Restart=always +RestartSec=5 +Environment=RUST_LOG=info + +[Install] +WantedBy=multi-user.target +``` + +--- + +## 7. Monitoring + +### Health Endpoint + +```bash +curl http://localhost:7700/v1/health +# {"status":"ok","version":"0.1.0"} +``` + +Use this for: +- Load balancer health checks +- Uptime monitoring (e.g., with `uptime-kuma`, Prometheus blackbox exporter) +- Deployment verification + +### Logs + +All request activity is logged via `tracing`. In production, pipe to a log +aggregator or use `journalctl -u warzone-server`. + +--- + +## 8. Security Considerations + +### The Server Is a Dumb Relay + +The server never sees plaintext message content. It stores and forwards +opaque encrypted blobs. Even if the server is fully compromised, an attacker +gains: + +- **Encrypted message blobs** (useless without recipient's private keys) +- **Public pre-key bundles** (public by design) +- **Metadata:** who is messaging whom, when, and how often + +### What the Server CAN See + +| Data | Visible to server | +|------|-------------------| +| Message plaintext | No | +| Sender fingerprint | Yes (in `WireMessage`) | +| Recipient fingerprint | Yes (used for routing) | +| Message size | Yes | +| Timing | Yes | +| IP addresses | Yes (from HTTP) | +| Pre-key bundles (public keys) | Yes | + +### Mitigations for Metadata (Future) + +- **Sealed sender** (Phase 6): hide sender identity from the server. +- **Padding:** fixed-size messages to prevent size-based analysis. +- **Onion routing** (Phase 6): hide IP addresses via relay chains. + +### Access Control + +The current server has **no authentication**. Anyone can: +- Register a key bundle for any fingerprint +- Poll messages for any fingerprint +- Send messages to any fingerprint + +**TODO (Phase 2):** authentication via Ed25519 challenge-response. Clients +sign requests to prove they own the fingerprint they claim. + +--- + +## 9. Backup and Recovery + +### Database Backup + +The sled database can be backed up by copying the entire data directory while +the server is stopped: + +```bash +systemctl stop warzone-server +cp -r /var/lib/warzone /backup/warzone-$(date +%Y%m%d) +systemctl start warzone-server +``` + +**Warning:** copying the sled directory while the server is running may +produce an inconsistent snapshot. Stop the server first or use filesystem-level +snapshots (LVM, ZFS, btrfs). + +### Recovery + +1. Stop the server. +2. Replace the data directory with the backup. +3. Start the server. + +Messages queued after the backup was taken will be lost. Since all messages +are E2E encrypted, there is no way to recover them from any other source. + +### Data Loss Impact + +- **Lost key bundles:** users must re-register. No security impact (public + data). +- **Lost message queue:** undelivered messages are permanently lost. Senders + will not know delivery failed (no delivery receipts yet). +- **Corrupted database:** sled includes crash recovery. If the database is + corrupt beyond recovery, delete it and start fresh. Users re-register.