From 719d4d9afde3727afee719ee68ee63ac32677a5b Mon Sep 17 00:00:00 2001 From: Honey Date: Sun, 3 May 2026 13:57:47 +0200 Subject: [PATCH] fix: standard ALTCHA PoW + lower default volume to 65% Replace custom PoW scheme with standard ALTCHA: SHA-256(salt+number) solver, base64-encoded {algorithm,challenge,number,salt,signature} verify payload. Add verbose logging to diagnose future failures. Lower default audio volume from 0.8 to 0.65. Co-Authored-By: Claude Sonnet 4.6 --- backend/lib/squid.js | 56 ++++++++++++++++++-------------------------- src/main.js | 2 +- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/backend/lib/squid.js b/backend/lib/squid.js index ebc00c0..121ae44 100644 --- a/backend/lib/squid.js +++ b/backend/lib/squid.js @@ -26,34 +26,20 @@ let _captchaCookie = null; let _captchaAt = 0; const CAPTCHA_TTL = 27 * 60 * 1000; // refresh 1 min before server's 28-min expiry -// Iterative SHA-256 PoW solver β€” yields every 1000 iterations to stay non-blocking +// Standard ALTCHA PoW solver: find number such that SHA-256(salt + number) == challenge async function solveAltcha(challenge) { - const { nonce, salt, cost, keyLength, keyPrefix } = challenge; - const saltBuf = Buffer.from(salt, 'hex'); - const nonceBuf = Buffer.from(nonce, 'hex'); - const prefixBuf = Buffer.from(keyPrefix, 'hex'); - const startMs = Date.now(); - const BATCH = 1000; + const { algorithm = 'SHA-256', challenge: target, salt, maxnumber = 1_000_000 } = challenge; + if (algorithm !== 'SHA-256') throw new Error(`ALTCHA: unsupported algorithm ${algorithm}`); + const startMs = Date.now(); - for (let base = 0; base < 1_000_000; base += BATCH) { - for (let i = 0; i < BATCH; i++) { - const counter = base + i; - const ctrBuf = Buffer.allocUnsafe(4); - ctrBuf.writeUInt32BE(counter); - // password = nonce || counter_be32 ; initial = salt || password - const initial = Buffer.concat([saltBuf, nonceBuf, ctrBuf]); - let derived = crypto.createHash('sha256').update(initial).digest().slice(0, keyLength); - for (let c = 0; c < cost; c++) { - derived = crypto.createHash('sha256').update(derived).digest().slice(0, keyLength); - } - if (derived.slice(0, prefixBuf.length).equals(prefixBuf)) { - return { counter, derivedKey: derived.toString('hex'), time: Date.now() - startMs }; - } + for (let number = 0; number <= maxnumber; number++) { + const hash = crypto.createHash('sha256').update(salt + String(number)).digest('hex'); + if (hash === target) { + return { number, time: Date.now() - startMs }; } - // Yield to event loop between batches - await new Promise(r => setImmediate(r)); + if (number % 10_000 === 0) await new Promise(r => setImmediate(r)); } - throw new Error('ALTCHA: no solution found within 1M iterations'); + throw new Error('ALTCHA: no solution found within maxnumber'); } function httpsGet(url) { @@ -97,24 +83,28 @@ function httpsPost(url, body, extraHeaders) { async function getAltchaCookie() { if (_captchaCookie && Date.now() - _captchaAt < CAPTCHA_TTL) return _captchaCookie; - // 1. Fetch challenge β€” response may wrap params in a "parameters" key + // 1. Fetch challenge β€” standard ALTCHA returns { algorithm, challenge, salt, signature, maxnumber } const chRes = await httpsGet(`${SQUID_BASE}/api/altcha/challenge`); - if (chRes.status !== 200) throw new Error(`ALTCHA challenge HTTP ${chRes.status}`); + if (chRes.status !== 200) throw new Error(`ALTCHA challenge HTTP ${chRes.status}: ${chRes.body.slice(0,200)}`); const chBody = JSON.parse(chRes.body); const challenge = chBody.parameters ?? chBody; // unwrap { parameters: {...} } if present + log('altcha', `πŸ” ALTCHA challenge: alg=${challenge.algorithm} max=${challenge.maxnumber}`); // 2. Solve PoW const solution = await solveAltcha(challenge); + log('altcha', `πŸ” ALTCHA gelΓΆst in ${solution.time}ms (number=${solution.number})`); - // 3. Verify β€” mirror the challenge response structure: { parameters, signature } + solution fields - const verifyBody = { - parameters: challenge, - solution: { number: solution.counter, derivedKey: solution.derivedKey }, - signature: chBody.signature, + // 3. Verify β€” standard ALTCHA payload: base64(JSON { algorithm, challenge, number, salt, signature }) + const payloadObj = { + algorithm: challenge.algorithm || 'SHA-256', + challenge: challenge.challenge, + number: solution.number, + salt: challenge.salt, + signature: challenge.signature, }; - const payload = Buffer.from(JSON.stringify(verifyBody)).toString('base64'); + const payload = Buffer.from(JSON.stringify(payloadObj)).toString('base64'); const vRes = await httpsPost(`${SQUID_BASE}/api/altcha/verify`, { payload }); - if (vRes.status !== 200) throw new Error(`ALTCHA verify HTTP ${vRes.status}`); + if (vRes.status !== 200) throw new Error(`ALTCHA verify HTTP ${vRes.status}: ${vRes.body.slice(0,200)}`); // Parse Set-Cookie: captcha_verified_at=; ... const setCookie = [vRes.headers['set-cookie']].flat().filter(Boolean); diff --git a/src/main.js b/src/main.js index 852441a..d63c549 100644 --- a/src/main.js +++ b/src/main.js @@ -133,7 +133,7 @@ window.addEventListener('load', () => { } }); - S.audio.volume = 0.8; + S.audio.volume = 0.65; // Re-register MediaSession handlers when returning from background/screen-off/flip document.addEventListener('visibilitychange', () => {