Skip to content

Instantly share code, notes, and snippets.

@zazaulola
Last active August 4, 2022 14:56
Show Gist options
  • Save zazaulola/6742e41611b85fc48931a79bacd8c42d to your computer and use it in GitHub Desktop.
Save zazaulola/6742e41611b85fc48931a79bacd8c42d to your computer and use it in GitHub Desktop.
PoW captcha for Node. All code is static - no sessions needed, no cookies needed, no database needed.
/** @format */
/**
*
* The principle of the algorithm is very simple.
*
* The module exports three functions:
*
* getTask()
* The first function does not need any arguments.
* It returns 4 strings, which should be sent to the client together
* with the source code of the second function.
*
* doTask()
* The second function runs in the client browser and calculates the task.
* For it to work, you have to pass 4 strings in its arguments.
* As a result it also returns 4 strings, which must be sent to the server.
*
* checkTask()
* The third function receives with its arguments 4 strings
* that came from the browser as a result of calculations,
* and returns the result of checking calculations.
*
* The epic nature of the algorithm is that there is no need for the server
* to write the task data to the database, and there is no need
* to keep track of unfinished calculations.
* The third function checkTask() will easily check the calculation results
* without having to track the client session,
* there is no need to read any cookies from the request.
*
* The algorithm works completely static.
*
* Having a random initialization vector, and the current timestamp
* combined with the server secret, we get a hash that will contain
* the result of the calculation.
*
* Using the same initialization vector and the current timestamp,
* combining it with the hash containing the result of the computation,
* we get a check hash.
*
* From the hash containing the result of calculations
* we remove a few characters, which forms
* a computational task for the browser.
*
* Having all the data to form a validation hash,
* the browser brute-force recovers the missing characters.
*
* After calculating the missing characters, the browser sends the results to
* the server. The server easily checks the results without having any idea
* where the original task data came from. For the third checkTask() function,
* we don't need any user or session data, and we don't need any data from DB.
* We only need 4 strings, returned by the second function.
*
* Possible attacks on the algorithm:
*
* 1. Replacing the initialization vector causes the result hash to be wrong.
* 2. Replacing the timestamp also causes an invalid hash of the result.
* 3. Replacing the check hash is also useless.
* 4. There are only 120 seconds to find the right hash pair.
* 5. It is possible to successfully specify the result hash
* without calculations only by having secret part of the source material
* for the result hash.
*/
let { randomBytes, createHash } = require('crypto');
/**
* Server secret
*/
const secret = randomBytes(32);
/**
* Task algorithm
*/
const algo = 'sha256';
/**
* Task difficult
*/
const difficult = 5;
/**
* Task lifetime
*/
const timeoutPeriod = 120 * 1000;
/**
* Digest calculator
* @param {...string} parts
* @returns {string}
*/
const digest = (...parts) => parts.reduce((hash, part) => (hash.write(part), hash), createHash(algo)).digest('hex');
module.exports = {
/**
* Create the task for the browser
* @returns {[string,string,string,string]}
*/
getTask() {
const iv = randomBytes(26).toString('hex');
const ts = Date.now();
const tsStr = ts.toString(16).padStart(12, 0);
const rs = digest(iv, tsStr, secret);
const ck = digest(iv, tsStr, rs);
const Nx = rs.substring(0, ck.length - difficult);
return [iv, tsStr, Nx, ck];
},
/**
* Task calculator
*
* it is used in browser
* @param {string} iv Initialization vector
* @param {string} tsStr timestamp in hex format
* @param {string} Nx Precalculated part of the task
* @param {string} ck Hash for the checker
* @returns {[string,string,string,string]}
*/
async doTask(iv, tsStr, Nx, ck) {
const difficult = ck.length - Nx.length;
const maxN = 16 ** difficult;
let cX, digStr;
do {
cX = Math.trunc(maxN * Math.random())
.toString(16)
.padStart(difficult, 0);
const digBuf = await crypto.subtle.digest('SHA-256', iv + tsStr + Nx + cX);
const digArr = [...new Uint8Array(digBuf)];
digStr = digArr.map(x => x.toString(16).padStart(2, 0)).join``;
} while (digStr !== ck);
return [iv, tsStr, Nx + cX, ck];
},
/**
* Task checker
* @param {*} iv Initialization vector
* @param {*} tsStr timestamp in hex format
* @param {*} rs result of calculations
* @param {*} ck Hash for the checker
* @returns {boolean}
*/
checkTask(iv, tsStr, rs, ck) {
const ts = parseInt(tsStr, 16);
// Task has expired
if (ts + timeoutPeriod < Date.now()) return false;
// Results are not matches with iv+ts+secret
if (digest(iv, tsStr, secret) != rs) return false;
// Results are not matches with checker
if (digest(iv, tsStr, rs) != ck) return false;
return true;
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment