altcha-lib 0.1.5 → 0.3.0
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 +41 -14
- package/cjs/dist/helpers.d.ts +4 -2
- package/cjs/dist/helpers.js +13 -5
- package/cjs/dist/index.d.ts +12 -3
- package/cjs/dist/index.js +82 -35
- package/cjs/dist/types.d.ts +21 -1
- package/dist/helpers.d.ts +4 -2
- package/dist/helpers.js +10 -4
- package/dist/index.d.ts +12 -3
- package/dist/index.js +80 -35
- package/dist/types.d.ts +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,11 +4,12 @@ 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)
|
|
7
|
+
This library utilizes [Web Crypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto).
|
|
8
8
|
|
|
9
9
|
- Node.js 16+
|
|
10
10
|
- Bun 1+
|
|
11
11
|
- Deno 1+
|
|
12
|
+
- All modern browsers
|
|
12
13
|
|
|
13
14
|
## Usage
|
|
14
15
|
|
|
@@ -37,15 +38,27 @@ Parameters:
|
|
|
37
38
|
|
|
38
39
|
- `options: ChallengeOptions`:
|
|
39
40
|
- `algorithm?: string`: Algorithm to use (`SHA-1`, `SHA-256`, `SHA-512`, default: `SHA-256`).
|
|
41
|
+
- `expires?: Date`: Optional `expires` time (as `Date` set into the future date).
|
|
40
42
|
- `hmacKey: string` (required): Signature HMAC key.
|
|
41
|
-
- `
|
|
43
|
+
- `maxnumber?: number`: Optional maximum number for the random number generator (defaults to 1,000,000).
|
|
42
44
|
- `number?: number`: Optional number to use. If not provided, a random number will be generated.
|
|
45
|
+
- `params?: Record<string, string>`: Optional parameters to be added to the salt as URL-encoded query string. Use `extractParams()` to read them.
|
|
43
46
|
- `salt?: string`: Optional salt string. If not provided, a random salt will be generated.
|
|
44
|
-
- `saltLength?: number
|
|
47
|
+
- `saltLength?: number`: Optional maximum lenght of the random salt (in bytes, defaults to 12).
|
|
45
48
|
|
|
46
49
|
Returns: `Promise<Challenge>`
|
|
47
50
|
|
|
48
|
-
### `
|
|
51
|
+
### `extractParams(payload)`
|
|
52
|
+
|
|
53
|
+
Extracts optional parameters from the challenge or payload.
|
|
54
|
+
|
|
55
|
+
Parameters:
|
|
56
|
+
|
|
57
|
+
- `payload: string | Payload | Challenge`
|
|
58
|
+
|
|
59
|
+
Returns: `Record<string, string>`
|
|
60
|
+
|
|
61
|
+
### `verifySolution(payload, hmacKey, checkExpires = true)`
|
|
49
62
|
|
|
50
63
|
Verifies an ALTCHA solution. The payload can be a Base64-encoded JSON payload (as submitted by the widget) or an object.
|
|
51
64
|
|
|
@@ -53,6 +66,7 @@ Parameters:
|
|
|
53
66
|
|
|
54
67
|
- `payload: string | Payload`
|
|
55
68
|
- `hmacKey: string`
|
|
69
|
+
- `checkExpires: boolean = true`: Whether to perform a check on the optional `expires` parameter. Will return `false` if challenge expired.
|
|
56
70
|
|
|
57
71
|
Returns: `Promise<boolean>`
|
|
58
72
|
|
|
@@ -65,11 +79,22 @@ Parameters:
|
|
|
65
79
|
- `challenge: string` (required): The challenge hash.
|
|
66
80
|
- `salt: string` (required): The challenge salt.
|
|
67
81
|
- `algorithm?: string`: Optional algorithm (default: `SHA-256`).
|
|
68
|
-
- `
|
|
82
|
+
- `maxnumber?: string`: Optional `maxnumber` to iterate to (default: 1e6).
|
|
69
83
|
- `start?: string`: Optional starting number (default: 0).
|
|
70
84
|
|
|
71
85
|
Returns: `{ controller: AbortController, promise: Promise<Solution | null> }`
|
|
72
86
|
|
|
87
|
+
### `verifyServerSignature(payload, hmacKey)`
|
|
88
|
+
|
|
89
|
+
Verifies the server signature returned by the API. The payload can be a Base64-encoded JSON payload or an object.
|
|
90
|
+
|
|
91
|
+
Parameters:
|
|
92
|
+
|
|
93
|
+
- `payload: string | ServerSignaturePayload`
|
|
94
|
+
- `hmacKey: string`
|
|
95
|
+
|
|
96
|
+
Returns: `Promise<{ verificationData: ServerSignatureVerificationData | null, verified: boolean }>`
|
|
97
|
+
|
|
73
98
|
### `solveChallengeWorkers(workerScript, concurrency, challenge, salt, algorithm?, max?, start?)`
|
|
74
99
|
|
|
75
100
|
Finds a solution to the given challenge with [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker) running concurrently.
|
|
@@ -81,7 +106,7 @@ Parameters:
|
|
|
81
106
|
- `challenge: string` (required): The challenge hash.
|
|
82
107
|
- `salt: string` (required): The challenge salt.
|
|
83
108
|
- `algorithm?: string`: Optional algorithm (default: `SHA-256`).
|
|
84
|
-
- `
|
|
109
|
+
- `maxnumber?: string`: Optional `maxnumber` to iterate to (default: 1e6).
|
|
85
110
|
- `start?: string`: Optional starting number (default: 0).
|
|
86
111
|
|
|
87
112
|
Returns: `Promise<Solution | null>`
|
|
@@ -103,16 +128,18 @@ const solution = await solveChallengeWorkers(
|
|
|
103
128
|
|
|
104
129
|
```
|
|
105
130
|
> solveChallenge()
|
|
106
|
-
- n = 1,000...............................
|
|
107
|
-
- n = 10,000..............................
|
|
108
|
-
- n =
|
|
109
|
-
- n =
|
|
131
|
+
- n = 1,000............................... 312 ops/s ±2.90%
|
|
132
|
+
- n = 10,000.............................. 31 ops/s ±1.50%
|
|
133
|
+
- n = 50,000.............................. 6 ops/s ±0.82%
|
|
134
|
+
- n = 100,000............................. 3 ops/s ±0.37%
|
|
135
|
+
- n = 500,000............................. 0 ops/s ±0.31%
|
|
110
136
|
|
|
111
137
|
> solveChallengeWorkers() (8 workers)
|
|
112
|
-
- n = 1,000...............................
|
|
113
|
-
- n = 10,000.............................. 31 ops/s ±
|
|
114
|
-
- n =
|
|
115
|
-
- n =
|
|
138
|
+
- n = 1,000............................... 62 ops/s ±3.99%
|
|
139
|
+
- n = 10,000.............................. 31 ops/s ±6.83%
|
|
140
|
+
- n = 50,000.............................. 11 ops/s ±4.00%
|
|
141
|
+
- n = 100,000............................. 7 ops/s ±2.32%
|
|
142
|
+
- n = 500,000............................. 1 ops/s ±1.89%
|
|
116
143
|
```
|
|
117
144
|
|
|
118
145
|
Run with Bun on MacBook Pro M3-Pro. See [/benchmark](/benchmark/) folder for more details.
|
package/cjs/dist/helpers.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import './crypto.js';
|
|
|
2
2
|
import type { Algorithm } from './types.js';
|
|
3
3
|
export declare const encoder: TextEncoder;
|
|
4
4
|
export declare function ab2hex(ab: ArrayBuffer | Uint8Array): string;
|
|
5
|
-
export declare function hash(algorithm: Algorithm,
|
|
6
|
-
export declare function
|
|
5
|
+
export declare function hash(algorithm: Algorithm, data: ArrayBuffer | string): Promise<ArrayBuffer>;
|
|
6
|
+
export declare function hashHex(algorithm: Algorithm, data: ArrayBuffer | string): Promise<string>;
|
|
7
|
+
export declare function hmac(algorithm: Algorithm, data: ArrayBuffer | string, secret: string): Promise<ArrayBuffer>;
|
|
8
|
+
export declare function hmacHex(algorithm: Algorithm, data: ArrayBuffer | string, secret: string): Promise<string>;
|
|
7
9
|
export declare function randomBytes(length: number): Uint8Array;
|
|
8
10
|
export declare function randomInt(max: number): number;
|
package/cjs/dist/helpers.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.randomInt = exports.randomBytes = exports.hmac = exports.hash = exports.ab2hex = exports.encoder = void 0;
|
|
3
|
+
exports.randomInt = exports.randomBytes = exports.hmacHex = exports.hmac = exports.hashHex = exports.hash = exports.ab2hex = exports.encoder = void 0;
|
|
4
4
|
// @denoify-line-ignore
|
|
5
5
|
require("./crypto.js");
|
|
6
6
|
exports.encoder = new TextEncoder();
|
|
@@ -10,18 +10,26 @@ function ab2hex(ab) {
|
|
|
10
10
|
.join('');
|
|
11
11
|
}
|
|
12
12
|
exports.ab2hex = ab2hex;
|
|
13
|
-
async function hash(algorithm,
|
|
14
|
-
return
|
|
13
|
+
async function hash(algorithm, data) {
|
|
14
|
+
return crypto.subtle.digest(algorithm.toUpperCase(), typeof data === 'string' ? exports.encoder.encode(data) : new Uint8Array(data));
|
|
15
15
|
}
|
|
16
16
|
exports.hash = hash;
|
|
17
|
-
async function
|
|
17
|
+
async function hashHex(algorithm, data) {
|
|
18
|
+
return ab2hex(await hash(algorithm, data));
|
|
19
|
+
}
|
|
20
|
+
exports.hashHex = hashHex;
|
|
21
|
+
async function hmac(algorithm, data, secret) {
|
|
18
22
|
const key = await crypto.subtle.importKey('raw', exports.encoder.encode(secret), {
|
|
19
23
|
name: 'HMAC',
|
|
20
24
|
hash: algorithm,
|
|
21
25
|
}, false, ['sign', 'verify']);
|
|
22
|
-
return
|
|
26
|
+
return crypto.subtle.sign('HMAC', key, typeof data === 'string' ? exports.encoder.encode(data) : new Uint8Array(data));
|
|
23
27
|
}
|
|
24
28
|
exports.hmac = hmac;
|
|
29
|
+
async function hmacHex(algorithm, data, secret) {
|
|
30
|
+
return ab2hex(await hmac(algorithm, data, secret));
|
|
31
|
+
}
|
|
32
|
+
exports.hmacHex = hmacHex;
|
|
25
33
|
function randomBytes(length) {
|
|
26
34
|
const ab = new Uint8Array(length);
|
|
27
35
|
crypto.getRandomValues(ab);
|
package/cjs/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type { Challenge, ChallengeOptions, Payload, Solution } from './types.js';
|
|
1
|
+
import type { Challenge, ChallengeOptions, Payload, ServerSignaturePayload, ServerSignatureVerificationData, Solution } from './types.js';
|
|
2
2
|
export declare function createChallenge(options: ChallengeOptions): Promise<Challenge>;
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function extractParams(payload: string | Payload | Challenge): {
|
|
4
|
+
[k: string]: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function verifySolution(payload: string | Payload, hmacKey: string, checkExpires?: boolean): Promise<boolean>;
|
|
7
|
+
export declare function verifyServerSignature(payload: string | ServerSignaturePayload, hmacKey: string): Promise<{
|
|
8
|
+
verificationData: ServerSignatureVerificationData | null;
|
|
9
|
+
verified: boolean | null;
|
|
10
|
+
}>;
|
|
4
11
|
export declare function solveChallenge(challenge: string, salt: string, algorithm?: string, max?: number, start?: number): {
|
|
5
12
|
promise: Promise<Solution | null>;
|
|
6
13
|
controller: AbortController;
|
|
@@ -8,8 +15,10 @@ export declare function solveChallenge(challenge: string, salt: string, algorith
|
|
|
8
15
|
export declare function solveChallengeWorkers(workerScript: string | URL | (() => Worker), concurrency: number, challenge: string, salt: string, algorithm?: string, max?: number, startNumber?: number): Promise<Solution | null>;
|
|
9
16
|
declare const _default: {
|
|
10
17
|
createChallenge: typeof createChallenge;
|
|
11
|
-
|
|
18
|
+
extractParams: typeof extractParams;
|
|
12
19
|
solveChallenge: typeof solveChallenge;
|
|
13
20
|
solveChallengeWorkers: typeof solveChallengeWorkers;
|
|
21
|
+
verifyServerSignature: typeof verifyServerSignature;
|
|
22
|
+
verifySolution: typeof verifySolution;
|
|
14
23
|
};
|
|
15
24
|
export default _default;
|
package/cjs/dist/index.js
CHANGED
|
@@ -1,30 +1,53 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.solveChallengeWorkers = exports.solveChallenge = exports.verifySolution = exports.createChallenge = void 0;
|
|
3
|
+
exports.solveChallengeWorkers = exports.solveChallenge = exports.verifyServerSignature = exports.verifySolution = exports.extractParams = 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
|
|
10
|
+
const maxnumber = options.maxnumber || options.maxNumber || DEFAULT_MAX_NUMBER;
|
|
11
11
|
const saltLength = options.saltLength || DEFAULT_SALT_LEN;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const params = new URLSearchParams(options.params);
|
|
13
|
+
if (options.expires) {
|
|
14
|
+
params.set('expires', String(Math.floor(options.expires.getTime() / 1000)));
|
|
15
|
+
}
|
|
16
|
+
let salt = options.salt || (0, helpers_js_1.ab2hex)((0, helpers_js_1.randomBytes)(saltLength));
|
|
17
|
+
// params.size doesn't work with Node 16
|
|
18
|
+
if (Object.keys(Object.fromEntries(params)).length) {
|
|
19
|
+
salt = salt + '?' + params.toString();
|
|
20
|
+
}
|
|
21
|
+
const number = options.number === void 0 ? (0, helpers_js_1.randomInt)(maxnumber) : options.number;
|
|
22
|
+
const challenge = await (0, helpers_js_1.hashHex)(algorithm, salt + number);
|
|
15
23
|
return {
|
|
16
24
|
algorithm,
|
|
17
25
|
challenge,
|
|
18
|
-
|
|
26
|
+
maxnumber,
|
|
19
27
|
salt,
|
|
20
|
-
signature: await (0, helpers_js_1.
|
|
28
|
+
signature: await (0, helpers_js_1.hmacHex)(algorithm, challenge, options.hmacKey),
|
|
21
29
|
};
|
|
22
30
|
}
|
|
23
31
|
exports.createChallenge = createChallenge;
|
|
24
|
-
|
|
32
|
+
function extractParams(payload) {
|
|
25
33
|
if (typeof payload === 'string') {
|
|
26
34
|
payload = JSON.parse(atob(payload));
|
|
27
35
|
}
|
|
36
|
+
return Object.fromEntries(new URLSearchParams(payload.salt.split('?')?.[1] || ''));
|
|
37
|
+
}
|
|
38
|
+
exports.extractParams = extractParams;
|
|
39
|
+
async function verifySolution(payload, hmacKey, checkExpires = true) {
|
|
40
|
+
if (typeof payload === 'string') {
|
|
41
|
+
payload = JSON.parse(atob(payload));
|
|
42
|
+
}
|
|
43
|
+
const params = extractParams(payload);
|
|
44
|
+
const expires = params.expires || params.expire;
|
|
45
|
+
if (checkExpires && expires) {
|
|
46
|
+
const date = new Date(parseInt(expires, 10) * 1000);
|
|
47
|
+
if (!isNaN(date.getTime()) && date.getTime() < Date.now()) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
28
51
|
const check = await createChallenge({
|
|
29
52
|
algorithm: payload.algorithm,
|
|
30
53
|
hmacKey,
|
|
@@ -35,34 +58,59 @@ async function verifySolution(payload, hmacKey) {
|
|
|
35
58
|
check.signature === payload.signature);
|
|
36
59
|
}
|
|
37
60
|
exports.verifySolution = verifySolution;
|
|
61
|
+
async function verifyServerSignature(payload, hmacKey) {
|
|
62
|
+
if (typeof payload === 'string') {
|
|
63
|
+
payload = JSON.parse(atob(payload));
|
|
64
|
+
}
|
|
65
|
+
const signature = await (0, helpers_js_1.hmacHex)(payload.algorithm, await (0, helpers_js_1.hash)(payload.algorithm, payload.verificationData), hmacKey);
|
|
66
|
+
let verificationData = null;
|
|
67
|
+
try {
|
|
68
|
+
const params = new URLSearchParams(payload.verificationData);
|
|
69
|
+
verificationData = {
|
|
70
|
+
...Object.fromEntries(params),
|
|
71
|
+
expire: parseInt(params.get('expire') || '0', 10),
|
|
72
|
+
fields: params.get('fields')?.split(','),
|
|
73
|
+
reasons: params.get('reasons')?.split(','),
|
|
74
|
+
score: params.get('score')
|
|
75
|
+
? parseFloat(params.get('score') || '0')
|
|
76
|
+
: void 0,
|
|
77
|
+
time: parseInt(params.get('time') || '0', 10),
|
|
78
|
+
verified: params.get('verified') === 'true',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// noop
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
verificationData,
|
|
86
|
+
verified: payload.verified === true &&
|
|
87
|
+
verificationData &&
|
|
88
|
+
verificationData.verified === true &&
|
|
89
|
+
verificationData.expire > Math.floor(Date.now() / 1000) &&
|
|
90
|
+
payload.signature === signature,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
exports.verifyServerSignature = verifyServerSignature;
|
|
38
94
|
function solveChallenge(challenge, salt, algorithm = 'SHA-256', max = 1e6, start = 0) {
|
|
39
95
|
const controller = new AbortController();
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (controller.signal.aborted
|
|
44
|
-
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
const fn = async () => {
|
|
98
|
+
for (let n = start; n <= max; n += 1) {
|
|
99
|
+
if (controller.signal.aborted) {
|
|
100
|
+
return null;
|
|
45
101
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
took: Date.now() - startTime,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
next(n + 1);
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
.catch(reject);
|
|
102
|
+
const t = await (0, helpers_js_1.hashHex)(algorithm, salt + n);
|
|
103
|
+
if (t === challenge) {
|
|
104
|
+
return {
|
|
105
|
+
number: n,
|
|
106
|
+
took: Date.now() - startTime,
|
|
107
|
+
};
|
|
60
108
|
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
64
112
|
return {
|
|
65
|
-
promise,
|
|
113
|
+
promise: fn(),
|
|
66
114
|
controller,
|
|
67
115
|
};
|
|
68
116
|
}
|
|
@@ -117,12 +165,11 @@ async function solveChallengeWorkers(workerScript, concurrency, challenge, salt,
|
|
|
117
165
|
return solutions.find((solution) => !!solution) || null;
|
|
118
166
|
}
|
|
119
167
|
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
|
-
}
|
|
123
168
|
exports.default = {
|
|
124
169
|
createChallenge,
|
|
125
|
-
|
|
170
|
+
extractParams,
|
|
126
171
|
solveChallenge,
|
|
127
172
|
solveChallengeWorkers,
|
|
173
|
+
verifyServerSignature,
|
|
174
|
+
verifySolution,
|
|
128
175
|
};
|
package/cjs/dist/types.d.ts
CHANGED
|
@@ -2,15 +2,18 @@ export type Algorithm = 'SHA-1' | 'SHA-256' | 'SHA-512';
|
|
|
2
2
|
export interface Challenge {
|
|
3
3
|
algorithm: Algorithm;
|
|
4
4
|
challenge: string;
|
|
5
|
-
|
|
5
|
+
maxnumber?: number;
|
|
6
6
|
salt: string;
|
|
7
7
|
signature: string;
|
|
8
8
|
}
|
|
9
9
|
export interface ChallengeOptions {
|
|
10
10
|
algorithm?: Algorithm;
|
|
11
|
+
expires?: Date;
|
|
11
12
|
hmacKey: string;
|
|
13
|
+
maxnumber?: number;
|
|
12
14
|
maxNumber?: number;
|
|
13
15
|
number?: number;
|
|
16
|
+
params?: Record<string, string>;
|
|
14
17
|
salt?: string;
|
|
15
18
|
saltLength?: number;
|
|
16
19
|
}
|
|
@@ -21,6 +24,23 @@ export interface Payload {
|
|
|
21
24
|
salt: string;
|
|
22
25
|
signature: string;
|
|
23
26
|
}
|
|
27
|
+
export interface ServerSignaturePayload {
|
|
28
|
+
algorithm: Algorithm;
|
|
29
|
+
signature: string;
|
|
30
|
+
verificationData: string;
|
|
31
|
+
verified: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface ServerSignatureVerificationData {
|
|
34
|
+
classification?: string;
|
|
35
|
+
email?: string;
|
|
36
|
+
expire: number;
|
|
37
|
+
fields?: string[];
|
|
38
|
+
fieldsHash?: string;
|
|
39
|
+
reasons?: string[];
|
|
40
|
+
score?: number;
|
|
41
|
+
time: number;
|
|
42
|
+
verified: boolean;
|
|
43
|
+
}
|
|
24
44
|
export interface Solution {
|
|
25
45
|
number: number;
|
|
26
46
|
took: number;
|
package/dist/helpers.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import './crypto.js';
|
|
|
2
2
|
import type { Algorithm } from './types.js';
|
|
3
3
|
export declare const encoder: TextEncoder;
|
|
4
4
|
export declare function ab2hex(ab: ArrayBuffer | Uint8Array): string;
|
|
5
|
-
export declare function hash(algorithm: Algorithm,
|
|
6
|
-
export declare function
|
|
5
|
+
export declare function hash(algorithm: Algorithm, data: ArrayBuffer | string): Promise<ArrayBuffer>;
|
|
6
|
+
export declare function hashHex(algorithm: Algorithm, data: ArrayBuffer | string): Promise<string>;
|
|
7
|
+
export declare function hmac(algorithm: Algorithm, data: ArrayBuffer | string, secret: string): Promise<ArrayBuffer>;
|
|
8
|
+
export declare function hmacHex(algorithm: Algorithm, data: ArrayBuffer | string, secret: string): Promise<string>;
|
|
7
9
|
export declare function randomBytes(length: number): Uint8Array;
|
|
8
10
|
export declare function randomInt(max: number): number;
|
package/dist/helpers.js
CHANGED
|
@@ -6,15 +6,21 @@ export function ab2hex(ab) {
|
|
|
6
6
|
.map((x) => x.toString(16).padStart(2, '0'))
|
|
7
7
|
.join('');
|
|
8
8
|
}
|
|
9
|
-
export async function hash(algorithm,
|
|
10
|
-
return
|
|
9
|
+
export async function hash(algorithm, data) {
|
|
10
|
+
return crypto.subtle.digest(algorithm.toUpperCase(), typeof data === 'string' ? encoder.encode(data) : new Uint8Array(data));
|
|
11
11
|
}
|
|
12
|
-
export async function
|
|
12
|
+
export async function hashHex(algorithm, data) {
|
|
13
|
+
return ab2hex(await hash(algorithm, data));
|
|
14
|
+
}
|
|
15
|
+
export async function hmac(algorithm, data, secret) {
|
|
13
16
|
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), {
|
|
14
17
|
name: 'HMAC',
|
|
15
18
|
hash: algorithm,
|
|
16
19
|
}, false, ['sign', 'verify']);
|
|
17
|
-
return
|
|
20
|
+
return crypto.subtle.sign('HMAC', key, typeof data === 'string' ? encoder.encode(data) : new Uint8Array(data));
|
|
21
|
+
}
|
|
22
|
+
export async function hmacHex(algorithm, data, secret) {
|
|
23
|
+
return ab2hex(await hmac(algorithm, data, secret));
|
|
18
24
|
}
|
|
19
25
|
export function randomBytes(length) {
|
|
20
26
|
const ab = new Uint8Array(length);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type { Challenge, ChallengeOptions, Payload, Solution } from './types.js';
|
|
1
|
+
import type { Challenge, ChallengeOptions, Payload, ServerSignaturePayload, ServerSignatureVerificationData, Solution } from './types.js';
|
|
2
2
|
export declare function createChallenge(options: ChallengeOptions): Promise<Challenge>;
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function extractParams(payload: string | Payload | Challenge): {
|
|
4
|
+
[k: string]: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function verifySolution(payload: string | Payload, hmacKey: string, checkExpires?: boolean): Promise<boolean>;
|
|
7
|
+
export declare function verifyServerSignature(payload: string | ServerSignaturePayload, hmacKey: string): Promise<{
|
|
8
|
+
verificationData: ServerSignatureVerificationData | null;
|
|
9
|
+
verified: boolean | null;
|
|
10
|
+
}>;
|
|
4
11
|
export declare function solveChallenge(challenge: string, salt: string, algorithm?: string, max?: number, start?: number): {
|
|
5
12
|
promise: Promise<Solution | null>;
|
|
6
13
|
controller: AbortController;
|
|
@@ -8,8 +15,10 @@ export declare function solveChallenge(challenge: string, salt: string, algorith
|
|
|
8
15
|
export declare function solveChallengeWorkers(workerScript: string | URL | (() => Worker), concurrency: number, challenge: string, salt: string, algorithm?: string, max?: number, startNumber?: number): Promise<Solution | null>;
|
|
9
16
|
declare const _default: {
|
|
10
17
|
createChallenge: typeof createChallenge;
|
|
11
|
-
|
|
18
|
+
extractParams: typeof extractParams;
|
|
12
19
|
solveChallenge: typeof solveChallenge;
|
|
13
20
|
solveChallengeWorkers: typeof solveChallengeWorkers;
|
|
21
|
+
verifyServerSignature: typeof verifyServerSignature;
|
|
22
|
+
verifySolution: typeof verifySolution;
|
|
14
23
|
};
|
|
15
24
|
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,48 @@
|
|
|
1
|
-
import { ab2hex,
|
|
1
|
+
import { ab2hex, hash, hashHex, hmacHex, 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
|
|
7
|
+
const maxnumber = options.maxnumber || options.maxNumber || DEFAULT_MAX_NUMBER;
|
|
8
8
|
const saltLength = options.saltLength || DEFAULT_SALT_LEN;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const params = new URLSearchParams(options.params);
|
|
10
|
+
if (options.expires) {
|
|
11
|
+
params.set('expires', String(Math.floor(options.expires.getTime() / 1000)));
|
|
12
|
+
}
|
|
13
|
+
let salt = options.salt || ab2hex(randomBytes(saltLength));
|
|
14
|
+
// params.size doesn't work with Node 16
|
|
15
|
+
if (Object.keys(Object.fromEntries(params)).length) {
|
|
16
|
+
salt = salt + '?' + params.toString();
|
|
17
|
+
}
|
|
18
|
+
const number = options.number === void 0 ? randomInt(maxnumber) : options.number;
|
|
19
|
+
const challenge = await hashHex(algorithm, salt + number);
|
|
12
20
|
return {
|
|
13
21
|
algorithm,
|
|
14
22
|
challenge,
|
|
15
|
-
|
|
23
|
+
maxnumber,
|
|
16
24
|
salt,
|
|
17
|
-
signature: await
|
|
25
|
+
signature: await hmacHex(algorithm, challenge, options.hmacKey),
|
|
18
26
|
};
|
|
19
27
|
}
|
|
20
|
-
export
|
|
28
|
+
export function extractParams(payload) {
|
|
21
29
|
if (typeof payload === 'string') {
|
|
22
30
|
payload = JSON.parse(atob(payload));
|
|
23
31
|
}
|
|
32
|
+
return Object.fromEntries(new URLSearchParams(payload.salt.split('?')?.[1] || ''));
|
|
33
|
+
}
|
|
34
|
+
export async function verifySolution(payload, hmacKey, checkExpires = true) {
|
|
35
|
+
if (typeof payload === 'string') {
|
|
36
|
+
payload = JSON.parse(atob(payload));
|
|
37
|
+
}
|
|
38
|
+
const params = extractParams(payload);
|
|
39
|
+
const expires = params.expires || params.expire;
|
|
40
|
+
if (checkExpires && expires) {
|
|
41
|
+
const date = new Date(parseInt(expires, 10) * 1000);
|
|
42
|
+
if (!isNaN(date.getTime()) && date.getTime() < Date.now()) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
24
46
|
const check = await createChallenge({
|
|
25
47
|
algorithm: payload.algorithm,
|
|
26
48
|
hmacKey,
|
|
@@ -30,34 +52,58 @@ export async function verifySolution(payload, hmacKey) {
|
|
|
30
52
|
return (check.challenge === payload.challenge &&
|
|
31
53
|
check.signature === payload.signature);
|
|
32
54
|
}
|
|
55
|
+
export async function verifyServerSignature(payload, hmacKey) {
|
|
56
|
+
if (typeof payload === 'string') {
|
|
57
|
+
payload = JSON.parse(atob(payload));
|
|
58
|
+
}
|
|
59
|
+
const signature = await hmacHex(payload.algorithm, await hash(payload.algorithm, payload.verificationData), hmacKey);
|
|
60
|
+
let verificationData = null;
|
|
61
|
+
try {
|
|
62
|
+
const params = new URLSearchParams(payload.verificationData);
|
|
63
|
+
verificationData = {
|
|
64
|
+
...Object.fromEntries(params),
|
|
65
|
+
expire: parseInt(params.get('expire') || '0', 10),
|
|
66
|
+
fields: params.get('fields')?.split(','),
|
|
67
|
+
reasons: params.get('reasons')?.split(','),
|
|
68
|
+
score: params.get('score')
|
|
69
|
+
? parseFloat(params.get('score') || '0')
|
|
70
|
+
: void 0,
|
|
71
|
+
time: parseInt(params.get('time') || '0', 10),
|
|
72
|
+
verified: params.get('verified') === 'true',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// noop
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
verificationData,
|
|
80
|
+
verified: payload.verified === true &&
|
|
81
|
+
verificationData &&
|
|
82
|
+
verificationData.verified === true &&
|
|
83
|
+
verificationData.expire > Math.floor(Date.now() / 1000) &&
|
|
84
|
+
payload.signature === signature,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
33
87
|
export function solveChallenge(challenge, salt, algorithm = 'SHA-256', max = 1e6, start = 0) {
|
|
34
88
|
const controller = new AbortController();
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (controller.signal.aborted
|
|
39
|
-
|
|
89
|
+
const startTime = Date.now();
|
|
90
|
+
const fn = async () => {
|
|
91
|
+
for (let n = start; n <= max; n += 1) {
|
|
92
|
+
if (controller.signal.aborted) {
|
|
93
|
+
return null;
|
|
40
94
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
took: Date.now() - startTime,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
next(n + 1);
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
.catch(reject);
|
|
95
|
+
const t = await hashHex(algorithm, salt + n);
|
|
96
|
+
if (t === challenge) {
|
|
97
|
+
return {
|
|
98
|
+
number: n,
|
|
99
|
+
took: Date.now() - startTime,
|
|
100
|
+
};
|
|
55
101
|
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
};
|
|
59
105
|
return {
|
|
60
|
-
promise,
|
|
106
|
+
promise: fn(),
|
|
61
107
|
controller,
|
|
62
108
|
};
|
|
63
109
|
}
|
|
@@ -110,12 +156,11 @@ export async function solveChallengeWorkers(workerScript, concurrency, challenge
|
|
|
110
156
|
}
|
|
111
157
|
return solutions.find((solution) => !!solution) || null;
|
|
112
158
|
}
|
|
113
|
-
async function hashChallenge(salt, num, algorithm) {
|
|
114
|
-
return ab2hex(await crypto.subtle.digest(algorithm.toUpperCase(), encoder.encode(salt + num)));
|
|
115
|
-
}
|
|
116
159
|
export default {
|
|
117
160
|
createChallenge,
|
|
118
|
-
|
|
161
|
+
extractParams,
|
|
119
162
|
solveChallenge,
|
|
120
163
|
solveChallengeWorkers,
|
|
164
|
+
verifyServerSignature,
|
|
165
|
+
verifySolution,
|
|
121
166
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -2,15 +2,18 @@ export type Algorithm = 'SHA-1' | 'SHA-256' | 'SHA-512';
|
|
|
2
2
|
export interface Challenge {
|
|
3
3
|
algorithm: Algorithm;
|
|
4
4
|
challenge: string;
|
|
5
|
-
|
|
5
|
+
maxnumber?: number;
|
|
6
6
|
salt: string;
|
|
7
7
|
signature: string;
|
|
8
8
|
}
|
|
9
9
|
export interface ChallengeOptions {
|
|
10
10
|
algorithm?: Algorithm;
|
|
11
|
+
expires?: Date;
|
|
11
12
|
hmacKey: string;
|
|
13
|
+
maxnumber?: number;
|
|
12
14
|
maxNumber?: number;
|
|
13
15
|
number?: number;
|
|
16
|
+
params?: Record<string, string>;
|
|
14
17
|
salt?: string;
|
|
15
18
|
saltLength?: number;
|
|
16
19
|
}
|
|
@@ -21,6 +24,23 @@ export interface Payload {
|
|
|
21
24
|
salt: string;
|
|
22
25
|
signature: string;
|
|
23
26
|
}
|
|
27
|
+
export interface ServerSignaturePayload {
|
|
28
|
+
algorithm: Algorithm;
|
|
29
|
+
signature: string;
|
|
30
|
+
verificationData: string;
|
|
31
|
+
verified: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface ServerSignatureVerificationData {
|
|
34
|
+
classification?: string;
|
|
35
|
+
email?: string;
|
|
36
|
+
expire: number;
|
|
37
|
+
fields?: string[];
|
|
38
|
+
fieldsHash?: string;
|
|
39
|
+
reasons?: string[];
|
|
40
|
+
score?: number;
|
|
41
|
+
time: number;
|
|
42
|
+
verified: boolean;
|
|
43
|
+
}
|
|
24
44
|
export interface Solution {
|
|
25
45
|
number: number;
|
|
26
46
|
took: number;
|