Add WASM self-test, bundle debug, /selftest and /bundleinfo commands
/selftest — runs full Alice→Bob encrypt/decrypt cycle within WASM (tests X3DH + Double Ratchet + bincode serialize/deserialize) /bundleinfo — dumps bundle contents, verifies SPK secret matches SPK public key in the registered bundle These help isolate whether the bug is in WASM crypto (self-test fails) or in CLI↔WASM interop (self-test passes but cross-client fails). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -144,7 +144,7 @@ const WEB_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import init, { WasmIdentity, WasmSession, decrypt_wire_message } from '/wasm/warzone_wasm.js';
|
import init, { WasmIdentity, WasmSession, decrypt_wire_message, self_test, debug_bundle_info } from '/wasm/warzone_wasm.js';
|
||||||
|
|
||||||
const SERVER = window.location.origin;
|
const SERVER = window.location.origin;
|
||||||
const $messages = document.getElementById('messages');
|
const $messages = document.getElementById('messages');
|
||||||
@@ -581,6 +581,20 @@ async function doSend() {
|
|||||||
addSys('SPK secret: ' + (mySpkSecretHex ? mySpkSecretHex.slice(0,16) + '...' : 'NONE'));
|
addSys('SPK secret: ' + (mySpkSecretHex ? mySpkSecretHex.slice(0,16) + '...' : 'NONE'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (text === '/selftest') {
|
||||||
|
try {
|
||||||
|
const r = self_test();
|
||||||
|
addSys(r);
|
||||||
|
} catch(e) { addSys('Self-test FAILED: ' + e); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (text === '/bundleinfo') {
|
||||||
|
try {
|
||||||
|
const r = debug_bundle_info(wasmIdentity);
|
||||||
|
addSys(r);
|
||||||
|
} catch(e) { addSys('Bundle info error: ' + e); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (text === '/quit') { window.close(); return; }
|
if (text === '/quit') { window.close(); return; }
|
||||||
if (text === '/glist') { await groupList(); return; }
|
if (text === '/glist') { await groupList(); return; }
|
||||||
if (text === '/dm') { currentGroup = null; addSys('Switched to DM mode'); $peerInput.value = localStorage.getItem('wz-peer') || ''; return; }
|
if (text === '/dm') { currentGroup = null; addSys('Switched to DM mode'); $peerInput.value = localStorage.getItem('wz-peer') || ''; return; }
|
||||||
|
|||||||
@@ -200,8 +200,95 @@ impl WasmSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Self-test (verifies full encrypt/decrypt cycle within WASM) ──
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn self_test() -> Result<String, JsValue> {
|
||||||
|
// Alice
|
||||||
|
let alice_seed = Seed::generate();
|
||||||
|
let alice_id = alice_seed.derive_identity();
|
||||||
|
let alice_pub = alice_id.public_identity();
|
||||||
|
|
||||||
|
// Bob
|
||||||
|
let bob_seed = Seed::generate();
|
||||||
|
let bob_id = bob_seed.derive_identity();
|
||||||
|
let bob_pub = bob_id.public_identity();
|
||||||
|
|
||||||
|
// Bob's pre-key bundle
|
||||||
|
let (bob_spk_secret, bob_spk) = generate_signed_pre_key(&bob_id, 1);
|
||||||
|
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(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let bob_bundle_bytes = bincode::serialize(&bob_bundle)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
|
// Alice initiates X3DH and encrypts
|
||||||
|
let x3dh_result = x3dh::initiate(&alice_id, &bob_bundle)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("X3DH initiate: {}", e)))?;
|
||||||
|
let their_spk = PublicKey::from(bob_bundle.signed_pre_key.public_key);
|
||||||
|
let mut alice_ratchet = RatchetState::init_alice(x3dh_result.shared_secret, their_spk);
|
||||||
|
let encrypted = alice_ratchet.encrypt(b"hello from WASM self-test")
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("encrypt: {}", e)))?;
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"WASM self-test: alice_fp={}, bob_fp={}, wire_bytes={}, decrypted='{}', PASS={}",
|
||||||
|
alice_pub.fingerprint, bob_pub.fingerprint, wire_bytes.len(), text,
|
||||||
|
text == "hello from WASM self-test"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// ── Decrypt ──
|
// ── Decrypt ──
|
||||||
|
|
||||||
|
/// Debug: dump what the WASM identity's bundle looks like (for comparing with CLI).
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn debug_bundle_info(identity: &mut WasmIdentity) -> Result<String, JsValue> {
|
||||||
|
let bundle_bytes = identity.bundle_bytes()?;
|
||||||
|
let bundle: PreKeyBundle = bincode::deserialize(&bundle_bytes)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
|
let spk_pub_hex = hex::encode(bundle.signed_pre_key.public_key);
|
||||||
|
let ik_hex = hex::encode(bundle.identity_key);
|
||||||
|
let iek_hex = hex::encode(bundle.identity_encryption_key);
|
||||||
|
let spk_secret_hex = identity.spk_secret_hex();
|
||||||
|
|
||||||
|
// Verify SPK matches
|
||||||
|
let spk_secret = x25519_dalek::StaticSecret::from(identity.spk_secret_bytes);
|
||||||
|
let derived_pub = PublicKey::from(&spk_secret);
|
||||||
|
let matches = *derived_pub.as_bytes() == bundle.signed_pre_key.public_key;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"bundle_size={}, ik={}, iek={}, spk_pub={}, spk_secret={}, spk_matches={}",
|
||||||
|
bundle_bytes.len(), &ik_hex[..16], &iek_hex[..16], &spk_pub_hex[..16], &spk_secret_hex[..16], matches
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// Decrypt a bincode WireMessage. `spk_secret_hex` is the signed pre-key secret
|
/// Decrypt a bincode WireMessage. `spk_secret_hex` is the signed pre-key secret
|
||||||
/// (stored in localStorage, generated during identity creation).
|
/// (stored in localStorage, generated during identity creation).
|
||||||
/// Returns JSON: { "sender": "fp", "text": "...", "new_session": bool, "session_data": "base64..." }
|
/// Returns JSON: { "sender": "fp", "text": "...", "new_session": bool, "session_data": "base64..." }
|
||||||
|
|||||||
Reference in New Issue
Block a user