Add debug logging to web client for WASM crypto troubleshooting

- DEBUG flag (default ON), toggle with /debug command
- Logs to browser console (F12 → Console tab)
- Covers: identity load, key registration, send encrypt,
  poll decrypt (both KeyExchange and session-based attempts)
- Shows: message sizes, session states, error details
- /debug OFF to disable once issue is found

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 08:45:47 +04:00
parent c7a31c674e
commit ab296df825

View File

@@ -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; }