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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-03 13:57:47 +02:00
1 parent 941a486913
commit 719d4d9afd
2 files changed
+24 -34

No files matched your search

+23 -33
View File
@@ -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=<value>; ...
const setCookie = [vRes.headers['set-cookie']].flat().filter(Boolean);
+1 -1
View File
@@ -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', () => {