altcha
Version:
Privacy-first CAPTCHA widget, compliant with global regulations (GDPR/HIPAA/CCPA/LGDP/DPDPA/PIPL) and WCAG accessible. No tracking, self-verifying.
164 lines (163 loc) • 4.93 kB
JavaScript
(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);
}
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);
}
};
}
async function deriveKey(parameters, salt, password) {
const { algorithm, keyLength = 32 } = parameters;
const iterations = Math.max(1, parameters.cost);
let data = void 0;
let derivedKey = void 0;
for (let i = 0; i < iterations; i++) {
if (i === 0) {
data = concatBuffers(salt, password);
} else {
data = derivedKey;
}
derivedKey = new Uint8Array(
(await crypto.subtle.digest(algorithm, data)).slice(0, keyLength)
);
}
return {
parameters: {},
derivedKey
};
}
handler({
deriveKey
});
})();