v0.0.2: add version display, detailed self-test with step-by-step decrypt

- Version shown on chat load (v0.0.2)
- Self-test now does step-by-step: X3DH shared secret comparison,
  then manual ratchet init + decrypt (not via decrypt_wire_message)
- Shows: rng output, shared_match, alice/bob shared secrets, decrypt result
- This isolates whether X3DH or ratchet or AEAD fails

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 09:19:01 +04:00
parent 54a66fa0ee
commit de3b74bb9d
5 changed files with 39 additions and 30 deletions

11
warzone/Cargo.lock generated
View File

@@ -2555,7 +2555,7 @@ dependencies = [
[[package]]
name = "warzone-client"
version = "0.1.0"
version = "0.0.2"
dependencies = [
"anyhow",
"argon2",
@@ -2584,7 +2584,7 @@ dependencies = [
[[package]]
name = "warzone-mule"
version = "0.1.0"
version = "0.0.2"
dependencies = [
"anyhow",
"clap",
@@ -2593,7 +2593,7 @@ dependencies = [
[[package]]
name = "warzone-protocol"
version = "0.1.0"
version = "0.0.2"
dependencies = [
"base64",
"bincode",
@@ -2616,7 +2616,7 @@ dependencies = [
[[package]]
name = "warzone-server"
version = "0.1.0"
version = "0.0.2"
dependencies = [
"anyhow",
"axum",
@@ -2642,7 +2642,7 @@ dependencies = [
[[package]]
name = "warzone-wasm"
version = "0.1.0"
version = "0.0.2"
dependencies = [
"base64",
"bincode",
@@ -2650,6 +2650,7 @@ dependencies = [
"getrandom 0.2.17",
"hex",
"js-sys",
"rand",
"serde",
"serde_json",
"uuid",

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "0.1.0"
version = "0.0.2"
edition = "2021"
license = "MIT"
rust-version = "1.75"

View File

@@ -160,6 +160,7 @@ let peerBundles = {}; // peerFP -> bundle bytes
let pollTimer = null;
let wasmReady = false;
const VERSION = '0.0.2';
let DEBUG = true; // toggle with /debug command
function dbg(...args) {
@@ -453,8 +454,8 @@ async function enterChat() {
await registerKey();
addSys('Identity loaded: ' + myFingerprint);
addSys('Key registered with server');
addSys('DM: paste peer fingerprint or @alias above');
addSys('/alias <name> · /g <group> · /glist · /info · /clear');
addSys('v' + VERSION + ' | DM: paste peer fingerprint or @alias above');
addSys('/alias · /g · /glist · /info · /selftest · /reset · /debug');
const savedPeer = localStorage.getItem('wz-peer');
if (savedPeer) $peerInput.value = savedPeer;

View File

@@ -19,6 +19,7 @@ hex.workspace = true
bincode.workspace = true
x25519-dalek.workspace = true
ed25519-dalek.workspace = true
rand.workspace = true
uuid = { version = "1", features = ["v4", "serde", "js"] }
# profile.release is set at workspace root

View File

@@ -204,6 +204,11 @@ impl WasmSession {
#[wasm_bindgen]
pub fn self_test() -> Result<String, JsValue> {
// Check randomness works
let mut rng_test = [0u8; 8];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut rng_test);
let rng_hex = hex::encode(rng_test);
// Alice
let alice_seed = Seed::generate();
let alice_id = alice_seed.derive_identity();
@@ -216,6 +221,7 @@ pub fn self_test() -> Result<String, JsValue> {
// 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(),
@@ -237,43 +243,43 @@ pub fn self_test() -> Result<String, JsValue> {
let encrypted = alice_ratchet.encrypt(b"hello from WASM self-test")
.map_err(|e| JsValue::from_str(&format!("encrypt: {}", e)))?;
let wire = WireMessage::KeyExchange {
// Clone encrypted for later use (wire takes ownership)
let encrypted_clone = encrypted.clone();
let _wire = WireMessage::KeyExchange {
sender_fingerprint: alice_pub.fingerprint.to_string(),
sender_identity_encryption_key: *alice_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,
};
let wire_bytes = bincode::serialize(&wire)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
// Bob decrypts using decrypt_wire_message
let bob_spk_hex = hex::encode(bob_spk_secret.to_bytes());
let bob_seed_hex = hex::encode(bob_seed.0);
let result_str = decrypt_wire_message(&bob_seed_hex, &bob_spk_hex, &wire_bytes, None)?;
let result: serde_json::Value = serde_json::from_str(&result_str)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let text = result.get("text").and_then(|v| v.as_str()).unwrap_or("MISSING");
// More detailed info
// Step-by-step Bob-side decrypt (NOT using decrypt_wire_message)
let alice_shared_hex = hex::encode(x3dh_result.shared_secret);
// Bob's side: compute shared secret for comparison
// Bob: X3DH respond
let bob_shared = x3dh::respond(
&bob_id, &bob_spk_secret, None,
&alice_pub.encryption, &x3dh_result.ephemeral_public,
).map_err(|e| JsValue::from_str(&format!("X3DH respond direct: {}", e)))?;
).map_err(|e| JsValue::from_str(&format!("X3DH respond: {}", e)))?;
let bob_shared_hex = hex::encode(bob_shared);
let shared_match = alice_shared_hex == bob_shared_hex;
// Bob: init ratchet
// Need a fresh copy of spk_secret (bob_spk_secret was moved into respond)
let bob_spk_secret2 = x25519_dalek::StaticSecret::from(bob_spk_secret_bytes);
let mut bob_ratchet = RatchetState::init_bob(bob_shared, bob_spk_secret2);
// Bob: decrypt
let decrypt_result = bob_ratchet.decrypt(&encrypted_clone);
let decrypt_text = match &decrypt_result {
Ok(plain) => String::from_utf8_lossy(plain).to_string(),
Err(e) => format!("DECRYPT_ERROR: {}", e),
};
Ok(format!(
"alice_fp={}, bob_fp={}, wire_bytes={}, alice_shared={}..., bob_shared={}..., shared_match={}, decrypted='{}', PASS={}",
alice_pub.fingerprint, bob_pub.fingerprint, wire_bytes.len(),
&alice_shared_hex[..16], &bob_shared_hex[..16], shared_match,
text, text == "hello from WASM self-test"
"rng={}, shared_match={}, alice_shared={}..., bob_shared={}..., decrypt='{}', PASS={}",
rng_hex, shared_match, &alice_shared_hex[..16], &bob_shared_hex[..16],
decrypt_text, decrypt_text == "hello from WASM self-test"
))
}