├── tsconfig.json ├── .npmignore ├── .editorconfig ├── .gitignore ├── src ├── types.ts ├── utils.ts ├── index.ts └── archiver.ts ├── test └── index.js ├── license ├── package.json └── readme.md /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsex/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | resources 2 | src 3 | tasks 4 | test 5 | 6 | .editorconfig 7 | tsconfig.json 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.err 3 | *.log 4 | ._* 5 | .cache 6 | .fseventsd 7 | .DocumentRevisions* 8 | .DS_Store 9 | .TemporaryItems 10 | .Trashes 11 | Thumbs.db 12 | 13 | dist 14 | node_modules 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | type Archive = [ 5 | n: bigint, 6 | a: bigint, 7 | t: bigint, 8 | Ck: bigint, 9 | Cm: Uint8Array 10 | ]; 11 | 12 | type Options = { 13 | /* ADVANCED */ 14 | primeBits?: number, 15 | primeRounds?: number, 16 | opsPerSecond?: number, 17 | /* MAIN */ 18 | duration: number, 19 | message: string 20 | }; 21 | 22 | /* EXPORT */ 23 | 24 | export type {Archive, Options}; 25 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | const sfme = ( a: bigint, t: bigint, n: bigint ): bigint => { //TODO: Maybe add a special-case for this in (power of two exponents) `fast-mod-exp`, somehow 5 | 6 | let x = a % n; 7 | 8 | if ( t <= BigInt ( Number.MAX_SAFE_INTEGER ) ) { 9 | 10 | for ( let i = Number ( t ); i > 0; i-- ) { 11 | 12 | x = ( x * x ) % n; 13 | 14 | } 15 | 16 | } else { 17 | 18 | for ( let i = t; i > 0n; i-- ) { 19 | 20 | x = ( x * x ) % n; 21 | 22 | } 23 | 24 | } 25 | 26 | return x % n; 27 | 28 | }; 29 | 30 | /* EXPORT */ 31 | 32 | export {sfme}; 33 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {describe} from 'fava'; 5 | import Puzzle from '../dist/index.js'; 6 | 7 | /* MAIN */ 8 | 9 | describe ( 'Cryto Puzzle', it => { 10 | 11 | it ( 'can generate and solve puzzles in about the specified amount of time', async t => { 12 | 13 | for ( const duration of [100, 1000, 2000, 3000, 4000] ) { 14 | 15 | const start = Date.now (); 16 | 17 | const message = String ( duration ); 18 | const puzzle = await Puzzle.generate ({ duration, message }); 19 | const solution = await Puzzle.solve ( puzzle ); 20 | 21 | const end = Date.now (); 22 | const elapsed = end - start; 23 | 24 | t.is ( message, solution ); 25 | t.true ( elapsed >= duration * 0.9 && elapsed <= duration * 1.1 ); 26 | 27 | } 28 | 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022-present Fabio Spampinato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypto-puzzle", 3 | "repository": "github:fabiospampinato/crypto-puzzle", 4 | "description": "A time-lock puzzle generator", 5 | "license": "MIT", 6 | "version": "5.0.3", 7 | "type": "module", 8 | "main": "dist/index.js", 9 | "exports": "./dist/index.js", 10 | "types": "./dist/index.d.ts", 11 | "scripts": { 12 | "clean": "tsex clean", 13 | "compile": "tsex compile", 14 | "compile:watch": "tsex compile --watch", 15 | "test": "tsex test", 16 | "test:watch": "tsex test --watch", 17 | "prepublishOnly": "tsex prepare" 18 | }, 19 | "keywords": [ 20 | "crypto", 21 | "time-lock", 22 | "puzzle", 23 | "pow", 24 | "proof-of-work", 25 | "captcha" 26 | ], 27 | "dependencies": { 28 | "bigint-encoding": "^1.0.1", 29 | "crypto-random-in-range": "^2.0.1", 30 | "crypto-random-prime": "^1.0.1", 31 | "crypto-random-uint8": "^2.0.1", 32 | "crypto-sha": "^2.1.1", 33 | "fast-mod-exp": "^1.0.1", 34 | "int32-encoding": "^1.0.1", 35 | "tiny-encryptor": "^1.0.1", 36 | "uint8-concat": "^1.0.1", 37 | "uint8-encoding": "^2.0.1" 38 | }, 39 | "devDependencies": { 40 | "fava": "^0.3.4", 41 | "tsex": "^4.0.2", 42 | "typescript": "^5.7.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import fme from 'fast-mod-exp'; 5 | import Encryptor from 'tiny-encryptor'; 6 | import getPrime from 'crypto-random-prime'; 7 | import getInRange from 'crypto-random-in-range'; 8 | import getRandomBytes from 'crypto-random-uint8'; 9 | import {sha256} from 'crypto-sha'; 10 | import U8 from 'uint8-encoding'; 11 | import BigEnc from 'bigint-encoding'; 12 | import Archiver from './archiver'; 13 | import {sfme} from './utils'; 14 | import type {Options} from './types'; 15 | 16 | /* MAIN */ 17 | 18 | //URL: https://people.csail.mit.edu/rivest/pubs/RSW96.pdf 19 | 20 | const CryptoPuzzle = { 21 | 22 | /* API */ 23 | 24 | generate: async ( options: Options ): Promise => { 25 | 26 | const PRIME_BITS = options.primeBits ?? 100; 27 | const PRIME_ROUNDS = options.primeRounds ?? 20; 28 | const OPS_PER_SECOND = options.opsPerSecond ?? 3_300_000; 29 | const DURATION = options.duration ?? 1_000; 30 | const MESSAGE = options.message; 31 | 32 | const p = getPrime ( PRIME_BITS, PRIME_ROUNDS ); 33 | const q = getPrime ( PRIME_BITS, PRIME_ROUNDS ); 34 | 35 | const n = p * q; 36 | const n1 = ( p - 1n ) * ( q - 1n ); 37 | 38 | const S = OPS_PER_SECOND; 39 | const T = DURATION; 40 | const t = BigInt ( Math.round ( Math.max ( 1, ( S / 1000 ) ) * T ) ); 41 | 42 | const K = await sha256.uint8 ( getRandomBytes ( 32 ) ); 43 | const M = MESSAGE; 44 | const Cm = await Encryptor.encrypt ( M, K ); 45 | 46 | const a = getInRange ( 1n, n - 1n ); 47 | const e = fme ( 2n, t, n1 ); 48 | const b = fme ( a, e, n ); 49 | const Ck = BigEnc.encode ( K ) + b; 50 | 51 | const archive = Archiver.archive ([ n, a, t, Ck, Cm ]); 52 | 53 | return archive; 54 | 55 | }, 56 | 57 | solve: async ( puzzle: Uint8Array ): Promise => { 58 | 59 | const [n, a, t, Ck, Cm] = Archiver.unarchive ( puzzle ); 60 | 61 | const b = sfme ( a, t, n ); 62 | const K = BigEnc.decode ( Ck - b ); 63 | const M_uint8 = await Encryptor.decrypt ( Cm, K ); 64 | const M = U8.decode ( M_uint8 ); 65 | 66 | return M; 67 | 68 | } 69 | 70 | }; 71 | 72 | /* EXPORT */ 73 | 74 | export default CryptoPuzzle; 75 | export type {Options}; 76 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Crypto Puzzle 2 | 3 | A [time-lock puzzle](https://people.csail.mit.edu/rivest/pubs/RSW96.pdf) generator. 4 | 5 | ## Description 6 | 7 | Time-lock puzzles are kind of a way to craft a message that can only be read after some point in the future, which you can pick. 8 | 9 | A time-lock puzzle is a computational puzzle that requires a deterministic number of operations to solve. By calculating how many operations per second an attacker, or the receiver of your message, could perform you can configure the amount of operations needed to decrypt the message such that at least the amount of time that you require must have passed for the message to have been decrypted. 10 | 11 | This works because the operations that need to be performed to solve a puzzle are not parallelizable, you can't solve time-lock puzzles meaningfully faster with a GPU or a million computers, and generating the puzzle is always cheap, because you know which prime numbers it's secured by, basically. 12 | 13 | Time-lock puzzles are basically little proof-of-works, they have lots of interesting applications, for example a solution like this can be used to fight spam, by requiring that each request that your server receives comes with its own solution to a unique time-lock puzzle, which would be cheap for you to generate, cheap for you to check, cheap for legitimate users to solve, but prohibitively expensive for abusers/spammers to solve many of. It can be used as a sort of transparent captcha that can't be bypassed. 14 | 15 | ## Install 16 | 17 | ```sh 18 | npm install crypto-puzzle 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```ts 24 | import Puzzle from 'crypto-puzzle'; 25 | 26 | // Let's generate a puzzle that can only be read in about 10 seconds from now 27 | 28 | const puzzle = await Puzzle.generate ({ 29 | /* OPTIONAL OPTIONS */ 30 | primeBits: 100, // Number of bits of entropy that the two internally generated primes will have 31 | primeRounds: 20, // Number of Miller-Rabin primality checks that the prime numbers will have to pass 32 | opsPerSecond: 3_300_000, // Rough number of operations per second that the attacker/receiver can perform, 3.3M is around what a MBP M1 Max can do 33 | /* REQUIRED OPTIONS */ 34 | duration: 10_000, // Rough minimum number of milliseconds that this puzzle will be unsolvable for 35 | message: 'Hey there!' // Message to encrypt inside the puzzle 36 | }); 37 | 38 | // Now let's solve the puzzle 39 | 40 | const solution = await Puzzle.solve ( puzzle ); 41 | 42 | // About 10 seconds later... 43 | 44 | console.log ( solution ); // => 'Hey there!' 45 | ``` 46 | 47 | ## License 48 | 49 | MIT © Fabio Spampinato 50 | -------------------------------------------------------------------------------- /src/archiver.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import BigEnc from 'bigint-encoding'; 5 | import Int32 from 'int32-encoding'; 6 | import concat from 'uint8-concat'; 7 | import type {Archive} from './types'; 8 | 9 | /* MAIN */ 10 | 11 | const Archiver = { 12 | 13 | /* API */ 14 | 15 | archive: ( archive: Archive ): Uint8Array => { 16 | 17 | const [n, a, t, Ck, Cm] = archive; 18 | 19 | const n_uint8 = BigEnc.decode ( n ); 20 | const n_length = Int32.encode ( n_uint8.length ); 21 | 22 | const a_uint8 = BigEnc.decode ( a ); 23 | const a_length = Int32.encode ( a_uint8.length ); 24 | 25 | const t_uint8 = BigEnc.decode ( t ); 26 | const t_length = Int32.encode ( t_uint8.length ); 27 | 28 | const Ck_uint8 = BigEnc.decode ( Ck ); 29 | const Ck_length = Int32.encode ( Ck_uint8.length ); 30 | 31 | return concat ([ n_length, n_uint8, a_length, a_uint8, t_length, t_uint8, Ck_length, Ck_uint8, Cm ]); 32 | 33 | }, 34 | 35 | unarchive: ( archive: Uint8Array ): Archive => { 36 | 37 | const n_length_offset = 0; 38 | const n_length_uint8 = archive.slice ( n_length_offset, n_length_offset + 4 ); 39 | const n_length = Int32.decode ( n_length_uint8 ); 40 | const n_offset = n_length_offset + 4; 41 | const n_uint8 = archive.slice ( n_offset, n_offset + n_length ); 42 | const n = BigEnc.encode ( n_uint8 ); 43 | 44 | const a_length_offset = n_offset + n_length; 45 | const a_length_uint8 = archive.slice ( a_length_offset, a_length_offset + 4 ); 46 | const a_length = Int32.decode ( a_length_uint8 ); 47 | const a_offset = a_length_offset + 4; 48 | const a_uint8 = archive.slice ( a_offset, a_offset + a_length ); 49 | const a = BigEnc.encode ( a_uint8 ); 50 | 51 | const t_length_offset = a_offset + a_length; 52 | const t_length_uint8 = archive.slice ( t_length_offset, t_length_offset + 4 ); 53 | const t_length = Int32.decode ( t_length_uint8 ); 54 | const t_offset = t_length_offset + 4; 55 | const t_uint8 = archive.slice ( t_offset, t_offset + t_length ); 56 | const t = BigEnc.encode ( t_uint8 ); 57 | 58 | const Ck_length_offset = t_offset + t_length; 59 | const Ck_length_uint8 = archive.slice ( Ck_length_offset, Ck_length_offset + 4 ); 60 | const Ck_length = Int32.decode ( Ck_length_uint8 ); 61 | const Ck_offset = Ck_length_offset + 4; 62 | const Ck_uint8 = archive.slice ( Ck_offset, Ck_offset + Ck_length ); 63 | const Ck = BigEnc.encode ( Ck_uint8 ); 64 | 65 | const Cm_offset = Ck_offset + Ck_length; 66 | const Cm = archive.slice ( Cm_offset ); 67 | 68 | return [n, a, t, Ck, Cm]; 69 | 70 | } 71 | 72 | }; 73 | 74 | /* EXPORT */ 75 | 76 | export default Archiver; 77 | --------------------------------------------------------------------------------