altcha-lib 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ ALTCHA JS Library is a lightweight, zero-dependency library designed for creatin
4
4
 
5
5
  ## Compatibility
6
6
 
7
- This library utilizes [Web Crypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) and is intended for server-side use.
7
+ This library utilizes [Web Crypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) and can be used in modern browsers and supported server environments:
8
8
 
9
9
  - Node.js 16+
10
10
  - Bun 1+
@@ -20,6 +20,7 @@ const hmacKey = 'secret hmac key';
20
20
  // Create a new challenge and send it to the client:
21
21
  const challenge = await createChallenge({
22
22
  hmacKey,
23
+ maxNumber: 100000, // the maximum random number
23
24
  });
24
25
 
25
26
  // When submitted, verify the payload:
@@ -42,6 +43,8 @@ Parameters:
42
43
  - `salt?: string`: Optional salt string. If not provided, a random salt will be generated.
43
44
  - `saltLength?: number` Optional maximum lenght of the random salt (in bytes, defaults to 12).
44
45
 
46
+ Returns: `Promise<Challenge>`
47
+
45
48
  ### `verifySolution(payload, hmacKey)`
46
49
 
47
50
  Verifies an ALTCHA solution. The payload can be a Base64-encoded JSON payload (as submitted by the widget) or an object.
@@ -51,6 +54,71 @@ Parameters:
51
54
  - `payload: string | Payload`
52
55
  - `hmacKey: string`
53
56
 
57
+ Returns: `Promise<boolean>`
58
+
59
+ ### `solveChallenge(challenge, salt, algorithm?, max?, start?)`
60
+
61
+ Finds a solution to the given challenge.
62
+
63
+ Parameters:
64
+
65
+ - `challenge: string` (required): The challenge hash.
66
+ - `salt: string` (required): The challenge salt.
67
+ - `algorithm?: string`: Optional algorithm (default: `SHA-256`).
68
+ - `max?: string`: Optional `maxnumber` to iterate to (default: 1e6).
69
+ - `start?: string`: Optional starting number (default: 0).
70
+
71
+ Returns: `{ controller: AbortController, promise: Promise<Solution | null> }`
72
+
73
+ ### `solveChallengeWorkers(workerScript, concurrency, challenge, salt, algorithm?, max?, start?)`
74
+
75
+ Finds a solution to the given challenge with [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker) running concurrently.
76
+
77
+ Parameters:
78
+
79
+ - `workerScript: string` (required): The path or URL of the worker script.
80
+ - `concurrency: number` (required): The concurrency (number of workers).
81
+ - `challenge: string` (required): The challenge hash.
82
+ - `salt: string` (required): The challenge salt.
83
+ - `algorithm?: string`: Optional algorithm (default: `SHA-256`).
84
+ - `max?: string`: Optional `maxnumber` to iterate to (default: 1e6).
85
+ - `start?: string`: Optional starting number (default: 0).
86
+
87
+ Returns: `Promise<Solution | null>`
88
+
89
+ Usage with `altcha-lib/worker`:
90
+
91
+ ```ts
92
+ import { solveChallengeWorkers } from 'altcha-lib';
93
+
94
+ const solution = await solveChallengeWorkers(
95
+ 'altcha-lib/worker', // Worker script URL or path
96
+ 8, // Spawn 8 workers
97
+ challenge,
98
+ salt,
99
+ );
100
+ ```
101
+
102
+ ## Benchmarks
103
+
104
+ ```
105
+ > solveChallenge()
106
+ - n = 1,000............................... 319 ops/s ±2.04%
107
+ - n = 10,000.............................. 31 ops/s ±1.02%
108
+ - n = 50,000.............................. 6 ops/s ±1.48%
109
+ - n = 100,000............................. 3 ops/s ±0.27%
110
+ - n = 500,000............................. 0 ops/s ±0.36%
111
+
112
+ > solveChallengeWorkers() (8 workers)
113
+ - n = 1,000............................... 62 ops/s ±5.69%
114
+ - n = 10,000.............................. 30 ops/s ±4.35%
115
+ - n = 50,000.............................. 12 ops/s ±2.89%
116
+ - n = 100,000............................. 7 ops/s ±2.33%
117
+ - n = 500,000............................. 1 ops/s ±2.22%
118
+ ```
119
+
120
+ Run with Bun on MacBook Pro M3-Pro. See [/benchmark](/benchmark/) folder for more details.
121
+
54
122
  ## License
55
123
 
56
124
  MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ if (!('crypto' in globalThis)) {
4
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
5
+ globalThis.crypto = require('node:crypto').webcrypto;
6
+ }
@@ -1,4 +1,6 @@
1
+ import './crypto.js';
1
2
  import type { Algorithm } from './types.js';
3
+ export declare const encoder: TextEncoder;
2
4
  export declare function ab2hex(ab: ArrayBuffer | Uint8Array): string;
3
5
  export declare function hash(algorithm: Algorithm, str: string): Promise<string>;
4
6
  export declare function hmac(algorithm: Algorithm, str: string, secret: string): Promise<string>;
@@ -1,11 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.randomInt = exports.randomBytes = exports.hmac = exports.hash = exports.ab2hex = void 0;
4
- const encoder = new TextEncoder();
5
- if (!('crypto' in globalThis)) {
6
- // eslint-disable-next-line @typescript-eslint/no-var-requires
7
- globalThis.crypto = require('node:crypto').webcrypto;
8
- }
3
+ exports.randomInt = exports.randomBytes = exports.hmac = exports.hash = exports.ab2hex = exports.encoder = void 0;
4
+ // @denoify-line-ignore
5
+ require("./crypto.js");
6
+ exports.encoder = new TextEncoder();
9
7
  function ab2hex(ab) {
10
8
  return [...new Uint8Array(ab)]
11
9
  .map((x) => x.toString(16).padStart(2, '0'))
@@ -13,15 +11,15 @@ function ab2hex(ab) {
13
11
  }
14
12
  exports.ab2hex = ab2hex;
15
13
  async function hash(algorithm, str) {
16
- return ab2hex(await crypto.subtle.digest(algorithm.toUpperCase(), encoder.encode(str)));
14
+ return ab2hex(await crypto.subtle.digest(algorithm.toUpperCase(), exports.encoder.encode(str)));
17
15
  }
18
16
  exports.hash = hash;
19
17
  async function hmac(algorithm, str, secret) {
20
- const key = await crypto.subtle.importKey('raw', encoder.encode(secret), {
18
+ const key = await crypto.subtle.importKey('raw', exports.encoder.encode(secret), {
21
19
  name: 'HMAC',
22
20
  hash: algorithm,
23
21
  }, false, ['sign', 'verify']);
24
- return ab2hex(await crypto.subtle.sign('HMAC', key, encoder.encode(str)));
22
+ return ab2hex(await crypto.subtle.sign('HMAC', key, exports.encoder.encode(str)));
25
23
  }
26
24
  exports.hmac = hmac;
27
25
  function randomBytes(length) {
@@ -1,3 +1,8 @@
1
- import type { Challenge, ChallengeOptions, Payload } from './types.js';
1
+ import type { Challenge, ChallengeOptions, Payload, Solution } from './types.js';
2
2
  export declare function createChallenge(options: ChallengeOptions): Promise<Challenge>;
3
3
  export declare function verifySolution(payload: string | Payload, hmacKey: string): Promise<boolean>;
4
+ export declare function solveChallenge(challenge: string, salt: string, algorithm?: string, max?: number, start?: number): {
5
+ promise: Promise<Solution | null>;
6
+ controller: AbortController;
7
+ };
8
+ export declare function solveChallengeWorkers(workerScript: string | URL | (() => Worker), concurrency: number, challenge: string, salt: string, algorithm?: string, max?: number, startNumber?: number): Promise<Solution | null>;
package/cjs/dist/index.js CHANGED
@@ -1,20 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.verifySolution = exports.createChallenge = void 0;
3
+ exports.solveChallengeWorkers = exports.solveChallenge = exports.verifySolution = exports.createChallenge = void 0;
4
4
  const helpers_js_1 = require("./helpers.js");
5
5
  const DEFAULT_MAX_NUMBER = 1e6;
6
6
  const DEFAULT_SALT_LEN = 12;
7
7
  const DEFAULT_ALG = 'SHA-256';
8
8
  async function createChallenge(options) {
9
9
  const algorithm = options.algorithm || DEFAULT_ALG;
10
- const maxNumber = options.maxNumber || DEFAULT_MAX_NUMBER;
10
+ const max = options.maxNumber || DEFAULT_MAX_NUMBER;
11
11
  const saltLength = options.saltLength || DEFAULT_SALT_LEN;
12
12
  const salt = options.salt || (0, helpers_js_1.ab2hex)((0, helpers_js_1.randomBytes)(saltLength));
13
- const number = options.number === void 0 ? (0, helpers_js_1.randomInt)(maxNumber) : options.number;
13
+ const number = options.number === void 0 ? (0, helpers_js_1.randomInt)(max) : options.number;
14
14
  const challenge = await (0, helpers_js_1.hash)(algorithm, salt + number);
15
15
  return {
16
16
  algorithm,
17
17
  challenge,
18
+ max,
18
19
  salt,
19
20
  signature: await (0, helpers_js_1.hmac)(algorithm, challenge, options.hmacKey),
20
21
  };
@@ -34,3 +35,88 @@ async function verifySolution(payload, hmacKey) {
34
35
  check.signature === payload.signature);
35
36
  }
36
37
  exports.verifySolution = verifySolution;
38
+ function solveChallenge(challenge, salt, algorithm = 'SHA-256', max = 1e6, start = 0) {
39
+ const controller = new AbortController();
40
+ const promise = new Promise((resolve, reject) => {
41
+ const startTime = Date.now();
42
+ const next = (n) => {
43
+ if (controller.signal.aborted || n > max) {
44
+ resolve(null);
45
+ }
46
+ else {
47
+ hashChallenge(salt, n, algorithm)
48
+ .then((t) => {
49
+ if (t === challenge) {
50
+ resolve({
51
+ number: n,
52
+ took: Date.now() - startTime,
53
+ });
54
+ }
55
+ else {
56
+ next(n + 1);
57
+ }
58
+ })
59
+ .catch(reject);
60
+ }
61
+ };
62
+ next(start);
63
+ });
64
+ return {
65
+ promise,
66
+ controller,
67
+ };
68
+ }
69
+ exports.solveChallenge = solveChallenge;
70
+ async function solveChallengeWorkers(workerScript, concurrency, challenge, salt, algorithm = 'SHA-256', max = 1e6, startNumber = 0) {
71
+ const workers = [];
72
+ if (concurrency < 1) {
73
+ throw new Error('Wrong number of workers configured.');
74
+ }
75
+ if (concurrency > 16) {
76
+ throw new Error('Too many workers. Max. 16 allowed workers.');
77
+ }
78
+ for (let i = 0; i < concurrency; i++) {
79
+ if (typeof workerScript === 'function') {
80
+ workers.push(workerScript());
81
+ }
82
+ else {
83
+ workers.push(new Worker(workerScript, {
84
+ type: 'module',
85
+ }));
86
+ }
87
+ }
88
+ const step = Math.ceil(max / concurrency);
89
+ const solutions = await Promise.all(workers.map((worker, i) => {
90
+ const start = startNumber + i * step;
91
+ return new Promise((resolve) => {
92
+ worker.addEventListener('message', (message) => {
93
+ if (message.data) {
94
+ for (const w of workers) {
95
+ if (w !== worker) {
96
+ w.postMessage({ type: 'abort' });
97
+ }
98
+ }
99
+ }
100
+ resolve(message.data);
101
+ });
102
+ worker.postMessage({
103
+ payload: {
104
+ algorithm,
105
+ challenge,
106
+ max: start + step,
107
+ salt,
108
+ start,
109
+ },
110
+ type: 'work',
111
+ });
112
+ });
113
+ }));
114
+ for (const worker of workers) {
115
+ worker.terminate();
116
+ }
117
+ return solutions.find((solution) => !!solution) || null;
118
+ }
119
+ exports.solveChallengeWorkers = solveChallengeWorkers;
120
+ async function hashChallenge(salt, num, algorithm) {
121
+ return (0, helpers_js_1.ab2hex)(await crypto.subtle.digest(algorithm.toUpperCase(), helpers_js_1.encoder.encode(salt + num)));
122
+ }
@@ -2,6 +2,7 @@ export type Algorithm = 'SHA-1' | 'SHA-256' | 'SHA-512';
2
2
  export interface Challenge {
3
3
  algorithm: Algorithm;
4
4
  challenge: string;
5
+ max?: number;
5
6
  salt: string;
6
7
  signature: string;
7
8
  }
@@ -20,3 +21,8 @@ export interface Payload {
20
21
  salt: string;
21
22
  signature: string;
22
23
  }
24
+ export interface Solution {
25
+ number: number;
26
+ took: number;
27
+ worker?: boolean;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_js_1 = require("./index.js");
4
+ let controller = undefined;
5
+ onmessage = async (message) => {
6
+ const { type, payload } = message.data;
7
+ if (type === 'abort') {
8
+ controller?.abort();
9
+ controller = undefined;
10
+ }
11
+ else if (type === 'work') {
12
+ const { alg, challenge, max, salt, start } = payload || {};
13
+ const result = (0, index_js_1.solveChallenge)(challenge, salt, alg, max, start);
14
+ controller = result.controller;
15
+ result.promise.then((solution) => {
16
+ self.postMessage(solution ? { ...solution, worker: true } : solution);
17
+ });
18
+ }
19
+ };
@@ -0,0 +1 @@
1
+ export {};
package/dist/crypto.js ADDED
@@ -0,0 +1,5 @@
1
+ if (!('crypto' in globalThis)) {
2
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
3
+ globalThis.crypto = require('node:crypto').webcrypto;
4
+ }
5
+ export {};
package/dist/helpers.d.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import './crypto.js';
1
2
  import type { Algorithm } from './types.js';
3
+ export declare const encoder: TextEncoder;
2
4
  export declare function ab2hex(ab: ArrayBuffer | Uint8Array): string;
3
5
  export declare function hash(algorithm: Algorithm, str: string): Promise<string>;
4
6
  export declare function hmac(algorithm: Algorithm, str: string, secret: string): Promise<string>;
package/dist/helpers.js CHANGED
@@ -1,8 +1,6 @@
1
- const encoder = new TextEncoder();
2
- if (!('crypto' in globalThis)) {
3
- // eslint-disable-next-line @typescript-eslint/no-var-requires
4
- globalThis.crypto = require('node:crypto').webcrypto;
5
- }
1
+ // @denoify-line-ignore
2
+ import './crypto.js';
3
+ export const encoder = new TextEncoder();
6
4
  export function ab2hex(ab) {
7
5
  return [...new Uint8Array(ab)]
8
6
  .map((x) => x.toString(16).padStart(2, '0'))
package/dist/index.d.ts CHANGED
@@ -1,3 +1,8 @@
1
- import type { Challenge, ChallengeOptions, Payload } from './types.js';
1
+ import type { Challenge, ChallengeOptions, Payload, Solution } from './types.js';
2
2
  export declare function createChallenge(options: ChallengeOptions): Promise<Challenge>;
3
3
  export declare function verifySolution(payload: string | Payload, hmacKey: string): Promise<boolean>;
4
+ export declare function solveChallenge(challenge: string, salt: string, algorithm?: string, max?: number, start?: number): {
5
+ promise: Promise<Solution | null>;
6
+ controller: AbortController;
7
+ };
8
+ export declare function solveChallengeWorkers(workerScript: string | URL | (() => Worker), concurrency: number, challenge: string, salt: string, algorithm?: string, max?: number, startNumber?: number): Promise<Solution | null>;
package/dist/index.js CHANGED
@@ -1,17 +1,18 @@
1
- import { ab2hex, hash, hmac, randomBytes, randomInt } from './helpers.js';
1
+ import { ab2hex, encoder, hash, hmac, randomBytes, randomInt, } from './helpers.js';
2
2
  const DEFAULT_MAX_NUMBER = 1e6;
3
3
  const DEFAULT_SALT_LEN = 12;
4
4
  const DEFAULT_ALG = 'SHA-256';
5
5
  export async function createChallenge(options) {
6
6
  const algorithm = options.algorithm || DEFAULT_ALG;
7
- const maxNumber = options.maxNumber || DEFAULT_MAX_NUMBER;
7
+ const max = options.maxNumber || DEFAULT_MAX_NUMBER;
8
8
  const saltLength = options.saltLength || DEFAULT_SALT_LEN;
9
9
  const salt = options.salt || ab2hex(randomBytes(saltLength));
10
- const number = options.number === void 0 ? randomInt(maxNumber) : options.number;
10
+ const number = options.number === void 0 ? randomInt(max) : options.number;
11
11
  const challenge = await hash(algorithm, salt + number);
12
12
  return {
13
13
  algorithm,
14
14
  challenge,
15
+ max,
15
16
  salt,
16
17
  signature: await hmac(algorithm, challenge, options.hmacKey),
17
18
  };
@@ -29,3 +30,86 @@ export async function verifySolution(payload, hmacKey) {
29
30
  return (check.challenge === payload.challenge &&
30
31
  check.signature === payload.signature);
31
32
  }
33
+ export function solveChallenge(challenge, salt, algorithm = 'SHA-256', max = 1e6, start = 0) {
34
+ const controller = new AbortController();
35
+ const promise = new Promise((resolve, reject) => {
36
+ const startTime = Date.now();
37
+ const next = (n) => {
38
+ if (controller.signal.aborted || n > max) {
39
+ resolve(null);
40
+ }
41
+ else {
42
+ hashChallenge(salt, n, algorithm)
43
+ .then((t) => {
44
+ if (t === challenge) {
45
+ resolve({
46
+ number: n,
47
+ took: Date.now() - startTime,
48
+ });
49
+ }
50
+ else {
51
+ next(n + 1);
52
+ }
53
+ })
54
+ .catch(reject);
55
+ }
56
+ };
57
+ next(start);
58
+ });
59
+ return {
60
+ promise,
61
+ controller,
62
+ };
63
+ }
64
+ export async function solveChallengeWorkers(workerScript, concurrency, challenge, salt, algorithm = 'SHA-256', max = 1e6, startNumber = 0) {
65
+ const workers = [];
66
+ if (concurrency < 1) {
67
+ throw new Error('Wrong number of workers configured.');
68
+ }
69
+ if (concurrency > 16) {
70
+ throw new Error('Too many workers. Max. 16 allowed workers.');
71
+ }
72
+ for (let i = 0; i < concurrency; i++) {
73
+ if (typeof workerScript === 'function') {
74
+ workers.push(workerScript());
75
+ }
76
+ else {
77
+ workers.push(new Worker(workerScript, {
78
+ type: 'module',
79
+ }));
80
+ }
81
+ }
82
+ const step = Math.ceil(max / concurrency);
83
+ const solutions = await Promise.all(workers.map((worker, i) => {
84
+ const start = startNumber + i * step;
85
+ return new Promise((resolve) => {
86
+ worker.addEventListener('message', (message) => {
87
+ if (message.data) {
88
+ for (const w of workers) {
89
+ if (w !== worker) {
90
+ w.postMessage({ type: 'abort' });
91
+ }
92
+ }
93
+ }
94
+ resolve(message.data);
95
+ });
96
+ worker.postMessage({
97
+ payload: {
98
+ algorithm,
99
+ challenge,
100
+ max: start + step,
101
+ salt,
102
+ start,
103
+ },
104
+ type: 'work',
105
+ });
106
+ });
107
+ }));
108
+ for (const worker of workers) {
109
+ worker.terminate();
110
+ }
111
+ return solutions.find((solution) => !!solution) || null;
112
+ }
113
+ async function hashChallenge(salt, num, algorithm) {
114
+ return ab2hex(await crypto.subtle.digest(algorithm.toUpperCase(), encoder.encode(salt + num)));
115
+ }
package/dist/types.d.ts CHANGED
@@ -2,6 +2,7 @@ export type Algorithm = 'SHA-1' | 'SHA-256' | 'SHA-512';
2
2
  export interface Challenge {
3
3
  algorithm: Algorithm;
4
4
  challenge: string;
5
+ max?: number;
5
6
  salt: string;
6
7
  signature: string;
7
8
  }
@@ -20,3 +21,8 @@ export interface Payload {
20
21
  salt: string;
21
22
  signature: string;
22
23
  }
24
+ export interface Solution {
25
+ number: number;
26
+ took: number;
27
+ worker?: boolean;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/worker.js ADDED
@@ -0,0 +1,17 @@
1
+ import { solveChallenge } from './index.js';
2
+ let controller = undefined;
3
+ onmessage = async (message) => {
4
+ const { type, payload } = message.data;
5
+ if (type === 'abort') {
6
+ controller?.abort();
7
+ controller = undefined;
8
+ }
9
+ else if (type === 'work') {
10
+ const { alg, challenge, max, salt, start } = payload || {};
11
+ const result = solveChallenge(challenge, salt, alg, max, start);
12
+ controller = result.controller;
13
+ result.promise.then((solution) => {
14
+ self.postMessage(solution ? { ...solution, worker: true } : solution);
15
+ });
16
+ }
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altcha-lib",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A library for creating and verifying ALTCHA challenges for Node.js, Bun and Deno.",
5
5
  "author": "Daniel Regeci",
6
6
  "license": "MIT",
@@ -19,8 +19,9 @@
19
19
  "denoify": "rimraf deno_dist && denoify && find deno_dist/. -type f -exec sed -i '' -e 's/node:node:/node:/g' {} +",
20
20
  "eslint": "eslint ./lib/**/*",
21
21
  "format": "prettier --write './(lib|tests)/**/*'",
22
- "test": "vitest",
23
- "test:deno": "deno test tests/deno.ts"
22
+ "test": "vitest --run",
23
+ "test:deno": "deno test --allow-read tests/deno.ts",
24
+ "prepare": "husky"
24
25
  },
25
26
  "files": [
26
27
  "cjs",
@@ -36,6 +37,12 @@
36
37
  "./types": {
37
38
  "types": "./dist/types.d.ts",
38
39
  "import": "./dist/types.js"
40
+ },
41
+ "./worker": {
42
+ "types": "./dist/worker.d.ts",
43
+ "import": "./dist/worker.js",
44
+ "require": "./cjs/dist/worker.js",
45
+ "default": "./dist/worker.js"
39
46
  }
40
47
  },
41
48
  "typesVersions": {
@@ -50,6 +57,7 @@
50
57
  "@typescript-eslint/eslint-plugin": "^6.21.0",
51
58
  "denoify": "^1.6.9",
52
59
  "eslint": "^8.56.0",
60
+ "husky": "^9.0.11",
53
61
  "prettier": "^3.2.5",
54
62
  "rimraf": "^5.0.5",
55
63
  "ts-node": "^10.9.1",