feat: Replace aCAPTCHA with official ALTCHA (altcha.org) Proof-of-Work web component widget
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
function bufferStartsWith(buffer, prefix) {
|
||||
if (prefix.length > buffer.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < prefix.length; i++) {
|
||||
if (buffer[i] !== prefix[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function bufferToHex(buffer) {
|
||||
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
function concatBuffers(a, b) {
|
||||
const out = new Uint8Array(a.length + b.length);
|
||||
out.set(a, 0);
|
||||
out.set(b, a.length);
|
||||
return out;
|
||||
}
|
||||
function hexToBuffer(hex) {
|
||||
if (hex.length % 2 !== 0) {
|
||||
throw new Error(`Hex string must have an even length. Got: ${hex}`);
|
||||
}
|
||||
const buffer = new ArrayBuffer(hex.length / 2);
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
const byteString = hex.substring(i, i + 2);
|
||||
const byteValue = parseInt(byteString, 16);
|
||||
view.setUint8(i / 2, byteValue);
|
||||
}
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
async function delay(ms) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
function timeDuration(start) {
|
||||
return Math.floor((performance.now() - start) * 10) / 10;
|
||||
}
|
||||
class PasswordBuffer {
|
||||
constructor(nonce, mode = "uint32") {
|
||||
this.nonce = nonce;
|
||||
this.mode = mode;
|
||||
this.buffer = new Uint8Array(this.nonce.length + this.COUNTER_BYTES);
|
||||
this.buffer.set(this.nonce, 0);
|
||||
this.dataView = new DataView(this.buffer.buffer);
|
||||
}
|
||||
nonce;
|
||||
mode;
|
||||
COUNTER_BYTES = 4;
|
||||
buffer;
|
||||
dataView;
|
||||
encoder = new TextEncoder();
|
||||
/**
|
||||
* Appends the counter to the nonce buffer.
|
||||
* In 'string' mode, encodes the counter as a UTF-8 string.
|
||||
* In 'uint32' mode, writes the counter as a big-endian 32-bit integer.
|
||||
*/
|
||||
setCounter(n) {
|
||||
if (this.mode === "string") {
|
||||
return concatBuffers(this.nonce, this.encoder.encode(n.toString()));
|
||||
}
|
||||
this.dataView.setUint32(this.nonce.length, n, false);
|
||||
return this.buffer;
|
||||
}
|
||||
}
|
||||
async function solveChallenge(options) {
|
||||
const {
|
||||
challenge,
|
||||
controller,
|
||||
counterMode = "uint32",
|
||||
counterStart = 0,
|
||||
counterStep = 1,
|
||||
deriveKey: deriveKey2,
|
||||
timeout = 9e4
|
||||
} = options;
|
||||
const { nonce, keyPrefix, salt } = challenge.parameters;
|
||||
const nonceBuf = hexToBuffer(nonce);
|
||||
const saltBuf = hexToBuffer(salt);
|
||||
const keyPrefixBuf = keyPrefix.length % 2 === 0 ? hexToBuffer(keyPrefix) : null;
|
||||
const password = new PasswordBuffer(nonceBuf, counterMode);
|
||||
const start = performance.now();
|
||||
let counter = counterStart;
|
||||
let iterations = 0;
|
||||
let derivedKeyHex = "";
|
||||
let lastYield = start;
|
||||
while (true) {
|
||||
if (controller?.signal.aborted || timeout && iterations % 10 === 0 && performance.now() - start > timeout) {
|
||||
return null;
|
||||
}
|
||||
const { derivedKey } = await deriveKey2(
|
||||
challenge.parameters,
|
||||
saltBuf,
|
||||
password.setCounter(counter)
|
||||
);
|
||||
if (iterations % 10 === 0 && performance.now() - lastYield > 200) {
|
||||
await delay(0);
|
||||
lastYield = performance.now();
|
||||
}
|
||||
if (keyPrefixBuf ? bufferStartsWith(derivedKey, keyPrefixBuf) : bufferToHex(derivedKey).startsWith(keyPrefix)) {
|
||||
derivedKeyHex = bufferToHex(derivedKey);
|
||||
break;
|
||||
}
|
||||
counter = counter + counterStep;
|
||||
iterations = iterations + 1;
|
||||
}
|
||||
return {
|
||||
counter,
|
||||
derivedKey: derivedKeyHex,
|
||||
time: timeDuration(start)
|
||||
};
|
||||
}
|
||||
function handler(options) {
|
||||
const { deriveKey: deriveKey2 } = options;
|
||||
let controller = void 0;
|
||||
self.onmessage = async (message) => {
|
||||
const { challenge, counterMode, counterStart, counterStep, timeout, type } = message.data;
|
||||
if (type === "abort") {
|
||||
controller?.abort();
|
||||
} else if (type === "work") {
|
||||
controller = new AbortController();
|
||||
let solution;
|
||||
try {
|
||||
solution = await solveChallenge({
|
||||
challenge,
|
||||
controller,
|
||||
counterStart,
|
||||
counterStep,
|
||||
deriveKey: deriveKey2,
|
||||
counterMode,
|
||||
timeout
|
||||
});
|
||||
} catch (err) {
|
||||
return self.postMessage({ error: err });
|
||||
}
|
||||
self.postMessage(solution);
|
||||
}
|
||||
};
|
||||
}
|
||||
function getDigest(algorithm) {
|
||||
switch (algorithm) {
|
||||
case "PBKDF2/SHA-512":
|
||||
return "SHA-512";
|
||||
case "PBKDF2/SHA-384":
|
||||
return "SHA-384";
|
||||
case "PBKDF2/SHA-256":
|
||||
default:
|
||||
return "SHA-256";
|
||||
}
|
||||
}
|
||||
async function deriveKey(parameters, salt, password) {
|
||||
const { algorithm, cost, keyLength = 32 } = parameters;
|
||||
const passwordKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
password,
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveKey"]
|
||||
);
|
||||
const derivedKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt,
|
||||
iterations: cost,
|
||||
hash: getDigest(algorithm)
|
||||
},
|
||||
passwordKey,
|
||||
{ name: "AES-GCM", length: keyLength * 8 },
|
||||
true,
|
||||
["encrypt"]
|
||||
);
|
||||
return {
|
||||
derivedKey: new Uint8Array(await crypto.subtle.exportKey("raw", derivedKey))
|
||||
};
|
||||
}
|
||||
handler({
|
||||
deriveKey
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user