From fa44a89ce6983cc3842cb27ff667666a058a3582 Mon Sep 17 00:00:00 2001 From: Tiago <70700766+tiagorangel1@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:46:42 +0100 Subject: [PATCH] add experimental wasm solver --- .gitignore | 4 + README.md | 2 + docs/index.md | 4 +- package.json | 3 +- solver/example.js | 6 +- wasm/.gitignore | 34 +++++ wasm/build.js | 30 ++++ wasm/src/README.md | 1 + wasm/src/package.json | 234 ++++++++++++++++++++++++++++ wasm/src/rust/Cargo.lock | 218 ++++++++++++++++++++++++++ wasm/src/rust/Cargo.toml | 12 ++ wasm/src/rust/src/lib.rs | 35 +++++ wasm/test/browser.html | 322 +++++++++++++++++++++++++++++++++++++++ wasm/test/node.js | 42 +++++ 14 files changed, 943 insertions(+), 4 deletions(-) create mode 100644 wasm/.gitignore create mode 100644 wasm/build.js create mode 100644 wasm/src/README.md create mode 100644 wasm/src/package.json create mode 100644 wasm/src/rust/Cargo.lock create mode 100644 wasm/src/rust/Cargo.toml create mode 100644 wasm/src/rust/src/lib.rs create mode 100644 wasm/test/browser.html create mode 100644 wasm/test/node.js diff --git a/.gitignore b/.gitignore index 93d41c0..d2adeb8 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,7 @@ docs/.vitepress/dist .vscode .data node_modules +debug/ +target/ +**/*.rs.bk +*.pdb \ No newline at end of file diff --git a/README.md b/README.md index 4c75fb7..f44c3d8 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ 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. + It's designed to be a drop-in replacement for existing CAPTCHA solutions, with a focus on performance and UX. Cap is built with JavaScript, runs on any JS runtime (Bun, Node.js, Deno), and has no dependencies. If you're not using any JS runtime, you can also use the standalone mode with Docker, which relies entirely on a simple REST API to create and validate challenges. diff --git a/docs/index.md b/docs/index.md index 39ba4e2..4012b05 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,6 +65,8 @@ 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. + It's designed to be a drop-in replacement for existing CAPTCHA solutions, with a focus on performance and UX. Cap is built with JavaScript, runs on any JS runtime (Bun, Node.js, Deno), and has no dependencies. If you're not using any JS runtime, you can also use the standalone mode with Docker, which relies entirely on a simple REST API to create and validate challenges. @@ -109,7 +111,7 @@ But unlike them, Cap is **computation-bound, not tracking-bound**. Cap is licensed under the Apache License 2.0. -*** +--- [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9920/badge)](https://www.bestpractices.dev/projects/9920) diff --git a/package.json b/package.json index bc78beb..1a54c9a 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,7 @@ "widget:publish": "cd widget && cd src && sudo npm publish --access public", "server:publish": "cd server && sudo npm publish --access public", "solver:publish": "cd solver && sudo npm publish --access public", - "cli:publish": "cd solver-cli && sudo npm publish --access public" + "cli:publish": "cd solver-cli && sudo npm publish --access public", + "wasm:publish": "cd solver-cli && sudo npm publish --access public" } } diff --git a/solver/example.js b/solver/example.js index a6326d2..f0bb688 100644 --- a/solver/example.js +++ b/solver/example.js @@ -18,9 +18,10 @@ const CHALLENGES = [ ["308758072931bb3b254a7b1ed351d04a", "3e49"], ["724f89bb167db4b881e1dc7b0949ac8f", "b82e"], ["8b79506e4630de15be225c18623eff65", "f0e5"], - ["0c21ade6e63a4e37b13cb8b087f31863", "65c9"], + ["0c21ade6e63a4e37b13cb8b087f31863", "65c9e"], ]; +const timeStart = new Date().getTime(); const solutions = await solver(CHALLENGES, { onProgress: (status) => { process.stdout.moveCursor(0, -1); @@ -32,4 +33,5 @@ const solutions = await solver(CHALLENGES, { process.stdout.moveCursor(0, -1); process.stdout.clearScreenDown(); -console.log("Solutions:", solutions); \ No newline at end of file +console.log("Solutions:", solutions); +console.log("Time taken:", ((new Date().getTime()) - timeStart).toFixed(2), "ms"); \ No newline at end of file diff --git a/wasm/.gitignore b/wasm/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/wasm/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/wasm/build.js b/wasm/build.js new file mode 100644 index 0000000..4571409 --- /dev/null +++ b/wasm/build.js @@ -0,0 +1,30 @@ +import { execSync } from "child_process"; +import path from "path"; +import fs from "fs"; + +const rustSrcDir = path.join(process.cwd(), "./src/rust"); +const nodeOutDir = path.join(process.cwd(), "./src/node"); +const browserOutDir = path.join(process.cwd(), "./src/browser"); +const packageName = "cap_wasm"; + +console.log(`Cleaning old build directories...`); +try { + fs.rmdirSync(nodeOutDir); +} catch {} +try { + fs.rmdirSync(browserOutDir); +} catch {} + +console.log(`\n Building for Node...`); +execSync( + `wasm-pack build "${rustSrcDir}" --target nodejs --out-dir "${nodeOutDir}" --out-name "${packageName}"`, + { stdio: "inherit" } +); + +console.log(`\n Building for web...`); +execSync( + `wasm-pack build "${rustSrcDir}" --target web --out-dir "${browserOutDir}" --out-name "${packageName}"`, + { stdio: "inherit" } +); + +console.log(`\nšŸŽ‰ All builds finished successfully!`); diff --git a/wasm/src/README.md b/wasm/src/README.md new file mode 100644 index 0000000..ba1be29 --- /dev/null +++ b/wasm/src/README.md @@ -0,0 +1 @@ +# Cap WASM \ No newline at end of file diff --git a/wasm/src/package.json b/wasm/src/package.json new file mode 100644 index 0000000..ec066a5 --- /dev/null +++ b/wasm/src/package.json @@ -0,0 +1,234 @@ +{ + "name": "@cap.js/wasm", + "version": "0.0.1", + "description": "WASM for Cap's solver", + "main": "index.js", + "files": [ + "node/", + "browser/", + "README.md" + ], + "keywords": [ + "account security", + "abuse detection", + "abuse prevention", + "api abuse", + "api defense", + "api analytics", + "api protection", + "api security", + "algorithm", + "algorithm implementation", + "alerting", + "anti-abuse", + "anti-automation", + "anti-bot", + "anti-bot component", + "anti-ddos", + "anti-dos", + "anti-exploitation", + "anti-spam", + "application security", + "authentication", + "authentication protocol", + "automated attacks", + "automated scraping", + "behavioral analysis", + "bot defense", + "bot detection", + "bot detection monitoring", + "bot management", + "bot mitigation", + "bot prevention", + "bot traffic", + "bots", + "brute force", + "bun", + "bun runtime", + "challenge generator", + "challenge string", + "challenge verifier", + "challenge-response", + "challenge-response protocol", + "client defense", + "client-side", + "client-server architecture", + "compute", + "compute bound", + "computation", + "computational challenge", + "computational complexity", + "computational defense", + "computational puzzle", + "configuration", + "cpu bound", + "cross-platform", + "crypto", + "crypto puzzle", + "cryptographic algorithm", + "cryptographic challenge", + "cryptographic defense", + "cryptographic puzzle", + "cryptography", + "cryptography library", + "customization", + "cybersecurity", + "ddos", + "ddos protection", + "defense mechanism", + "degree of difficulty", + "dependencies", + "deployment", + "design", + "developer tool", + "development", + "development environment", + "difficulty", + "digest", + "digital security", + "distributed computing", + "dos", + "encryption", + "exploit mitigation", + "form security", + "fraud detection", + "fraud prevention", + "frontend development", + "frontend js", + "fullstack js", + "hash function", + "hash puzzle", + "hashcash", + "hashing", + "hashing library", + "hcaptcha", + "human test", + "human verification", + "image captcha", + "incident response", + "infrastructure security", + "integration", + "integrity", + "internet security", + "invisible captcha", + "js", + "js runtime", + "javascript", + "javascript tool", + "library", + "lightweight", + "logging", + "logic", + "login security", + "low overhead", + "machine learning", + "malicious traffic", + "minimal", + "mining", + "mitigation technique", + "module", + "modules", + "monitoring", + "minimalist", + "node", + "node.js", + "node.js runtime", + "node.js tool", + "nonce", + "non-blocking i/o", + "online security", + "open source", + "optimization", + "package", + "parallel computation", + "performant", + "performance", + "performance monitoring", + "performance optimization", + "performance testing", + "pnpm", + "pow", + "process", + "production", + "programming", + "proof", + "proof generation", + "proof generator", + "proof of work", + "proof verification", + "proof verifier", + "proof-of-work", + "quick", + "rate limiting", + "ratelimit", + "recaptcha", + "reliability", + "request", + "resilience", + "resource efficient", + "scrapers", + "secure hashing", + "security analytics", + "security challenge", + "security component", + "security library", + "security measure", + "security model", + "security monitoring", + "security protocol", + "security system", + "security technique", + "security testing", + "server defense", + "software development", + "solver", + "spam bots", + "spam filtering", + "spam mitigation", + "spam prevention", + "target difficulty", + "threat intelligence", + "traffic management", + "traffic monitoring", + "turing test", + "unwanted traffic", + "user experience", + "validation", + "validation process", + "verification", + "verification process", + "verifier", + "vulnerability", + "web application protection", + "web application security", + "web defense", + "web defense system", + "web development", + "web scraping", + "web security", + "web security tool", + "web services", + "web worker", + "web workers", + "website defense", + "website protection", + "website security", + "work validation", + "worker thread", + "workers", + "captcha solver" + ], + "author": "Tiago", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/tiagorangel1/cap.git" + }, + "bugs": { + "url": "https://github.com/tiagorangel1/cap/issues" + }, + "scripts": { + "npm:publish": "sudo npm publish --access public" + }, + "homepage": "https://github.com/tiagorangel1/cap#readme" +} diff --git a/wasm/src/rust/Cargo.lock b/wasm/src/rust/Cargo.lock new file mode 100644 index 0000000..6d1978a --- /dev/null +++ b/wasm/src/rust/Cargo.lock @@ -0,0 +1,218 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cap_wasm" +version = "0.1.0" +dependencies = [ + "hex", + "sha2", + "wasm-bindgen", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] diff --git a/wasm/src/rust/Cargo.toml b/wasm/src/rust/Cargo.toml new file mode 100644 index 0000000..e7e6e4e --- /dev/null +++ b/wasm/src/rust/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cap_wasm" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" +sha2 = "0.10" +hex = "0.4" \ No newline at end of file diff --git a/wasm/src/rust/src/lib.rs b/wasm/src/rust/src/lib.rs new file mode 100644 index 0000000..9e7e601 --- /dev/null +++ b/wasm/src/rust/src/lib.rs @@ -0,0 +1,35 @@ +use wasm_bindgen::prelude::*; +use sha2::{Sha256, Digest}; +use hex; + +#[wasm_bindgen] +pub fn solve_pow(salt: String, target: String) -> u64 { + let salt_bytes = salt.as_bytes(); + let target_len = target.len(); + + // Determine how many bytes of the hash we need to + // compare based on the hex target length + let required_hash_bytes = (target_len + 1) / 2; + + for nonce in 0..u64::MAX { + let nonce_str = nonce.to_string(); + let nonce_bytes = nonce_str.as_bytes(); + + let mut hasher = Sha256::new(); + + hasher.update(salt_bytes); + hasher.update(nonce_bytes); + + let hash_result = hasher.finalize(); // Returns GenericArray + + let hash_prefix_bytes = &hash_result[0..required_hash_bytes.min(32)]; + + let hash_prefix_hex = hex::encode(hash_prefix_bytes); + + if hash_prefix_hex.starts_with(&target) { + return nonce; + } + } + + unreachable!("Solution should be found before exhausting u64::MAX"); +} \ No newline at end of file diff --git a/wasm/test/browser.html b/wasm/test/browser.html new file mode 100644 index 0000000..72eeb33 --- /dev/null +++ b/wasm/test/browser.html @@ -0,0 +1,322 @@ + + + + + + + + +
+ +
Status: Idle
+ +

Results:

+ +
+ + + + + + diff --git a/wasm/test/node.js b/wasm/test/node.js new file mode 100644 index 0000000..d4ccddc --- /dev/null +++ b/wasm/test/node.js @@ -0,0 +1,42 @@ +import { performance } from "perf_hooks"; + +import { solve_pow } from "../src/node/cap_wasm.js"; + +const challenges = [ + ["e455cea65e98bc3c36287f43769da211", "dceb"], + ["fb8d25f6abac5aa9b6360051f37e010b", "93f1"], + ["91ef47db578fbeb2565d3f9c82bb7960", "3698"], + ["b7ad7667486a691cda8ef297098f64a7", "d72a"], + ["1aca3fb7cef7a2be0dee563ed4136758", "3b58"], + ["d9ec39af92b430e5a329274d8aa58fa8", "e1d3"], + ["781a3cc9217d73c908a321d3fdabd62f", "22c6"], + ["e37a0752c9ac2f3d2517747fde373ac9", "f6f1"], + ["bba070197569f322beda5b240f639a95", "4751"], + ["89297515aeac646bee9653ba405e0beb", "a7de"], + ["444571a0d5039c15be6141d6cd8434f9", "a783"], + ["ba75f2bf8e9b92cc32caa17237a52d14", "7e30"], + ["22bfc18ba8e3ecee080c5d1ef64ed6e9", "5fcf"], + ["885fb78ff76b4eddd2f5bc04ac5ee673", "93e5"], + ["308758072931bb3b254a7b1ed351d04a", "3e49"], + ["724f89bb167db4b881e1dc7b0949ac8f", "b82e"], + ["8b79506e4630de15be225c18623eff65", "f0e5"], + ["0c21ade6e63a4e37b13cb8b087f31863", "65c9"], +]; + +async function runSolverTest() { + const startTime = performance.now(); + + for (let i = 0; i < challenges.length; i++) { + const [salt, target] = challenges[i]; + const nonce = solve_pow(salt, target); + + console.log(`[${i + 1}/${challenges.length}] ${salt}:${target}:${nonce}`); + } + + const endTime = performance.now(); + const totalTime = (endTime - startTime) / 1000; + + console.log(`Solved challenges in ${totalTime.toFixed(3)}s`); +} + +runSolverTest();