Skip to content

Instantly share code, notes, and snippets.

@pabloko
Created December 25, 2024 00:20
Show Gist options
  • Save pabloko/76906fdebd6e7888021207b79384b47c to your computer and use it in GitHub Desktop.
Save pabloko/76906fdebd6e7888021207b79384b47c to your computer and use it in GitHub Desktop.
PoWCaptcha - Proof of work captchas

PoWCaptcha - Proof of Work CAPTCHA

PoWCaptcha is a simple, transparent CAPTCHA system based on Proof of Work (PoW) that aims to stop bots with minimal user interaction. It leverages the same principles as cryptocurrencies and blockchain to validate human users without requiring image recognition or complex puzzles.

Why PoWCaptcha?

Captchas are getting less effective. OCR tools, AI, and captcha solving services are quicker than humans and cheap. At the same time, exposing a contact form without protection invites spam and bot traffic. PoWCaptcha offers a minimal wall to deter lazy bots—making the verification process computationally expensive for bots but invisible to humans.

Instead of bothering the user with complex tasks, PoWCaptcha shifts the burden to the computer, performing backend calculations involving:

  • User data
  • Server integrity
  • Token expiration
  • Proof of Work validation

Key Features

  • Proof of Work: The system perform cryptographic work to validate and obtain the data.
  • Transparent User Experience: The CAPTCHA process doesn’t interfere with user interaction but leverages the computer’s processing power.
  • Minimal Bot Resistance: The challenge is designed to be computationally expensive for bots but lightweight for legitimate users.
  • Configurable: Customize token expiration, difficulty, and other settings to balance between security and user experience.
  • Privacy: Hosted solution that does not involve user tracking or third site sign-in.

Installation & Setup

Simply put PoWCaptcha.php in your project. The library will handle token generation and validation.

Example Usage

1. Generate a Token

Use the API to generate a new token:

PoWCaptcha.php?getToken

2. Validate a Token

To validate the token, use:

PoWCaptcha.php?validateToken=XXXX...

Returns HTTP 200 and JSON data if valid, 401 if invalid.

3. Embeddable JavaScript

<script src="/PoWCaptcha.php?script"></script>

Basic example:

<form method="get" action="/PoWCaptcha.php">
    <input id="token" name="validateToken" type="text" value="Calculating..." />
    <button>Check</button>
</form>
<script src="/PoWCaptcha.php?script"></script>
<script>
  PoWCaptcha({username: "user01", password: "test01password@@"}, (tok) => {
    token.value = tok;
  });
</script>

The PoWCaptcha function generates a token based on user data (like username and password) and obtains a valid token for the provided data that the server can retrive.

Proof of Work Process

  1. Token Generation: The server creates a token using a random string, timestamp, and a cryptographic hash.
  2. Computational Challenge: The client (user's computer) must solve a PoW challenge and validate the token with data.
  3. Validation: The server verifies that the submitted token matches the expected result by recalculating the cryptographic hash and obtains the data.

Security Considerations

  • Integrity: Ensures the token has not been tampered with using a secret key.
  • Expiration: Tokens are time-limited to prevent replay attacks.
  • Effort for Bots: The proof-of-work mechanism forces bots to expend computational resources, reducing the chance of automated / brute force attacks.
<?php
/**
* PoWCaptcha - proof of work captcha
* GitHub @pabloko (gists)
*/
// BEGIN CONFIGURE
$PRIV_TOKEN = "@@Your private and secret token@@"; // used as salt for integrity check
$HASH_COUNT = 5; // amount of bytes to calculate on the proof-of-work
$VALID_TIME = 3600; // time in seconds while token remains valid
$ENABLE_API = true; //weather to allow api via GET
// END CONFIGURE
// Utility...
function generateRandomString($length = 4)
{
$characters = '0123456789abcdef';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++)
$randomString .= $characters[random_int(0, $charactersLength - 1)];
return $randomString;
}
// Generate a fresh token valid for configured period of time
function generateHashString()
{
global $PRIV_TOKEN; global $HASH_COUNT;
$rnd = generateRandomString(16);
$timestamp = time();
$expectedPart = generateRandomString($HASH_COUNT);
return base64_encode($rnd . ";" . $timestamp . ";" . sha1($rnd . ";" . $timestamp . ";" . $PRIV_TOKEN . ";" . $expectedPart) . ";" . $expectedPart . ";X-Response-Hash;");
}
// Validate a PoW recalculated token with data
function validateHashString($token)
{
global $PRIV_TOKEN; global $VALID_TIME;
$decodedString = base64_decode($token);
$parts = explode(";", $decodedString);
if (count($parts) !== 6) return null;
$timeCheck = time() - (int)$parts[1];
// check is valid in time
if ($timeCheck > $VALID_TIME || $timeCheck < 0) return null;
// do integrity check
if ($parts[2] !== sha1($parts[0] . ";" . (int)$parts[1] . ";" . $PRIV_TOKEN . ";" . $parts[3])) return null;
// do proof-of-work verification
if (!str_ends_with(sha1($parts[2] . $parts[5] . (int)$parts[4]), $parts[3])) return null;
return base64_decode($parts[5]);
}
// simple API
// obtain a fresh token
// PoWCaptcha.php?getToken
if ($ENABLE_API && isset($_GET['getToken']))
{
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
die(generateHashString());
}
// check a PoW recalculated token and output data or http error
// PoWCaptcha.php?validateToken=XXXX...
if ($ENABLE_API && isset($_GET['validateToken']))
{
header('Content-Type: application/json');
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
$result = validateHashString($_GET['validateToken']);
http_response_code($result ? 200 : 401);
die($result);
}
// embeddable script output
// PoWCaptcha.php?script
if ($ENABLE_API && isset($_GET['script']))
{
header('Content-Type: application/javascript');
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
?>/**
* PoWCaptcha - proof of work captcha
* GitHub @pabloko (gists)
*/
<?php
// function PoWCaptcha(data, callback)
// {
// var _token = '< ?php echo generateHashString(); ? >'
// function createWorker(fn)
// {
// var blob = new Blob(['self.onmessage = ', fn.toString()], { type: 'text/javascript' });
// var url = window.URL.createObjectURL(blob);
// return new Worker(url);
// }
// function tPoW(e)
// {
// function SHA1(f){function $(f,$){return f<<$|f>>>32-$}function r(f){var $,r,o,e="";for($=0;$<=6;$+=2)r=f>>>4*$+4&15,o=f>>>4*$&15,e+=r.toString(16)+o.toString(16);return e}function o(f){var $,r,o="";for($=7;$>=0;$--)o+=(r=f>>>4*$&15).toString(16);return o}var e,_,t,a,C,h,x,n,c,d=Array(80),A=1732584193,u=4023233417,s=2562383102,i=271733878,g=3285377520,m=(f=function f($){$=$.replace(/\r\n/g,"\n");for(var r="",o=0;o<$.length;o++){var e=$.charCodeAt(o);e<128?r+=String.fromCharCode(e):e>127&&e<2048?(r+=String.fromCharCode(e>>6|192),r+=String.fromCharCode(63&e|128)):(r+=String.fromCharCode(e>>12|224),r+=String.fromCharCode(e>>6&63|128),r+=String.fromCharCode(63&e|128))}return r}(f)).length,p=[];for(_=0;_<m-3;_+=4)t=f.charCodeAt(_)<<24|f.charCodeAt(_+1)<<16|f.charCodeAt(_+2)<<8|f.charCodeAt(_+3),p.push(t);switch(m%4){case 0:_=2147483648;break;case 1:_=f.charCodeAt(m-1)<<24|8388608;break;case 2:_=f.charCodeAt(m-2)<<24|f.charCodeAt(m-1)<<16|32768;break;case 3:_=f.charCodeAt(m-3)<<24|f.charCodeAt(m-2)<<16|f.charCodeAt(m-1)<<8|128}for(p.push(_);p.length%16!=14;)p.push(0);for(p.push(m>>>29),p.push(m<<3&4294967295),e=0;e<p.length;e+=16){for(_=0;_<16;_++)d[_]=p[e+_];for(_=16;_<=79;_++)d[_]=$(d[_-3]^d[_-8]^d[_-14]^d[_-16],1);for(_=0,a=A,C=u,h=s,x=i,n=g;_<=19;_++)c=$(a,5)+(C&h|~C&x)+n+d[_]+1518500249&4294967295,n=x,x=h,h=$(C,30),C=a,a=c;for(_=20;_<=39;_++)c=$(a,5)+(C^h^x)+n+d[_]+1859775393&4294967295,n=x,x=h,h=$(C,30),C=a,a=c;for(_=40;_<=59;_++)c=$(a,5)+(C&h|C&x|h&x)+n+d[_]+2400959708&4294967295,n=x,x=h,h=$(C,30),C=a,a=c;for(_=60;_<=79;_++)c=$(a,5)+(C^h^x)+n+d[_]+3395469782&4294967295,n=x,x=h,h=$(C,30),C=a,a=c;A=A+a&4294967295,u=u+C&4294967295,s=s+h&4294967295,i=i+x&4294967295,g=g+n&4294967295}var c=o(A)+o(u)+o(s)+o(i)+o(g);return c.toLowerCase()}
// var pow = ""
// try {
// var part = atob(e.data).split(';')
// if (part.length != 6 && part[4] !== "X-Response-Hash") return null;
// var i = 0;
// while(!SHA1(part[2] + part[5] + i).endsWith(part[3])) {i++;}
// part[4] = i
// pow = btoa(part.join(';'))
// } catch (ex) { /*console.error(ex);*/ }
// self.postMessage(pow)
// }
// var tPoWworker = createWorker(tPoW)
// tPoWworker.postMessage(btoa(atob(_token) + btoa(JSON.stringify(data))))
// tPoWworker.onmessage = function (e)
// {
// if (callback) callback(e.data == "" ? null : e.data)
// tPoWworker.terminate();
// }
// }
?>function PoWCaptcha(r,e){var t,o,a,n=(t=function r(e){function t(r){function e(r,e){return r<<e|r>>>32-e}function t(r){var e,t,o,a="";for(e=0;e<=6;e+=2)t=r>>>4*e+4&15,o=r>>>4*e&15,a+=t.toString(16)+o.toString(16);return a}function o(r){var e,t,o="";for(e=7;e>=0;e--)o+=(t=r>>>4*e&15).toString(16);return o}var a,n,_,h,c,f,s,$,C,i=Array(80),d=1732584193,u=4023233417,g=2562383102,p=271733878,l=3285377520,v=(r=function r(e){e=e.replace(/\r\n/g,"\n");for(var t="",o=0;o<e.length;o++){var a=e.charCodeAt(o);a<128?t+=String.fromCharCode(a):a>127&&a<2048?(t+=String.fromCharCode(a>>6|192),t+=String.fromCharCode(63&a|128)):(t+=String.fromCharCode(a>>12|224),t+=String.fromCharCode(a>>6&63|128),t+=String.fromCharCode(63&a|128))}return t}(r)).length,A=[];for(n=0;n<v-3;n+=4)_=r.charCodeAt(n)<<24|r.charCodeAt(n+1)<<16|r.charCodeAt(n+2)<<8|r.charCodeAt(n+3),A.push(_);switch(v%4){case 0:n=2147483648;break;case 1:n=r.charCodeAt(v-1)<<24|8388608;break;case 2:n=r.charCodeAt(v-2)<<24|r.charCodeAt(v-1)<<16|32768;break;case 3:n=r.charCodeAt(v-3)<<24|r.charCodeAt(v-2)<<16|r.charCodeAt(v-1)<<8|128}for(A.push(n);A.length%16!=14;)A.push(0);for(A.push(v>>>29),A.push(v<<3&4294967295),a=0;a<A.length;a+=16){for(n=0;n<16;n++)i[n]=A[a+n];for(n=16;n<=79;n++)i[n]=e(i[n-3]^i[n-8]^i[n-14]^i[n-16],1);for(n=0,h=d,c=u,f=g,s=p,$=l;n<=19;n++)C=e(h,5)+(c&f|~c&s)+$+i[n]+1518500249&4294967295,$=s,s=f,f=e(c,30),c=h,h=C;for(n=20;n<=39;n++)C=e(h,5)+(c^f^s)+$+i[n]+1859775393&4294967295,$=s,s=f,f=e(c,30),c=h,h=C;for(n=40;n<=59;n++)C=e(h,5)+(c&f|c&s|f&s)+$+i[n]+2400959708&4294967295,$=s,s=f,f=e(c,30),c=h,h=C;for(n=60;n<=79;n++)C=e(h,5)+(c^f^s)+$+i[n]+3395469782&4294967295,$=s,s=f,f=e(c,30),c=h,h=C;d=d+h&4294967295,u=u+c&4294967295,g=g+f&4294967295,p=p+s&4294967295,l=l+$&4294967295}var C=o(d)+o(u)+o(g)+o(p)+o(l);return C.toLowerCase()}var o="";try{var a=atob(e.data).split(";");if(6!=a.length&&"X-Response-Hash"!==a[4])return null;for(var n=0;!t(a[2]+a[5]+n).endsWith(a[3]);)n++;a[4]=n,o=btoa(a.join(";"))}catch(_){}self.postMessage(o)},o=new Blob(["self.onmessage = ",t.toString()],{type:"text/javascript"}),a=window.URL.createObjectURL(o),new Worker(a));n.postMessage(btoa(atob("<?php echo generateHashString(); ?>")+btoa(JSON.stringify(r)))),n.onmessage=function(r){e&&e(""==r.data?null:r.data),n.terminate()}}<?php
die();
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment