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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user