From b998f268000e894be11cda7a5cf2a61a72cbf6a2 Mon Sep 17 00:00:00 2001
From: Tiago <70700766+tiagorangel1@users.noreply.github.com>
Date: Wed, 23 Apr 2025 14:31:12 +0100
Subject: [PATCH] feat: switch to WASM solver!
---
demo/main/server.js | 14 ++-
docs/.vitepress/config.mjs | 14 ++-
docs/guide/effectiveness.md | 52 ++++++-----
docs/guide/workings.md | 30 ++++++
docs/index.md | 2 +-
wasm/test/browser.html | 15 +--
widget/src/cap.compat.min.js | 2 +-
widget/src/cap.min.js | 2 +-
widget/src/package.json | 2 +-
widget/src/src/cap.js | 175 +++++++++++++++--------------------
10 files changed, 165 insertions(+), 143 deletions(-)
create mode 100644 docs/guide/workings.md
diff --git a/demo/main/server.js b/demo/main/server.js
index add8d23..fae1691 100644
--- a/demo/main/server.js
+++ b/demo/main/server.js
@@ -15,18 +15,22 @@ fastify.get("/", (req, res) => {
fastify.get("/cap.js", (req, res) => {
res.header("Content-Type", "application/javascript");
- res.send(fs.createReadStream(path.join(__dirname, "../../widget/src/src/cap.js")));
+ res.send(
+ fs.createReadStream(path.join(__dirname, "../../widget/src/src/cap.js"))
+ );
});
fastify.get("/cap-floating.js", (req, res) => {
res.header("Content-Type", "application/javascript");
- res.send(fs.createReadStream(path.join(__dirname, "../../widget/src/src/cap-floating.js")));
+ res.send(
+ fs.createReadStream(
+ path.join(__dirname, "../../widget/src/src/cap-floating.js")
+ )
+ );
});
fastify.post("/api/challenge", (req, res) => {
- res.send(
- cap.createChallenge()
- );
+ res.send(cap.createChallenge());
});
fastify.post("/api/redeem", async (req, res) => {
diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs
index 6133ef3..d506031 100644
--- a/docs/.vitepress/config.mjs
+++ b/docs/.vitepress/config.mjs
@@ -87,8 +87,8 @@ export default defineConfig({
{
src: "https://cdn.jsdelivr.net/npm/@cap.js/widget@latest",
async: true,
- }
- ]
+ },
+ ],
],
themeConfig: {
search: {
@@ -108,7 +108,6 @@ export default defineConfig({
sidebar: [
{ text: "Quickstart", link: "/guide/index.md" },
- { text: "Effectiveness", link: "/guide/effectiveness.md" },
{ text: "Alternatives", link: "/guide/alternatives.md" },
{ text: "Philosophy", link: "/guide/philosophy.md" },
{
@@ -128,8 +127,15 @@ export default defineConfig({
{ text: "@cap.js/cli", link: "/guide/cli.md" },
],
},
+ {
+ text: "Proof-of-work",
+ items: [
+ { text: "Effectiveness", link: "/guide/effectiveness.md" },
+ { text: "How does it work", link: "/guide/workings.md" },
+ ],
+ },
{ text: "Benchmark", link: "/guide/benchmark.md" },
- { text: "Demo", link: "/guide/demo.md" }
+ { text: "Demo", link: "/guide/demo.md" },
],
socialLinks: [
diff --git a/docs/guide/effectiveness.md b/docs/guide/effectiveness.md
index d65e262..bcd1fdc 100644
--- a/docs/guide/effectiveness.md
+++ b/docs/guide/effectiveness.md
@@ -1,37 +1,47 @@
# Effectiveness
-Cap is designed to reduce spam and abuse on websites and web apps. While it won't block all spam, it's highly effective at keeping most bots away.
+Cap reduces spam and abuse on websites and web apps. While it won't block _all_ spam (no CAPTCHA is a silver bullet), it significantly reduces it by making automated abuse costly. The core principle behind proof-of-work CAPTCHAs like Cap is **proving effort** rather than just verifying if you're human through interaction analysis or complex puzzles.
-## How PoW works
+When you verify yourself, Cap performs these steps:
-Cap uses a Proof-of-Work (PoW) system, similar to Friendly Captcha and Altcha. Instead of simply verifying if you're human by analysing your mouse movement and interaction with the website, Cap creates a computational task that bots find hard to solve.
+1. **Requests Challenges:** Asks the server for multiple unique challenges. Each includes a salt and a target difficulty. (Using multiple challenges helps adjust difficulty and track progress.)
+2. **Solves Computationally:** Uses Web Workers and WebAssembly (WASM) on your device to rapidly test nonce values, combining them with the challenge salt. In case
+3. **Finds Solutions:** Continues until it finds solutions whose cryptographic hashes meet the target difficulty specified in the challenges.
+4. **Validates:** Sends the successful nonce solutions back to the server for validation.
+5. **Issues Token:** If validation succeeds, the server issues a short-lived token that proves the work was done, granting access.
-When you verify yourself, Cap asks the server for multiple challenges (we use multiple challenges in order to help adjust difficulty levels and track progress more accurately), which include a unique salt and target. The server generates these challenges and sends them to the Cap widget. Using Web Workers and WASM, Cap tries to find a challenge with a hash starting with a specific string for each challenge by combining the salt with a random string.
+This computational work makes spam expensive for attackers but remains quick (typically milliseconds) and unintrusive for legitimate users.
-Once solved, the widget sends the results back to the server for validation. If successful, the server gives a token that can be used to prove you're human. This extra complexity makes it much harder for simple bots to bypass.
+**Example: The Economics of Spam**
+Imagine sending 10,000 spam messages costs $1, potentially earning $10 – a profitable venture. If Cap increases the computational cost so that sending those messages now costs $100, the spammer loses $90. This eliminates the financial incentive.
-## Privacy
+## Privacy & Security
-Cap is fully compliant with GDPR and CCPA. It doesn't use cookies or track you in intrusive ways. We never sell your data, and we don't even collect personal information. No ads, no tracking. Period.
+Cap prioritizes user privacy and is designed to be GDPR and CCPA compliant.
-## Security
+- **No Tracking:** It doesn't use cookies or employ intrusive tracking methods.
+- **No Data Selling:** We never sell user data.
+- **Minimal Data:** We don't collect personal information beyond what's essential for the PoW process itself. No ads, no tracking, no telemetry. Period.
+- **IP Addresses:** Not stored by default.
+- **Challenge Storage:** Challenges exist only in memory on the server to prevent tampering and expire quickly (10 minutes by default).
+- **Token Storage:** Only _hashed_ tokens are stored persistently (in `.data/tokensList.json` by default) to validate user sessions, and they also expire (20 minutes by default).
-- IP addresses are not stored by default
-- Challenges are stored in memory to make sure they are not tampered with (expire after 10 minutes by default), while tokens are stored in a file (hashed tokens only, this is `.data/tokensList.json` by default and expire after 20 minutes)
+## Why Proof-of-Work?
-## Why you might not want to use this
+Every CAPTCHA can eventually be solved, whether by sophisticated bots or humans paid via CAPTCHA farms. The crucial difference lies in the _cost_ imposed on attackers.
-Cap isn't a silver bullet: it won’t block all spam, but it will significantly reduce it. The core principle behind proof-of-work CAPTCHAs is that they are designed to prove effort rather than just verifying a human user.
+The goal is to make automated abuse prohibitively expensive while keeping the experience fast and virtually invisible for real users. Proof-of-work strikes an effective balance, deterring abuse by requiring computational effort rather than relying solely on human verification methods that bots continuously learn to mimic.
-**Here's an example:**
+For a deeper dive into the technical aspects, you might find [this research paper](https://www.researchgate.net/publication/374638786_Proof-of-Work_CAPTCHA_with_password_cracking_functionality) insightful.
-Imagine I can send 10,000 emails to 10,000 random recipients for $1. If I can earn $10 in return from these emails, it becomes highly profitable. However, if the cost of sending these emails increases from $1 to $100, it is no longer profitable. In fact, I would be losing money. This is where CAPTCHA logic comes into play: by making the cost of spamming increase dramatically, we can reduce its viability.
+---
-By applying similar concepts to CAPTCHA, we make it costly for bots to spam, effectively discouraging them from trying.
+**Key improvements in this merged version:**
-If you want to read more about proof-of-work, I recommend reading [this whitepaper](https://www.researchgate.net/publication/374638786_Proof-of-Work_CAPTCHA_with_password_cracking_functionality)
-
-
-## Why proof-of-work?
-
-Every captcha is solvable by bots or by paid humans. The only question is how to make it costly for bots but invisible for users. Proof-of-work is the perfect balance for this problem.
\ No newline at end of file
+- **Stronger Opening:** Clearly states the goal and the "not a silver bullet" caveat upfront.
+- **Integrated PoW:** Uses the clear, numbered list from V2.
+- **Enhanced PoW Details:** Incorporates V1's useful details about salt, target, multiple challenges, and how nonces/hashes work, but phrased concisely.
+- **Clearer Economics Example:** Directly ties the cost increase to Cap.
+- **Combined Privacy & Security:** Merges these related concepts logically using clear bullet points, incorporating the specifics from both versions (memory storage, hashing, defaults).
+- **Refined "Why PoW?":** Uses strong phrasing ("prohibitively expensive," "virtually invisible," "strikes an effective balance") combining good points from both versions.
+- **Better Flow:** Information progresses logically from effectiveness to privacy/security and the underlying rationale.
diff --git a/docs/guide/workings.md b/docs/guide/workings.md
new file mode 100644
index 0000000..77e82e7
--- /dev/null
+++ b/docs/guide/workings.md
@@ -0,0 +1,30 @@
+# How does cap work?
+
+> [!NOTE]
+> This is a more technical explanation of how Cap works. If you're looking for a more general overview, check out the [Effectiveness](./effectiveness.md) page.
+
+#### Initialization
+
+1. When Cap is initialized, it automatically registers a custom element for the widget in the browser's DOM.
+2. The widget creates a shadow DOM and appends all necessary HTML elements to it, ensuring encapsulation from the rest of the page.
+
+#### Requesting the challenge
+
+3. When a solution is requested, the widget sends a request to the server to create a challenge set.
+ - Each challenge consists of a unique salt and a target hash prefix.
+ - The goal is to find a nonce value that, when combined with the salt and hashed using SHA-256, produces a hash that begins with the target prefix.
+ - Instead of generating just one challenge, Cap creates multiple challenges to enable progress tracking and optimize performance.
+
+#### Computing the solution
+
+4. The widget leverages Web Workers to solve the challenges in parallel:
+ - Each worker attempts to find a valid nonce by repeatedly:
+ - Combining the salt with different nonce values
+ - Computing the SHA-256 hash of this combination
+ - Checking if the resulting hash begins with the target prefix
+ - Workers increment the nonce systematically until a matching hash is found
+
+#### Redeeming the solution for a token
+
+5. Once a valid solution is found, the widget sends the result back to the server for validation.
+6. Upon successful verification, the server redeems the solution and issues a token that can be used to authenticate the request.
diff --git a/docs/index.md b/docs/index.md
index 4012b05..4b8eb18 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -65,7 +65,7 @@ There are also some other helpful packages:
- **[Standalone mode](https://capjs.js.org/guide/standalone.html)**: Docker image that helps you use Cap with any language or framework. It runs a simple REST API that can be used to create and validate challenges and an interactive UI to manage your keys.
-- **@cap.js/wasm**: Experimental WASM solvers built using Rust.
+- **@cap.js/wasm**: WASM solvers for Node and Web built with Rust.
It's designed to be a drop-in replacement for existing CAPTCHA solutions, with a focus on performance and UX.
diff --git a/wasm/test/browser.html b/wasm/test/browser.html
index 72eeb33..de12674 100644
--- a/wasm/test/browser.html
+++ b/wasm/test/browser.html
@@ -60,6 +60,9 @@
const workerCodeTemplate = (() => {
const baseURL = "${baseURL}";
+
+ // const relativeWasmWrapperPath = "https://cdn.jsdelivr.net/npm/@cap.js/wasm@0.0.3/browser/cap_wasm.min.js";
+
const relativeWasmWrapperPath = "../src/browser/cap_wasm.js";
let solve_pow_function = null;
@@ -71,13 +74,11 @@
initPromise = import(absoluteWasmWrapperUrl)
.then((wasmModule) => {
- return wasmModule
- .default()
- .then((instance) => {
- solve_pow_function = (
- instance && instance.exports ? instance.exports : wasmModule
- ).solve_pow;
- });
+ return wasmModule.default().then((instance) => {
+ solve_pow_function = (
+ instance && instance.exports ? instance.exports : wasmModule
+ ).solve_pow;
+ });
})
.catch((error) => {
console.error("[worker] setup failed", error);
diff --git a/widget/src/cap.compat.min.js b/widget/src/cap.compat.min.js
index 90d9377..b4a77eb 100644
--- a/widget/src/cap.compat.min.js
+++ b/widget/src/cap.compat.min.js
@@ -1 +1 @@
-"use strict";function _classPrivateFieldLooseBase(e,t){if(!{}.hasOwnProperty.call(e,t))throw new TypeError("attempted to use private field on non-instance");return e}var id=0;function _classPrivateFieldLooseKey(e){return"__private_"+id+++"_"+e}!function(){let e;const t=function(){var e;return null!=(e=window)&&e.CAP_CUSTOM_FETCH?window.CAP_CUSTOM_FETCH(...arguments):fetch(...arguments)},s=function(e,t){return void 0===t&&(t=1e4),new Promise(((s,a)=>{const i=setTimeout((()=>{a(new Error("Initialize timeout"))}),t),r=()=>{e()?(clearTimeout(i),s()):setTimeout(r,500)};r()}))};var a=_classPrivateFieldLooseKey("workerUrl"),i=_classPrivateFieldLooseKey("resetTimer"),r=_classPrivateFieldLooseKey("workersCount"),o=_classPrivateFieldLooseKey("token"),n=_classPrivateFieldLooseKey("shadow"),c=_classPrivateFieldLooseKey("div"),l=_classPrivateFieldLooseKey("host"),d=_classPrivateFieldLooseKey("solving"),h=_classPrivateFieldLooseKey("eventHandlers");class p extends HTMLElement{static get observedAttributes(){return["onsolve","onprogress","onreset","onerror","workers"]}constructor(){super(),Object.defineProperty(this,a,{writable:!0,value:""}),Object.defineProperty(this,i,{writable:!0,value:null}),Object.defineProperty(this,r,{writable:!0,value:navigator.hardwareConcurrency||8}),Object.defineProperty(this,o,{writable:!0,value:null}),Object.defineProperty(this,n,{writable:!0,value:void 0}),Object.defineProperty(this,c,{writable:!0,value:void 0}),Object.defineProperty(this,l,{writable:!0,value:void 0}),Object.defineProperty(this,d,{writable:!0,value:!1}),Object.defineProperty(this,h,{writable:!0,value:void 0}),_classPrivateFieldLooseBase(this,h)[h]&&_classPrivateFieldLooseBase(this,h)[h].forEach(((e,t)=>{this.removeEventListener(t.slice(2),e)})),_classPrivateFieldLooseBase(this,h)[h]=new Map,this.boundHandleProgress=this.handleProgress.bind(this),this.boundHandleSolve=this.handleSolve.bind(this),this.boundHandleError=this.handleError.bind(this),this.boundHandleReset=this.handleReset.bind(this)}async initialize(){_classPrivateFieldLooseBase(this,a)[a]&&URL.revokeObjectURL(_classPrivateFieldLooseBase(this,a)[a]);try{await s((()=>!!e)),_classPrivateFieldLooseBase(this,a)[a]=URL.createObjectURL(new Blob([e],{type:"application/javascript"}))}catch(e){throw this.error("Failed to initialize worker"),e}}attributeChangedCallback(e,t,s){if(e.startsWith("on")){const t=e.slice(2),a=_classPrivateFieldLooseBase(this,h)[h].get(e);if(a&&this.removeEventListener(t,a),s){const s=t=>{const s=this.getAttribute(e);"function"==typeof window[s]&&window[s].call(this,t)};_classPrivateFieldLooseBase(this,h)[h].set(e,s),this.addEventListener(t,s)}}}async connectedCallback(){_classPrivateFieldLooseBase(this,l)[l]=this,_classPrivateFieldLooseBase(this,n)[n]=this.attachShadow({mode:"open"}),_classPrivateFieldLooseBase(this,c)[c]=document.createElement("div"),this.createUI(),this.addEventListeners(),await this.initialize(),_classPrivateFieldLooseBase(this,c)[c].removeAttribute("disabled");const e=this.getAttribute("data-cap-worker-count");this.setWorkersCount(parseInt(e)?parseInt(e,10):navigator.hardwareConcurrency||8),_classPrivateFieldLooseBase(this,l)[l].innerHTML=''}async solve(){if(!_classPrivateFieldLooseBase(this,d)[d])try{_classPrivateFieldLooseBase(this,d)[d]=!0,this.updateUI("verifying","Verifying...",!0),await s((()=>!!_classPrivateFieldLooseBase(this,a)[a])),this.dispatchEvent("progress",{progress:0});try{const e=this.getAttribute("data-cap-api-endpoint");if(!e)throw new Error("Missing API endpoint");const{challenge:s,token:a}=await(await t(`${e}challenge`,{method:"POST"})).json(),r=await this.solveChallenges(s),n=await(await t(`${e}redeem`,{method:"POST",body:JSON.stringify({token:a,solutions:r}),headers:{"Content-Type":"application/json"}})).json();if(this.dispatchEvent("progress",{progress:100}),!n.success)throw new Error("Invalid solution");this.querySelector("input[name='cap-token']")&&(this.querySelector("input[name='cap-token']").value=n.token),this.dispatchEvent("solve",{token:n.token}),_classPrivateFieldLooseBase(this,o)[o]=n.token,_classPrivateFieldLooseBase(this,i)[i]&&clearTimeout(_classPrivateFieldLooseBase(this,i)[i]);const c=new Date(n.expires).getTime()-Date.now();return c>0&&c<864e5?_classPrivateFieldLooseBase(this,i)[i]=setTimeout((()=>this.reset()),c):this.error("Invalid expiration time"),{success:!0,token:_classPrivateFieldLooseBase(this,o)[o]}}catch(e){throw this.error(e.message),e}}finally{_classPrivateFieldLooseBase(this,d)[d]=!1}}async solveChallenges(e){const t=e.length;let s=0;const i=Array(_classPrivateFieldLooseBase(this,r)[r]).fill(null).map((()=>new Worker(_classPrivateFieldLooseBase(this,a)[a]))),o=(e,r)=>{let[o,n]=e;return new Promise(((e,c)=>{const l=i[r],d=setTimeout((()=>{l.terminate(),i[r]=new Worker(_classPrivateFieldLooseBase(this,a)[a]),c(new Error("Worker timeout"))}),3e4);l.onmessage=a=>{let{data:i}=a;i.found&&(clearTimeout(d),s++,this.dispatchEvent("progress",{progress:Math.round(s/t*100)}),e([o,n,i.nonce]))},l.onerror=e=>{clearTimeout(d),this.error(`Error in worker: ${e}`),c(e)},l.postMessage({salt:o,target:n})}))},n=[];try{for(let t=0;to(e,t))));n.push(...a)}}finally{i.forEach((e=>e.terminate()))}return n}setWorkersCount(e){const t=parseInt(e,10),s=Math.min(navigator.hardwareConcurrency||8,16);_classPrivateFieldLooseBase(this,r)[r]=!isNaN(t)&&t>0&&t<=s?t:navigator.hardwareConcurrency||8}createUI(){_classPrivateFieldLooseBase(this,c)[c].classList.add("captcha"),_classPrivateFieldLooseBase(this,c)[c].setAttribute("role","button"),_classPrivateFieldLooseBase(this,c)[c].setAttribute("tabindex","0"),_classPrivateFieldLooseBase(this,c)[c].setAttribute("disabled","true"),_classPrivateFieldLooseBase(this,c)[c].innerHTML='