Files
featherChat/warzone/crates/warzone-protocol/src/prekey.rs
Siavash Sameni 7451ad69bc Fix X3DH + add web client served by warzone-server
X3DH fix:
- Added identity_encryption_key (X25519) to PreKeyBundle
- initiate() and respond() now use correct DH operations per Signal spec:
  DH1=IK_a*SPK_b, DH2=EK_a*IK_b, DH3=EK_a*SPK_b, DH4=EK_a*OPK_b
- All 17 tests pass including x3dh_shared_secret_matches

Web client (served at /):
- Identity generation with seed (stored in localStorage)
- Recovery from hex-encoded seed
- Auto-load saved identity on page load
- Fingerprint display (same format as CLI: xxxx:xxxx:xxxx:xxxx)
- Key registration with server via /v1/keys/register
- Chat UI with message polling (5s interval)
- Commands: /help, /info, /seed
- Dark theme matching warzone aesthetic

Both clients (CLI + Web) now exist:
- CLI: warzone init, warzone info, warzone recover
- Web: http://localhost:7700/ (served by warzone-server)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:32:46 +04:00

116 lines
3.6 KiB
Rust

use ed25519_dalek::{Signature, Signer, Verifier};
use serde::{Deserialize, Serialize};
use x25519_dalek::{PublicKey, StaticSecret};
use crate::errors::ProtocolError;
use crate::identity::IdentityKeyPair;
/// A signed pre-key (medium-term, rotated periodically).
#[derive(Clone, Serialize, Deserialize)]
pub struct SignedPreKey {
pub id: u32,
pub public_key: [u8; 32],
pub signature: Vec<u8>,
pub timestamp: i64,
}
impl SignedPreKey {
/// Verify the signature against the identity signing key.
pub fn verify(&self, identity_key: &ed25519_dalek::VerifyingKey) -> Result<(), ProtocolError> {
let sig =
Signature::from_slice(&self.signature).map_err(|_| ProtocolError::InvalidSignature)?;
identity_key
.verify(&self.public_key, &sig)
.map_err(|_| ProtocolError::PreKeySignatureInvalid)
}
}
/// A one-time pre-key (used once, then discarded).
pub struct OneTimePreKey {
pub id: u32,
pub secret: StaticSecret,
pub public: PublicKey,
}
/// The public portion of a one-time pre-key (sent to server).
#[derive(Clone, Serialize, Deserialize)]
pub struct OneTimePreKeyPublic {
pub id: u32,
pub public_key: [u8; 32],
}
/// A full pre-key bundle that the server stores for a user.
/// Fetched by others to initiate X3DH key exchange.
#[derive(Clone, Serialize, Deserialize)]
pub struct PreKeyBundle {
pub identity_key: [u8; 32], // Ed25519 verifying key bytes
pub identity_encryption_key: [u8; 32], // X25519 public key bytes
pub signed_pre_key: SignedPreKey,
pub one_time_pre_key: Option<OneTimePreKeyPublic>,
}
/// Generate a signed pre-key.
pub fn generate_signed_pre_key(identity: &IdentityKeyPair, id: u32) -> (StaticSecret, SignedPreKey) {
let secret = StaticSecret::random_from_rng(rand::rngs::OsRng);
let public = PublicKey::from(&secret);
let signature = identity.signing.sign(public.as_bytes());
let spk = SignedPreKey {
id,
public_key: *public.as_bytes(),
signature: signature.to_bytes().to_vec(),
timestamp: chrono::Utc::now().timestamp(),
};
(secret, spk)
}
/// Generate a batch of one-time pre-keys.
pub fn generate_one_time_pre_keys(start_id: u32, count: u32) -> Vec<OneTimePreKey> {
(start_id..start_id + count)
.map(|id| {
let secret = StaticSecret::random_from_rng(rand::rngs::OsRng);
let public = PublicKey::from(&secret);
OneTimePreKey {
id,
secret,
public,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::Seed;
#[test]
fn signed_pre_key_verify() {
let seed = Seed::generate();
let identity = seed.derive_identity();
let (_secret, spk) = generate_signed_pre_key(&identity, 1);
let pub_id = identity.public_identity();
assert!(spk.verify(&pub_id.signing).is_ok());
}
#[test]
fn signed_pre_key_reject_tampered() {
let seed = Seed::generate();
let identity = seed.derive_identity();
let (_secret, mut spk) = generate_signed_pre_key(&identity, 1);
spk.public_key[0] ^= 0xff; // tamper
let pub_id = identity.public_identity();
assert!(spk.verify(&pub_id.signing).is_err());
}
#[test]
fn generate_otpks() {
let keys = generate_one_time_pre_keys(0, 10);
assert_eq!(keys.len(), 10);
// All public keys should be unique
let pubs: Vec<_> = keys.iter().map(|k| *k.public.as_bytes()).collect();
let unique: std::collections::HashSet<_> = pubs.iter().collect();
assert_eq!(unique.len(), 10);
}
}