mirror of
https://github.com/altcha-org/altcha-lib.git
synced 2026-01-24 20:08:44 +00:00
0.2.1
This commit is contained in:
44
README.md
44
README.md
@@ -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.
|
||||
|
||||
@@ -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
6
cjs/dist/index.d.ts
vendored
@@ -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;
|
||||
|
||||
67
cjs/dist/index.js
vendored
67
cjs/dist/index.js
vendored
@@ -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 startTime = Date.now();
|
||||
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) => {
|
||||
if (t === challenge) {
|
||||
resolve({
|
||||
number: n,
|
||||
took: Date.now() - startTime,
|
||||
});
|
||||
}
|
||||
else {
|
||||
next(n + 1);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
const t = await (0, helpers_js_1.hashHex)(algorithm, salt + n);
|
||||
if (t === challenge) {
|
||||
return {
|
||||
number: n,
|
||||
took: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
};
|
||||
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
2
cjs/dist/types.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
if (t === challenge) {
|
||||
resolve({
|
||||
number: n,
|
||||
took: Date.now() - startTime,
|
||||
});
|
||||
} else {
|
||||
next(n + 1);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
const startTime = Date.now();
|
||||
const fn = async () => {
|
||||
for (let n = start; n <= max; n += 1) {
|
||||
if (controller.signal.aborted) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
next(start);
|
||||
}) as Promise<Solution | null>;
|
||||
const t = await hashHex(algorithm as Algorithm, salt + n);
|
||||
if (t === challenge) {
|
||||
return {
|
||||
number: n,
|
||||
took: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
promise,
|
||||
promise: fn(),
|
||||
controller,
|
||||
};
|
||||
}
|
||||
@@ -198,6 +215,7 @@ export async function solveChallengeWorkers(
|
||||
|
||||
export default {
|
||||
createChallenge,
|
||||
extractParams,
|
||||
solveChallenge,
|
||||
solveChallengeWorkers,
|
||||
verifyServerSignature,
|
||||
|
||||
@@ -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
6
dist/index.d.ts
vendored
@@ -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;
|
||||
|
||||
64
dist/index.js
vendored
64
dist/index.js
vendored
@@ -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 startTime = Date.now();
|
||||
const fn = async () => {
|
||||
for (let n = start; n <= max; n += 1) {
|
||||
if (controller.signal.aborted) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
hashHex(algorithm, salt + n)
|
||||
.then((t) => {
|
||||
if (t === challenge) {
|
||||
resolve({
|
||||
number: n,
|
||||
took: Date.now() - startTime,
|
||||
});
|
||||
}
|
||||
else {
|
||||
next(n + 1);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
const t = await hashHex(algorithm, salt + n);
|
||||
if (t === challenge) {
|
||||
return {
|
||||
number: n,
|
||||
took: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
};
|
||||
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
2
dist/types.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
66
lib/index.ts
66
lib/index.ts
@@ -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) => {
|
||||
if (t === challenge) {
|
||||
resolve({
|
||||
number: n,
|
||||
took: Date.now() - startTime,
|
||||
});
|
||||
} else {
|
||||
next(n + 1);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
const startTime = Date.now();
|
||||
const fn = async () => {
|
||||
for (let n = start; n <= max; n += 1) {
|
||||
if (controller.signal.aborted) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
next(start);
|
||||
}) as Promise<Solution | null>;
|
||||
const t = await hashHex(algorithm as Algorithm, salt + n);
|
||||
if (t === challenge) {
|
||||
return {
|
||||
number: n,
|
||||
took: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
promise,
|
||||
promise: fn(),
|
||||
controller,
|
||||
};
|
||||
}
|
||||
@@ -198,6 +215,7 @@ export async function solveChallengeWorkers(
|
||||
|
||||
export default {
|
||||
createChallenge,
|
||||
extractParams,
|
||||
solveChallenge,
|
||||
solveChallengeWorkers,
|
||||
verifyServerSignature,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user