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>
|
||||
|
||||
<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 $messages = document.getElementById('messages');
|
||||
@@ -581,6 +581,20 @@ async function doSend() {
|
||||
addSys('SPK secret: ' + (mySpkSecretHex ? mySpkSecretHex.slice(0,16) + '...' : 'NONE'));
|
||||
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 === '/glist') { await groupList(); 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 ──
|
||||
|
||||
/// 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
|
||||
/// (stored in localStorage, generated during identity creation).
|
||||
/// Returns JSON: { "sender": "fp", "text": "...", "new_session": bool, "session_data": "base64..." }
|
||||
|
||||
Reference in New Issue
Block a user