Skip to content

Commit

Permalink
0.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
ovx committed May 12, 2024
1 parent cbc8624 commit e0f866b
Show file tree
Hide file tree
Showing 15 changed files with 353 additions and 135 deletions.
44 changes: 30 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -37,22 +38,35 @@ 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.

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>`

Expand All @@ -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> }`
Expand All @@ -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>`
Expand All @@ -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.
Expand Down
10 changes: 5 additions & 5 deletions benchmark/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -107,7 +107,7 @@ await benchmark(`solveChallengeWorkers() (${workers} workers)`, (bench) => {
challenge5.challenge,
challenge5.salt,
challenge5.algorithm,
challenge5.max,
challenge5.maxnumber,
);
});
});
6 changes: 5 additions & 1 deletion cjs/dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
67 changes: 41 additions & 26 deletions cjs/dist/index.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
};
}
Expand Down Expand Up @@ -152,6 +166,7 @@ async function solveChallengeWorkers(workerScript, concurrency, challenge, salt,
exports.solveChallengeWorkers = solveChallengeWorkers;
exports.default = {
createChallenge,
extractParams,
solveChallenge,
solveChallengeWorkers,
verifyServerSignature,
Expand Down
2 changes: 2 additions & 0 deletions cjs/dist/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
44 changes: 30 additions & 14 deletions deno_dist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -37,22 +38,35 @@ 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.

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>`

Expand All @@ -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> }`
Expand All @@ -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>`
Expand All @@ -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.
Expand Down
Loading

0 comments on commit e0f866b

Please sign in to comment.