diff --git a/fp_altcha_spamschutz/assets/fp-altcha.js b/fp_altcha_spamschutz/assets/fp-altcha.js new file mode 100644 index 0000000..350dba9 --- /dev/null +++ b/fp_altcha_spamschutz/assets/fp-altcha.js @@ -0,0 +1,147 @@ +/*! + * fp-altcha.js -- kleiner, eigenstaendiger Loeser fuer das klassische ALTCHA V1 + * Proof-of-Work-Verfahren (SHA-256 Hashcash-Stil). + * + * Sucht fuer eine vom Server vorgegebene Aufgabe {algorithm, challenge, salt, maxnumber, signature} + * eine Zahl n mit 0 <= n <= maxnumber, fuer die SHA-256(salt + n) == challenge gilt, und traegt das + * Ergebnis base64-kodiert in ein verstecktes Formularfeld "altcha" ein. Die Verifikation auf dem + * Server erfolgt mit der offiziellen ALTCHA-PHP-Bibliothek (siehe src/Vendor/AltchaOrg/Altcha/V1). + * + * Laeuft vollstaendig lokal im Browser -- es wird keine Anfrage an einen Drittanbieter gesendet. + */ +(function () { + 'use strict'; + + function bufferToHex(buffer) { + var bytes = new Uint8Array(buffer); + var hex = ''; + for (var i = 0; i < bytes.length; i++) { + var h = bytes[i].toString(16); + hex += h.length === 1 ? '0' + h : h; + } + return hex; + } + + function base64Encode(str) { + var bytes = new TextEncoder().encode(str); + var binary = ''; + for (var i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + } + + async function solve(algorithm, salt, target, maxNumber) { + if (algorithm !== 'SHA-256') { + // Nur SHA-256 wird von diesem kleinen Loeser unterstuetzt. + return null; + } + + var encoder = new TextEncoder(); + + for (var n = 0; n <= maxNumber; n++) { + var data = encoder.encode(salt + n); + var digest = await crypto.subtle.digest('SHA-256', data); + if (bufferToHex(digest) === target) { + return n; + } + } + + return null; + } + + async function processWidget(container) { + var raw = container.getAttribute('data-fp-altcha'); + if (!raw) { + return; + } + + var task; + try { + task = JSON.parse(raw); + } catch (e) { + return; + } + + var status = container.querySelector('.fp-altcha-status'); + var input = container.querySelector('.fp-altcha-input'); + if (!input) { + return; + } + + try { + var number = await solve(task.algorithm, task.salt, task.challenge, task.maxnumber); + + if (number === null) { + if (status) { + status.textContent = container.getAttribute('data-label-failed') || 'Sicherheitspruefung fehlgeschlagen. Bitte Seite neu laden.'; + } + container.setAttribute('data-fp-altcha-state', 'failed'); + return; + } + + var payload = { + algorithm: task.algorithm, + challenge: task.challenge, + number: number, + salt: task.salt, + signature: task.signature + }; + + input.value = base64Encode(JSON.stringify(payload)); + container.setAttribute('data-fp-altcha-state', 'verified'); + + if (status) { + status.innerHTML = ' ' + + (container.getAttribute('data-label-verified') || 'Sicherheitspruefung bestaetigt'); + } + } catch (e) { + container.setAttribute('data-fp-altcha-state', 'failed'); + if (status) { + status.textContent = container.getAttribute('data-label-failed') || 'Sicherheitspruefung fehlgeschlagen. Bitte Seite neu laden.'; + } + } + } + + function guardForm(form) { + form.addEventListener('submit', function (event) { + var widgets = form.querySelectorAll('.fp-altcha'); + for (var i = 0; i < widgets.length; i++) { + var state = widgets[i].getAttribute('data-fp-altcha-state'); + if (state !== 'verified') { + event.preventDefault(); + var status = widgets[i].querySelector('.fp-altcha-status'); + if (status) { + status.textContent = 'Bitte einen Moment warten, die Sicherheitspruefung laeuft noch.'; + } + return; + } + } + }); + } + + function init() { + if (!window.crypto || !window.crypto.subtle) { + // Sehr alte Browser ohne Web Crypto API: Pruefung kann nicht durchgefuehrt werden. + // Das Formular bleibt dann serverseitig blockiert (kein gueltiges "altcha" Feld). + return; + } + + var containers = document.querySelectorAll('.fp-altcha[data-fp-altcha]'); + containers.forEach(function (container) { + processWidget(container); + + var form = container.closest('form'); + if (form && !form.hasAttribute('data-fp-altcha-guarded')) { + form.setAttribute('data-fp-altcha-guarded', '1'); + guardForm(form); + } + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})();