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:
1 parent
941a486913
commit
719d4d9afd
2 files changed
+24
-34
No files matched your search
+23
-33
@@ -26,34 +26,20 @@ let _captchaCookie = null;
|
|||||||
let _captchaAt = 0;
|
let _captchaAt = 0;
|
||||||
const CAPTCHA_TTL = 27 * 60 * 1000; // refresh 1 min before server's 28-min expiry
|
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) {
|
async function solveAltcha(challenge) {
|
||||||
const { nonce, salt, cost, keyLength, keyPrefix } = challenge;
|
const { algorithm = 'SHA-256', challenge: target, salt, maxnumber = 1_000_000 } = challenge;
|
||||||
const saltBuf = Buffer.from(salt, 'hex');
|
if (algorithm !== 'SHA-256') throw new Error(`ALTCHA: unsupported algorithm ${algorithm}`);
|
||||||
const nonceBuf = Buffer.from(nonce, 'hex');
|
const startMs = Date.now();
|
||||||
const prefixBuf = Buffer.from(keyPrefix, 'hex');
|
|
||||||
const startMs = Date.now();
|
|
||||||
const BATCH = 1000;
|
|
||||||
|
|
||||||
for (let base = 0; base < 1_000_000; base += BATCH) {
|
for (let number = 0; number <= maxnumber; number++) {
|
||||||
for (let i = 0; i < BATCH; i++) {
|
const hash = crypto.createHash('sha256').update(salt + String(number)).digest('hex');
|
||||||
const counter = base + i;
|
if (hash === target) {
|
||||||
const ctrBuf = Buffer.allocUnsafe(4);
|
return { number, time: Date.now() - startMs };
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Yield to event loop between batches
|
if (number % 10_000 === 0) await new Promise(r => setImmediate(r));
|
||||||
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) {
|
function httpsGet(url) {
|
||||||
@@ -97,24 +83,28 @@ function httpsPost(url, body, extraHeaders) {
|
|||||||
async function getAltchaCookie() {
|
async function getAltchaCookie() {
|
||||||
if (_captchaCookie && Date.now() - _captchaAt < CAPTCHA_TTL) return _captchaCookie;
|
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`);
|
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 chBody = JSON.parse(chRes.body);
|
||||||
const challenge = chBody.parameters ?? chBody; // unwrap { parameters: {...} } if present
|
const challenge = chBody.parameters ?? chBody; // unwrap { parameters: {...} } if present
|
||||||
|
log('altcha', `🔐 ALTCHA challenge: alg=${challenge.algorithm} max=${challenge.maxnumber}`);
|
||||||
|
|
||||||
// 2. Solve PoW
|
// 2. Solve PoW
|
||||||
const solution = await solveAltcha(challenge);
|
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
|
// 3. Verify — standard ALTCHA payload: base64(JSON { algorithm, challenge, number, salt, signature })
|
||||||
const verifyBody = {
|
const payloadObj = {
|
||||||
parameters: challenge,
|
algorithm: challenge.algorithm || 'SHA-256',
|
||||||
solution: { number: solution.counter, derivedKey: solution.derivedKey },
|
challenge: challenge.challenge,
|
||||||
signature: chBody.signature,
|
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 });
|
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>; ...
|
// Parse Set-Cookie: captcha_verified_at=<value>; ...
|
||||||
const setCookie = [vRes.headers['set-cookie']].flat().filter(Boolean);
|
const setCookie = [vRes.headers['set-cookie']].flat().filter(Boolean);
|
||||||
|
|||||||
+1
-1
@@ -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
|
// Re-register MediaSession handlers when returning from background/screen-off/flip
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user