altcha-lib 1.4.1 → 2.0.0-beta.1
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/LICENSE.txt +1 -1
- package/README.md +44 -149
- package/bin/cli.mjs +35 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/cjs/v2/algorithms/argon2id.d.ts +2 -0
- package/dist/cjs/v2/algorithms/argon2id.js +26 -0
- package/dist/cjs/v2/algorithms/pbkdf2.d.ts +2 -0
- package/dist/cjs/v2/algorithms/pbkdf2.js +23 -0
- package/dist/cjs/v2/algorithms/scrypt.d.ts +2 -0
- package/dist/cjs/v2/algorithms/scrypt.js +17 -0
- package/dist/cjs/v2/algorithms/sha.d.ts +2 -0
- package/dist/cjs/v2/algorithms/sha.js +35 -0
- package/dist/cjs/v2/algorithms/web/pbkdf2.d.ts +2 -0
- package/dist/cjs/v2/algorithms/web/pbkdf2.js +19 -0
- package/dist/cjs/v2/algorithms/web/sha.d.ts +2 -0
- package/dist/cjs/v2/algorithms/web/sha.js +23 -0
- package/dist/cjs/v2/capped-map.d.ts +7 -0
- package/dist/cjs/v2/capped-map.js +18 -0
- package/dist/cjs/v2/frameworks/express.d.ts +21 -0
- package/dist/cjs/v2/frameworks/express.js +77 -0
- package/dist/cjs/v2/frameworks/fastify.d.ts +26 -0
- package/dist/cjs/v2/frameworks/fastify.js +88 -0
- package/dist/cjs/v2/frameworks/h3.d.ts +37 -0
- package/dist/cjs/v2/frameworks/h3.js +89 -0
- package/dist/cjs/v2/frameworks/hono.d.ts +99 -0
- package/dist/cjs/v2/frameworks/hono.js +86 -0
- package/dist/cjs/v2/frameworks/nestjs.d.ts +79 -0
- package/dist/cjs/v2/frameworks/nestjs.js +198 -0
- package/dist/cjs/v2/frameworks/nextjs.d.ts +21 -0
- package/dist/cjs/v2/frameworks/nextjs.js +112 -0
- package/dist/cjs/v2/frameworks/shared.d.ts +8 -0
- package/dist/cjs/v2/frameworks/shared.js +121 -0
- package/dist/cjs/v2/frameworks/sveltekit.d.ts +29 -0
- package/dist/cjs/v2/frameworks/sveltekit.js +101 -0
- package/dist/cjs/v2/frameworks/types.d.ts +47 -0
- package/dist/cjs/v2/frameworks/types.js +2 -0
- package/dist/cjs/v2/helpers.d.ts +27 -0
- package/dist/cjs/v2/helpers.js +127 -0
- package/dist/cjs/v2/index.d.ts +19 -0
- package/dist/cjs/v2/index.js +28 -0
- package/dist/cjs/v2/obfuscation.d.ts +11 -0
- package/dist/cjs/v2/obfuscation.js +74 -0
- package/dist/cjs/v2/pow.d.ts +60 -0
- package/dist/cjs/v2/pow.js +286 -0
- package/dist/cjs/v2/server-signature.d.ts +12 -0
- package/dist/cjs/v2/server-signature.js +68 -0
- package/dist/cjs/v2/types.d.ts +277 -0
- package/dist/cjs/v2/types.js +18 -0
- package/dist/cjs/v2/workers/argon2id.js +7 -0
- package/dist/cjs/v2/workers/pbkdf2.js +7 -0
- package/dist/cjs/v2/workers/scrypt.d.ts +1 -0
- package/dist/cjs/v2/workers/scrypt.js +7 -0
- package/dist/cjs/v2/workers/sha.d.ts +1 -0
- package/dist/cjs/v2/workers/sha.js +7 -0
- package/dist/cjs/v2/workers/shared.d.ts +4 -0
- package/dist/cjs/v2/workers/shared.js +32 -0
- package/dist/esm/tsconfig.build.tsbuildinfo +1 -0
- package/dist/esm/v1/types.js +1 -0
- package/dist/esm/v1/worker.d.ts +1 -0
- package/dist/esm/v2/algorithms/argon2id.d.ts +2 -0
- package/dist/esm/v2/algorithms/argon2id.js +20 -0
- package/dist/esm/v2/algorithms/pbkdf2.d.ts +2 -0
- package/dist/esm/v2/algorithms/pbkdf2.js +20 -0
- package/dist/esm/v2/algorithms/scrypt.d.ts +2 -0
- package/dist/esm/v2/algorithms/scrypt.js +14 -0
- package/dist/esm/v2/algorithms/sha.d.ts +2 -0
- package/dist/esm/v2/algorithms/sha.js +32 -0
- package/dist/esm/v2/algorithms/web/pbkdf2.d.ts +2 -0
- package/dist/esm/v2/algorithms/web/pbkdf2.js +16 -0
- package/dist/esm/v2/algorithms/web/sha.d.ts +2 -0
- package/dist/esm/v2/algorithms/web/sha.js +20 -0
- package/dist/esm/v2/capped-map.d.ts +7 -0
- package/dist/esm/v2/capped-map.js +15 -0
- package/dist/esm/v2/frameworks/express.d.ts +21 -0
- package/dist/esm/v2/frameworks/express.js +71 -0
- package/dist/esm/v2/frameworks/fastify.d.ts +26 -0
- package/dist/esm/v2/frameworks/fastify.js +82 -0
- package/dist/esm/v2/frameworks/h3.d.ts +37 -0
- package/dist/esm/v2/frameworks/h3.js +83 -0
- package/dist/esm/v2/frameworks/hono.d.ts +99 -0
- package/dist/esm/v2/frameworks/hono.js +80 -0
- package/dist/esm/v2/frameworks/nestjs.d.ts +79 -0
- package/dist/esm/v2/frameworks/nestjs.js +202 -0
- package/dist/esm/v2/frameworks/nextjs.d.ts +21 -0
- package/dist/esm/v2/frameworks/nextjs.js +106 -0
- package/dist/esm/v2/frameworks/shared.d.ts +8 -0
- package/dist/esm/v2/frameworks/shared.js +117 -0
- package/dist/esm/v2/frameworks/sveltekit.d.ts +29 -0
- package/dist/esm/v2/frameworks/sveltekit.js +95 -0
- package/dist/esm/v2/frameworks/types.d.ts +47 -0
- package/dist/esm/v2/frameworks/types.js +1 -0
- package/dist/esm/v2/helpers.d.ts +27 -0
- package/dist/esm/v2/helpers.js +112 -0
- package/dist/esm/v2/index.d.ts +19 -0
- package/dist/esm/v2/index.js +17 -0
- package/dist/esm/v2/obfuscation.d.ts +11 -0
- package/dist/esm/v2/obfuscation.js +70 -0
- package/dist/esm/v2/pow.d.ts +60 -0
- package/dist/esm/v2/pow.js +281 -0
- package/dist/esm/v2/server-signature.d.ts +12 -0
- package/dist/esm/v2/server-signature.js +63 -0
- package/dist/esm/v2/types.d.ts +277 -0
- package/dist/esm/v2/types.js +15 -0
- package/dist/esm/v2/workers/argon2id.d.ts +1 -0
- package/dist/esm/v2/workers/argon2id.js +5 -0
- package/dist/esm/v2/workers/pbkdf2.d.ts +1 -0
- package/dist/esm/v2/workers/pbkdf2.js +5 -0
- package/dist/esm/v2/workers/scrypt.d.ts +1 -0
- package/dist/esm/v2/workers/scrypt.js +5 -0
- package/dist/esm/v2/workers/sha.d.ts +1 -0
- package/dist/esm/v2/workers/sha.js +5 -0
- package/dist/esm/v2/workers/shared.d.ts +4 -0
- package/dist/esm/v2/workers/shared.js +29 -0
- package/package.json +138 -27
- package/cjs/dist/tsconfig.cjs.tsbuildinfo +0 -1
- package/cjs/package.json +0 -3
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- /package/{cjs/dist → dist/cjs/v1}/helpers.d.ts +0 -0
- /package/{cjs/dist → dist/cjs/v1}/helpers.js +0 -0
- /package/{cjs/dist → dist/cjs/v1}/index.d.ts +0 -0
- /package/{cjs/dist → dist/cjs/v1}/index.js +0 -0
- /package/{cjs/dist → dist/cjs/v1}/types.d.ts +0 -0
- /package/{cjs/dist → dist/cjs/v1}/types.js +0 -0
- /package/{cjs/dist → dist/cjs/v1}/worker.d.ts +0 -0
- /package/{cjs/dist → dist/cjs/v1}/worker.js +0 -0
- /package/dist/{types.js → cjs/v2/workers/argon2id.d.ts} +0 -0
- /package/dist/{worker.d.ts → cjs/v2/workers/pbkdf2.d.ts} +0 -0
- /package/dist/{helpers.d.ts → esm/v1/helpers.d.ts} +0 -0
- /package/dist/{helpers.js → esm/v1/helpers.js} +0 -0
- /package/dist/{index.d.ts → esm/v1/index.d.ts} +0 -0
- /package/dist/{index.js → esm/v1/index.js} +0 -0
- /package/dist/{types.d.ts → esm/v1/types.d.ts} +0 -0
- /package/dist/{worker.js → esm/v1/worker.js} +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { deriveKey as derivedKeyPBKDF2 } from './algorithms/pbkdf2.js';
|
|
2
|
+
import { createChallenge, solveChallenge, solveChallengeWorkers, } from './pow.js';
|
|
3
|
+
import { bufferToHex, hexToBuffer } from './helpers.js';
|
|
4
|
+
export async function deobfuscate(obfuscatedData, options = {}) {
|
|
5
|
+
const { concurrency = navigator.hardwareConcurrency, createWorker, deriveKey = derivedKeyPBKDF2, } = options;
|
|
6
|
+
let challenge = null;
|
|
7
|
+
try {
|
|
8
|
+
challenge = JSON.parse(atob(obfuscatedData));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error(`Unable to parse obfuscated data.`);
|
|
12
|
+
}
|
|
13
|
+
if (!challenge ||
|
|
14
|
+
typeof challenge !== 'object' ||
|
|
15
|
+
!('parameters' in challenge) ||
|
|
16
|
+
!('cipher' in challenge)) {
|
|
17
|
+
throw new Error(`Invalid obfuscated data format.`);
|
|
18
|
+
}
|
|
19
|
+
const cipher = challenge.cipher;
|
|
20
|
+
let solution = null;
|
|
21
|
+
if (createWorker) {
|
|
22
|
+
solution = await solveChallengeWorkers({
|
|
23
|
+
challenge,
|
|
24
|
+
concurrency,
|
|
25
|
+
createWorker,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
solution = await solveChallenge({
|
|
30
|
+
challenge,
|
|
31
|
+
deriveKey,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (!solution) {
|
|
35
|
+
throw new Error('Unable to find solution.');
|
|
36
|
+
}
|
|
37
|
+
const key = await crypto.subtle.importKey('raw', hexToBuffer(solution.derivedKey), { name: 'AES-GCM' }, false, ['decrypt']);
|
|
38
|
+
const result = await crypto.subtle.decrypt({
|
|
39
|
+
name: 'AES-GCM',
|
|
40
|
+
iv: hexToBuffer(cipher.iv),
|
|
41
|
+
}, key, hexToBuffer(cipher.data));
|
|
42
|
+
return new TextDecoder().decode(result);
|
|
43
|
+
}
|
|
44
|
+
export async function obfuscate(str, options = {}) {
|
|
45
|
+
const { deriveKey = derivedKeyPBKDF2 } = options;
|
|
46
|
+
const counterMin = options?.counterMin || 20;
|
|
47
|
+
const counterMax = options?.counterMax || 200;
|
|
48
|
+
const { parameters } = await createChallenge({
|
|
49
|
+
algorithm: 'PBKDF2/SHA-256',
|
|
50
|
+
cost: 5000,
|
|
51
|
+
deriveKey,
|
|
52
|
+
counter: Math.floor(Math.random() * (counterMax - counterMin + 1)) + counterMin,
|
|
53
|
+
keyPrefixLength: 32,
|
|
54
|
+
...options,
|
|
55
|
+
});
|
|
56
|
+
const key = await crypto.subtle.importKey('raw', hexToBuffer(parameters.keyPrefix), { name: 'AES-GCM' }, false, ['encrypt']);
|
|
57
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
58
|
+
const data = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, new TextEncoder().encode(str));
|
|
59
|
+
return btoa(JSON.stringify({
|
|
60
|
+
parameters: {
|
|
61
|
+
...parameters,
|
|
62
|
+
// Return only half the derived key
|
|
63
|
+
keyPrefix: parameters.keyPrefix.slice(0, parameters.keyLength || 32),
|
|
64
|
+
},
|
|
65
|
+
cipher: {
|
|
66
|
+
iv: bufferToHex(iv),
|
|
67
|
+
data: bufferToHex(data),
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type CreateChallengeOptions, type Challenge, type ChallengeParameters, type SolveChallengeOptions, type Solution, type VerifySolutionOptions, type VerifySolutionResult, HmacAlgorithm } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Manages a buffer that combines a nonce with a counter value.
|
|
4
|
+
* Used to generate unique passwords for each iteration of the challenge solver.
|
|
5
|
+
*/
|
|
6
|
+
export declare class PasswordBuffer {
|
|
7
|
+
readonly nonce: Uint8Array;
|
|
8
|
+
readonly mode: 'uint32' | 'string';
|
|
9
|
+
readonly COUNTER_BYTES = 4;
|
|
10
|
+
readonly buffer: Uint8Array;
|
|
11
|
+
readonly dataView: DataView;
|
|
12
|
+
readonly encoder: TextEncoder;
|
|
13
|
+
constructor(nonce: Uint8Array, mode?: 'uint32' | 'string');
|
|
14
|
+
/**
|
|
15
|
+
* Appends the counter to the nonce buffer.
|
|
16
|
+
* In 'string' mode, encodes the counter as a UTF-8 string.
|
|
17
|
+
* In 'uint32' mode, writes the counter as a big-endian 32-bit integer.
|
|
18
|
+
*/
|
|
19
|
+
setCounter(n: number): Uint8Array<ArrayBufferLike>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new proof-of-work challenge.
|
|
23
|
+
*
|
|
24
|
+
* Generates random nonce and salt, optionally pre-computes a key prefix
|
|
25
|
+
* from a known counter value, and optionally signs the challenge with HMAC.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createChallenge(options: CreateChallengeOptions): Promise<Challenge>;
|
|
28
|
+
/**
|
|
29
|
+
* Solves a challenge by brute-forcing counter values until the derived key
|
|
30
|
+
* starts with the required prefix. Returns the solution or null on timeout/abort.
|
|
31
|
+
*/
|
|
32
|
+
export declare function solveChallenge(options: SolveChallengeOptions): Promise<Solution | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Solves a challenge using multiple Web Workers in parallel.
|
|
35
|
+
* Each worker tests a different subset of counter values (interleaved by concurrency).
|
|
36
|
+
* Automatically retries with fewer workers on out-of-memory errors.
|
|
37
|
+
*/
|
|
38
|
+
export declare function solveChallengeWorkers(options: Omit<SolveChallengeOptions, 'deriveKey'> & {
|
|
39
|
+
concurrency: number;
|
|
40
|
+
createWorker: (algorithm: string) => Worker | Promise<Worker>;
|
|
41
|
+
onOutOfMemory?: (concurrency: number) => number | void;
|
|
42
|
+
}): Promise<Solution | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Signs challenge parameters with HMAC.
|
|
45
|
+
* Optionally also signs the derived key separately for additional verification.
|
|
46
|
+
*/
|
|
47
|
+
export declare function signChallenge(algorithm: HmacAlgorithm, parameters: ChallengeParameters, derivedKey: Uint8Array | null | undefined, hmacSignatureSecret: string, hmacKeySignatureSecret?: string): Promise<{
|
|
48
|
+
parameters: ChallengeParameters;
|
|
49
|
+
signature: string;
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Verifies a submitted solution against a challenge.
|
|
53
|
+
*
|
|
54
|
+
* Checks (in order):
|
|
55
|
+
* 1. Whether the challenge has expired.
|
|
56
|
+
* 2. Whether the challenge has signature parameter.
|
|
57
|
+
* 3. Whether the challenge signature is valid (tamper check).
|
|
58
|
+
* 4. Whether the derived key matches — either via key signature or by re-deriving.
|
|
59
|
+
*/
|
|
60
|
+
export declare function verifySolution(options: VerifySolutionOptions): Promise<VerifySolutionResult>;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { bufferStartsWith, bufferToHex, canonicalJSON, concatBuffers, constantTimeEqual, delay, hexToBuffer, hmac, sortKeys, timeDuration, } from './helpers.js';
|
|
2
|
+
import { HmacAlgorithm, } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Manages a buffer that combines a nonce with a counter value.
|
|
5
|
+
* Used to generate unique passwords for each iteration of the challenge solver.
|
|
6
|
+
*/
|
|
7
|
+
export class PasswordBuffer {
|
|
8
|
+
nonce;
|
|
9
|
+
mode;
|
|
10
|
+
COUNTER_BYTES = 4;
|
|
11
|
+
buffer;
|
|
12
|
+
dataView;
|
|
13
|
+
encoder = new TextEncoder();
|
|
14
|
+
constructor(nonce, mode = 'uint32') {
|
|
15
|
+
this.nonce = nonce;
|
|
16
|
+
this.mode = mode;
|
|
17
|
+
this.buffer = new Uint8Array(this.nonce.length + this.COUNTER_BYTES);
|
|
18
|
+
this.buffer.set(this.nonce, 0);
|
|
19
|
+
this.dataView = new DataView(this.buffer.buffer);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Appends the counter to the nonce buffer.
|
|
23
|
+
* In 'string' mode, encodes the counter as a UTF-8 string.
|
|
24
|
+
* In 'uint32' mode, writes the counter as a big-endian 32-bit integer.
|
|
25
|
+
*/
|
|
26
|
+
setCounter(n) {
|
|
27
|
+
if (this.mode === 'string') {
|
|
28
|
+
return concatBuffers(this.nonce, this.encoder.encode(n.toString()));
|
|
29
|
+
}
|
|
30
|
+
this.dataView.setUint32(this.nonce.length, n, false);
|
|
31
|
+
return this.buffer;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new proof-of-work challenge.
|
|
36
|
+
*
|
|
37
|
+
* Generates random nonce and salt, optionally pre-computes a key prefix
|
|
38
|
+
* from a known counter value, and optionally signs the challenge with HMAC.
|
|
39
|
+
*/
|
|
40
|
+
export async function createChallenge(options) {
|
|
41
|
+
const { algorithm, counter, counterMode = 'uint32', cost, deriveKey, data, expiresAt, hmacAlgorithm = HmacAlgorithm.SHA_256, hmacKeySignatureSecret, hmacSignatureSecret, keyLength = 32, keyPrefix = '00', keyPrefixLength = keyLength / 2, memoryCost, parallelism, } = options;
|
|
42
|
+
const parameters = {
|
|
43
|
+
algorithm,
|
|
44
|
+
nonce: bufferToHex(crypto.getRandomValues(new Uint8Array(16))),
|
|
45
|
+
salt: bufferToHex(crypto.getRandomValues(new Uint8Array(16))),
|
|
46
|
+
cost,
|
|
47
|
+
keyLength,
|
|
48
|
+
memoryCost,
|
|
49
|
+
parallelism,
|
|
50
|
+
keyPrefix,
|
|
51
|
+
expiresAt: expiresAt instanceof Date
|
|
52
|
+
? Math.floor(expiresAt.getTime() / 1_000)
|
|
53
|
+
: expiresAt,
|
|
54
|
+
data,
|
|
55
|
+
};
|
|
56
|
+
// If a counter is provided, derive the key and extract the prefix the solver must match.
|
|
57
|
+
let deriveKeyResult = null;
|
|
58
|
+
if (counter !== undefined) {
|
|
59
|
+
const nonceBuf = hexToBuffer(parameters.nonce);
|
|
60
|
+
deriveKeyResult = await deriveKey(parameters, hexToBuffer(parameters.salt), new PasswordBuffer(nonceBuf, counterMode).setCounter(counter));
|
|
61
|
+
if (deriveKeyResult.parameters) {
|
|
62
|
+
Object.assign(parameters, deriveKeyResult.parameters);
|
|
63
|
+
}
|
|
64
|
+
parameters.keyPrefix = bufferToHex(deriveKeyResult.derivedKey.slice(0, keyPrefixLength));
|
|
65
|
+
}
|
|
66
|
+
// Return unsigned challenge if no HMAC secret is provided.
|
|
67
|
+
if (!hmacSignatureSecret) {
|
|
68
|
+
return {
|
|
69
|
+
parameters: sortKeys(parameters),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return signChallenge(hmacAlgorithm, parameters, deriveKeyResult?.derivedKey, hmacSignatureSecret, hmacKeySignatureSecret);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Solves a challenge by brute-forcing counter values until the derived key
|
|
76
|
+
* starts with the required prefix. Returns the solution or null on timeout/abort.
|
|
77
|
+
*/
|
|
78
|
+
export async function solveChallenge(options) {
|
|
79
|
+
const { challenge, controller, counterMode = 'uint32', counterStart = 0, counterStep = 1, deriveKey, timeout = 90_000, } = options;
|
|
80
|
+
const { nonce, keyPrefix, salt } = challenge.parameters;
|
|
81
|
+
const nonceBuf = hexToBuffer(nonce);
|
|
82
|
+
const saltBuf = hexToBuffer(salt);
|
|
83
|
+
const keyPrefixBuf = keyPrefix.length % 2 === 0 ? hexToBuffer(keyPrefix) : null;
|
|
84
|
+
const password = new PasswordBuffer(nonceBuf, counterMode);
|
|
85
|
+
const start = performance.now();
|
|
86
|
+
let counter = counterStart;
|
|
87
|
+
let derivedKeyHex = '';
|
|
88
|
+
let lastYield = start;
|
|
89
|
+
while (true) {
|
|
90
|
+
// Check for abort signal or timeout every 10 iterations.
|
|
91
|
+
if (controller?.signal.aborted ||
|
|
92
|
+
(timeout && counter % 10 === 0 && performance.now() - start > timeout)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const { derivedKey } = await deriveKey(challenge.parameters, saltBuf, password.setCounter(counter));
|
|
96
|
+
// Yield to the event loop periodically.
|
|
97
|
+
if (counter % 10 === 0 && performance.now() - lastYield > 200) {
|
|
98
|
+
await delay(0);
|
|
99
|
+
lastYield = performance.now();
|
|
100
|
+
}
|
|
101
|
+
// Check if the derived key matches the required prefix.
|
|
102
|
+
if (keyPrefixBuf
|
|
103
|
+
? bufferStartsWith(derivedKey, keyPrefixBuf)
|
|
104
|
+
: bufferToHex(derivedKey).startsWith(keyPrefix)) {
|
|
105
|
+
derivedKeyHex = bufferToHex(derivedKey);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
counter = counter + counterStep;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
counter,
|
|
112
|
+
derivedKey: derivedKeyHex,
|
|
113
|
+
time: timeDuration(start),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Solves a challenge using multiple Web Workers in parallel.
|
|
118
|
+
* Each worker tests a different subset of counter values (interleaved by concurrency).
|
|
119
|
+
* Automatically retries with fewer workers on out-of-memory errors.
|
|
120
|
+
*/
|
|
121
|
+
export async function solveChallengeWorkers(options) {
|
|
122
|
+
const { challenge, concurrency = navigator.hardwareConcurrency, controller = new AbortController(), createWorker, onOutOfMemory = (c) => (c > 1 ? Math.floor(c / 2) : 0), counterMode, } = options;
|
|
123
|
+
const workersConcurrency = Math.min(16, Math.max(1, concurrency));
|
|
124
|
+
const workersInstances = [];
|
|
125
|
+
const terminate = () => {
|
|
126
|
+
for (const worker of workersInstances) {
|
|
127
|
+
worker.terminate();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
for (let i = 0; i < workersConcurrency; i++) {
|
|
131
|
+
workersInstances.push(await createWorker(challenge.parameters.algorithm));
|
|
132
|
+
}
|
|
133
|
+
let solution = null;
|
|
134
|
+
try {
|
|
135
|
+
// Race all workers — first one to find a solution wins.
|
|
136
|
+
solution = await Promise.race(workersInstances.map((worker, i) => {
|
|
137
|
+
controller.signal.addEventListener('abort', () => {
|
|
138
|
+
worker.postMessage({ type: 'abort' });
|
|
139
|
+
});
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
worker.addEventListener('error', (err) => {
|
|
142
|
+
reject(err);
|
|
143
|
+
});
|
|
144
|
+
worker.addEventListener('message', (message) => {
|
|
145
|
+
if (message.data) {
|
|
146
|
+
// Tell other workers to stop once one finds the answer.
|
|
147
|
+
for (const w of workersInstances) {
|
|
148
|
+
if (w !== worker) {
|
|
149
|
+
w.postMessage({ type: 'abort' });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (message.data.error) {
|
|
153
|
+
return reject(new Error(message.data.error));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
resolve(message.data);
|
|
157
|
+
});
|
|
158
|
+
// Each worker starts at a different offset and steps by concurrency count.
|
|
159
|
+
worker.postMessage({
|
|
160
|
+
challenge,
|
|
161
|
+
counterMode,
|
|
162
|
+
counterStart: i,
|
|
163
|
+
counterStep: workersConcurrency,
|
|
164
|
+
type: 'work',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
// On OOM, retry with fewer workers if the callback allows it.
|
|
171
|
+
const isOOM = err instanceof Error && !!err?.message?.includes('Out of memory');
|
|
172
|
+
if (isOOM) {
|
|
173
|
+
if (onOutOfMemory) {
|
|
174
|
+
terminate();
|
|
175
|
+
const retryConcurrency = onOutOfMemory(workersConcurrency);
|
|
176
|
+
if (retryConcurrency) {
|
|
177
|
+
return solveChallengeWorkers({
|
|
178
|
+
...options,
|
|
179
|
+
challenge,
|
|
180
|
+
controller,
|
|
181
|
+
concurrency: retryConcurrency,
|
|
182
|
+
createWorker,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
terminate();
|
|
191
|
+
}
|
|
192
|
+
if (controller.signal.aborted) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return solution || null;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Signs challenge parameters with HMAC.
|
|
199
|
+
* Optionally also signs the derived key separately for additional verification.
|
|
200
|
+
*/
|
|
201
|
+
export async function signChallenge(algorithm, parameters, derivedKey, hmacSignatureSecret, hmacKeySignatureSecret) {
|
|
202
|
+
if (derivedKey && hmacKeySignatureSecret) {
|
|
203
|
+
parameters.keySignature = bufferToHex(await hmac(algorithm, derivedKey, hmacKeySignatureSecret));
|
|
204
|
+
}
|
|
205
|
+
parameters = sortKeys(parameters);
|
|
206
|
+
return {
|
|
207
|
+
parameters,
|
|
208
|
+
signature: bufferToHex(await hmac(algorithm, JSON.stringify(parameters), hmacSignatureSecret)),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Verifies a submitted solution against a challenge.
|
|
213
|
+
*
|
|
214
|
+
* Checks (in order):
|
|
215
|
+
* 1. Whether the challenge has expired.
|
|
216
|
+
* 2. Whether the challenge has signature parameter.
|
|
217
|
+
* 3. Whether the challenge signature is valid (tamper check).
|
|
218
|
+
* 4. Whether the derived key matches — either via key signature or by re-deriving.
|
|
219
|
+
*/
|
|
220
|
+
export async function verifySolution(options) {
|
|
221
|
+
const { challenge, counterMode, deriveKey, hmacAlgorithm = HmacAlgorithm.SHA_256, hmacKeySignatureSecret, hmacSignatureSecret, solution, } = options;
|
|
222
|
+
const start = performance.now();
|
|
223
|
+
// 1. Check expiration.
|
|
224
|
+
if (challenge.parameters.expiresAt &&
|
|
225
|
+
challenge.parameters.expiresAt < Date.now() / 1_000) {
|
|
226
|
+
return {
|
|
227
|
+
expired: true,
|
|
228
|
+
invalidSignature: null,
|
|
229
|
+
invalidSolution: null,
|
|
230
|
+
time: timeDuration(start),
|
|
231
|
+
verified: false,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// 2. Signature parameter check.
|
|
235
|
+
if (!challenge.signature) {
|
|
236
|
+
return {
|
|
237
|
+
expired: false,
|
|
238
|
+
invalidSignature: true,
|
|
239
|
+
invalidSolution: null,
|
|
240
|
+
time: timeDuration(start),
|
|
241
|
+
verified: false,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// 3. Verify challenge signature to ensure parameters haven't been tampered with.
|
|
245
|
+
const signatureCheck = bufferToHex(await hmac(hmacAlgorithm, canonicalJSON(challenge.parameters), hmacSignatureSecret));
|
|
246
|
+
const signatureVerified = constantTimeEqual(challenge.signature, signatureCheck);
|
|
247
|
+
if (!signatureVerified) {
|
|
248
|
+
return {
|
|
249
|
+
expired: false,
|
|
250
|
+
invalidSignature: true,
|
|
251
|
+
invalidSolution: null,
|
|
252
|
+
time: timeDuration(start),
|
|
253
|
+
verified: false,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// 4a. If a key signature exists, verify the derived key against it (faster path).
|
|
257
|
+
if (challenge.parameters.keySignature && hmacKeySignatureSecret) {
|
|
258
|
+
const derivedKeySignatureCheck = bufferToHex(await hmac(hmacAlgorithm, hexToBuffer(solution.derivedKey), hmacKeySignatureSecret));
|
|
259
|
+
const derivedKeySignatureValid = constantTimeEqual(challenge.parameters.keySignature, derivedKeySignatureCheck);
|
|
260
|
+
return {
|
|
261
|
+
expired: false,
|
|
262
|
+
invalidSignature: false,
|
|
263
|
+
invalidSolution: !derivedKeySignatureValid,
|
|
264
|
+
time: timeDuration(start),
|
|
265
|
+
verified: derivedKeySignatureValid,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// 4b. Otherwise, re-derive the key from the solution's counter and compare.
|
|
269
|
+
const nonceBuf = hexToBuffer(challenge.parameters.nonce);
|
|
270
|
+
const saltBuf = hexToBuffer(challenge.parameters.salt);
|
|
271
|
+
const { derivedKey } = await deriveKey(challenge.parameters, saltBuf, new PasswordBuffer(nonceBuf, counterMode).setCounter(solution.counter));
|
|
272
|
+
const derivedKeyHex = bufferToHex(derivedKey);
|
|
273
|
+
const invalidSolution = !constantTimeEqual(derivedKeyHex, solution.derivedKey);
|
|
274
|
+
return {
|
|
275
|
+
expired: false,
|
|
276
|
+
invalidSignature: false,
|
|
277
|
+
invalidSolution,
|
|
278
|
+
time: timeDuration(start),
|
|
279
|
+
verified: !invalidSolution && signatureVerified,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ServerSignaturePayload, type ServerSignatureVerificationData, type VerifyServerSignatureResult } from './types.js';
|
|
2
|
+
export declare function parseVerificationData(data: string, convertToArray?: string[]): ServerSignatureVerificationData | null;
|
|
3
|
+
export declare function verifyFieldsHash(options: {
|
|
4
|
+
formData: FormData | Record<string, unknown>;
|
|
5
|
+
fields: string[];
|
|
6
|
+
fieldsHash: string;
|
|
7
|
+
algorithm?: string;
|
|
8
|
+
}): Promise<boolean>;
|
|
9
|
+
export declare function verifyServerSignature(options: {
|
|
10
|
+
payload: ServerSignaturePayload;
|
|
11
|
+
hmacSecret: string;
|
|
12
|
+
}): Promise<VerifyServerSignatureResult>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { bufferToHex, constantTimeEqual, hash, hmac, timeDuration, } from './helpers.js';
|
|
2
|
+
export function parseVerificationData(data, convertToArray = ['fields', 'reasons']) {
|
|
3
|
+
const verificationData = {};
|
|
4
|
+
try {
|
|
5
|
+
const params = new URLSearchParams(data);
|
|
6
|
+
for (const [key, value] of params.entries()) {
|
|
7
|
+
if (value === 'true' || value === 'false') {
|
|
8
|
+
// Boolean
|
|
9
|
+
verificationData[key] = value === 'true';
|
|
10
|
+
}
|
|
11
|
+
else if (value !== null && /^\d+?$/.test(value)) {
|
|
12
|
+
// Integer
|
|
13
|
+
verificationData[key] = parseInt(value, 10);
|
|
14
|
+
}
|
|
15
|
+
else if (value !== null && /^\d+\.\d+?$/.test(value)) {
|
|
16
|
+
// Float
|
|
17
|
+
verificationData[key] = parseFloat(value);
|
|
18
|
+
}
|
|
19
|
+
else if (value !== null) {
|
|
20
|
+
// String
|
|
21
|
+
verificationData[key] =
|
|
22
|
+
convertToArray.includes(key) && value.length
|
|
23
|
+
? value.trim().split(',')
|
|
24
|
+
: value.trim();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return verificationData;
|
|
32
|
+
}
|
|
33
|
+
export async function verifyFieldsHash(options) {
|
|
34
|
+
const { algorithm = 'SHA-256', formData, fields, fieldsHash } = options;
|
|
35
|
+
const data = formData instanceof FormData ? Object.fromEntries(formData) : formData;
|
|
36
|
+
const lines = [];
|
|
37
|
+
for (const field of fields) {
|
|
38
|
+
lines.push(String(data[field] || ''));
|
|
39
|
+
}
|
|
40
|
+
return bufferToHex(await hash(algorithm, lines.join('\n'))) === fieldsHash;
|
|
41
|
+
}
|
|
42
|
+
export async function verifyServerSignature(options) {
|
|
43
|
+
const { hmacSecret, payload } = options;
|
|
44
|
+
const start = performance.now();
|
|
45
|
+
const signature = bufferToHex(await hmac(payload.algorithm, await hash(payload.algorithm, payload.verificationData), hmacSecret));
|
|
46
|
+
const verificationData = parseVerificationData(payload.verificationData);
|
|
47
|
+
const expired = !!verificationData &&
|
|
48
|
+
!!verificationData.expire &&
|
|
49
|
+
verificationData.expire < Math.floor(Date.now() / 1000);
|
|
50
|
+
const invalidSignature = !constantTimeEqual(payload.signature, signature);
|
|
51
|
+
const invalidSolution = !verificationData ||
|
|
52
|
+
verificationData.verified !== true ||
|
|
53
|
+
payload.verified !== true;
|
|
54
|
+
const verified = !expired && !invalidSignature && !invalidSolution;
|
|
55
|
+
return {
|
|
56
|
+
expired,
|
|
57
|
+
invalidSignature,
|
|
58
|
+
invalidSolution,
|
|
59
|
+
time: timeDuration(start),
|
|
60
|
+
verificationData,
|
|
61
|
+
verified,
|
|
62
|
+
};
|
|
63
|
+
}
|