Compare commits
2 Commits
e364f437a2
...
60a7006ed9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60a7006ed9 | ||
|
|
82f5061aa1 |
5
warzone/Cargo.lock
generated
5
warzone/Cargo.lock
generated
@@ -2557,7 +2557,10 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"base64",
|
||||||
|
"bincode",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"hex",
|
"hex",
|
||||||
@@ -2570,7 +2573,9 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
"warzone-protocol",
|
"warzone-protocol",
|
||||||
|
"x25519-dalek",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,8 @@ chacha20poly1305.workspace = true
|
|||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
zeroize.workspace = true
|
zeroize.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
x25519-dalek.workspace = true
|
||||||
|
bincode.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
|
use anyhow::Result;
|
||||||
use warzone_protocol::identity::Seed;
|
use warzone_protocol::identity::Seed;
|
||||||
|
use warzone_protocol::prekey::{
|
||||||
|
generate_one_time_pre_keys, generate_signed_pre_key, OneTimePreKeyPublic, PreKeyBundle,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::keystore;
|
use crate::keystore;
|
||||||
|
use crate::net::ServerClient;
|
||||||
|
use crate::storage::LocalDb;
|
||||||
|
|
||||||
pub fn run() -> anyhow::Result<()> {
|
pub fn run() -> Result<()> {
|
||||||
let seed = Seed::generate();
|
let seed = Seed::generate();
|
||||||
let identity = seed.derive_identity();
|
let identity = seed.derive_identity();
|
||||||
let pub_id = identity.public_identity();
|
let pub_id = identity.public_identity();
|
||||||
@@ -23,5 +29,70 @@ pub fn run() -> anyhow::Result<()> {
|
|||||||
keystore::save_seed(&seed)?;
|
keystore::save_seed(&seed)?;
|
||||||
println!("Seed saved to ~/.warzone/identity.seed");
|
println!("Seed saved to ~/.warzone/identity.seed");
|
||||||
|
|
||||||
|
// Generate pre-keys and store secrets locally
|
||||||
|
let db = LocalDb::open()?;
|
||||||
|
|
||||||
|
let (spk_secret, signed_pre_key) = generate_signed_pre_key(&identity, 1);
|
||||||
|
db.save_signed_pre_key(1, &spk_secret)?;
|
||||||
|
|
||||||
|
let otpks = generate_one_time_pre_keys(0, 10);
|
||||||
|
for otpk in &otpks {
|
||||||
|
db.save_one_time_pre_key(otpk.id, &otpk.secret)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Generated 1 signed pre-key + {} one-time pre-keys",
|
||||||
|
otpks.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build bundle for server registration
|
||||||
|
let bundle = PreKeyBundle {
|
||||||
|
identity_key: *pub_id.signing.as_bytes(),
|
||||||
|
identity_encryption_key: *pub_id.encryption.as_bytes(),
|
||||||
|
signed_pre_key,
|
||||||
|
one_time_pre_key: Some(OneTimePreKeyPublic {
|
||||||
|
id: otpks[0].id,
|
||||||
|
public_key: *otpks[0].public.as_bytes(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store bundle locally for later registration
|
||||||
|
let bundle_bytes = bincode::serialize(&bundle)?;
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||||
|
let bundle_path = std::path::Path::new(&home)
|
||||||
|
.join(".warzone")
|
||||||
|
.join("bundle.bin");
|
||||||
|
std::fs::write(&bundle_path, &bundle_bytes)?;
|
||||||
|
|
||||||
|
println!("\nTo register with a server, run:");
|
||||||
|
println!(
|
||||||
|
" warzone send <recipient-fingerprint> <message> -s http://server:7700"
|
||||||
|
);
|
||||||
|
println!("\nOr register your key bundle manually:");
|
||||||
|
println!(" (bundle auto-registered on first send)");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register the local bundle with a server. Called automatically before first send.
|
||||||
|
pub async fn register_with_server(server_url: &str) -> Result<()> {
|
||||||
|
let seed = keystore::load_seed()?;
|
||||||
|
let identity = seed.derive_identity();
|
||||||
|
let pub_id = identity.public_identity();
|
||||||
|
let fp = pub_id.fingerprint.to_string();
|
||||||
|
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||||
|
let bundle_path = std::path::Path::new(&home)
|
||||||
|
.join(".warzone")
|
||||||
|
.join("bundle.bin");
|
||||||
|
|
||||||
|
let bundle_bytes = std::fs::read(&bundle_path)
|
||||||
|
.map_err(|_| anyhow::anyhow!("No bundle found. Run `warzone init` first."))?;
|
||||||
|
let bundle: PreKeyBundle = bincode::deserialize(&bundle_bytes)?;
|
||||||
|
|
||||||
|
let client = ServerClient::new(server_url);
|
||||||
|
client.register_bundle(&fp, &bundle).await?;
|
||||||
|
println!("Bundle registered with {}", server_url);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod recover;
|
pub mod recover;
|
||||||
|
pub mod send;
|
||||||
|
pub mod recv;
|
||||||
|
|||||||
117
warzone/crates/warzone-client/src/cli/recv.rs
Normal file
117
warzone/crates/warzone-client/src/cli/recv.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use warzone_protocol::ratchet::RatchetState;
|
||||||
|
use warzone_protocol::types::Fingerprint;
|
||||||
|
use warzone_protocol::x3dh;
|
||||||
|
use x25519_dalek::PublicKey;
|
||||||
|
|
||||||
|
use crate::cli::send::WireMessage;
|
||||||
|
use crate::keystore;
|
||||||
|
use crate::net::ServerClient;
|
||||||
|
use crate::storage::LocalDb;
|
||||||
|
|
||||||
|
pub async fn run(server_url: &str) -> Result<()> {
|
||||||
|
let seed = keystore::load_seed()?;
|
||||||
|
let identity = seed.derive_identity();
|
||||||
|
let our_pub = identity.public_identity();
|
||||||
|
let our_fp = our_pub.fingerprint.to_string();
|
||||||
|
let db = LocalDb::open()?;
|
||||||
|
let client = ServerClient::new(server_url);
|
||||||
|
|
||||||
|
println!("Polling for messages as {}...", our_fp);
|
||||||
|
|
||||||
|
let messages = client.poll_messages(&our_fp).await?;
|
||||||
|
|
||||||
|
if messages.is_empty() {
|
||||||
|
println!("No new messages.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Received {} message(s):\n", messages.len());
|
||||||
|
|
||||||
|
for raw in &messages {
|
||||||
|
match bincode::deserialize::<WireMessage>(raw) {
|
||||||
|
Ok(WireMessage::KeyExchange {
|
||||||
|
sender_fingerprint,
|
||||||
|
sender_identity_encryption_key,
|
||||||
|
ephemeral_public,
|
||||||
|
used_one_time_pre_key_id,
|
||||||
|
ratchet_message,
|
||||||
|
}) => {
|
||||||
|
let sender_fp = Fingerprint::from_hex(&sender_fingerprint)
|
||||||
|
.context("invalid sender fingerprint")?;
|
||||||
|
|
||||||
|
// Load our signed pre-key secret
|
||||||
|
let spk_id = 1u32; // We use ID 1 for our signed pre-key
|
||||||
|
let spk_secret = db
|
||||||
|
.load_signed_pre_key(spk_id)?
|
||||||
|
.context("missing signed pre-key — run `warzone init` first")?;
|
||||||
|
|
||||||
|
// Load one-time pre-key if used
|
||||||
|
let otpk_secret = if let Some(id) = used_one_time_pre_key_id {
|
||||||
|
db.take_one_time_pre_key(id)?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// X3DH respond
|
||||||
|
let their_identity_x25519 = PublicKey::from(sender_identity_encryption_key);
|
||||||
|
let their_ephemeral = PublicKey::from(ephemeral_public);
|
||||||
|
|
||||||
|
let shared_secret = x3dh::respond(
|
||||||
|
&identity,
|
||||||
|
&spk_secret,
|
||||||
|
otpk_secret.as_ref(),
|
||||||
|
&their_identity_x25519,
|
||||||
|
&their_ephemeral,
|
||||||
|
)
|
||||||
|
.context("X3DH respond failed")?;
|
||||||
|
|
||||||
|
// Init ratchet as Bob
|
||||||
|
let mut state = RatchetState::init_bob(shared_secret, spk_secret);
|
||||||
|
|
||||||
|
// Decrypt the message
|
||||||
|
match state.decrypt(&ratchet_message) {
|
||||||
|
Ok(plaintext) => {
|
||||||
|
let text = String::from_utf8_lossy(&plaintext);
|
||||||
|
println!(" [{}] {}: {}", "new session", sender_fingerprint, text);
|
||||||
|
db.save_session(&sender_fp, &state)?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" [{}] decrypt failed: {}", sender_fingerprint, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(WireMessage::Message {
|
||||||
|
sender_fingerprint,
|
||||||
|
ratchet_message,
|
||||||
|
}) => {
|
||||||
|
let sender_fp = Fingerprint::from_hex(&sender_fingerprint)
|
||||||
|
.context("invalid sender fingerprint")?;
|
||||||
|
|
||||||
|
match db.load_session(&sender_fp)? {
|
||||||
|
Some(mut state) => match state.decrypt(&ratchet_message) {
|
||||||
|
Ok(plaintext) => {
|
||||||
|
let text = String::from_utf8_lossy(&plaintext);
|
||||||
|
println!(" {}: {}", sender_fingerprint, text);
|
||||||
|
db.save_session(&sender_fp, &state)?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" [{}] decrypt failed: {}", sender_fingerprint, e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
eprintln!(
|
||||||
|
" [{}] no session — cannot decrypt (need key exchange first)",
|
||||||
|
sender_fingerprint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" failed to deserialize message: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
91
warzone/crates/warzone-client/src/cli/send.rs
Normal file
91
warzone/crates/warzone-client/src/cli/send.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use warzone_protocol::identity::Seed;
|
||||||
|
use warzone_protocol::message::{MessageContent, MessageType, WarzoneMessage};
|
||||||
|
use warzone_protocol::ratchet::{RatchetMessage, RatchetState};
|
||||||
|
use warzone_protocol::types::{Fingerprint, MessageId, SessionId};
|
||||||
|
use warzone_protocol::x3dh;
|
||||||
|
use x25519_dalek::PublicKey;
|
||||||
|
|
||||||
|
use crate::keystore;
|
||||||
|
use crate::net::ServerClient;
|
||||||
|
use crate::storage::LocalDb;
|
||||||
|
|
||||||
|
/// The wire envelope: contains either a key exchange init or a ratchet message.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum WireMessage {
|
||||||
|
/// First message to a peer: includes X3DH ephemeral key + ratchet message.
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
/// Subsequent messages: just ratchet messages.
|
||||||
|
Message {
|
||||||
|
sender_fingerprint: String,
|
||||||
|
ratchet_message: RatchetMessage,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(recipient_fp: &str, message: &str, server_url: &str) -> Result<()> {
|
||||||
|
let seed = keystore::load_seed()?;
|
||||||
|
let identity = seed.derive_identity();
|
||||||
|
let our_pub = identity.public_identity();
|
||||||
|
let db = LocalDb::open()?;
|
||||||
|
let client = ServerClient::new(server_url);
|
||||||
|
|
||||||
|
let recipient = Fingerprint::from_hex(recipient_fp)
|
||||||
|
.context("invalid recipient fingerprint")?;
|
||||||
|
|
||||||
|
// Check for existing session
|
||||||
|
let mut ratchet = db.load_session(&recipient)?;
|
||||||
|
|
||||||
|
let wire_msg = if let Some(ref mut state) = ratchet {
|
||||||
|
// Existing session — just encrypt with ratchet
|
||||||
|
let encrypted = state.encrypt(message.as_bytes())
|
||||||
|
.context("ratchet encrypt failed")?;
|
||||||
|
db.save_session(&recipient, state)?;
|
||||||
|
|
||||||
|
WireMessage::Message {
|
||||||
|
sender_fingerprint: our_pub.fingerprint.to_string(),
|
||||||
|
ratchet_message: encrypted,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No session — perform X3DH key exchange
|
||||||
|
println!("No existing session. Fetching key bundle for {}...", recipient_fp);
|
||||||
|
|
||||||
|
let bundle = client.fetch_bundle(recipient_fp).await
|
||||||
|
.context("failed to fetch recipient's bundle. Are they registered?")?;
|
||||||
|
|
||||||
|
let x3dh_result = x3dh::initiate(&identity, &bundle)
|
||||||
|
.context("X3DH key exchange failed")?;
|
||||||
|
|
||||||
|
// Init ratchet as Alice
|
||||||
|
let their_spk = PublicKey::from(bundle.signed_pre_key.public_key);
|
||||||
|
let mut state = RatchetState::init_alice(x3dh_result.shared_secret, their_spk);
|
||||||
|
|
||||||
|
let encrypted = state.encrypt(message.as_bytes())
|
||||||
|
.context("ratchet encrypt failed")?;
|
||||||
|
|
||||||
|
// Save session
|
||||||
|
db.save_session(&recipient, &state)?;
|
||||||
|
|
||||||
|
WireMessage::KeyExchange {
|
||||||
|
sender_fingerprint: our_pub.fingerprint.to_string(),
|
||||||
|
sender_identity_encryption_key: *our_pub.encryption.as_bytes(),
|
||||||
|
ephemeral_public: *x3dh_result.ephemeral_public.as_bytes(),
|
||||||
|
used_one_time_pre_key_id: x3dh_result.used_one_time_pre_key_id,
|
||||||
|
ratchet_message: encrypted,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Serialize and send
|
||||||
|
let encoded = bincode::serialize(&wire_msg)
|
||||||
|
.context("failed to serialize wire message")?;
|
||||||
|
|
||||||
|
client.send_message(recipient_fp, &encoded).await?;
|
||||||
|
|
||||||
|
println!("Message sent to {}", recipient_fp);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ struct Cli {
|
|||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Generate a new identity (seed + keypair)
|
/// Generate a new identity (seed + keypair + pre-keys)
|
||||||
Init,
|
Init,
|
||||||
/// Recover identity from BIP39 mnemonic
|
/// Recover identity from BIP39 mnemonic
|
||||||
Recover {
|
Recover {
|
||||||
@@ -25,6 +25,12 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
/// Show your fingerprint and public key
|
/// Show your fingerprint and public key
|
||||||
Info,
|
Info,
|
||||||
|
/// Register your key bundle with a server
|
||||||
|
Register {
|
||||||
|
/// Server URL
|
||||||
|
#[arg(short, long, default_value = "http://localhost:7700")]
|
||||||
|
server: String,
|
||||||
|
},
|
||||||
/// Send an encrypted message
|
/// Send an encrypted message
|
||||||
Send {
|
Send {
|
||||||
/// Recipient fingerprint (e.g. a3f8:c912:44be:7d01)
|
/// Recipient fingerprint (e.g. a3f8:c912:44be:7d01)
|
||||||
@@ -49,22 +55,30 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Init => cli::init::run()?,
|
Commands::Init => cli::init::run()?,
|
||||||
Commands::Recover { words } => cli::recover::run(&words.join(" "))?,
|
Commands::Recover { words } => cli::recover::run(&words.join(" "))?,
|
||||||
Commands::Info => cli::info::run()?,
|
Commands::Info => cli::info::run()?,
|
||||||
|
Commands::Register { server } => {
|
||||||
|
cli::init::register_with_server(&server).await?;
|
||||||
|
}
|
||||||
Commands::Send {
|
Commands::Send {
|
||||||
recipient,
|
recipient,
|
||||||
message,
|
message,
|
||||||
server,
|
server,
|
||||||
} => {
|
} => {
|
||||||
println!("TODO: send '{}' to {} via {}", message, recipient, server);
|
// Auto-register bundle on first send
|
||||||
|
if let Err(_) = cli::init::register_with_server(&server).await {
|
||||||
|
eprintln!("Warning: failed to register bundle with server");
|
||||||
|
}
|
||||||
|
cli::send::run(&recipient, &message, &server).await?;
|
||||||
}
|
}
|
||||||
Commands::Recv { server } => {
|
Commands::Recv { server } => {
|
||||||
println!("TODO: poll messages from {}", server);
|
cli::recv::run(&server).await?;
|
||||||
}
|
}
|
||||||
Commands::Chat { server } => {
|
Commands::Chat { server } => {
|
||||||
println!("TODO: launch TUI connected to {}", server);
|
println!("TODO: launch TUI connected to {}", server);
|
||||||
|
|||||||
@@ -1,2 +1,123 @@
|
|||||||
// HTTP client for talking to warzone-server.
|
//! HTTP client for talking to warzone-server.
|
||||||
// TODO: implement in Phase 1 step 9.
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,94 @@
|
|||||||
// Local sled database: sessions, contacts, message history.
|
//! Local sled database: sessions, pre-keys, message history.
|
||||||
// TODO: implement in Phase 1 step 9.
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use warzone_protocol::ratchet::RatchetState;
|
||||||
|
use warzone_protocol::types::Fingerprint;
|
||||||
|
use x25519_dalek::StaticSecret;
|
||||||
|
|
||||||
|
pub struct LocalDb {
|
||||||
|
sessions: sled::Tree,
|
||||||
|
pre_keys: sled::Tree,
|
||||||
|
_db: sled::Db,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalDb {
|
||||||
|
pub fn open() -> Result<Self> {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||||
|
let path = std::path::Path::new(&home).join(".warzone").join("db");
|
||||||
|
let db = sled::open(&path).context("failed to open local database")?;
|
||||||
|
let sessions = db.open_tree("sessions")?;
|
||||||
|
let pre_keys = db.open_tree("pre_keys")?;
|
||||||
|
Ok(LocalDb {
|
||||||
|
sessions,
|
||||||
|
pre_keys,
|
||||||
|
_db: db,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a ratchet session for a peer.
|
||||||
|
pub fn save_session(&self, peer: &Fingerprint, state: &RatchetState) -> Result<()> {
|
||||||
|
let key = peer.to_hex();
|
||||||
|
let data = bincode::serialize(state).context("failed to serialize session")?;
|
||||||
|
self.sessions.insert(key.as_bytes(), data)?;
|
||||||
|
self.sessions.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a ratchet session for a peer.
|
||||||
|
pub fn load_session(&self, peer: &Fingerprint) -> Result<Option<RatchetState>> {
|
||||||
|
let key = peer.to_hex();
|
||||||
|
match self.sessions.get(key.as_bytes())? {
|
||||||
|
Some(data) => {
|
||||||
|
let state = bincode::deserialize(&data)
|
||||||
|
.context("failed to deserialize session")?;
|
||||||
|
Ok(Some(state))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store the signed pre-key secret (for X3DH respond).
|
||||||
|
pub fn save_signed_pre_key(&self, id: u32, secret: &StaticSecret) -> Result<()> {
|
||||||
|
let key = format!("spk:{}", id);
|
||||||
|
self.pre_keys
|
||||||
|
.insert(key.as_bytes(), secret.to_bytes().as_slice())?;
|
||||||
|
self.pre_keys.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the signed pre-key secret.
|
||||||
|
pub fn load_signed_pre_key(&self, id: u32) -> Result<Option<StaticSecret>> {
|
||||||
|
let key = format!("spk:{}", id);
|
||||||
|
match self.pre_keys.get(key.as_bytes())? {
|
||||||
|
Some(data) => {
|
||||||
|
let mut bytes = [0u8; 32];
|
||||||
|
bytes.copy_from_slice(&data);
|
||||||
|
Ok(Some(StaticSecret::from(bytes)))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a one-time pre-key secret.
|
||||||
|
pub fn save_one_time_pre_key(&self, id: u32, secret: &StaticSecret) -> Result<()> {
|
||||||
|
let key = format!("otpk:{}", id);
|
||||||
|
self.pre_keys
|
||||||
|
.insert(key.as_bytes(), secret.to_bytes().as_slice())?;
|
||||||
|
self.pre_keys.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load and remove a one-time pre-key secret.
|
||||||
|
pub fn take_one_time_pre_key(&self, id: u32) -> Result<Option<StaticSecret>> {
|
||||||
|
let key = format!("otpk:{}", id);
|
||||||
|
match self.pre_keys.remove(key.as_bytes())? {
|
||||||
|
Some(data) => {
|
||||||
|
let mut bytes = [0u8; 32];
|
||||||
|
bytes.copy_from_slice(&data);
|
||||||
|
self.pre_keys.flush()?;
|
||||||
|
Ok(Some(StaticSecret::from(bytes)))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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