This commit is contained in:
Daniel Regeci
2024-05-12 09:28:08 -03:00
parent cbc8624a02
commit e0f866b4a2
15 changed files with 353 additions and 135 deletions

View File

@@ -4,11 +4,12 @@ ALTCHA JS Library is a lightweight, zero-dependency library designed for creatin
## Compatibility
This library utilizes [Web Crypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) and is intended for server-side use.
This library utilizes [Web Crypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto).
- Node.js 16+
- Bun 1+
- Deno 1+
- All modern browsers
## Usage
@@ -37,15 +38,27 @@ Parameters:
- `options: ChallengeOptions`:
- `algorithm?: string`: Algorithm to use (`SHA-1`, `SHA-256`, `SHA-512`, default: `SHA-256`).
- `expires?: Date`: Optional `expires` time (as `Date` set into the future date).
- `hmacKey: string` (required): Signature HMAC key.
- `maxNumber?: number` Optional maximum number for the random number generator (defaults to 1,000,000).
- `maxnumber?: number`: Optional maximum number for the random number generator (defaults to 1,000,000).
- `number?: number`: Optional number to use. If not provided, a random number will be generated.
- `params?: Record<string, string>`: Optional parameters to be added to the salt as URL-encoded query string. Use `extractParams()` to read them.
- `salt?: string`: Optional salt string. If not provided, a random salt will be generated.
- `saltLength?: number` Optional maximum lenght of the random salt (in bytes, defaults to 12).
- `saltLength?: number`: Optional maximum lenght of the random salt (in bytes, defaults to 12).
Returns: `Promise<Challenge>`
### `verifySolution(payload, hmacKey)`
### `extractParams(payload)`
Extracts optional parameters from the challenge or payload.
Parameters:
- `payload: string | Payload | Challenge`
Returns: `Record<string, string>`
### `verifySolution(payload, hmacKey, checkExpires = true)`
Verifies an ALTCHA solution. The payload can be a Base64-encoded JSON payload (as submitted by the widget) or an object.
@@ -53,6 +66,7 @@ Parameters:
- `payload: string | Payload`
- `hmacKey: string`
- `checkExpires: boolean = true`: Whether to perform a check on the optional `expires` parameter. Will return `false` if challenge expired.
Returns: `Promise<boolean>`
@@ -65,7 +79,7 @@ Parameters:
- `challenge: string` (required): The challenge hash.
- `salt: string` (required): The challenge salt.
- `algorithm?: string`: Optional algorithm (default: `SHA-256`).
- `max?: string`: Optional `maxnumber` to iterate to (default: 1e6).
- `maxnumber?: string`: Optional `maxnumber` to iterate to (default: 1e6).
- `start?: string`: Optional starting number (default: 0).
Returns: `{ controller: AbortController, promise: Promise<Solution | null> }`
@@ -92,7 +106,7 @@ Parameters:
- `challenge: string` (required): The challenge hash.
- `salt: string` (required): The challenge salt.
- `algorithm?: string`: Optional algorithm (default: `SHA-256`).
- `max?: string`: Optional `maxnumber` to iterate to (default: 1e6).
- `maxnumber?: string`: Optional `maxnumber` to iterate to (default: 1e6).
- `start?: string`: Optional starting number (default: 0).
Returns: `Promise<Solution | null>`
@@ -114,16 +128,18 @@ const solution = await solveChallengeWorkers(
```
> solveChallenge()
- n = 1,000............................... 317 ops/s ±2.63%
- n = 10,000.............................. 32 ops/s ±1.88%
- n = 100,000............................. 3 ops/s ±0.34%
- n = 500,000............................. 0 ops/s ±0.32%
- n = 1,000............................... 312 ops/s ±2.90%
- n = 10,000.............................. 31 ops/s ±1.50%
- n = 50,000.............................. 6 ops/s ±0.82%
- n = 100,000............................. 3 ops/s ±0.37%
- n = 500,000............................. 0 ops/s ±0.31%
> solveChallengeWorkers() (8 workers)
- n = 1,000............................... 66 ops/s ±3.44%
- n = 10,000.............................. 31 ops/s ±4.28%
- n = 100,000............................. 7 ops/s ±4.40%
- n = 500,000............................. 1 ops/s ±2.49%
- n = 1,000............................... 62 ops/s ±3.99%
- n = 10,000.............................. 31 ops/s ±6.83%
- n = 50,000.............................. 11 ops/s ±4.00%
- n = 100,000............................. 7 ops/s ±2.32%
- n = 500,000............................. 1 ops/s ±1.89%
```
Run with Bun on MacBook Pro M3-Pro. See [/benchmark](/benchmark/) folder for more details.

View File

@@ -67,7 +67,7 @@ await benchmark(`solveChallengeWorkers() (${workers} workers)`, (bench) => {
challenge1.challenge,
challenge1.salt,
challenge1.algorithm,
challenge1.max,
challenge1.maxnumber,
);
})
.add('n = 10,000', async () => {
@@ -77,7 +77,7 @@ await benchmark(`solveChallengeWorkers() (${workers} workers)`, (bench) => {
challenge2.challenge,
challenge2.salt,
challenge2.algorithm,
challenge2.max,
challenge2.maxnumber,
);
})
.add('n = 50,000', async () => {
@@ -87,7 +87,7 @@ await benchmark(`solveChallengeWorkers() (${workers} workers)`, (bench) => {
challenge3.challenge,
challenge3.salt,
challenge3.algorithm,
challenge3.max,
challenge3.maxnumber,
);
})
.add('n = 100,000', async () => {
@@ -97,7 +97,7 @@ await benchmark(`solveChallengeWorkers() (${workers} workers)`, (bench) => {
challenge4.challenge,
challenge4.salt,
challenge4.algorithm,
challenge4.max,
challenge4.maxnumber,
);
})
.add('n = 500,000', async () => {
@@ -107,7 +107,7 @@ await benchmark(`solveChallengeWorkers() (${workers} workers)`, (bench) => {
challenge5.challenge,
challenge5.salt,
challenge5.algorithm,
challenge5.max,
challenge5.maxnumber,
);
});
});

6
cjs/dist/index.d.ts vendored
View File

@@ -1,6 +1,9 @@
import type { Challenge, ChallengeOptions, Payload, ServerSignaturePayload, ServerSignatureVerificationData, Solution } from './types.js';
export declare function createChallenge(options: ChallengeOptions): Promise<Challenge>;
export declare function verifySolution(payload: string | Payload, hmacKey: string): Promise<boolean>;
export declare function extractParams(payload: string | Payload | Challenge): {
[k: string]: string;
};
export declare function verifySolution(payload: string | Payload, hmacKey: string, checkExpires?: boolean): Promise<boolean>;
export declare function verifyServerSignature(payload: string | ServerSignaturePayload, hmacKey: string): Promise<{
verificationData: ServerSignatureVerificationData | null;
verified: boolean | null;
@@ -12,6 +15,7 @@ export declare function solveChallenge(challenge: string, salt: string, algorith
export declare function solveChallengeWorkers(workerScript: string | URL | (() => Worker), concurrency: number, challenge: string, salt: string, algorithm?: string, max?: number, startNumber?: number): Promise<Solution | null>;
declare const _default: {
createChallenge: typeof createChallenge;
extractParams: typeof extractParams;
solveChallenge: typeof solveChallenge;
solveChallengeWorkers: typeof solveChallengeWorkers;
verifyServerSignature: typeof verifyServerSignature;

59
cjs/dist/index.js vendored
View File

@@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.solveChallengeWorkers = exports.solveChallenge = exports.verifyServerSignature = exports.verifySolution = exports.createChallenge = void 0;
exports.solveChallengeWorkers = exports.solveChallenge = exports.verifyServerSignature = exports.verifySolution = exports.extractParams = exports.createChallenge = void 0;
const helpers_js_1 = require("./helpers.js");
const DEFAULT_MAX_NUMBER = 1e6;
const DEFAULT_SALT_LEN = 12;
@@ -9,7 +9,14 @@ async function createChallenge(options) {
const algorithm = options.algorithm || DEFAULT_ALG;
const maxnumber = options.maxnumber || options.maxNumber || DEFAULT_MAX_NUMBER;
const saltLength = options.saltLength || DEFAULT_SALT_LEN;
const salt = options.salt || (0, helpers_js_1.ab2hex)((0, helpers_js_1.randomBytes)(saltLength));
const params = new URLSearchParams(options.params);
if (options.expires) {
params.set('expires', String(Math.floor(options.expires.getTime() / 1000)));
}
let salt = options.salt || (0, helpers_js_1.ab2hex)((0, helpers_js_1.randomBytes)(saltLength));
if (params.size) {
salt = salt + '?' + params.toString();
}
const number = options.number === void 0 ? (0, helpers_js_1.randomInt)(maxnumber) : options.number;
const challenge = await (0, helpers_js_1.hashHex)(algorithm, salt + number);
return {
@@ -21,10 +28,25 @@ async function createChallenge(options) {
};
}
exports.createChallenge = createChallenge;
async function verifySolution(payload, hmacKey) {
function extractParams(payload) {
if (typeof payload === 'string') {
payload = JSON.parse(atob(payload));
}
return Object.fromEntries(new URLSearchParams(payload.salt.split('?')?.[1] || ''));
}
exports.extractParams = extractParams;
async function verifySolution(payload, hmacKey, checkExpires = true) {
if (typeof payload === 'string') {
payload = JSON.parse(atob(payload));
}
const params = extractParams(payload);
const expires = params.expires || params.expire;
if (checkExpires && expires) {
const date = new Date(parseInt(expires, 10) * 1000);
if (!isNaN(date.getTime()) && date.getTime() < Date.now()) {
return false;
}
}
const check = await createChallenge({
algorithm: payload.algorithm,
hmacKey,
@@ -70,32 +92,24 @@ async function verifyServerSignature(payload, hmacKey) {
exports.verifyServerSignature = verifyServerSignature;
function solveChallenge(challenge, salt, algorithm = 'SHA-256', max = 1e6, start = 0) {
const controller = new AbortController();
const promise = new Promise((resolve, reject) => {
const startTime = Date.now();
const next = (n) => {
if (controller.signal.aborted || n > max) {
resolve(null);
const fn = async () => {
for (let n = start; n <= max; n += 1) {
if (controller.signal.aborted) {
return null;
}
else {
(0, helpers_js_1.hashHex)(algorithm, salt + n)
.then((t) => {
const t = await (0, helpers_js_1.hashHex)(algorithm, salt + n);
if (t === challenge) {
resolve({
return {
number: n,
took: Date.now() - startTime,
});
}
else {
next(n + 1);
}
})
.catch(reject);
}
};
next(start);
});
}
}
return null;
};
return {
promise,
promise: fn(),
controller,
};
}
@@ -152,6 +166,7 @@ async function solveChallengeWorkers(workerScript, concurrency, challenge, salt,
exports.solveChallengeWorkers = solveChallengeWorkers;
exports.default = {
createChallenge,
extractParams,
solveChallenge,
solveChallengeWorkers,
verifyServerSignature,

2
cjs/dist/types.d.ts vendored
View File

@@ -8,10 +8,12 @@ export interface Challenge {
}
export interface ChallengeOptions {
algorithm?: Algorithm;
expires?: Date;
hmacKey: string;
maxnumber?: number;
maxNumber?: number;
number?: number;
params?: Record<string, string>;
salt?: string;
saltLength?: number;
}

View File

@@ -4,11 +4,12 @@ ALTCHA JS Library is a lightweight, zero-dependency library designed for creatin
## Compatibility
This library utilizes [Web Crypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) and is intended for server-side use.
This library utilizes [Web Crypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto).
- Node.js 16+
- Bun 1+
- Deno 1+
- All modern browsers
## Usage
@@ -37,15 +38,27 @@ Parameters:
- `options: ChallengeOptions`:
- `algorithm?: string`: Algorithm to use (`SHA-1`, `SHA-256`, `SHA-512`, default: `SHA-256`).
- `expires?: Date`: Optional `expires` time (as `Date` set into the future date).
- `hmacKey: string` (required): Signature HMAC key.
- `maxNumber?: number` Optional maximum number for the random number generator (defaults to 1,000,000).
- `maxnumber?: number`: Optional maximum number for the random number generator (defaults to 1,000,000).
- `number?: number`: Optional number to use. If not provided, a random number will be generated.
- `params?: Record<string, string>`: Optional parameters to be added to the salt as URL-encoded query string. Use `extractParams()` to read them.
- `salt?: string`: Optional salt string. If not provided, a random salt will be generated.
- `saltLength?: number` Optional maximum lenght of the random salt (in bytes, defaults to 12).
- `saltLength?: number`: Optional maximum lenght of the random salt (in bytes, defaults to 12).
Returns: `Promise<Challenge>`
### `verifySolution(payload, hmacKey)`
### `extractParams(payload)`
Extracts optional parameters from the challenge or payload.
Parameters:
- `payload: string | Payload | Challenge`
Returns: `Record<string, string>`
### `verifySolution(payload, hmacKey, checkExpires = true)`
Verifies an ALTCHA solution. The payload can be a Base64-encoded JSON payload (as submitted by the widget) or an object.
@@ -53,6 +66,7 @@ Parameters:
- `payload: string | Payload`
- `hmacKey: string`
- `checkExpires: boolean = true`: Whether to perform a check on the optional `expires` parameter. Will return `false` if challenge expired.
Returns: `Promise<boolean>`
@@ -65,7 +79,7 @@ Parameters:
- `challenge: string` (required): The challenge hash.
- `salt: string` (required): The challenge salt.
- `algorithm?: string`: Optional algorithm (default: `SHA-256`).
- `max?: string`: Optional `maxnumber` to iterate to (default: 1e6).
- `maxnumber?: string`: Optional `maxnumber` to iterate to (default: 1e6).
- `start?: string`: Optional starting number (default: 0).
Returns: `{ controller: AbortController, promise: Promise<Solution | null> }`
@@ -92,7 +106,7 @@ Parameters:
- `challenge: string` (required): The challenge hash.
- `salt: string` (required): The challenge salt.
- `algorithm?: string`: Optional algorithm (default: `SHA-256`).
- `max?: string`: Optional `maxnumber` to iterate to (default: 1e6).
- `maxnumber?: string`: Optional `maxnumber` to iterate to (default: 1e6).
- `start?: string`: Optional starting number (default: 0).
Returns: `Promise<Solution | null>`
@@ -114,16 +128,18 @@ const solution = await solveChallengeWorkers(
```
> solveChallenge()
- n = 1,000............................... 317 ops/s ±2.63%
- n = 10,000.............................. 32 ops/s ±1.88%
- n = 100,000............................. 3 ops/s ±0.34%
- n = 500,000............................. 0 ops/s ±0.32%
- n = 1,000............................... 312 ops/s ±2.90%
- n = 10,000.............................. 31 ops/s ±1.50%
- n = 50,000.............................. 6 ops/s ±0.82%
- n = 100,000............................. 3 ops/s ±0.37%
- n = 500,000............................. 0 ops/s ±0.31%
> solveChallengeWorkers() (8 workers)
- n = 1,000............................... 66 ops/s ±3.44%
- n = 10,000.............................. 31 ops/s ±4.28%
- n = 100,000............................. 7 ops/s ±4.40%
- n = 500,000............................. 1 ops/s ±2.49%
- n = 1,000............................... 62 ops/s ±3.99%
- n = 10,000.............................. 31 ops/s ±6.83%
- n = 50,000.............................. 11 ops/s ±4.00%
- n = 100,000............................. 7 ops/s ±2.32%
- n = 500,000............................. 1 ops/s ±1.89%
```
Run with Bun on MacBook Pro M3-Pro. See [/benchmark](/benchmark/) folder for more details.

View File

@@ -27,7 +27,14 @@ export async function createChallenge(
const maxnumber =
options.maxnumber || options.maxNumber || DEFAULT_MAX_NUMBER;
const saltLength = options.saltLength || DEFAULT_SALT_LEN;
const salt = options.salt || ab2hex(randomBytes(saltLength));
const params = new URLSearchParams(options.params);
if (options.expires) {
params.set('expires', String(Math.floor(options.expires.getTime() / 1000)));
}
let salt = options.salt || ab2hex(randomBytes(saltLength));
if (params.size) {
salt = salt + '?' + params.toString();
}
const number =
options.number === void 0 ? randomInt(maxnumber) : options.number;
const challenge = await hashHex(algorithm, salt + number);
@@ -40,13 +47,29 @@ export async function createChallenge(
};
}
export function extractParams(payload: string | Payload | Challenge) {
if (typeof payload === 'string') {
payload = JSON.parse(atob(payload)) as Payload;
}
return Object.fromEntries(new URLSearchParams(payload.salt.split('?')?.[1] || ''));
}
export async function verifySolution(
payload: string | Payload,
hmacKey: string
hmacKey: string,
checkExpires: boolean = true
) {
if (typeof payload === 'string') {
payload = JSON.parse(atob(payload)) as Payload;
}
const params = extractParams(payload);
const expires = params.expires || params.expire;
if (checkExpires && expires) {
const date = new Date(parseInt(expires, 10) * 1000);
if (!isNaN(date.getTime()) && date.getTime() < Date.now()) {
return false;
}
}
const check = await createChallenge({
algorithm: payload.algorithm,
hmacKey,
@@ -107,30 +130,24 @@ export function solveChallenge(
start: number = 0
): { promise: Promise<Solution | null>; controller: AbortController } {
const controller = new AbortController();
const promise = new Promise((resolve, reject) => {
const startTime = Date.now();
const next = (n: number) => {
if (controller.signal.aborted || n > max) {
resolve(null);
} else {
hashHex(algorithm as Algorithm, salt + n)
.then((t) => {
const fn = async () => {
for (let n = start; n <= max; n += 1) {
if (controller.signal.aborted) {
return null;
}
const t = await hashHex(algorithm as Algorithm, salt + n);
if (t === challenge) {
resolve({
return {
number: n,
took: Date.now() - startTime,
});
} else {
next(n + 1);
}
})
.catch(reject);
}
};
next(start);
}) as Promise<Solution | null>;
}
}
return null;
}
return {
promise,
promise: fn(),
controller,
};
}
@@ -198,6 +215,7 @@ export async function solveChallengeWorkers(
export default {
createChallenge,
extractParams,
solveChallenge,
solveChallengeWorkers,
verifyServerSignature,

View File

@@ -10,10 +10,12 @@ export interface Challenge {
export interface ChallengeOptions {
algorithm?: Algorithm;
expires?: Date;
hmacKey: string;
maxnumber?: number;
maxNumber?: number;
number?: number;
params?: Record<string, string>;
salt?: string;
saltLength?: number;
}

6
dist/index.d.ts vendored
View File

@@ -1,6 +1,9 @@
import type { Challenge, ChallengeOptions, Payload, ServerSignaturePayload, ServerSignatureVerificationData, Solution } from './types.js';
export declare function createChallenge(options: ChallengeOptions): Promise<Challenge>;
export declare function verifySolution(payload: string | Payload, hmacKey: string): Promise<boolean>;
export declare function extractParams(payload: string | Payload | Challenge): {
[k: string]: string;
};
export declare function verifySolution(payload: string | Payload, hmacKey: string, checkExpires?: boolean): Promise<boolean>;
export declare function verifyServerSignature(payload: string | ServerSignaturePayload, hmacKey: string): Promise<{
verificationData: ServerSignatureVerificationData | null;
verified: boolean | null;
@@ -12,6 +15,7 @@ export declare function solveChallenge(challenge: string, salt: string, algorith
export declare function solveChallengeWorkers(workerScript: string | URL | (() => Worker), concurrency: number, challenge: string, salt: string, algorithm?: string, max?: number, startNumber?: number): Promise<Solution | null>;
declare const _default: {
createChallenge: typeof createChallenge;
extractParams: typeof extractParams;
solveChallenge: typeof solveChallenge;
solveChallengeWorkers: typeof solveChallengeWorkers;
verifyServerSignature: typeof verifyServerSignature;

56
dist/index.js vendored
View File

@@ -6,7 +6,14 @@ export async function createChallenge(options) {
const algorithm = options.algorithm || DEFAULT_ALG;
const maxnumber = options.maxnumber || options.maxNumber || DEFAULT_MAX_NUMBER;
const saltLength = options.saltLength || DEFAULT_SALT_LEN;
const salt = options.salt || ab2hex(randomBytes(saltLength));
const params = new URLSearchParams(options.params);
if (options.expires) {
params.set('expires', String(Math.floor(options.expires.getTime() / 1000)));
}
let salt = options.salt || ab2hex(randomBytes(saltLength));
if (params.size) {
salt = salt + '?' + params.toString();
}
const number = options.number === void 0 ? randomInt(maxnumber) : options.number;
const challenge = await hashHex(algorithm, salt + number);
return {
@@ -17,10 +24,24 @@ export async function createChallenge(options) {
signature: await hmacHex(algorithm, challenge, options.hmacKey),
};
}
export async function verifySolution(payload, hmacKey) {
export function extractParams(payload) {
if (typeof payload === 'string') {
payload = JSON.parse(atob(payload));
}
return Object.fromEntries(new URLSearchParams(payload.salt.split('?')?.[1] || ''));
}
export async function verifySolution(payload, hmacKey, checkExpires = true) {
if (typeof payload === 'string') {
payload = JSON.parse(atob(payload));
}
const params = extractParams(payload);
const expires = params.expires || params.expire;
if (checkExpires && expires) {
const date = new Date(parseInt(expires, 10) * 1000);
if (!isNaN(date.getTime()) && date.getTime() < Date.now()) {
return false;
}
}
const check = await createChallenge({
algorithm: payload.algorithm,
hmacKey,
@@ -64,32 +85,24 @@ export async function verifyServerSignature(payload, hmacKey) {
}
export function solveChallenge(challenge, salt, algorithm = 'SHA-256', max = 1e6, start = 0) {
const controller = new AbortController();
const promise = new Promise((resolve, reject) => {
const startTime = Date.now();
const next = (n) => {
if (controller.signal.aborted || n > max) {
resolve(null);
const fn = async () => {
for (let n = start; n <= max; n += 1) {
if (controller.signal.aborted) {
return null;
}
else {
hashHex(algorithm, salt + n)
.then((t) => {
const t = await hashHex(algorithm, salt + n);
if (t === challenge) {
resolve({
return {
number: n,
took: Date.now() - startTime,
});
}
else {
next(n + 1);
}
})
.catch(reject);
}
};
next(start);
});
}
}
return null;
};
return {
promise,
promise: fn(),
controller,
};
}
@@ -144,6 +157,7 @@ export async function solveChallengeWorkers(workerScript, concurrency, challenge
}
export default {
createChallenge,
extractParams,
solveChallenge,
solveChallengeWorkers,
verifyServerSignature,

2
dist/types.d.ts vendored
View File

@@ -8,10 +8,12 @@ export interface Challenge {
}
export interface ChallengeOptions {
algorithm?: Algorithm;
expires?: Date;
hmacKey: string;
maxnumber?: number;
maxNumber?: number;
number?: number;
params?: Record<string, string>;
salt?: string;
saltLength?: number;
}

View File

@@ -27,7 +27,14 @@ export async function createChallenge(
const maxnumber =
options.maxnumber || options.maxNumber || DEFAULT_MAX_NUMBER;
const saltLength = options.saltLength || DEFAULT_SALT_LEN;
const salt = options.salt || ab2hex(randomBytes(saltLength));
const params = new URLSearchParams(options.params);
if (options.expires) {
params.set('expires', String(Math.floor(options.expires.getTime() / 1000)));
}
let salt = options.salt || ab2hex(randomBytes(saltLength));
if (params.size) {
salt = salt + '?' + params.toString();
}
const number =
options.number === void 0 ? randomInt(maxnumber) : options.number;
const challenge = await hashHex(algorithm, salt + number);
@@ -40,13 +47,29 @@ export async function createChallenge(
};
}
export function extractParams(payload: string | Payload | Challenge) {
if (typeof payload === 'string') {
payload = JSON.parse(atob(payload)) as Payload;
}
return Object.fromEntries(new URLSearchParams(payload.salt.split('?')?.[1] || ''));
}
export async function verifySolution(
payload: string | Payload,
hmacKey: string
hmacKey: string,
checkExpires: boolean = true
) {
if (typeof payload === 'string') {
payload = JSON.parse(atob(payload)) as Payload;
}
const params = extractParams(payload);
const expires = params.expires || params.expire;
if (checkExpires && expires) {
const date = new Date(parseInt(expires, 10) * 1000);
if (!isNaN(date.getTime()) && date.getTime() < Date.now()) {
return false;
}
}
const check = await createChallenge({
algorithm: payload.algorithm,
hmacKey,
@@ -107,30 +130,24 @@ export function solveChallenge(
start: number = 0
): { promise: Promise<Solution | null>; controller: AbortController } {
const controller = new AbortController();
const promise = new Promise((resolve, reject) => {
const startTime = Date.now();
const next = (n: number) => {
if (controller.signal.aborted || n > max) {
resolve(null);
} else {
hashHex(algorithm as Algorithm, salt + n)
.then((t) => {
const fn = async () => {
for (let n = start; n <= max; n += 1) {
if (controller.signal.aborted) {
return null;
}
const t = await hashHex(algorithm as Algorithm, salt + n);
if (t === challenge) {
resolve({
return {
number: n,
took: Date.now() - startTime,
});
} else {
next(n + 1);
}
})
.catch(reject);
}
};
next(start);
}) as Promise<Solution | null>;
}
}
return null;
}
return {
promise,
promise: fn(),
controller,
};
}
@@ -198,6 +215,7 @@ export async function solveChallengeWorkers(
export default {
createChallenge,
extractParams,
solveChallenge,
solveChallengeWorkers,
verifyServerSignature,

View File

@@ -10,10 +10,12 @@ export interface Challenge {
export interface ChallengeOptions {
algorithm?: Algorithm;
expires?: Date;
hmacKey: string;
maxnumber?: number;
maxNumber?: number;
number?: number;
params?: Record<string, string>;
salt?: string;
saltLength?: number;
}

View File

@@ -1,6 +1,6 @@
{
"name": "altcha-lib",
"version": "0.2.0",
"version": "0.2.1",
"description": "A library for creating and verifying ALTCHA challenges for Node.js, Bun and Deno.",
"author": "Daniel Regeci",
"license": "MIT",

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
createChallenge,
extractParams,
solveChallenge,
solveChallengeWorkers,
verifySolution,
@@ -60,6 +61,70 @@ describe('challenge', () => {
expect(challenge.challenge.length).toEqual(128);
expect(challenge.signature.length).toEqual(128);
});
it('should return a new challenge with expires param', async () => {
const expires = new Date(Date.now() + 3600000);
const challenge = await createChallenge({
algorithm: 'SHA-256',
expires,
hmacKey,
});
expect(challenge).toEqual({
algorithm: 'SHA-256',
challenge: expect.any(String),
maxnumber: expect.any(Number),
salt: expect.any(String),
signature: expect.any(String),
} satisfies Challenge);
expect(challenge.salt.length).toBeGreaterThan(24);
expect(challenge.salt.includes('?expires=')).toBeTruthy();
expect(challenge.challenge.length).toEqual(64);
expect(challenge.signature.length).toEqual(64);
});
it('should return a new challenge with custom params', async () => {
const challenge = await createChallenge({
algorithm: 'SHA-256',
hmacKey,
params: {
abc: '123',
xyz: '000'
},
});
expect(challenge).toEqual({
algorithm: 'SHA-256',
challenge: expect.any(String),
maxnumber: expect.any(Number),
salt: expect.any(String),
signature: expect.any(String),
} satisfies Challenge);
expect(challenge.salt.length).toBeGreaterThan(24);
expect(challenge.salt.endsWith('?abc=123&xyz=000')).toBeTruthy();
expect(challenge.challenge.length).toEqual(64);
expect(challenge.signature.length).toEqual(64);
});
});
describe('extractParams', () => {
it('should extract custom params from payload', async () => {
const number = 100;
const challenge = await createChallenge({
number,
hmacKey,
params: {
abc: '123',
xyz: '000'
},
});
const params = extractParams({
...challenge,
number,
});
expect(params).toEqual({
abc: '123',
xyz: '000'
});
});
});
describe('verifySolution()', () => {
@@ -82,6 +147,26 @@ describe('challenge', () => {
expect(ok).toEqual(true);
});
it('should return true with expires in the future', async () => {
const number = 100;
const challenge = await createChallenge({
expires: new Date(Date.now() + 3600000),
number,
hmacKey,
});
const ok = await verifySolution(
{
algorithm: challenge.algorithm,
challenge: challenge.challenge,
number,
salt: challenge.salt,
signature: challenge.signature,
},
hmacKey
);
expect(ok).toEqual(true);
});
it('should return false if number is incorrect', async () => {
const challenge = await createChallenge({
number: 100,
@@ -175,6 +260,26 @@ describe('challenge', () => {
);
expect(ok).toEqual(false);
});
it('should return false if the challenge expired', async () => {
const number = 100;
const challenge = await createChallenge({
expires: new Date(Date.now() - 3600000),
number,
hmacKey,
});
const ok = await verifySolution(
{
algorithm: challenge.algorithm,
challenge: challenge.challenge,
number,
salt: challenge.salt,
signature: challenge.signature,
},
hmacKey
);
expect(ok).toEqual(false);
});
});
describe('solveChallenge', () => {