UNPKG

altcha

Version:

Privacy-first CAPTCHA widget, compliant with global regulations (GDPR/HIPAA/CCPA/LGDP/DPDPA/PIPL) and WCAG accessible. No tracking, self-verifying.

180 lines (179 loc) 5.27 kB
(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); } }; } 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 }); })();