Wire E2E messaging: send, recv, session persistence, auto-registration
CLI client (warzone): - `warzone init` now generates pre-key bundle (1 SPK + 10 OTPKs), stores secrets in local sled DB, saves bundle for server registration - `warzone register -s <url>` registers bundle with server - `warzone send <fp> <msg> -s <url>` full E2E flow: - Auto-registers bundle on first use - Fetches recipient's pre-key bundle - Performs X3DH key exchange (first message) or uses existing session - Encrypts with Double Ratchet - Sends WireMessage envelope to server - `warzone recv -s <url>` polls and decrypts: - Handles KeyExchange messages (X3DH respond + ratchet init as Bob) - Handles Message (decrypt with existing ratchet session) - Saves session state after each decrypt Wire protocol (WireMessage enum): - KeyExchange variant: sender identity, ephemeral key, OTPK id, ratchet msg - Message variant: sender fingerprint + ratchet message Session persistence: - Ratchet state serialized with bincode, stored in sled (~/.warzone/db) - Pre-key secrets stored in sled, OTPKs consumed on use - Sessions keyed by peer fingerprint Networking (net.rs): - register_bundle, fetch_bundle, send_message, poll_messages - JSON API over HTTP, bundles serialized with bincode + base64 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,123 @@
|
||||
// HTTP client for talking to warzone-server.
|
||||
// TODO: implement in Phase 1 step 9.
|
||||
//! HTTP client for talking to warzone-server.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use warzone_protocol::prekey::PreKeyBundle;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerClient {
|
||||
pub base_url: String,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RegisterRequest {
|
||||
fingerprint: String,
|
||||
bundle: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SendRequest {
|
||||
to: String,
|
||||
message: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BundleResponse {
|
||||
fingerprint: String,
|
||||
bundle: String, // base64
|
||||
}
|
||||
|
||||
impl ServerClient {
|
||||
pub fn new(base_url: &str) -> Self {
|
||||
ServerClient {
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register our pre-key bundle with the server.
|
||||
pub async fn register_bundle(
|
||||
&self,
|
||||
fingerprint: &str,
|
||||
bundle: &PreKeyBundle,
|
||||
) -> Result<()> {
|
||||
let encoded =
|
||||
bincode::serialize(bundle).context("failed to serialize bundle")?;
|
||||
self.client
|
||||
.post(format!("{}/v1/keys/register", self.base_url))
|
||||
.json(&RegisterRequest {
|
||||
fingerprint: fingerprint.to_string(),
|
||||
bundle: encoded,
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("failed to register bundle")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch a user's pre-key bundle from the server.
|
||||
pub async fn fetch_bundle(&self, fingerprint: &str) -> Result<PreKeyBundle> {
|
||||
let resp: BundleResponse = self
|
||||
.client
|
||||
.get(format!(
|
||||
"{}/v1/keys/{}",
|
||||
self.base_url, fingerprint
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.context("failed to fetch bundle")?
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse bundle response")?;
|
||||
|
||||
let bytes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&resp.bundle,
|
||||
)
|
||||
.context("failed to decode base64 bundle")?;
|
||||
|
||||
bincode::deserialize(&bytes).context("failed to deserialize bundle")
|
||||
}
|
||||
|
||||
/// Send an encrypted message to the server for delivery.
|
||||
pub async fn send_message(&self, to: &str, message: &[u8]) -> Result<()> {
|
||||
self.client
|
||||
.post(format!("{}/v1/messages/send", self.base_url))
|
||||
.json(&SendRequest {
|
||||
to: to.to_string(),
|
||||
message: message.to_vec(),
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("failed to send message")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Poll for messages addressed to us.
|
||||
pub async fn poll_messages(&self, fingerprint: &str) -> Result<Vec<Vec<u8>>> {
|
||||
let resp: Vec<String> = self
|
||||
.client
|
||||
.get(format!(
|
||||
"{}/v1/messages/poll/{}",
|
||||
self.base_url, fingerprint
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.context("failed to poll messages")?
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse poll response")?;
|
||||
|
||||
let mut messages = Vec::new();
|
||||
for b64 in resp {
|
||||
if let Ok(bytes) = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&b64,
|
||||
) {
|
||||
messages.push(bytes);
|
||||
}
|
||||
}
|
||||
Ok(messages)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user