Add documentation: protocol spec, server admin, client guide
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) <noreply@anthropic.com>
This commit is contained in:
507
warzone/docs/CLIENT.md
Normal file
507
warzone/docs/CLIENT.md
Normal file
@@ -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 <recipient-fingerprint> <message> -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 \<words...\>
|
||||||
|
|
||||||
|
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 <fingerprint>` 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 <server>`.
|
||||||
|
|
||||||
|
### "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.
|
||||||
520
warzone/docs/PROTOCOL.md
Normal file
520
warzone/docs/PROTOCOL.md
Normal file
@@ -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<u8>, // 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<OneTimePreKeyPublic>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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:<id>`, `otpk:<id>`).
|
||||||
|
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<u8>, // 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<u8>
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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<u32>,
|
||||||
|
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<u8>, // 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<u8>,
|
||||||
|
pub signature: Vec<u8>, // 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<u8> },
|
||||||
|
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)
|
||||||
429
warzone/docs/SERVER.md
Normal file
429
warzone/docs/SERVER.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user