Changed HKDF expand info strings to match featherChat's identity.rs: - "warzone-ed25519-identity" → "warzone-ed25519" - "warzone-x25519-identity" → "warzone-x25519" Same BIP39 seed now produces identical Ed25519/X25519 keypairs in both featherChat and WZP. This is the prerequisite for shared identity. Also added FEATHERCHAT_INTEGRATION.md (1209 lines) from featherChat repo documenting the full integration plan with confirmed code references. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
48 KiB
WarzonePhone (WZP) Integration with featherChat
Version: 0.2.0 Date: 2026-03-28 Status: Confirmed Design Document (based on real code access to both codebases)
All items in this document are marked [CONFIRMED] and reference actual
source code in the warzone/ (featherChat) and warzone-phone/ (WZP)
repositories. The previous speculative draft has been fully replaced.
1. Executive Summary
featherChat (Warzone Messenger)
A seed-based, end-to-end encrypted messaging system in Rust (v0.0.20).
Crate structure (warzone/Cargo.toml):
| Crate | Purpose |
|---|---|
warzone-protocol |
Core crypto & wire types (X3DH, Double Ratchet, Sender Keys, identity) |
warzone-server |
axum HTTP + WebSocket server with sled embedded DB |
warzone-client |
CLI/TUI client (clap + ratatui) |
warzone-wasm |
WASM bridge for web client |
warzone-mule |
Mule binary (placeholder) |
Key primitives: Ed25519 signing, X25519 DH, ChaCha20-Poly1305 AEAD, HKDF-SHA256, Argon2id. Identity derived from a single BIP39 seed.
WarzonePhone (WZP)
An encrypted voice calling system in Rust (v0.1.0, edition 2024, rust 1.85+).
Crate structure (warzone-phone/Cargo.toml):
| Crate | Purpose |
|---|---|
wzp-proto |
Shared types, traits, session state machine, jitter buffer, quality controller |
wzp-codec |
Adaptive audio encoding: Opus (24k/16k/6k) + Codec2 (3200/1200 bps) |
wzp-fec |
RaptorQ fountain codes with temporal interleaving |
wzp-crypto |
Per-call ChaCha20-Poly1305 sessions, X25519 key exchange, rekeying |
wzp-transport |
QUIC (quinn) with DATAGRAM frames for media, reliable streams for signaling |
wzp-relay |
Relay daemon: recv - FEC decode - jitter buffer - FEC encode - send |
wzp-client |
End-to-end voice call pipeline + cpal audio I/O |
Key primitives: X25519 ephemeral DH, ChaCha20-Poly1305 AEAD, Ed25519 signing, HKDF-SHA256, RaptorQ FEC, Opus + Codec2 codecs, QUIC transport.
Why Integrate
[CONFIRMED] Both systems derive identity from a 32-byte seed via HKDF and
share the same cryptographic primitive stack (Ed25519, X25519, ChaCha20-Poly1305,
HKDF-SHA256). WZP's KeyExchange trait (wzp-proto/src/traits.rs:141-176)
explicitly documents compatibility with the "Warzone identity model" and its
from_identity_seed() method uses the same HKDF derivation pattern.
Integration benefits:
- Single identity -- one BIP39 mnemonic controls messaging, calling, and Ethereum wallet.
- Reuse crypto infrastructure -- featherChat's X3DH sessions provide authenticated peer relationships; WZP's per-call ephemeral exchange builds on the same identity keys.
- Encrypted signaling -- call setup can travel through featherChat's E2E encrypted Double Ratchet channels.
- Shared contact/group model -- featherChat groups map to WZP call rooms.
- Warzone resilience -- voice messages as file attachments, missed call notifications via mule delivery.
2. Shared Identity Model
featherChat Key Derivation
[CONFIRMED] warzone-protocol/src/identity.rs:29-47 (Seed::derive_identity()):
BIP39 Seed (32 bytes)
|
+-- HKDF(ikm=seed, salt="", info="warzone-ed25519") --> Ed25519 signing keypair
| |
| +-> SHA-256[:16] = Fingerprint
|
+-- HKDF(ikm=seed, salt="", info="warzone-x25519") --> X25519 encryption keypair
|
+-- HKDF(ikm=seed, salt="", info="warzone-secp256k1") --> secp256k1 keypair (Ethereum)
|
+-- HKDF(ikm=seed, salt="", info="warzone-history") --> History encryption key
Fingerprint: SHA-256(Ed25519_pubkey)[:16], displayed as
xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx.
WZP Key Derivation
[CONFIRMED] wzp-crypto/src/handshake.rs:32-53 (WarzoneKeyExchange::from_identity_seed()):
32-byte Seed
|
+-- HKDF(ikm=seed, salt=None, info="warzone-ed25519-identity") --> Ed25519 signing keypair
|
+-- HKDF(ikm=seed, salt=None, info="warzone-x25519-identity") --> X25519 static keypair
Fingerprint: SHA-256(Ed25519_pubkey)[:16] -- identical algorithm to featherChat.
See wzp-crypto/src/handshake.rs:66-71.
Identity Compatibility Gap
[CONFIRMED] The HKDF info strings differ:
| Key | featherChat info | WZP info |
|---|---|---|
| Ed25519 | "warzone-ed25519" |
"warzone-ed25519-identity" |
| X25519 | "warzone-x25519" |
"warzone-x25519-identity" |
This means the same seed produces DIFFERENT keypairs in each system.
Resolution required: One of the two must be updated to match. The
recommended approach is to update WZP to use featherChat's info strings
("warzone-ed25519" and "warzone-x25519"), since featherChat is the
established system with deployed users and stored identities. This is a
two-line change in wzp-crypto/src/handshake.rs:36,43.
Per-Call Ephemeral Keys (WZP-specific)
[CONFIRMED] WZP generates per-call ephemeral X25519 keypairs
(wzp-crypto/src/handshake.rs:55-59). The call session key is derived from:
shared_secret = X25519_DH(our_ephemeral_secret, peer_ephemeral_pub)
session_key = HKDF(ikm=shared_secret, salt=None, info="warzone-session-key")
This is independent of featherChat's X3DH/Double Ratchet -- each call creates fresh ephemeral keys for perfect forward secrecy per call.
3. Authentication Flow
featherChat Challenge-Response Auth
[CONFIRMED] warzone-server/src/routes/auth.rs:1-11:
Step 1: Client -> Server POST /v1/auth/challenge { fingerprint }
Step 2: Server -> Client { challenge: random_hex(32), expires_at }
Challenge valid 60 seconds (CHALLENGE_TTL_SECS = 60)
Step 3: Client -> Server POST /v1/auth/verify {
fingerprint,
challenge,
signature // Ed25519 sign(challenge_bytes)
}
Step 4: Server verifies Ed25519 signature against stored PreKeyBundle
(auth.rs:117-154)
Step 5: Server -> Client { token: random_hex(32), expires_at }
Token valid 7 days (TOKEN_TTL_SECS = 604800)
Step 6: Client includes Authorization: Bearer <token> on requests
Challenges stored in-memory (LazyLock<Mutex<HashMap>>, auth.rs:54-55).
Tokens stored in tokens sled tree (key: token bytes, value: JSON
{fingerprint, expires_at}). The validate_token() function (auth.rs:177-186)
checks existence and expiry.
WZP Authentication Model
[CONFIRMED] WZP does NOT have its own authentication server or HTTP endpoints. Authentication is entirely peer-to-peer during the QUIC handshake:
-
Caller sends
SignalMessage::CallOffercontaining their Ed25519 identity public key, ephemeral X25519 public key, and an Ed25519 signature over(ephemeral_pub || "call-offer"). Seewzp-client/src/handshake.rs:22-45. -
Callee verifies the signature against the caller's identity public key, then sends
SignalMessage::CallAnswerwith their own identity key, ephemeral key, and signature over(ephemeral_pub || "call-answer"). Seewzp-relay/src/handshake.rs:19-80. -
Both sides derive the shared session key from the ephemeral DH.
Integrated Auth Flow
For WZP to use featherChat infrastructure, the flow is:
featherChat Client featherChat Server WZP Relay/Peer
| | |
Unlock seed (passphrase + Argon2id) | |
| | |
POST /v1/auth/challenge | |
POST /v1/auth/verify (Ed25519 sig) | |
|<--- bearer token (7d TTL) ------| |
| | |
Send CallSignal via featherChat WS | |
(Double Ratchet encrypted) |--- WS push ------------->|
| | |
| Connect QUIC to WZP relay/peer | |
| SignalMessage::CallOffer --------------------------------->|
| (identity_pub, ephemeral_pub, signature) |
| | |
|<------------------------------------- SignalMessage::CallAnswer
| (identity_pub, ephemeral_pub, signature) |
| | |
| Both derive ChaCha20-Poly1305 session |
| ================ encrypted media flows ===================|
WZP validates peer identity via Ed25519 signature verification (wzp-crypto/src/handshake.rs:79-88) rather than tokens. The featherChat token is used only for accessing featherChat server resources (key bundles, message relay, group membership).
Proposed Server-Side Addition
A POST /v1/auth/validate endpoint should be added to featherChat server
to allow WZP relays to verify bearer tokens:
POST /v1/auth/validate
Body: { "token": "hex..." }
Response: { "valid": true, "fingerprint": "a3f8c912...", "expires_at": ... }
This reuses the existing validate_token() function from auth.rs:177-186.
4. Signaling Integration
WZP Signal Messages
[CONFIRMED] wzp-proto/src/packet.rs:249-310 defines SignalMessage:
pub enum SignalMessage {
CallOffer {
identity_pub: [u8; 32],
ephemeral_pub: [u8; 32],
signature: Vec<u8>,
supported_profiles: Vec<QualityProfile>,
},
CallAnswer {
identity_pub: [u8; 32],
ephemeral_pub: [u8; 32],
signature: Vec<u8>,
chosen_profile: QualityProfile,
},
IceCandidate { candidate: String },
Rekey {
new_ephemeral_pub: [u8; 32],
signature: Vec<u8>,
},
QualityUpdate {
report: QualityReport,
recommended_profile: QualityProfile,
},
Ping { timestamp_ms: u64 },
Pong { timestamp_ms: u64 },
Hangup { reason: HangupReason },
}
These are serialized as JSON over reliable QUIC streams
(wzp-transport/src/reliable.rs:12-58, length-prefixed framing: 4-byte
BE length + serde_json payload).
Bridging Signaling via featherChat
To initiate a WZP call through featherChat, a new WireMessage variant
should be added to warzone-protocol/src/message.rs:
/// VoIP call signaling via WarzonePhone.
/// Encrypted by the existing Double Ratchet session.
CallSignal {
id: String,
sender_fingerprint: String,
signal: Vec<u8>, // Serialized wzp_proto::SignalMessage (JSON)
},
The signal field carries the serialized SignalMessage opaquely. The
featherChat server treats it identically to any other WireMessage -- an
encrypted blob routed via WebSocket.
Signaling Flow (1:1 Call)
Alice (featherChat+WZP) featherChat Server Bob (featherChat+WZP)
| | |
| WireMessage::CallSignal | |
| { signal: CallOffer{...} } | |
| (Double Ratchet encrypted) | |
|----------------------------->|--- WS push ------------>|
| | |
| | WireMessage::CallSignal|
| | { signal: CallAnswer } |
|<-----------------------------|<------------------------|
| | |
| WireMessage::CallSignal | |
| { signal: IceCandidate } | |
|----------------------------->|------------------------>|
| | |
| ============ QUIC connection established ============ |
| ============ ephemeral X25519 DH complete =========== |
| ============ ChaCha20-Poly1305 media flows ========== |
| | |
| WireMessage::CallSignal | |
| { signal: Hangup{Normal} } | |
|----------------------------->|------------------------>|
Server-Side Changes Required
-
extract_message_id()inroutes/ws.rs:25-41-- add match arm:WireMessage::CallSignal { id, .. } => Some(id), -
No new routes needed --
CallSignalmessages flow through existing WebSocket relay (routes/ws.rs:43-190) and HTTP send/poll endpoints. The server treats them as opaque encrypted blobs. -
DedupTracker -- existing bounded FIFO (10,000 IDs) handles call signaling dedup automatically.
5. Media Security
Per-Call Encryption
[CONFIRMED] WZP uses per-call ChaCha20-Poly1305 sessions, NOT DTLS-SRTP.
Key Exchange: Ephemeral X25519 DH between caller and callee, expanded via
HKDF (wzp-crypto/src/handshake.rs:90-114):
shared_secret = X25519_DH(our_ephemeral, peer_ephemeral)
session_key = HKDF(ikm=shared_secret, salt=None, info="warzone-session-key")
cipher = ChaCha20-Poly1305(session_key)
Nonce Construction: Deterministic, not transmitted. 12-byte nonce layout
(wzp-crypto/src/nonce.rs:17-24):
Bytes 0-3: session_id (SHA-256(session_key)[:4])
Bytes 4-7: sequence_number (u32 big-endian)
Byte 8: direction (0=Send, 1=Recv)
Bytes 9-11: zero padding
This saves 12 bytes per packet since nonces are never on the wire.
AEAD: Media packet header bytes serve as AAD (authenticated associated
data), so the header is authenticated but not encrypted
(wzp-crypto/src/session.rs:62-87). Encryption overhead is 16 bytes
(Poly1305 tag) per packet.
Rekeying (Forward Secrecy)
[CONFIRMED] wzp-crypto/src/rekey.rs:1-68:
- Rekey interval: every 2^16 (65,536) packets (
REKEY_INTERVAL). - Rekeying uses fresh ephemeral X25519 DH mixed with the old key via HKDF:
new_dh = X25519(our_new_ephemeral, peer_new_ephemeral) new_key = HKDF(ikm=new_dh, salt=old_key, info="warzone-rekey") - Old key material is zeroized after derivation (rekey.rs:54-55).
- Session sequence counters reset to zero after rekey (session.rs:134-135).
- Rekeying is signaled via
SignalMessage::Rekeyover the reliable QUIC stream (packet.rs:281-286), with Ed25519 signature over(new_ephemeral_pub || session_id).
Anti-Replay Protection
[CONFIRMED] wzp-crypto/src/anti_replay.rs:1-136:
- Sliding window bitmap: 1024-packet window (
WINDOW_SIZE = 1024). - Bitmap stored as
Vec<u64>(16 words for 1024 bits). - Handles u16 sequence number wrapping correctly (RFC 1982 serial arithmetic).
- Rejects duplicates and packets older than the window.
Comparison with Previous DTLS-SRTP Proposal
The previous speculative document proposed DTLS-SRTP. The actual WZP implementation uses a custom, lighter-weight approach:
| Aspect | Previous Proposal (DTLS-SRTP) | Actual WZP Implementation |
|---|---|---|
| Key exchange | DTLS handshake | Ephemeral X25519 DH via QUIC reliable stream |
| Encryption | SRTP (AES-128-CM or AES-256-GCM) | ChaCha20-Poly1305 (same as featherChat) |
| Nonce | SRTP packet index | Deterministic: session_id + seq + direction |
| Rekeying | DTLS renegotiation | Ephemeral DH + HKDF mixing every 65536 packets |
| Anti-replay | SRTP replay window | 1024-packet bitmap window |
| Certificate | X.509 (DTLS) | Ed25519 identity key (Warzone identity model) |
| Transport | UDP (DTLS + SRTP) | QUIC DATAGRAM frames |
The actual approach is more aligned with WireGuard's design philosophy than WebRTC's.
6. Architecture Diagram
Confirmed System Architecture
+==========================================================+
| featherChat Clients |
| |
| +----------------+ +----------------+ +--------------+ |
| | CLI/TUI Client | | Web Client | | WZP Client | |
| | (warzone- | | (warzone-wasm) | | (wzp-client) | |
| | client) | | | | cpal audio | |
| +-------+--------+ +-------+--------+ +------+-------+ |
| | | | |
| +-------+--------------------+-------------------+------+ |
| | warzone-protocol | |
| | Identity . X3DH . Double Ratchet . Sender Keys | |
| | + CallSignal WireMessage variant (new) | |
| +------------------------------+------------------------+ |
+==========================================================+
|
HTTP / WebSocket / bincode
|
+==========================================================+
| featherChat Server |
| |
| +----------+ +----------+ +---------+ +-------------+ |
| | HTTP API | | WebSocket| | Auth | | Message | |
| | (axum) | | Relay | | (Ed25519| | Router + | |
| | :7700 | | | | challng)| | Dedup | |
| +----+-----+ +----+-----+ +----+----+ +------+------+ |
| | | | | |
| +----+-------------+-------------+---------------+------+ |
| | sled Database | |
| | keys . messages . groups . aliases . tokens | |
| +-------------------------------------------------------+ |
+==========================================================+
|
(future: federation)
|
+==========================================================+
| WZP Infrastructure |
| |
| +---------------------------------------------------+ |
| | WZP Relay Daemon | |
| | (wzp-relay crate) | |
| | | |
| | +----------------+ +---------------------------+ | |
| | | QUIC Endpoint | | Per-Session Pipeline | | |
| | | (quinn :4433) | | recv->FEC->jitter->FEC-> | | |
| | | ALPN: "wzp" | | send (no audio decode) | | |
| | +----------------+ +---------------------------+ | |
| | | |
| | +----------------+ +---------------------------+ | |
| | | Session Mgr | | Path Quality Monitor | | |
| | | (max 100 conc) | | (EWMA loss/RTT/jitter) | | |
| | +----------------+ +---------------------------+ | |
| +---------------------------------------------------+ |
+==========================================================+
Signaling vs Media Paths
SIGNALING PATH (E2E encrypted via featherChat)
==================================================
Alice featherChat Server Bob
| CallSignal (WS relay) |
| (Double Ratchet encrypted) |
| ---------> route as opaque blob ---------> | CallOffer
| <--------- route as opaque blob <--------- | CallAnswer
| ---------> route as opaque blob ---------> | IceCandidate
| |
MEDIA PATH (ChaCha20-Poly1305 encrypted, via QUIC)
==================================================
| |
| --- QUIC connect (ALPN "wzp") -----------> | (P2P or via relay)
| --- SignalMessage::CallOffer (reliable) --> | identity + ephemeral keys
| <-- SignalMessage::CallAnswer (reliable) -- | identity + ephemeral keys
| |
| === QUIC DATAGRAM: encrypted MediaPacket =>| audio (Opus/Codec2)
| <== QUIC DATAGRAM: encrypted MediaPacket ==| + FEC repair symbols
| |
WHAT EACH COMPONENT SEES
==================================================
featherChat Server:
- Opaque bincode blobs (CallSignal variant)
- Sender + recipient fingerprints (metadata)
- Cannot read signaling content
WZP Relay:
- Encrypted MediaPackets (cannot decrypt audio)
- FEC block structure (can forward repair symbols)
- Packet timing + sizes (traffic analysis possible)
- IP addresses of both peers
Neither server:
- Plaintext audio
- Session keys
- Call content
7. Codec Details
Codec Stack
[CONFIRMED] wzp-proto/src/codec_id.rs:1-68 and wzp-codec/src/lib.rs:
| Codec | Bitrate | Frame Duration | Sample Rate | Wire Format | Use Case |
|---|---|---|---|---|---|
| Opus 24k | 24 kbps | 20 ms | 48 kHz | Variable (~60 bytes/frame) | Good conditions |
| Opus 16k | 16 kbps | 20 ms | 48 kHz | Variable (~40 bytes/frame) | Moderate conditions |
| Opus 6k | 6 kbps | 40 ms | 48 kHz | Variable (~30 bytes/frame) | Degraded conditions |
| Codec2 3200 | 3.2 kbps | 20 ms | 8 kHz | 8 bytes/frame | Poor conditions |
| Codec2 1200 | 1.2 kbps | 40 ms | 8 kHz | 6 bytes/frame | Catastrophic conditions |
Opus: Via audiopus crate (libopus bindings). Supports inband FEC and DTX
(wzp-proto/src/traits.rs:27-31).
Codec2: Via the pure-Rust codec2 crate. Provides military-grade voice
coding at extremely low bitrates.
Adaptive Codec Switching
[CONFIRMED] wzp-codec/src/adaptive.rs:
AdaptiveEncoderwraps bothOpusEncoderandCodec2Encoder.- Callers always provide 48 kHz mono PCM; resampling is handled internally.
- When Codec2 is active: 48 kHz -> 8 kHz downsampling (6:1 decimation with
box filter) before encoding (
wzp-codec/src/resample.rs:10-21). - When decoding Codec2: 8 kHz -> 48 kHz upsampling (linear interpolation)
after decoding (
resample.rs:27-51). - Profile switching via
set_profile()is instantaneous -- both inner codecs are always instantiated.
Quality Profiles
[CONFIRMED] wzp-proto/src/codec_id.rs:82-113:
| Profile | Codec | FEC Ratio | Frame Duration | Frames/Block | Total Bitrate |
|---|---|---|---|---|---|
| GOOD | Opus 24k | 0.2 (20%) | 20 ms | 5 | ~28.8 kbps |
| DEGRADED | Opus 6k | 0.5 (50%) | 40 ms | 10 | ~9.0 kbps |
| CATASTROPHIC | Codec2 1200 | 1.0 (100%) | 40 ms | 8 | ~2.4 kbps |
8. Transport Layer
QUIC via quinn
[CONFIRMED] wzp-transport/src/lib.rs and sub-modules:
WZP uses QUIC (RFC 9000) via the quinn crate (v0.11) as its transport layer.
This is fundamentally different from WebRTC's UDP+DTLS+SRTP stack.
ALPN protocol: "wzp" (wzp-transport/src/config.rs:27,47).
Two transport modes on one QUIC connection:
| Mode | QUIC Feature | Used For | Reliability |
|---|---|---|---|
| Media | DATAGRAM frames | MediaPacket (audio + FEC) |
Unreliable (fire-and-forget) |
| Signaling | Bidirectional streams | SignalMessage (JSON) |
Reliable, ordered |
QUIC Configuration
[CONFIRMED] wzp-transport/src/config.rs:60-83:
| Parameter | Value | Rationale |
|---|---|---|
| Idle timeout | 30 seconds | Tolerant of lossy links |
| Keep-alive interval | 5 seconds | Prevents NAT timeout |
| DATAGRAM receive buffer | 64 KB | Sufficient for media burst |
| Receive window | 256 KB | Conservative for bandwidth-constrained links |
| Send window | 128 KB | Prevents buffer bloat |
| Stream receive window | 64 KB per stream | Signaling messages are small |
| Initial RTT estimate | 300 ms | Aggressive for high-latency links |
Media Packet Transport
[CONFIRMED] wzp-transport/src/datagram.rs and wzp-transport/src/quic.rs:42-67:
Media packets are serialized via MediaPacket::to_bytes() and sent as QUIC
DATAGRAM frames. MTU checking is performed before send
(quic.rs:47-54). The PathMonitor records send/receive observations for
quality estimation (quic.rs:57-60).
Signaling Transport
[CONFIRMED] wzp-transport/src/reliable.rs:9-58:
Signaling messages use length-prefixed JSON framing over QUIC bidirectional
streams. Format: [4-byte BE length][JSON payload]. Maximum message size: 1 MB.
Each signal message opens a new bidi stream and finishes the send side after
writing.
Path Quality Monitoring
[CONFIRMED] wzp-transport/src/path_monitor.rs:
- EWMA smoothing factor: 0.1 (
ALPHA). - Tracks: loss percentage, RTT, jitter (RTT variance), bandwidth estimate.
- Loss estimated from sent/received packet count gaps.
- Bandwidth estimated from bytes received over time.
9. Forward Error Correction (FEC)
RaptorQ Fountain Codes
[CONFIRMED] wzp-fec/src/lib.rs and sub-modules. Uses the raptorq crate (v2).
Architecture:
- Source symbols = encoded audio frames (one per codec frame).
- Frames are grouped into blocks (configurable frames_per_block).
- After a block is full, repair symbols are generated at the configured ratio.
- Decoder can reconstruct the full block from any sufficient subset of source + repair symbols.
Adaptive FEC (wzp-fec/src/adaptive.rs:18-49):
| Profile | Frames/Block | Repair Ratio | Symbol Size | Overhead |
|---|---|---|---|---|
| GOOD | 5 | 0.2 (20%) | 256 bytes | 1.2x |
| DEGRADED | 10 | 0.5 (50%) | 256 bytes | 1.5x |
| CATASTROPHIC | 8 | 1.0 (100%) | 256 bytes | 2.0x |
FEC traits (wzp-proto/src/traits.rs:52-93):
trait FecEncoder: Send + Sync {
fn add_source_symbol(&mut self, data: &[u8]) -> Result<(), FecError>;
fn generate_repair(&mut self, ratio: f32) -> Result<Vec<(u8, Vec<u8>)>, FecError>;
fn finalize_block(&mut self) -> Result<u8, FecError>;
fn current_block_id(&self) -> u8;
fn current_block_size(&self) -> usize;
}
trait FecDecoder: Send + Sync {
fn add_symbol(&mut self, block_id: u8, symbol_index: u8, is_repair: bool, data: &[u8]) -> ...;
fn try_decode(&mut self, block_id: u8) -> Result<Option<Vec<Vec<u8>>>, FecError>;
fn expire_before(&mut self, block_id: u8);
}
FEC in Media Packet Header
[CONFIRMED] wzp-proto/src/packet.rs:1-43:
The 12-byte MediaHeader carries FEC metadata:
- Bit 6 (
is_repair): distinguishes source from repair symbols. - Bits for
fec_ratio_encoded: 7-bit value (0-127) encoding the FEC ratio. - Byte 8 (
fec_block): block ID (wrapping u8). - Byte 9 (
fec_symbol): symbol index within the block.
10. Relay Architecture
WZP Relay Daemon
[CONFIRMED] wzp-relay/src/lib.rs and sub-modules.
The relay is a forwarding node that bridges two QUIC endpoints without
decoding audio. Pipeline: recv -> FEC decode -> jitter buffer -> FEC encode -> send.
Key design: The relay operates on encrypted, FEC-protected packets. It can reassemble FEC blocks and re-encode them for the next hop, but it never accesses plaintext audio.
Relay Configuration
[CONFIRMED] wzp-relay/src/config.rs:8-35:
| Parameter | Default | Purpose |
|---|---|---|
| Listen address | 0.0.0.0:4433 |
Client-facing QUIC endpoint |
| Remote relay | None |
Inter-relay link (if chained) |
| Max sessions | 100 | Concurrent call limit |
| Jitter target depth | 50 packets (1s) | Target buffer before playout |
| Jitter max depth | 250 packets (5s) | Maximum buffer before eviction |
Relay Pipeline
[CONFIRMED] wzp-relay/src/pipeline.rs:42-230:
Each RelayPipeline instance manages one direction of a call:
- Ingest: Incoming
MediaPacketfed to FEC decoder + quality controller. - FEC Decode: If a complete block's worth of symbols received, recover source frames.
- Jitter Buffer: Reorder recovered frames by sequence number.
- Playout: Pop frames in order for forwarding (with PLC gap marking).
- Outbound FEC: Re-encode with FEC for the next hop.
Quality tier changes are detected from QualityReport trailers in packets.
On tier change, FEC encoder/decoder are reconfigured (pipeline.rs:93-107).
Session Management
[CONFIRMED] wzp-relay/src/session_mgr.rs:
- Each call gets a
RelaySessionwith two pipelines (upstream + downstream). SessionManagertracks all active sessions in aHashMap<SessionId, RelaySession>.- Capacity limited to
max_sessions(default 100). - Idle sessions expire after timeout (
expire_idle()method). - Session state machine from
wzp-proto::Sessiongoverns lifecycle.
Relay Handshake
[CONFIRMED] wzp-relay/src/handshake.rs:19-80:
The relay performs the callee side of the WZP key exchange:
- Receive
CallOffer, verify caller's Ed25519 signature. - Generate own ephemeral X25519 keypair.
- Sign
(ephemeral_pub || "call-answer"). - Derive
ChaChaSessionfrom X25519 DH. - Choose the best quality profile from caller's supported list (prefer highest bitrate).
- Send
CallAnswer.
11. Jitter Buffer
[CONFIRMED] wzp-proto/src/jitter.rs:
- Data structure:
BTreeMap<u16, MediaPacket>ordered by sequence number. - Wrapping-aware: Uses RFC 1982 serial number arithmetic for u16 sequence
comparison (
seq_before(), jitter.rs:174-177). - Default configuration (
default_5s()): target 50 packets (1s), max 250 packets (5s), min 25 packets (0.5s) before playout begins. - Playout results:
Packet(data available),Missing(gap -- trigger PLC),NotReady(insufficient buffer depth). - Statistics tracked: packets received, played, lost, late, duplicate, current depth.
- Eviction: When buffer exceeds
max_depth, oldest packets are evicted.
12. Adaptive Quality Control
[CONFIRMED] wzp-proto/src/quality.rs:
Tier Classification
| Tier | Loss Threshold | RTT Threshold | Profile |
|---|---|---|---|
| Good | < 10% | < 400 ms | Opus 24k, 20% FEC |
| Degraded | 10-40% | 400-600 ms | Opus 6k, 50% FEC |
| Catastrophic | > 40% | > 600 ms | Codec2 1200, 100% FEC |
Hysteresis
- Downgrade threshold: 3 consecutive reports in a worse tier (fast reaction).
- Upgrade threshold: 10 consecutive reports in a better tier (slow, cautious).
- Step-at-a-time upgrades: Catastrophic -> Degraded -> Good (never skip).
- History: Sliding window of 20 recent
QualityReportobservations. - Force override:
force_profile()disables adaptive logic.
Quality Reports
[CONFIRMED] wzp-proto/src/packet.rs:143-184:
4-byte QualityReport appended to media packets when the Q flag is set:
| Field | Size | Encoding |
|---|---|---|
| loss_pct | 1 byte | 0-255 maps to 0-100% |
| rtt_4ms | 1 byte | RTT in 4ms units (0-1020 ms range) |
| jitter_ms | 1 byte | Jitter in milliseconds |
| bitrate_cap_kbps | 1 byte | Max receive bitrate in kbps |
13. Session State Machine
[CONFIRMED] wzp-proto/src/session.rs:1-144:
Idle --> Connecting --> Handshaking --> Active <--> Rekeying --> Active
|
Closed
| Transition | From | To | Trigger |
|---|---|---|---|
| Initiate | Idle | Connecting | User starts call |
| Connected | Connecting | Handshaking | QUIC connection established |
| HandshakeComplete | Handshaking | Active | Crypto handshake done |
| RekeyStart | Active | Rekeying | Periodic or requested rekey |
| RekeyComplete | Rekeying | Active | New keys installed |
| Terminate/ConnectionLost | Any active | Closed | Hangup or error |
Media continues flowing during Rekeying (is_media_active() returns
true for both Active and Rekeying states, session.rs:137-138).
Session tracks: unique 16-byte session ID, last transition timestamp, rekey count.
14. Group Calls
featherChat Groups as Call Rooms
[CONFIRMED] featherChat group infrastructure (from ARCHITECTURE.md):
POST /v1/groups/create-- create groupPOST /v1/groups/:name/join-- join groupGET /v1/groups/:name/members-- list members with aliasesPOST /v1/groups/:name/send-- fan-out message to all members
A featherChat group maps 1:1 to a WZP conference call room.
Group Call Architecture
WZP currently implements 1:1 calls. Group calls require:
-
Signaling: Use featherChat's group message fan-out to distribute
CallSignalto all members via their 1:1 encrypted channels. -
Media topology: Two options:
- Mesh: Each participant connects directly to every other (O(N^2) connections). Suitable for 2-4 participants.
- SFU: Each participant sends one stream to a relay; relay forwards to all others. The WZP relay crate already supports per-session pipeline management.
-
Media encryption for groups: featherChat already implements Sender Keys for group messaging (
warzone-protocol/src/sender_keys.rs). The same concept applies to media:- Each participant generates a media Sender Key.
- Distribute via 1:1 encrypted featherChat channels.
- Encrypt QUIC DATAGRAM payloads with Sender Key instead of per-pair session key.
- SFU/relay forwards encrypted packets without decryption.
-
WZP relay as SFU: The relay's
SessionManager(max 100 sessions) and pipeline architecture could be extended to fan-out mode. The relay already operates on encrypted packets without decoding audio, making it suitable as a zero-knowledge SFU.
Key Rotation on Membership Change
When a member joins or leaves:
- All remaining participants generate new media Sender Keys.
- Distribute via 1:1 featherChat channels.
- Relay is notified of membership change.
- Old keys are zeroized.
This matches featherChat's existing group key rotation behavior for chat.
15. Offline / Warzone Scenarios
Voice Messages as File Attachments
[CONFIRMED] featherChat supports file transfer up to 10 MB via
WireMessage::FileHeader + WireMessage::FileChunk (64 KB chunks).
Opus at 6 kbps: ~80 minutes per 10 MB. Codec2 at 1.2 kbps: ~400 minutes per 10 MB.
Record voice message:
1. Capture mic via wzp-client AudioCapture (48 kHz mono)
2. Encode with wzp-codec (Opus or Codec2)
3. Package as .opus / .c2 file
4. Send via featherChat: WireMessage::FileHeader + FileChunk
5. Recipient decodes and plays via wzp-codec decoder
No WZP relay infrastructure needed. Pure featherChat + wzp-codec.
Call Signaling via Mule Protocol
featherChat's mule protocol provides physical message relay for disconnected networks. The mule can deliver:
- Missed call notifications (CallSignal that expired)
- Voice messages (encoded audio file attachments)
- Call history (who tried to call, when)
The mule cannot enable real-time calls -- this is acknowledged.
LoRa: Text-Only, No Voice
LoRa (~250 byte payload) is incompatible with real-time voice. It can carry compact missed call notifications:
[1] version = 0x01
[1] type = 0x04 (missed_call)
[8] sender fingerprint (truncated)
[8] recipient fingerprint (truncated)
[4] timestamp (unix 32-bit)
[16] call_id
[1] media_type (0x01=audio, 0x02=video)
---
39 bytes total
16. Implementation Roadmap
Phase A: Identity Alignment (1-2 days)
Goal: Same seed produces same identity in both systems.
- Change WZP HKDF info strings in
wzp-crypto/src/handshake.rs:36,43:"warzone-ed25519-identity"->"warzone-ed25519""warzone-x25519-identity"->"warzone-x25519"
- Verify fingerprints match between featherChat and WZP for the same seed.
- Add cross-crate test:
warzone-protocol::Seed+wzp-crypto::WarzoneKeyExchangeproduce identical Ed25519 public keys and fingerprints.
Risk: Low. Two-line change + test.
Phase B: CallSignal WireMessage (1 week)
Goal: Call signaling flows through featherChat's encrypted channels.
- Add
CallSignalvariant toWireMessageinwarzone-protocol/src/message.rs. - Update
extract_message_id()inroutes/ws.rsandroutes/messages.rs. - Handle
CallSignalin TUI poll loop (tui/app.rs). - Handle in
decrypt_wire_message()inwarzone-wasm/src/lib.rs. - WZP client sends/receives
CallSignalvia featherChat WebSocket.
Dependencies: Phase A complete. Risk: Low. Follows existing WireMessage variant pattern (documented in ARCHITECTURE.md: "Adding New WireMessage Variants").
Phase C: Token Validation Endpoint (1-2 days)
Goal: WZP relays can verify featherChat bearer tokens.
- Add
POST /v1/auth/validatetoroutes/auth.rs, reusingvalidate_token(). - WZP relay calls this endpoint before accepting sessions.
Dependencies: None (independent of Phase A/B). Risk: Low. Wraps existing function.
Phase D: Integrated 1:1 Calls (2-4 weeks)
Goal: End-to-end voice call: featherChat signaling + WZP media.
- WZP client reads featherChat seed from
~/.warzone/identity.seed. - Call flow: featherChat WS for signaling -> QUIC for media.
- QUIC connection establishment via
wzp-transport::connect()/accept(). - Ephemeral X25519 handshake via
wzp-client::perform_handshake(). - Media pipeline:
AudioCapture->CallEncoder->QuinnTransport->QuinnTransport->CallDecoder->AudioPlayback. - Adaptive quality control via
AdaptiveQualityController.
Dependencies: Phases A, B, C. Risk: Medium. Full pipeline integration, real audio I/O.
Phase E: Relay Deployment (2 weeks)
Goal: WZP relay bridges peers behind NAT.
- Deploy
wzp-relaydaemon alongside featherChat server. - ICE-like candidate exchange via featherChat
CallSignal::IceCandidate. - Fallback: peers connect through relay when direct QUIC fails.
Dependencies: Phase D complete. Risk: Medium. NAT traversal is complex.
Phase F: Group Calls (4-6 weeks)
Goal: featherChat groups map to WZP conference calls.
- Extend
wzp-relaySessionManager for multi-party fan-out. - Sender Key distribution for media encryption via featherChat.
- Participant management (join/leave/kick mapped from featherChat groups).
- Scalability target: 10-20 participants.
Dependencies: Phase E complete. Risk: High. Multi-party media + Sender Keys is novel.
17. API Contracts
featherChat: New WireMessage Variant
// warzone-protocol/src/message.rs
WireMessage::CallSignal {
id: String, // UUID for dedup
sender_fingerprint: String, // caller's fingerprint
signal: Vec<u8>, // Serialized wzp_proto::SignalMessage (JSON)
}
featherChat: Token Validation Endpoint
POST /v1/auth/validate
Content-Type: application/json
Request: { "token": "hex..." }
Response: { "valid": true, "fingerprint": "a3f8c912...", "expires_at": 1711843600 }
or: { "valid": false, "error": "token expired" }
WZP: SignalMessage (existing, via QUIC reliable stream)
// wzp-proto/src/packet.rs
SignalMessage::CallOffer { identity_pub, ephemeral_pub, signature, supported_profiles }
SignalMessage::CallAnswer { identity_pub, ephemeral_pub, signature, chosen_profile }
SignalMessage::IceCandidate { candidate }
SignalMessage::Rekey { new_ephemeral_pub, signature }
SignalMessage::QualityUpdate { report, recommended_profile }
SignalMessage::Ping { timestamp_ms }
SignalMessage::Pong { timestamp_ms }
SignalMessage::Hangup { reason }
WZP: MediaPacket (existing, via QUIC DATAGRAM)
12-byte header:
Byte 0: [V:1][T:1][CodecID:4][Q:1][FecRatioHi:1]
Byte 1: [FecRatioLo:6][unused:2]
Byte 2-3: Sequence number (BE u16)
Byte 4-7: Timestamp ms (BE u32)
Byte 8: FEC block ID
Byte 9: FEC symbol index
Byte 10: Reserved
Byte 11: CSRC count
Payload: Encrypted audio frame (ChaCha20-Poly1305, 16-byte tag appended)
Optional 4-byte QualityReport trailer (when Q flag set):
Byte 0: loss_pct (0-255)
Byte 1: rtt_4ms (0-255 = 0-1020ms)
Byte 2: jitter_ms
Byte 3: bitrate_cap_kbps
WZP: Client Pipeline APIs
// wzp-client: encode side
let mut encoder = CallEncoder::new(&CallConfig::default());
let packets: Vec<MediaPacket> = encoder.encode_frame(&pcm_960_samples)?;
// Each packet goes through: transport.send_media(&packet).await
// wzp-client: decode side
let mut decoder = CallDecoder::new(&CallConfig::default());
decoder.ingest(received_packet);
let samples: Option<usize> = decoder.decode_next(&mut pcm_buffer);
// wzp-client: audio I/O
let capture = AudioCapture::start()?; // 48 kHz mono, 960 samples/frame
let playback = AudioPlayback::start()?;
let frame: Option<Vec<i16>> = capture.read_frame(); // blocking
playback.write_frame(&decoded_pcm);
// wzp-crypto: key exchange
let mut kx = WarzoneKeyExchange::from_identity_seed(&seed);
let eph_pub = kx.generate_ephemeral();
let session: Box<dyn CryptoSession> = kx.derive_session(&peer_eph_pub)?;
// wzp-crypto: encrypt/decrypt media
session.encrypt(header_aad, plaintext, &mut ciphertext)?;
session.decrypt(header_aad, ciphertext, &mut plaintext)?;
// wzp-transport: QUIC connection
let endpoint = create_endpoint(bind_addr, Some(server_config))?;
let conn = connect(&endpoint, peer_addr, "localhost", client_config).await?;
let transport = QuinnTransport::new(conn);
transport.send_media(&packet).await?;
transport.send_signal(&signal_msg).await?;
18. Security Analysis
Combined Threat Model
| Threat | featherChat Mitigation | WZP Mitigation | Residual Risk |
|---|---|---|---|
| Server reads call signaling | Double Ratchet E2E encryption | N/A (tunneled through featherChat) | None -- server sees opaque blobs |
| Server performs MITM on call | Pre-key bundle signed by Ed25519 identity | CallOffer/Answer signed by Ed25519 identity | Fingerprint verification required (TOFU) |
| Relay reads audio | N/A | ChaCha20-Poly1305 per-call encryption | None -- relay sees encrypted datagrams |
| Replay of media packets | N/A | Anti-replay window (1024 packets) | Old packets beyond window are rejected |
| Long call key compromise | N/A | Rekey every 65536 packets (~22 min at 50 pps) | Window of 65536 packets between rekeys |
| Call metadata | Server sees WireMessage routing (sender/recipient fp) | Relay sees IP addresses and packet timing | Both see who is calling whom |
| Codec fingerprinting | N/A | CodecId is in the encrypted payload (after ChaCha20) but header codec field is authenticated-only | Header reveals codec in use (4-bit field) |
| Nonce reuse | N/A | Deterministic nonce: session_id + seq + direction; reset on rekey | Safe as long as seq counter doesn't wrap within a rekey epoch (2^16 limit enforced) |
| Token theft | 7-day TTL, local storage | Tokens not used for media auth (Ed25519 signatures instead) | Device compromise = token + seed compromise |
| Seed compromise | Both systems compromised | All derived keys compromised | Catastrophic -- protect seed above all else |
Comparison with Signal Calling
| Aspect | featherChat + WZP (Confirmed) | Signal Calling |
|---|---|---|
| Signaling encryption | Double Ratchet (E2E) | Signal Protocol (E2E) |
| Media encryption | ChaCha20-Poly1305 (per-call ephemeral) | SRTP via DTLS-SRTP |
| Key exchange | Ephemeral X25519 DH | DTLS handshake |
| Nonce scheme | Deterministic (not transmitted) | SRTP packet index |
| Forward secrecy | Rekey every 2^16 packets | DTLS renegotiation |
| Anti-replay | 1024-packet bitmap window | SRTP replay window |
| FEC | RaptorQ fountain codes (adaptive) | Opus inband FEC only |
| Codec range | Opus 24k-6k + Codec2 3200-1200 | Opus only |
| Transport | QUIC (DATAGRAM + streams) | ICE/DTLS/SRTP over UDP |
| NAT traversal | QUIC relay (wzp-relay) | TURN relay |
| Group calls | Planned: Sender Keys + SFU relay | SFU + Sender Keys |
| Identity | Seed-based (BIP39 mnemonic) | Phone number |
| Obfuscation | Trait defined (Phase 2 planned) | None standard |
Key Advantages of WZP Approach
- Unified crypto stack: ChaCha20-Poly1305 + X25519 + Ed25519 everywhere (same as featherChat messaging). No DTLS/SRTP complexity.
- Extreme low-bitrate resilience: Codec2 at 1.2 kbps with 100% FEC enables voice calls at ~2.4 kbps total bandwidth.
- RaptorQ FEC: Fountain codes provide better loss recovery than Opus inband FEC, especially at high loss rates (>20%).
- QUIC transport: Built-in congestion control, multiplexing, and NAT traversal. DATAGRAM frames provide unreliable delivery without head-of-line blocking.
- Obfuscation ready:
ObfuscationLayertrait (wzp-proto/src/traits.rs:218-232) defined for DPI evasion on client-relay links.
Known Limitations
- No sealed sender -- featherChat server sees sender/recipient fingerprints for CallSignal messages. Same limitation as chat.
- Header codec field is not encrypted -- the MediaHeader is used as AAD (authenticated but cleartext). An observer can see which codec tier is active.
- Relay sees packet timing -- traffic analysis reveals voice activity patterns. Mitigation: constant-bitrate encoding + DTX disabled.
- HKDF info string mismatch (see Section 2) -- must be resolved before deployment.
- No post-quantum protection -- all key exchanges use classical X25519. Hybrid X25519 + ML-KEM is feasible but not implemented.
- Self-signed QUIC certificates -- current config uses
SkipServerVerification(wzp-transport/src/config.rs:88-134). Production deployment needs proper certificate validation or identity-based verification.
Appendix A: featherChat Code References
| Component | File | Key Types/Functions |
|---|---|---|
| Seed & Identity | warzone-protocol/src/identity.rs |
Seed, IdentityKeyPair, PublicIdentity |
| Wire Protocol | warzone-protocol/src/message.rs |
WireMessage enum (7 variants) |
| Server Auth | warzone-server/src/routes/auth.rs |
create_challenge(), verify_challenge(), validate_token() |
| WebSocket Relay | warzone-server/src/routes/ws.rs |
handle_socket(), extract_message_id() |
Appendix B: WZP Code References
| Component | File | Key Types/Functions |
|---|---|---|
| Protocol types | wzp-proto/src/packet.rs |
MediaHeader, MediaPacket, QualityReport, SignalMessage, HangupReason |
| Codec IDs | wzp-proto/src/codec_id.rs |
CodecId (5 variants), QualityProfile (GOOD/DEGRADED/CATASTROPHIC) |
| Traits | wzp-proto/src/traits.rs |
AudioEncoder, AudioDecoder, FecEncoder, FecDecoder, CryptoSession, KeyExchange, MediaTransport, ObfuscationLayer, QualityController |
| Session FSM | wzp-proto/src/session.rs |
Session, SessionState, SessionEvent |
| Jitter buffer | wzp-proto/src/jitter.rs |
JitterBuffer, PlayoutResult |
| Quality control | wzp-proto/src/quality.rs |
AdaptiveQualityController, Tier |
| Adaptive codec | wzp-codec/src/adaptive.rs |
AdaptiveEncoder, AdaptiveDecoder |
| Resampling | wzp-codec/src/resample.rs |
resample_48k_to_8k(), resample_8k_to_48k() |
| Key exchange | wzp-crypto/src/handshake.rs |
WarzoneKeyExchange |
| Crypto session | wzp-crypto/src/session.rs |
ChaChaSession |
| Nonce | wzp-crypto/src/nonce.rs |
build_nonce(), Direction |
| Rekeying | wzp-crypto/src/rekey.rs |
RekeyManager (interval: 2^16 packets) |
| Anti-replay | wzp-crypto/src/anti_replay.rs |
AntiReplayWindow (1024-packet bitmap) |
| FEC adaptive | wzp-fec/src/adaptive.rs |
AdaptiveFec |
| QUIC config | wzp-transport/src/config.rs |
server_config(), client_config(), ALPN "wzp" |
| QUIC transport | wzp-transport/src/quic.rs |
QuinnTransport |
| Path monitor | wzp-transport/src/path_monitor.rs |
PathMonitor (EWMA alpha=0.1) |
| Connection | wzp-transport/src/connection.rs |
create_endpoint(), connect(), accept() |
| Relay pipeline | wzp-relay/src/pipeline.rs |
RelayPipeline, PipelineStats |
| Relay sessions | wzp-relay/src/session_mgr.rs |
SessionManager, RelaySession |
| Relay config | wzp-relay/src/config.rs |
RelayConfig (listen :4433, max 100 sessions) |
| Relay handshake | wzp-relay/src/handshake.rs |
accept_handshake() |
| Client pipeline | wzp-client/src/call.rs |
CallEncoder, CallDecoder, CallConfig |
| Client handshake | wzp-client/src/handshake.rs |
perform_handshake() |
| Audio I/O | wzp-client/src/audio_io.rs |
AudioCapture, AudioPlayback (cpal, 48 kHz mono) |
| Benchmarks | wzp-client/src/bench.rs |
bench_codec_roundtrip(), bench_fec_recovery(), bench_encrypt_decrypt(), bench_full_pipeline() |