/**
* SPDX-License-Identifier: CC0-1.0
*
* Copyright (C) 2025 Woj. Kosior <koszko@koszko.org>
*/
/*
#+begin_src manifest-jq
.matches = ["<all_urls>"]
#+end_src
*/
if (false) { /* #+begin_src background-js */
blockJsOnCookie(/^techaro[.]lol-anubis-auth=/);
} /* #+end_src */
/* Solve Anubis challenge. */
function byteArray2Hex(arrayLike) {
const byte2Hex = byte => ("0" + byte.toString(16)).slice(-2);
return [...arrayLike].map(byte2Hex).join("");
}
const countingSchemes = {
up: {
startNum: "1000000000",
delta: 1
},
down: {
/* Server parses nonce as Int which might be a signed 32-bit type. */
startNum: "2147483647",
delta: -1
}
};
const ascii0 = 48;
const ascii9 = 57;
let finished = false;
// let hashCount = 0;
// var start_time = Date.now();
async function powLoop(randomData, hash0sNeeded, counting) {
const {startNum, delta} = counting;
const inputArray = new TextEncoder().encode(randomData + startNum);
const lastIdx = inputArray.length - 1;
const halfedByteIdx = Math.floor(hash0sNeeded / 2);
const bytesChecked = Math.ceil(hash0sNeeded / 2);
while (!finished) {
const digest = new Uint8Array(
await crypto.subtle.digest("SHA-256", inputArray)
);
const savedByte = digest[halfedByteIdx];
digest[halfedByteIdx] >>= 4;
let success = true;
for (let idx= 0; idx < bytesChecked; idx++)
success = success && digest[idx] === 0;
if (!success) {
let idx = lastIdx;
let add = delta;
while (true) {
const byte = inputArray[idx] += add;
if (byte > ascii9) {
inputArray[idx] = ascii0;
add = 1;
} else if (byte < ascii0) {
inputArray[idx] = ascii9;
add = -1;
} else {
break;
}
idx--;
}
continue;
}
digest[halfedByteIdx] = savedByte;
const suffixArray = inputArray.slice(-startNum.length);
const suffix = parseInt(new TextDecoder().decode(suffixArray));
finished = true;
return [digest, suffix];
}
}
async function solvePow(randomData, hash0sNeeded) {
const startMillis = Date.now();
/*
* Under both IceCat 140.3.1-gnu1 and ungoogled-chromium 140.0.7339.207-1
* (both from Guix) on a Core 2 CPU the best hash/s result was observed when
* running 2 async loops. TODO: use workers to leverage hw concurrency.
*/
const [digest, suffix] = await Promise.any([
powLoop(randomData, hash0sNeeded, countingSchemes.up),
powLoop(randomData, hash0sNeeded, countingSchemes.down)
]);
return {
response: byteArray2Hex(digest),
nonce: suffix,
elapsedTime: Date.now() - startMillis
};
}
// (async () => {
// for (let i = 0; i < 15; i++) {
// await new Promise(resolve => setTimeout(resolve, 1000));
// if (i < 5) {
// start_time = Date.now();
// hashCount = 0;
// continue;
// }
// const ms = Date.now() - start_time;
// const hps = Math.round(hashCount / ms * 1000);
// console.log(`${hashCount} hashes (${hps} hash/s)`);
// }
// stop = true;
// })()
async function solvePreact(randomData, waitTime) {
const inputArray = new TextEncoder().encode(randomData);
/*
* The server checks if enough time has passed. Challenge's difficulty
* corresponds to a unit of 80 or 95 milliseconds (depending on Anubis
* version).
*/
const [digestBuffer, _] = await Promise.all([
crypto.subtle.digest("SHA-256", inputArray),
new Promise(resolve => setTimeout(resolve, 95 * waitTime))
]);
return {result: byteArray2Hex(new Uint8Array(digestBuffer))};
}
function err(what) {
console.error(what);
/* TODO: Display message on the page. */
}
/*
* Note that there exists a variant of Anubis "modified for phpBB", where
*
* - the relevant <script> element has id "challenge" rather than
* "anubis_challenge",
* - a <script> elem with id "anubis_settings" is present and holds the path to
* use in the GET request (and element with id "anubis_base_prefix" seems to
* be absent),
* - difficulty is provided as a string (like "4"), and
* - "timestamp" present in the challenge data has to be included in the GET
* request.
*
* The code below aims to handle both Anubis variants.
*/
const settingsScript = document.getElementById("anubis_settings");
const challengeScript =
document.getElementById("anubis_challenge") ||
(settingsScript && document.getElementById("challenge"));
const anubisPrefixScript = document.getElementById("anubis_base_prefix");
const anubisUrlScript = document.getElementById("anubis_public_url");
async function solve() {
const unsupportedAnubisErr =
what => err(`${what} Maybe the extension needs tweaking to ` +
"support this version of Anubis?");
const badDataFormatErr =
() => unsupportedAnubisErr("Challenge data format not understood.");
let anubisPrefix = "", anubisUrl = null, challengeData, settingsData;
try {
challengeData = JSON.parse(challengeScript.textContent);
if (!anubisPrefixScript) {
console.warn("No Anubis base prefix found in page, trying empty " +
"string.");
} else {
anubisPrefix = JSON.parse(anubisPrefixScript.textContent);
}
if (anubisUrlScript) {
const anubisUrlString = JSON.parse(anubisUrlScript.textContent);
if (anubisUrlString)
anubisUrl = new URL(anubisUrlString);
}
if (settingsScript)
settingsData = JSON.parse(settingsScript.textContent);
} catch(ex) {
console.error(ex);
return badDataFormatErr();
}
challengeData = new Object(challengeData);
if (challengeData.rules?.algorithm === "metarefresh")
return;
/*
* Older Anubis versions (and the "modified for phpBB" variant) have the
* random data under `challenge' rather than `challenge.randomData'.
*/
const randomData =
(challengeData.challenge?.randomData || challengeData.challenge);
const challengeId = challengeData.challenge?.id;
const difficulty = challengeData.rules?.difficulty;
const timestamp = challengeData.timestamp || "";
const routePrefix =
(settingsData?.route_prefix ||
`${anubisPrefix}/.within.website/x/cmd/anubis/api/pass-challenge`);
if (typeof randomData !== "string" ||
!/^[0-9]+$/.test(difficulty) ||
(challengeId && typeof challengeId !== "string") ||
typeof anubisPrefix !== "string" ||
!/^[0-9]*$/.test(timestamp) ||
(routePrefix && typeof routePrefix !== "string"))
return badDataFormatErr();
if (!["fast", "preact", "slow"].includes(challengeData.rules.algorithm))
return unsupportedAnubisErr("Unsupported challenge algorithm.");
const anubisizedLocation = () => "" + new URL(
/[^:]+:[/]*[^/]+(.*)/.exec(window.location.href + "")[1],
anubisUrl
);
const isAnubisLocation = (anubisUrl &&
anubisizedLocation() === window.location.href);
const redirectTarget = isAnubisLocation ?
new URLSearchParams(window.location.search).get("redir") :
window.location.href;
if (!redirectTarget)
return unsupportedAnubisErr("Failed to extract redirect target.");
const solver = challengeData.rules.algorithm === "preact" ?
solvePreact : solvePow;
const solutionUrlParams = await solver(randomData, difficulty * 1);
const destination = new URL(routePrefix + "?", window.location.href);
destination.search = new URLSearchParams({
...solutionUrlParams,
...(challengeId && {id: challengeId}),
...(timestamp && {timestamp}),
redir: redirectTarget
});
window.location.href = destination;
}
if (challengeScript && !window.location.href.startsWith("file://"))
solve();