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