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>
This commit is contained in:
Siavash Sameni
2026-03-26 21:32:46 +04:00
parent 651396fa13
commit 7451ad69bc
2421 changed files with 1183 additions and 25 deletions

View File

@@ -37,27 +37,17 @@ pub fn initiate(
.verify(&their_identity)
.map_err(|_| ProtocolError::X3DHFailed("signed pre-key verification failed".into()))?;
// Bob's X25519 identity key: we need to convert Ed25519 verifying key → X25519
// For simplicity, we store X25519 public keys separately in bundles.
// Here we use the signed_pre_key's public key and the identity encryption key.
// In our model, the bundle carries the Ed25519 identity key for signing verification,
// but X3DH uses X25519 keys. We'll derive Bob's X25519 identity from the bundle.
//
// TODO: The bundle should also carry the X25519 identity public key.
// For now, we'll use the signed pre-key as SPK and skip IK DH.
// This is a simplification — full X3DH has 4 DH ops with IK.
let ephemeral_secret = StaticSecret::random_from_rng(rand::rngs::OsRng);
let ephemeral_public = PublicKey::from(&ephemeral_secret);
let their_spk = PublicKey::from(their_bundle.signed_pre_key.public_key);
let their_identity_x25519 = PublicKey::from(their_bundle.identity_encryption_key);
// DH1: our_identity_x25519 * their_signed_pre_key
let dh1 = our_identity.encryption.diffie_hellman(&their_spk);
// DH2: our_ephemeral * their_identity_x25519
// TODO: need their X25519 identity key in bundle. Using SPK as stand-in.
let dh2 = ephemeral_secret.diffie_hellman(&their_spk);
let dh2 = ephemeral_secret.diffie_hellman(&their_identity_x25519);
// DH3: our_ephemeral * their_signed_pre_key
let dh3 = ephemeral_secret.diffie_hellman(&their_spk);
@@ -98,15 +88,15 @@ pub fn respond(
our_identity: &IdentityKeyPair,
our_signed_pre_key_secret: &StaticSecret,
our_one_time_pre_key_secret: Option<&StaticSecret>,
their_identity_x25519: &PublicKey,
their_ephemeral_public: &PublicKey,
) -> Result<[u8; 32], ProtocolError> {
let their_eph = *their_ephemeral_public;
// DH1: their_identity_x25519 * our_signed_pre_key
// TODO: need their X25519 identity key. Using ephemeral as stand-in.
let dh1 = our_signed_pre_key_secret.diffie_hellman(&their_eph);
// DH1: our_signed_pre_key * their_identity_x25519
let dh1 = our_signed_pre_key_secret.diffie_hellman(their_identity_x25519);
// DH2: their_ephemeral * our_identity_x25519
// DH2: our_identity_x25519 * their_ephemeral
let dh2 = our_identity.encryption.diffie_hellman(&their_eph);
// DH3: their_ephemeral * our_signed_pre_key
@@ -130,8 +120,6 @@ pub fn respond(
Ok(shared_secret)
}
// TODO: Full X3DH implementation requires X25519 identity keys in the bundle.
// Current implementation is simplified. Fix in step 5 of the implementation plan.
#[cfg(test)]
mod tests {
@@ -151,8 +139,11 @@ mod tests {
let bob_otpks = generate_one_time_pre_keys(0, 1);
let bob_pub = bob_id.public_identity();
let alice_pub = alice_id.public_identity();
let bundle = PreKeyBundle {
identity_key: *bob_pub.signing.as_bytes(),
identity_encryption_key: *bob_pub.encryption.as_bytes(),
signed_pre_key: bob_spk,
one_time_pre_key: Some(crate::prekey::OneTimePreKeyPublic {
id: bob_otpks[0].id,
@@ -165,6 +156,7 @@ mod tests {
&bob_id,
&bob_spk_secret,
Some(&bob_otpks[0].secret),
&alice_pub.encryption,
&alice_result.ephemeral_public,
)
.unwrap();