Files
featherChat/warzone/crates/warzone-server/src/routes/keys.rs
Siavash Sameni 3e0889e5dc v0.0.21: TUI overhaul, WZP call infrastructure, security hardening, federation
TUI:
- Split 1,756-line app.rs monolith into 7 modules (types, draw, commands, input, file_transfer, network, mod)
- Message timestamps [HH:MM], scrolling (PageUp/Down/arrows), connection status dot, unread badge
- /help command, terminal bell on incoming DM, /devices + /kick commands
- 44 unit tests (types, input, draw with TestBackend)

Server — WZP Call Infrastructure (FC-2/3/5/6/7/10):
- Call state management (CallState, CallStatus, active_calls, calls + missed_calls sled trees)
- WS call signal awareness (Offer/Answer/Hangup update state, missed call on offline)
- Group call endpoint (POST /groups/:name/call with SHA-256 room ID, fan-out)
- Presence API (GET /presence/:fp, POST /presence/batch)
- Missed call flush on WS reconnect
- WZP relay config + CORS

Server — Security (FC-P1):
- Auth enforcement middleware (AuthFingerprint extractor on 13 write handlers)
- Session auto-recovery (delete corrupted ratchet, show [session reset])
- WS connection cap (5/fingerprint) + global concurrency limit (200)
- Device management (GET /devices, POST /devices/:id/kick, POST /devices/revoke-all)

Server — Federation:
- Two-server federation via JSON config (--federation flag)
- Periodic presence sync (every 5s, full-state, self-healing)
- Message forwarding via HTTP POST with SHA-256(secret||body) auth
- Graceful degradation (peer down = queue locally)
- deliver_or_queue() replaces push-or-queue in ws.rs + messages.rs

Client — Group Messaging:
- SenderKeyDistribution storage + GroupSenderKey decryption in TUI
- sender_keys sled tree in LocalDb

WASM:
- All 8 WireMessage variants handled (no more "unsupported")
- decrypt_group_message() + create_sender_key_from_distribution() exports
- CallSignal parsing with signal_type mapping

Docs:
- ARCHITECTURE.md rewritten with Mermaid diagrams
- README.md created
- TASK_PLAN.md with FC-P{phase}-T{task} naming
- PROGRESS.md updated to v0.0.21

WZP submodule updated to 6f4e8eb (IAX2 trunking, adaptive quality, metrics, all S-tasks done)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:45:58 +04:00

171 lines
5.5 KiB
Rust

use axum::{
extract::{Path, State},
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use crate::state::AppState;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/keys/register", post(register_keys))
.route("/keys/replenish", post(replenish_otpks))
.route("/keys/list", get(list_keys))
.route("/keys/:fingerprint", get(get_bundle))
.route("/keys/:fingerprint/otpk-count", get(otpk_count))
.route("/keys/:fingerprint/devices", get(list_devices))
}
/// Debug endpoint: list all registered fingerprints.
async fn list_keys(State(state): State<AppState>) -> Json<serde_json::Value> {
let keys: Vec<String> = state
.db
.keys
.iter()
.filter_map(|item| {
item.ok()
.and_then(|(k, _)| String::from_utf8(k.to_vec()).ok())
})
.collect();
tracing::info!("Listed {} registered keys", keys.len());
Json(serde_json::json!({ "keys": keys, "count": keys.len() }))
}
/// Normalize fingerprint: strip colons, lowercase.
fn normalize_fp(fp: &str) -> String {
fp.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect::<String>()
.to_lowercase()
}
#[derive(Deserialize)]
struct RegisterRequest {
fingerprint: String,
#[serde(default)]
device_id: Option<String>,
bundle: Vec<u8>,
}
#[derive(Serialize)]
struct RegisterResponse {
ok: bool,
}
async fn register_keys(
_auth: crate::auth_middleware::AuthFingerprint,
State(state): State<AppState>,
Json(req): Json<RegisterRequest>,
) -> Json<RegisterResponse> {
let fp = normalize_fp(&req.fingerprint);
let device_id = req.device_id.unwrap_or_else(|| "default".to_string());
// Store bundle keyed by fingerprint (primary, used for lookup)
let _ = state.db.keys.insert(fp.as_bytes(), req.bundle.clone());
// Also store per-device: device:<fp>:<device_id> → bundle
let device_key = format!("device:{}:{}", fp, device_id);
let _ = state.db.keys.insert(device_key.as_bytes(), req.bundle);
tracing::info!("Registered bundle for {} (device: {})", fp, device_id);
Json(RegisterResponse { ok: true })
}
async fn get_bundle(
State(state): State<AppState>,
Path(fingerprint): Path<String>,
) -> Result<Json<serde_json::Value>, axum::http::StatusCode> {
let key = normalize_fp(&fingerprint);
tracing::info!("get_bundle: raw path='{}', normalized='{}'", fingerprint, key);
// Debug: list what's in the DB
let all_keys: Vec<String> = state.db.keys.iter()
.filter_map(|r| r.ok().and_then(|(k, _)| String::from_utf8(k.to_vec()).ok()))
.collect();
tracing::info!("get_bundle: DB contains {} keys: {:?}", all_keys.len(), all_keys);
match state.db.keys.get(key.as_bytes()) {
Ok(Some(data)) => {
tracing::info!("get_bundle: FOUND {} bytes for {}", data.len(), key);
Ok(Json(serde_json::json!({
"fingerprint": fingerprint,
"bundle": base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &data),
})))
}
Ok(None) => {
tracing::warn!("get_bundle: NOT FOUND for key '{}'", key);
Err(axum::http::StatusCode::NOT_FOUND)
}
Err(e) => {
tracing::error!("get_bundle: DB error: {}", e);
Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
/// Check how many one-time pre-keys remain for a fingerprint.
async fn otpk_count(
State(state): State<AppState>,
Path(fingerprint): Path<String>,
) -> Json<serde_json::Value> {
let fp = normalize_fp(&fingerprint);
let prefix = format!("otpk:{}:", fp);
let count = state.db.keys.scan_prefix(prefix.as_bytes()).count();
Json(serde_json::json!({ "fingerprint": fp, "otpk_count": count }))
}
#[derive(Deserialize)]
struct ReplenishRequest {
fingerprint: String,
/// One-time pre-keys: list of {id, public_key_hex}
otpks: Vec<OtpkEntry>,
}
#[derive(Deserialize)]
struct OtpkEntry {
id: u32,
public_key: String, // hex-encoded 32-byte X25519 public key
}
/// Upload additional one-time pre-keys.
async fn replenish_otpks(
_auth: crate::auth_middleware::AuthFingerprint,
State(state): State<AppState>,
Json(req): Json<ReplenishRequest>,
) -> Json<serde_json::Value> {
let fp = normalize_fp(&req.fingerprint);
let mut stored = 0;
for otpk in &req.otpks {
let key = format!("otpk:{}:{}", fp, otpk.id);
let _ = state.db.keys.insert(key.as_bytes(), otpk.public_key.as_bytes());
stored += 1;
}
let prefix = format!("otpk:{}:", fp);
let total = state.db.keys.scan_prefix(prefix.as_bytes()).count();
tracing::info!("Replenished {} OTPKs for {} (total: {})", stored, fp, total);
Json(serde_json::json!({ "ok": true, "stored": stored, "total": total }))
}
/// List all registered devices for a fingerprint.
async fn list_devices(
State(state): State<AppState>,
Path(fingerprint): Path<String>,
) -> Json<serde_json::Value> {
let fp = normalize_fp(&fingerprint);
let prefix = format!("device:{}:", fp);
let devices: Vec<String> = state.db.keys.scan_prefix(prefix.as_bytes())
.filter_map(|item| {
item.ok().and_then(|(k, _)| {
let key_str = String::from_utf8_lossy(&k).to_string();
// key format: device:<fp>:<device_id>
key_str.rsplit(':').next().map(|s| s.to_string())
})
})
.collect();
Json(serde_json::json!({ "fingerprint": fp, "devices": devices, "count": devices.len() }))
}