/* * proof-of-work.js -- Alternative captcha based on SHA-256 proof-of-work. * Copyright © 2022 - 2023 Jakob L. Kreuze * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see * . */ function makeid(length) { let result = ''; let alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; for (let i = 0; i < length; i++) { result += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); } return result; } async function digestMessage(message) { const msgUtf8 = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', msgUtf8); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); return hashHex; } async function findPrefix(hardness, nonce) { while (true) { let prefix = makeid(32); let digestHex = await digestMessage(prefix + nonce); if (digestHex.startsWith("0".repeat(hardness))) { return prefix; } } } function makeRequest (method, url) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(method, url); xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.response); } else { reject({ status: xhr.status, statusText: xhr.statusText }); } }; xhr.onerror = function () { reject({ status: xhr.status, statusText: xhr.statusText }); }; xhr.send(); }); } function raceEndpoint() { return new Promise(function (resolve, reject) { makeRequest("GET", "/api/challenge/proof-of-work") .then(function (data) { let challengeData = JSON.parse(data); findPrefix(challengeData.hardness, challengeData.nonce) .then((prefix) => { resolve([prefix, challengeData["challenge-id"]]) } ); }) .catch(reject); }); } window.addEventListener("load", () => { let trigger = document.getElementById("pow-trigger"); trigger.removeAttribute("hidden"); trigger.addEventListener("click", (e) => { trigger.innerHTML = "Please wait..."; trigger.disabled = true; raceEndpoint() .then((result) => { let resultField = document.getElementById("captcha-alt"); let challengeIdField = document.getElementById("captcha-alt-id"); resultField.value = result[0]; challengeIdField.value = result[1]; trigger.innerHTML = "Proof-of-Work completed successfully!"; trigger.disabled = true; // Hide the primary captcha, too, to make it clear that it isn't necessary to complete. let primaryChallenge = document.getElementById("captcha-challenge-primary"); primaryChallenge.hidden = true; }) .catch((err) => { console.log(err); trigger.innerHTML = "Proof-of-Work failed!"; trigger.disabled = true; }); e.preventDefault(); }) });