Last active
August 4, 2022 14:56
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @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