Server auth (challenge-response) + OTP key replenishment
Authentication:
- POST /v1/auth/challenge {fingerprint} → {challenge, expires_at}
- POST /v1/auth/verify {fingerprint, challenge, signature} → {token}
- Client signs challenge with Ed25519 identity key
- Server verifies against stored public key
- Returns bearer token valid for 7 days
- Web clients get token without sig verify (Phase 2: WASM)
- validate_token() helper for protecting endpoints
OTP Key Replenishment:
- GET /v1/keys/:fp/otpk-count → {otpk_count}
- POST /v1/keys/replenish {fingerprint, otpks: [{id, public_key}]}
- OTPKs stored individually: otpk:<fp>:<id> → public_key
- Returns total count after replenishment
Phase 1 complete:
- [x] Seed-based identity + BIP39
- [x] X3DH + Double Ratchet (forward secrecy)
- [x] Pre-key bundles
- [x] Server (keys, messages, groups, aliases, auth)
- [x] CLI TUI + Web client
- [x] Aliases with TTL + recovery
- [x] Seed encryption (Argon2id + ChaCha20)
- [x] Server auth (challenge-response + tokens)
- [x] OTP key replenishment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,10 @@ 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))
|
||||
}
|
||||
|
||||
/// Debug endpoint: list all registered fingerprints.
|
||||
@@ -89,3 +91,48 @@ async fn get_bundle(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
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 }))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user