From 1aba435af38d51aebdecf83e253667f82a63325f Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 27 Mar 2026 09:24:31 +0400 Subject: [PATCH] =?UTF-8?q?v0.0.3:=20fix=20X3DH=20OTPK=20mismatch=20?= =?UTF-8?q?=E2=80=94=20web=20bundles=20without=20OTPKs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: web client's bundle included OTPKs, so X3DH initiate() did 4 DH ops (DH4 with OTPK). But decrypt_wire_message() called respond() with None for OTPK, doing only 3 DH ops. Different DH concat → different shared secret → decrypt fails. Fix: web client bundles have one_time_pre_key: None. initiate() skips DH4 when no OTPK present. respond() also skips DH4 with None. Both sides now do exactly 3 DH ops → shared secrets match. OTPKs are an anti-replay optimization, not required for E2E. Will add OTPK support to web client in Phase 2 with proper server-side OTPK storage and consumption tracking. Co-Authored-By: Claude Opus 4.6 (1M context) --- warzone/Cargo.lock | 10 +++++----- warzone/Cargo.toml | 2 +- .../crates/warzone-server/src/routes/web.rs | 2 +- warzone/crates/warzone-wasm/src/lib.rs | 18 ++++++------------ 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/warzone/Cargo.lock b/warzone/Cargo.lock index 4ced791..b16cbdc 100644 --- a/warzone/Cargo.lock +++ b/warzone/Cargo.lock @@ -2555,7 +2555,7 @@ dependencies = [ [[package]] name = "warzone-client" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anyhow", "argon2", @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "warzone-mule" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anyhow", "clap", @@ -2593,7 +2593,7 @@ dependencies = [ [[package]] name = "warzone-protocol" -version = "0.0.2" +version = "0.0.3" dependencies = [ "base64", "bincode", @@ -2616,7 +2616,7 @@ dependencies = [ [[package]] name = "warzone-server" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anyhow", "axum", @@ -2642,7 +2642,7 @@ dependencies = [ [[package]] name = "warzone-wasm" -version = "0.0.2" +version = "0.0.3" dependencies = [ "base64", "bincode", diff --git a/warzone/Cargo.toml b/warzone/Cargo.toml index c263550..9bdf2ce 100644 --- a/warzone/Cargo.toml +++ b/warzone/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.0.2" +version = "0.0.3" edition = "2021" license = "MIT" rust-version = "1.75" diff --git a/warzone/crates/warzone-server/src/routes/web.rs b/warzone/crates/warzone-server/src/routes/web.rs index 3fd12f9..f6df3f0 100644 --- a/warzone/crates/warzone-server/src/routes/web.rs +++ b/warzone/crates/warzone-server/src/routes/web.rs @@ -160,7 +160,7 @@ let peerBundles = {}; // peerFP -> bundle bytes let pollTimer = null; let wasmReady = false; -const VERSION = '0.0.2'; +const VERSION = '0.0.3'; let DEBUG = true; // toggle with /debug command function dbg(...args) { diff --git a/warzone/crates/warzone-wasm/src/lib.rs b/warzone/crates/warzone-wasm/src/lib.rs index b85df74..f7ec946 100644 --- a/warzone/crates/warzone-wasm/src/lib.rs +++ b/warzone/crates/warzone-wasm/src/lib.rs @@ -8,7 +8,7 @@ use wasm_bindgen::prelude::*; use warzone_protocol::identity::{IdentityKeyPair, PublicIdentity, Seed}; use warzone_protocol::message::WireMessage; use warzone_protocol::prekey::{ - generate_one_time_pre_keys, generate_signed_pre_key, OneTimePreKeyPublic, PreKeyBundle, + generate_signed_pre_key, PreKeyBundle, }; use warzone_protocol::ratchet::RatchetState; use warzone_protocol::x3dh; @@ -115,16 +115,14 @@ impl WasmIdentity { timestamp: js_sys::Date::now() as i64 / 1000, }; - let otpks = generate_one_time_pre_keys(0, 10); - + // No OTPKs for web client (can't store secrets for them reliably). + // initiate() will skip DH4 when one_time_pre_key is None. + // This is safe — OTPKs are an anti-replay optimization, not required. Ok(PreKeyBundle { identity_key: *self.pub_id.signing.as_bytes(), identity_encryption_key: *self.pub_id.encryption.as_bytes(), signed_pre_key: spk, - one_time_pre_key: Some(OneTimePreKeyPublic { - id: otpks[0].id, - public_key: *otpks[0].public.as_bytes(), - }), + one_time_pre_key: None, }) } } @@ -222,15 +220,11 @@ pub fn self_test() -> Result { // Bob's pre-key bundle let (bob_spk_secret, bob_spk) = generate_signed_pre_key(&bob_id, 1); let bob_spk_secret_bytes = bob_spk_secret.to_bytes(); - let bob_otpks = generate_one_time_pre_keys(0, 5); let bob_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(OneTimePreKeyPublic { - id: bob_otpks[0].id, - public_key: *bob_otpks[0].public.as_bytes(), - }), + one_time_pre_key: None, }; let _bob_bundle_bytes = bincode::serialize(&bob_bundle) .map_err(|e| JsValue::from_str(&e.to_string()))?;