diff --git a/warzone/crates/warzone-server/src/routes/web.rs b/warzone/crates/warzone-server/src/routes/web.rs index 933159b..b0fb649 100644 --- a/warzone/crates/warzone-server/src/routes/web.rs +++ b/warzone/crates/warzone-server/src/routes/web.rs @@ -160,6 +160,12 @@ let peerBundles = {}; // peerFP -> bundle bytes let pollTimer = null; let wasmReady = false; +let DEBUG = true; // toggle with /debug command + +function dbg(...args) { + if (DEBUG) console.log('[WZ]', ...args); +} + function normFP(fp) { return fp.replace(/[^0-9a-fA-F]/g, '').toLowerCase(); } @@ -189,11 +195,13 @@ function generateNewIdentity() { function loadSavedIdentity() { const saved = localStorage.getItem('wz-seed'); - if (!saved) return false; + if (!saved) { dbg('No saved seed'); return false; } try { initIdentityFromSeed(saved); + dbg('Loaded identity:', myFingerprint); return true; } catch(e) { + dbg('Failed to load identity:', e); localStorage.removeItem('wz-seed'); return false; } @@ -202,11 +210,13 @@ function loadSavedIdentity() { async function registerKey() { const fp = normFP(myFingerprint); const bundleBytes = wasmIdentity.bundle_bytes(); + dbg('Registering key, fp:', fp, 'bundle size:', bundleBytes.length); await fetch(SERVER + '/v1/keys/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fingerprint: fp, bundle: Array.from(bundleBytes) }) }); + dbg('Key registered'); } async function fetchPeerBundle(peerFP) { @@ -223,27 +233,36 @@ async function fetchPeerBundle(peerFP) { async function sendEncrypted(peerFP, plaintext) { const fp = normFP(peerFP); + dbg('sendEncrypted to:', fp, 'text length:', plaintext.length); + const bundleBytes = await fetchPeerBundle(fp); + dbg('Got peer bundle, size:', bundleBytes.length); let wireBytes; if (sessions[fp]) { - // Existing session - const sess = WasmSession.restore(sessions[fp].data); - wireBytes = sess.encrypt(wasmIdentity, plaintext); - sessions[fp].data = sess.save(); - localStorage.setItem('wz-sessions', JSON.stringify( - Object.fromEntries(Object.entries(sessions).map(([k,v]) => [k, v.data])) - )); + dbg('Using existing session for', fp); + try { + const sess = WasmSession.restore(sessions[fp].data); + wireBytes = sess.encrypt(wasmIdentity, plaintext); + sessions[fp].data = sess.save(); + } catch(e) { + dbg('Existing session encrypt failed, creating new:', e.message); + const sess = WasmSession.initiate(wasmIdentity, bundleBytes); + wireBytes = sess.encrypt_key_exchange(wasmIdentity, bundleBytes, plaintext); + sessions[fp] = { data: sess.save() }; + } } else { - // New session via X3DH + dbg('New session (X3DH) for', fp); const sess = WasmSession.initiate(wasmIdentity, bundleBytes); wireBytes = sess.encrypt_key_exchange(wasmIdentity, bundleBytes, plaintext); sessions[fp] = { data: sess.save() }; - localStorage.setItem('wz-sessions', JSON.stringify( - Object.fromEntries(Object.entries(sessions).map(([k,v]) => [k, v.data])) - )); } + localStorage.setItem('wz-sessions', JSON.stringify( + Object.fromEntries(Object.entries(sessions).map(([k,v]) => [k, v.data])) + )); + + dbg('Sending wire message, size:', wireBytes.length); await fetch(SERVER + '/v1/messages/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -253,6 +272,7 @@ async function sendEncrypted(peerFP, plaintext) { message: Array.from(wireBytes) }) }); + dbg('Message sent'); } async function pollMessages() { @@ -263,70 +283,78 @@ async function pollMessages() { if (!resp.ok) return; const msgs = await resp.json(); - for (const b64 of msgs) { + dbg('Poll got', msgs.length, 'messages, sessions:', Object.keys(sessions)); + + for (let i = 0; i < msgs.length; i++) { + const b64 = msgs[i]; try { const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0)); + dbg('Msg', i, ':', bytes.length, 'bytes, first 4:', Array.from(bytes.slice(0, 4))); - // Find existing session for this sender (we don't know sender yet) - // Try decrypt with each session, or let decrypt_wire_message handle it - const existingSessionsJson = {}; - for (const [k, v] of Object.entries(sessions)) { - existingSessionsJson[k] = v.data; + // First try: KeyExchange (no existing session needed) + let decrypted = false; + try { + dbg('Trying decrypt as KeyExchange (no session)...'); + const resultStr = decrypt_wire_message(mySeedHex, bytes, null); + const result = JSON.parse(resultStr); + dbg('Decrypted!', result.new_session ? 'new session' : 'existing', 'from:', result.sender); + + const senderFP = normFP(result.sender); + sessions[senderFP] = { data: result.session_data }; + localStorage.setItem('wz-sessions', JSON.stringify( + Object.fromEntries(Object.entries(sessions).map(([k,v]) => [k, v.data])) + )); + + let fromLabel = result.sender.slice(0, 19); + try { + const ar = await fetch(SERVER + '/v1/alias/whois/' + senderFP); + const ad = await ar.json(); + if (ad.alias) fromLabel = '@' + ad.alias; + } catch(e) {} + + addMsg(fromLabel, result.text, false); + decrypted = true; + } catch(e) { + dbg('KeyExchange decrypt failed:', e.message || e); } - // Try to decrypt (WASM handles both KeyExchange and Message) - const resultStr = decrypt_wire_message( - mySeedHex, - bytes, - null // existing session found internally - ); - const result = JSON.parse(resultStr); - - // Save updated session - const senderFP = normFP(result.sender); - sessions[senderFP] = { data: result.session_data }; - localStorage.setItem('wz-sessions', JSON.stringify( - Object.fromEntries(Object.entries(sessions).map(([k,v]) => [k, v.data])) - )); - - // Resolve alias - let fromLabel = result.sender.slice(0, 19); - try { - const ar = await fetch(SERVER + '/v1/alias/whois/' + senderFP); - const ad = await ar.json(); - if (ad.alias) fromLabel = '@' + ad.alias; - } catch(e) {} - - addMsg(fromLabel, result.text, false); - } catch(e) { - // Try with existing session - try { - const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0)); - // Brute-force try each session - let decrypted = false; + // Second try: existing sessions + if (!decrypted) { for (const [senderFP, sessData] of Object.entries(sessions)) { try { + dbg('Trying session for', senderFP); const resultStr = decrypt_wire_message(mySeedHex, bytes, sessData.data); const result = JSON.parse(resultStr); + dbg('Decrypted with session', senderFP, ':', result.text.slice(0, 30)); + sessions[senderFP] = { data: result.session_data }; localStorage.setItem('wz-sessions', JSON.stringify( Object.fromEntries(Object.entries(sessions).map(([k,v]) => [k, v.data])) )); + let fromLabel = result.sender.slice(0, 19); try { const ar = await fetch(SERVER + '/v1/alias/whois/' + normFP(result.sender)); const ad = await ar.json(); if (ad.alias) fromLabel = '@' + ad.alias; } catch(e2) {} + addMsg(fromLabel, result.text, false); decrypted = true; break; - } catch(e2) { continue; } + } catch(e2) { + dbg('Session', senderFP, 'failed:', e2.message || e2); + } } - if (!decrypted) addSys('[message could not be decrypted]'); - } catch(e2) { - addSys('[failed to process message]'); } + + if (!decrypted) { + dbg('ALL decrypt attempts failed for msg', i); + addSys('[message could not be decrypted]'); + } + } catch(e) { + dbg('Message processing error:', e); + addSys('[failed to process message]'); } } } catch(e) { /* server offline */ } @@ -524,6 +552,7 @@ async function doSend() { return; } if (text === '/clear') { $messages.innerHTML = ''; return; } + if (text === '/debug') { DEBUG = !DEBUG; addSys('Debug logging: ' + (DEBUG ? 'ON (check browser console)' : 'OFF')); 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; }