altcha-lib 2.0.0 → 2.0.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/README.md +236 -1
- package/bin/cli.mjs +276 -21
- package/dist/cjs/v2/pow.js +4 -2
- package/dist/esm/v2/pow.js +4 -2
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/package.json +2 -2
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +0 -1
- package/dist/esm/tsconfig.build.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
ALTCHA TS/JS Library is a lightweight library for creating and verifying [ALTCHA](https://altcha.org) challenges on the server.
|
|
4
4
|
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install altcha-lib
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createChallenge, deriveHmacKeySecret, randomInt, verifySolution } from 'altcha-lib';
|
|
15
|
+
import { deriveKey } from 'altcha-lib/algorithms/pbkdf2';
|
|
16
|
+
|
|
17
|
+
const HMAC_SECRET = 'your-secret-key';
|
|
18
|
+
const HMAC_KEY_SECRET = 'your-other-secret-key';
|
|
19
|
+
|
|
20
|
+
// On the server: create a challenge and send it to the client
|
|
21
|
+
const challenge = await createChallenge({
|
|
22
|
+
algorithm: 'PBKDF2/SHA-256',
|
|
23
|
+
cost: 5_000,
|
|
24
|
+
counter: randomInt(5_000, 10_000),
|
|
25
|
+
deriveKey,
|
|
26
|
+
hmacSignatureSecret: HMAC_SECRET,
|
|
27
|
+
hmacKeySignatureSecret: HMAC_KEY_SECRET,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// On the server: verify the solution submitted by the client
|
|
31
|
+
const result = await verifySolution({
|
|
32
|
+
challenge: payload.challenge,
|
|
33
|
+
solution: payload.solution,
|
|
34
|
+
deriveKey,
|
|
35
|
+
hmacSignatureSecret: HMAC_SECRET,
|
|
36
|
+
hmacKeySignatureSecret: HMAC_KEY_SECRET,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (result.verified) {
|
|
40
|
+
// challenge passed
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
5
44
|
## Get Started
|
|
6
45
|
|
|
7
46
|
The library includes plugins for several popular frameworks to simplify integration.
|
|
@@ -20,6 +59,7 @@ If your framework is not listed, see the [Advanced Usage](/docs/advanced-usage.m
|
|
|
20
59
|
|
|
21
60
|
- Advanced Usage: [`/docs/advanced-usage.md`](/docs/advanced-usage.md)
|
|
22
61
|
- Algorithms: [`/docs/algorithms.md`](/docs/algorithms.md)
|
|
62
|
+
- CLI Usage: [`/docs/cli.md`](/docs/cli.md)
|
|
23
63
|
- Configuration Options: [`/docs/configuration-options.md`](/docs/configuration-options.md)
|
|
24
64
|
- Obfuscation: [`/docs/obfuscation.md`](/docs/obfuscation.md)
|
|
25
65
|
- Store: [`/docs/store.md`](/docs/store.md)
|
|
@@ -36,7 +76,7 @@ If your framework is not listed, see the [Advanced Usage](/docs/advanced-usage.m
|
|
|
36
76
|
| Deno | 2+ | Argon2 not available natively |
|
|
37
77
|
| WinterCG runtimes | — | Use WebCrypto [algorithms](/docs/algorithms.md) |
|
|
38
78
|
|
|
39
|
-
|
|
79
|
+
## Run Examples
|
|
40
80
|
|
|
41
81
|
**Express example (Node.js with `tsx`):**
|
|
42
82
|
|
|
@@ -70,6 +110,201 @@ The API for the previous PoW version (v1) remains available under the `altcha-li
|
|
|
70
110
|
import { createChallenge } from 'altcha-lib/v1';
|
|
71
111
|
```
|
|
72
112
|
|
|
113
|
+
## API Reference
|
|
114
|
+
|
|
115
|
+
The default import path (`altcha-lib`) uses the v2 API. The v1 API is available at `altcha-lib/v1`.
|
|
116
|
+
|
|
117
|
+
### v2 (`altcha-lib`)
|
|
118
|
+
|
|
119
|
+
#### `createChallenge(options: CreateChallengeOptions): Promise<Challenge>`
|
|
120
|
+
|
|
121
|
+
Creates a new proof-of-work challenge. Generates a random nonce and salt, optionally pre-computes a key prefix from a known counter value, and optionally signs the challenge with HMAC.
|
|
122
|
+
|
|
123
|
+
| Option | Type | Description |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `algorithm` | `string` | Key derivation algorithm (e.g. `'PBKDF2/SHA-256'`, `'ARGON2ID'`, `'SCRYPT'`). |
|
|
126
|
+
| `cost` | `number` | Algorithm-specific cost controlling computational difficulty. |
|
|
127
|
+
| `deriveKey` | `DeriveKeyFunction` | Key derivation function. |
|
|
128
|
+
| `counter` | `number?` | Known counter value for deterministic mode. |
|
|
129
|
+
| `counterMode` | `'uint32' \| 'string'` | Counter encoding format. Defaults to `'uint32'`. |
|
|
130
|
+
| `data` | `Record<string, ...>?` | Arbitrary metadata embedded in the challenge. |
|
|
131
|
+
| `expiresAt` | `number \| Date?` | Expiry timestamp (seconds) or Date. |
|
|
132
|
+
| `hmacAlgorithm` | `HmacAlgorithm?` | HMAC digest algorithm. Defaults to `'SHA-256'`. |
|
|
133
|
+
| `hmacKeySignatureSecret` | `string?` | HMAC secret for signing derived keys (deterministic mode). |
|
|
134
|
+
| `hmacSignatureSecret` | `string?` | HMAC secret for signing the challenge payload. |
|
|
135
|
+
| `keyLength` | `number?` | Derived key length in bytes. Defaults to `32`. |
|
|
136
|
+
| `keyPrefix` | `string?` | Required prefix the derived key must match. |
|
|
137
|
+
| `keyPrefixLength` | `number?` | Number of bytes used as prefix in deterministic mode. Defaults to `keyLength / 2`. |
|
|
138
|
+
| `memoryCost` | `number?` | Memory cost in KiB (Argon2id/scrypt only). |
|
|
139
|
+
| `parallelism` | `number?` | Parallelism setting (Argon2id/scrypt only). |
|
|
140
|
+
|
|
141
|
+
#### `solveChallenge(options: SolveChallengeOptions): Promise<Solution | null>`
|
|
142
|
+
|
|
143
|
+
Solves a challenge by brute-forcing counter values until the derived key starts with the required prefix. Returns `null` on timeout or abort.
|
|
144
|
+
|
|
145
|
+
| Option | Type | Description |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `challenge` | `Challenge` | The challenge to solve. |
|
|
148
|
+
| `deriveKey` | `DeriveKeyFunction` | Key derivation function. |
|
|
149
|
+
| `controller` | `AbortController?` | For cancelling the solve operation. |
|
|
150
|
+
| `counterStart` | `number?` | Initial counter value. Defaults to `0`. |
|
|
151
|
+
| `counterStep` | `number?` | Increment between attempts. Defaults to `1`. |
|
|
152
|
+
| `counterMode` | `'uint32' \| 'string'?` | Counter encoding format. Defaults to `'uint32'`. |
|
|
153
|
+
| `timeout` | `number?` | Timeout in milliseconds. Defaults to `90000`. |
|
|
154
|
+
|
|
155
|
+
#### `solveChallengeWorkers(options): Promise<Solution | null>`
|
|
156
|
+
|
|
157
|
+
Solves a challenge using multiple Web Workers in parallel. Each worker tests a different interleaved subset of counter values. Automatically retries with fewer workers on out-of-memory errors.
|
|
158
|
+
|
|
159
|
+
| Option | Type | Description |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `challenge` | `Challenge` | The challenge to solve. |
|
|
162
|
+
| `concurrency` | `number` | Number of workers to use (max 16). |
|
|
163
|
+
| `createWorker` | `(algorithm: string) => Worker \| Promise<Worker>` | Factory function to create a worker. |
|
|
164
|
+
| `controller` | `AbortController?` | For cancelling the solve operation. |
|
|
165
|
+
| `counterMode` | `'uint32' \| 'string'?` | Counter encoding format. |
|
|
166
|
+
| `onOutOfMemory` | `(concurrency: number) => number \| void?` | Called on OOM; return new concurrency to retry or falsy to abort. |
|
|
167
|
+
| `timeout` | `number?` | Timeout in milliseconds. |
|
|
168
|
+
|
|
169
|
+
#### `verifySolution(options: VerifySolutionOptions): Promise<VerifySolutionResult>`
|
|
170
|
+
|
|
171
|
+
Verifies a submitted solution against a challenge. Checks expiration, challenge signature integrity, and that the derived key matches the solution.
|
|
172
|
+
|
|
173
|
+
| Option | Type | Description |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `challenge` | `Challenge` | The original challenge. |
|
|
176
|
+
| `solution` | `Solution` | The solution to verify. |
|
|
177
|
+
| `hmacSignatureSecret` | `string` | HMAC secret used when the challenge was created. |
|
|
178
|
+
| `deriveKey` | `DeriveKeyFunction` | Key derivation function. |
|
|
179
|
+
| `counterMode` | `'uint32' \| 'string'?` | Counter encoding format. |
|
|
180
|
+
| `hmacAlgorithm` | `HmacAlgorithm?` | HMAC digest algorithm. Defaults to `'SHA-256'`. |
|
|
181
|
+
| `hmacKeySignatureSecret` | `string?` | HMAC secret for verifying derived-key signatures. |
|
|
182
|
+
|
|
183
|
+
Returns `VerifySolutionResult`:
|
|
184
|
+
|
|
185
|
+
| Field | Type | Description |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `verified` | `boolean` | Whether the solution is valid. |
|
|
188
|
+
| `expired` | `boolean` | Whether the challenge has expired. |
|
|
189
|
+
| `invalidSignature` | `boolean \| null` | Whether the challenge signature is invalid. |
|
|
190
|
+
| `invalidSolution` | `boolean \| null` | Whether the solution is incorrect. |
|
|
191
|
+
| `time` | `number` | Time taken to verify in milliseconds. |
|
|
192
|
+
|
|
193
|
+
#### `verifyFieldsHash(options): Promise<boolean>`
|
|
194
|
+
|
|
195
|
+
Verifies the SHA hash of specified form fields.
|
|
196
|
+
|
|
197
|
+
| Option | Type | Description |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| `formData` | `FormData \| Record<string, unknown>` | The form data to verify. |
|
|
200
|
+
| `fields` | `string[]` | Field names to include in the hash. |
|
|
201
|
+
| `fieldsHash` | `string` | The expected hash value. |
|
|
202
|
+
| `algorithm` | `string?` | Hash algorithm. Defaults to `'SHA-256'`. |
|
|
203
|
+
|
|
204
|
+
#### `verifyServerSignature(options): Promise<VerifyServerSignatureResult>`
|
|
205
|
+
|
|
206
|
+
Verifies a server signature payload from ALTCHA Sentinel.
|
|
207
|
+
|
|
208
|
+
| Option | Type | Description |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `payload` | `ServerSignaturePayload` | The payload to verify. |
|
|
211
|
+
| `hmacSecret` | `string` | The HMAC secret. |
|
|
212
|
+
|
|
213
|
+
Returns `VerifyServerSignatureResult` (extends `VerifySolutionResult`):
|
|
214
|
+
|
|
215
|
+
| Field | Type | Description |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| `verified` | `boolean` | Whether the signature is valid. |
|
|
218
|
+
| `verificationData` | `ServerSignatureVerificationData \| null` | Parsed verification data. |
|
|
219
|
+
|
|
220
|
+
#### `obfuscate(str: string, options?): Promise<string>`
|
|
221
|
+
|
|
222
|
+
> Import from `altcha-lib/obfuscation`
|
|
223
|
+
|
|
224
|
+
Encrypts a string using AES-GCM, with the key derived from a PoW challenge. Returns a base64-encoded payload.
|
|
225
|
+
|
|
226
|
+
| Option | Type | Description |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `counterMin` | `number?` | Minimum counter value. Defaults to `20`. |
|
|
229
|
+
| `counterMax` | `number?` | Maximum counter value. Defaults to `200`. |
|
|
230
|
+
| `deriveKey` | `DeriveKeyFunction?` | Key derivation function. Defaults to PBKDF2. |
|
|
231
|
+
| `...` | | Any `CreateChallengeOptions` fields. |
|
|
232
|
+
|
|
233
|
+
#### `deobfuscate(obfuscatedData: string, options?): Promise<string>`
|
|
234
|
+
|
|
235
|
+
> Import from `altcha-lib/obfuscation`
|
|
236
|
+
|
|
237
|
+
Decrypts an obfuscated string by solving the embedded PoW challenge and using the derived key.
|
|
238
|
+
|
|
239
|
+
| Option | Type | Description |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `concurrency` | `number?` | Worker concurrency. Defaults to up to 4. |
|
|
242
|
+
| `createWorker` | `(algorithm: string) => Worker?` | Factory to create a worker for solving. |
|
|
243
|
+
| `deriveKey` | `DeriveKeyFunction?` | Key derivation function. Defaults to PBKDF2. |
|
|
244
|
+
|
|
245
|
+
#### `randomInt(max: number, min?: number): number`
|
|
246
|
+
|
|
247
|
+
Returns a cryptographically random integer between `min` (default `1`) and `max`.
|
|
248
|
+
|
|
249
|
+
#### `class CappedMap<K, V>`
|
|
250
|
+
|
|
251
|
+
A `Map` subclass with a fixed maximum size. When full, the oldest entry is evicted on insertion.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
new CappedMap({ maxSize: number })
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### `enum HmacAlgorithm`
|
|
258
|
+
|
|
259
|
+
| Value | String |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `HmacAlgorithm.SHA_256` | `'SHA-256'` |
|
|
262
|
+
| `HmacAlgorithm.SHA_384` | `'SHA-384'` |
|
|
263
|
+
| `HmacAlgorithm.SHA_512` | `'SHA-512'` |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### v1 (`altcha-lib/v1`)
|
|
268
|
+
|
|
269
|
+
#### `createChallenge(options: ChallengeOptions): Promise<Challenge>`
|
|
270
|
+
|
|
271
|
+
Creates a v1 SHA-based proof-of-work challenge.
|
|
272
|
+
|
|
273
|
+
| Option | Type | Description |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `hmacKey` | `string` | **Required.** HMAC key for signing. |
|
|
276
|
+
| `algorithm` | `'SHA-1' \| 'SHA-256' \| 'SHA-512'?` | Hash algorithm. Defaults to `'SHA-256'`. |
|
|
277
|
+
| `expires` | `Date?` | Expiry date embedded in the salt. |
|
|
278
|
+
| `maxNumber` | `number?` | Maximum random number. Defaults to `1000000`. |
|
|
279
|
+
| `number` | `number?` | Fixed number (skips random selection). |
|
|
280
|
+
| `params` | `Record<string, string>?` | Extra parameters embedded in the salt. |
|
|
281
|
+
| `salt` | `string?` | Custom salt value. |
|
|
282
|
+
| `saltLength` | `number?` | Random salt length in bytes. Defaults to `12`. |
|
|
283
|
+
|
|
284
|
+
#### `verifySolution(payload: string | Payload, hmacKey: string, checkExpires?: boolean): Promise<boolean>`
|
|
285
|
+
|
|
286
|
+
Verifies a v1 solution payload.
|
|
287
|
+
|
|
288
|
+
#### `verifyFieldsHash(formData, fields, fieldsHash, algorithm?): Promise<boolean>`
|
|
289
|
+
|
|
290
|
+
Verifies the hash of specified form fields.
|
|
291
|
+
|
|
292
|
+
#### `verifyServerSignature(payload: string | ServerSignaturePayload, hmacKey: string): Promise<{ verified: boolean, verificationData: ServerSignatureVerificationData | null }>`
|
|
293
|
+
|
|
294
|
+
Verifies a v1 server signature.
|
|
295
|
+
|
|
296
|
+
#### `solveChallenge(challenge, salt, algorithm?, max?, start?): { promise: Promise<Solution | null>, controller: AbortController }`
|
|
297
|
+
|
|
298
|
+
Solves a v1 challenge by brute force.
|
|
299
|
+
|
|
300
|
+
#### `solveChallengeWorkers(workerScript, concurrency, challenge, salt, algorithm?, max?, startNumber?): Promise<Solution | null>`
|
|
301
|
+
|
|
302
|
+
Solves a v1 challenge using Web Workers.
|
|
303
|
+
|
|
304
|
+
#### `extractParams(payload: string | Payload | Challenge): Record<string, string>`
|
|
305
|
+
|
|
306
|
+
Extracts URL parameters embedded in a challenge salt.
|
|
307
|
+
|
|
73
308
|
## License
|
|
74
309
|
|
|
75
310
|
MIT
|
package/bin/cli.mjs
CHANGED
|
@@ -1,35 +1,290 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { Worker as NodeWorker } from 'node:worker_threads';
|
|
3
5
|
import { obfuscate, deobfuscate } from 'altcha-lib/obfuscation';
|
|
6
|
+
import { createChallenge, solveChallengeWorkers, verifySolution } from 'altcha-lib';
|
|
7
|
+
|
|
8
|
+
const ALGORITHMS = [
|
|
9
|
+
'PBKDF2/SHA-256',
|
|
10
|
+
'PBKDF2/SHA-384',
|
|
11
|
+
'PBKDF2/SHA-512',
|
|
12
|
+
'SCRYPT',
|
|
13
|
+
'ARGON2ID',
|
|
14
|
+
'SHA-256',
|
|
15
|
+
'SHA-384',
|
|
16
|
+
'SHA-512',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const USAGE = `Usage: altcha-lib <command> [options]
|
|
20
|
+
|
|
21
|
+
Commands:
|
|
22
|
+
obfuscate [data] Obfuscate a string
|
|
23
|
+
deobfuscate [data] Deobfuscate a string
|
|
24
|
+
create Create a new challenge
|
|
25
|
+
solve [challenge] Solve a challenge (JSON)
|
|
26
|
+
--workers <n> Number of worker threads (default: 1)
|
|
27
|
+
verify [challenge] [solution] Verify a challenge solution (JSON files or inline JSON)
|
|
28
|
+
|
|
29
|
+
Options for create:
|
|
30
|
+
--algorithm <algo> Algorithm (default: PBKDF2/SHA-256)
|
|
31
|
+
Supported: ${ALGORITHMS.join(', ')}
|
|
32
|
+
--cost <n> Cost parameter (default: 5000 for SHA/PBKDF2, 16384 for SCRYPT, 3 for ARGON2ID)
|
|
33
|
+
--hmac-secret <secret> HMAC secret for signing the challenge
|
|
34
|
+
--hmac-key-secret <secret> HMAC secret for signing the derived key (deterministic mode)
|
|
35
|
+
--counter <n> Known counter value for deterministic mode
|
|
36
|
+
--expires <seconds> Seconds until the challenge expires
|
|
37
|
+
--key-prefix <prefix> Key prefix override
|
|
38
|
+
--memory-cost <n> Memory cost (SCRYPT/ARGON2ID only)
|
|
39
|
+
--parallelism <n> Parallelism (SCRYPT/ARGON2ID only)
|
|
40
|
+
|
|
41
|
+
Options for verify:
|
|
42
|
+
--hmac-secret <secret> HMAC secret used when creating the challenge
|
|
43
|
+
--hmac-key-secret <secret> HMAC key secret used when creating the challenge (deterministic mode)
|
|
44
|
+
`;
|
|
4
45
|
|
|
5
46
|
const [command, ...rest] = process.argv.slice(2);
|
|
6
47
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
48
|
+
const COMMANDS = ['obfuscate', 'deobfuscate', 'create', 'solve', 'verify'];
|
|
49
|
+
|
|
50
|
+
if (!command || !COMMANDS.includes(command)) {
|
|
51
|
+
process.stderr.write(USAGE);
|
|
52
|
+
process.exit(1);
|
|
10
53
|
}
|
|
11
54
|
|
|
12
55
|
function readStdin() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
let buf = '';
|
|
58
|
+
process.stdin.setEncoding('utf-8');
|
|
59
|
+
process.stdin.on('data', (chunk) => (buf += chunk));
|
|
60
|
+
process.stdin.on('end', () => resolve(buf.trim()));
|
|
61
|
+
process.stdin.on('error', reject);
|
|
62
|
+
});
|
|
20
63
|
}
|
|
21
64
|
|
|
22
|
-
|
|
23
|
-
|
|
65
|
+
async function readInput(arg, label) {
|
|
66
|
+
let raw;
|
|
67
|
+
if (arg) {
|
|
68
|
+
try {
|
|
69
|
+
raw = (await readFile(arg, 'utf-8')).trim();
|
|
70
|
+
} catch {
|
|
71
|
+
// Not a readable file — treat as inline JSON string
|
|
72
|
+
raw = arg;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
raw = await readStdin();
|
|
76
|
+
}
|
|
77
|
+
if (!raw) {
|
|
78
|
+
console.error(`Error: No ${label} provided`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
return raw;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArgs(args) {
|
|
85
|
+
const opts = {};
|
|
86
|
+
const positional = [];
|
|
87
|
+
for (let i = 0; i < args.length; i++) {
|
|
88
|
+
if (args[i].startsWith('--')) {
|
|
89
|
+
const key = args[i].slice(2);
|
|
90
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
91
|
+
opts[key] = args[++i];
|
|
92
|
+
} else {
|
|
93
|
+
opts[key] = true;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
positional.push(args[i]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { opts, positional };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getWorkerName(algorithm) {
|
|
103
|
+
const algo = (algorithm || '').toUpperCase();
|
|
104
|
+
if (algo.startsWith('PBKDF2')) return 'pbkdf2';
|
|
105
|
+
if (algo === 'SCRYPT') return 'scrypt';
|
|
106
|
+
if (algo === 'ARGON2ID') return 'argon2id';
|
|
107
|
+
return 'sha';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createWorker(algorithm) {
|
|
111
|
+
const workerModuleUrl = new URL(
|
|
112
|
+
`../dist/esm/v2/workers/${getWorkerName(algorithm)}.js`,
|
|
113
|
+
import.meta.url
|
|
114
|
+
).href;
|
|
115
|
+
// Shim Web Worker globals (self.onmessage / self.postMessage) so the
|
|
116
|
+
// existing dist worker modules work inside a node:worker_threads context.
|
|
117
|
+
const shim = `
|
|
118
|
+
import { parentPort } from 'node:worker_threads';
|
|
119
|
+
const self = { onmessage: null, postMessage(d) { parentPort.postMessage(d); } };
|
|
120
|
+
globalThis.self = self;
|
|
121
|
+
parentPort.on('message', (data) => { if (typeof self.onmessage === 'function') self.onmessage({ data }); });
|
|
122
|
+
await import(${JSON.stringify(workerModuleUrl)});
|
|
123
|
+
`;
|
|
124
|
+
const worker = new NodeWorker(
|
|
125
|
+
new URL(`data:text/javascript,${encodeURIComponent(shim)}`),
|
|
126
|
+
{ type: 'module' }
|
|
127
|
+
);
|
|
128
|
+
// Adapt EventEmitter API to the EventTarget API expected by solveChallengeWorkers.
|
|
129
|
+
worker.addEventListener = (event, handler) => {
|
|
130
|
+
worker.on(event === 'message' ? 'message' : event, (data) =>
|
|
131
|
+
event === 'message' ? handler({ data }) : handler(data)
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
return worker;
|
|
135
|
+
}
|
|
24
136
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
137
|
+
async function getDeriveKey(algorithm) {
|
|
138
|
+
const algo = (algorithm || 'PBKDF2/SHA-256').toUpperCase();
|
|
139
|
+
if (algo.startsWith('PBKDF2')) {
|
|
140
|
+
const mod = await import('altcha-lib/algorithms/pbkdf2');
|
|
141
|
+
return mod.deriveKey;
|
|
142
|
+
} else if (algo === 'SCRYPT') {
|
|
143
|
+
const mod = await import('altcha-lib/algorithms/scrypt');
|
|
144
|
+
return mod.deriveKey;
|
|
145
|
+
} else if (algo === 'ARGON2ID') {
|
|
146
|
+
const mod = await import('altcha-lib/algorithms/argon2id');
|
|
147
|
+
return mod.deriveKey;
|
|
148
|
+
} else {
|
|
149
|
+
const mod = await import('altcha-lib/algorithms/sha');
|
|
150
|
+
return mod.deriveKey;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getDefaultCost(algorithm) {
|
|
155
|
+
const algo = (algorithm || 'SHA-256').toUpperCase();
|
|
156
|
+
if (algo === 'SCRYPT') return 16384;
|
|
157
|
+
if (algo === 'ARGON2ID') return 3;
|
|
158
|
+
return 5000;
|
|
159
|
+
}
|
|
29
160
|
|
|
30
|
-
|
|
31
|
-
|
|
161
|
+
function getDefaultMemoryCost(algorithm) {
|
|
162
|
+
const algo = (algorithm || '').toUpperCase();
|
|
163
|
+
if (algo === 'SCRYPT') return 8;
|
|
164
|
+
if (algo === 'ARGON2ID') return 65536;
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
if (command === 'obfuscate' || command === 'deobfuscate') {
|
|
170
|
+
const data = rest.length ? rest.join(' ') : await readStdin();
|
|
171
|
+
if (!data) {
|
|
172
|
+
console.error(`Error: No data provided for ${command}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
const result =
|
|
176
|
+
command === 'obfuscate' ? await obfuscate(data) : await deobfuscate(data);
|
|
177
|
+
console.log(result);
|
|
178
|
+
} else if (command === 'create') {
|
|
179
|
+
const { opts } = parseArgs(rest);
|
|
180
|
+
const algorithm = opts['algorithm'] || 'SHA-256';
|
|
181
|
+
const cost = opts['cost']
|
|
182
|
+
? parseInt(opts['cost'], 10)
|
|
183
|
+
: getDefaultCost(algorithm);
|
|
184
|
+
const hmacSecret = opts['hmac-secret'];
|
|
185
|
+
const hmacKeySecret = opts['hmac-key-secret'];
|
|
186
|
+
const counter = opts['counter'] !== undefined ? parseInt(opts['counter'], 10) : undefined;
|
|
187
|
+
const expires = opts['expires'] ? parseInt(opts['expires'], 10) : undefined;
|
|
188
|
+
const keyPrefix = opts['key-prefix'];
|
|
189
|
+
const memoryCost = opts['memory-cost']
|
|
190
|
+
? parseInt(opts['memory-cost'], 10)
|
|
191
|
+
: getDefaultMemoryCost(algorithm);
|
|
192
|
+
const parallelism = opts['parallelism']
|
|
193
|
+
? parseInt(opts['parallelism'], 10)
|
|
194
|
+
: undefined;
|
|
195
|
+
|
|
196
|
+
const deriveKey = await getDeriveKey(algorithm);
|
|
197
|
+
const challenge = await createChallenge({
|
|
198
|
+
algorithm,
|
|
199
|
+
cost,
|
|
200
|
+
counter,
|
|
201
|
+
deriveKey,
|
|
202
|
+
expiresAt: expires ? new Date(Date.now() + expires * 1000) : undefined,
|
|
203
|
+
hmacSignatureSecret: hmacSecret,
|
|
204
|
+
hmacKeySignatureSecret: hmacKeySecret,
|
|
205
|
+
keyPrefix,
|
|
206
|
+
memoryCost,
|
|
207
|
+
parallelism,
|
|
208
|
+
});
|
|
209
|
+
console.log(JSON.stringify(challenge, null, 2));
|
|
210
|
+
} else if (command === 'solve') {
|
|
211
|
+
const { opts, positional } = parseArgs(rest);
|
|
212
|
+
const workers = opts['workers'] ? parseInt(opts['workers'], 10) : undefined;
|
|
213
|
+
const raw = await readInput(positional.length ? positional[0] : null, 'challenge JSON');
|
|
214
|
+
let challenge;
|
|
215
|
+
try {
|
|
216
|
+
challenge = JSON.parse(raw);
|
|
217
|
+
} catch {
|
|
218
|
+
console.error('Error: Invalid JSON for challenge');
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
const algorithm = challenge?.parameters?.algorithm;
|
|
222
|
+
const solution = await solveChallengeWorkers({
|
|
223
|
+
challenge,
|
|
224
|
+
concurrency: workers ?? 1,
|
|
225
|
+
createWorker: () => createWorker(algorithm),
|
|
226
|
+
});
|
|
227
|
+
if (!solution) {
|
|
228
|
+
console.error('Error: Failed to solve challenge (timeout or aborted)');
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
console.log(JSON.stringify(solution, null, 2));
|
|
232
|
+
} else if (command === 'verify') {
|
|
233
|
+
const { opts, positional } = parseArgs(rest);
|
|
234
|
+
const hmacSecret = opts['hmac-secret'];
|
|
235
|
+
const hmacKeySecret = opts['hmac-key-secret'];
|
|
236
|
+
if (!hmacSecret) {
|
|
237
|
+
console.error('Error: --hmac-secret is required for verify');
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
let challenge, solution;
|
|
241
|
+
if (positional.length >= 2) {
|
|
242
|
+
const rawChallenge = await readInput(positional[0], 'challenge JSON');
|
|
243
|
+
const rawSolution = await readInput(positional[1], 'solution JSON');
|
|
244
|
+
try {
|
|
245
|
+
challenge = JSON.parse(rawChallenge);
|
|
246
|
+
} catch {
|
|
247
|
+
console.error('Error: Invalid JSON for challenge');
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
solution = JSON.parse(rawSolution);
|
|
252
|
+
} catch {
|
|
253
|
+
console.error('Error: Invalid JSON for solution');
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
const raw = await readInput(positional.length ? positional[0] : null, 'payload JSON');
|
|
258
|
+
let payload;
|
|
259
|
+
try {
|
|
260
|
+
payload = JSON.parse(raw);
|
|
261
|
+
} catch {
|
|
262
|
+
console.error('Error: Invalid JSON for payload');
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
({ challenge, solution } = payload);
|
|
266
|
+
if (!challenge || !solution) {
|
|
267
|
+
console.error(
|
|
268
|
+
'Error: Payload must contain "challenge" and "solution" fields'
|
|
269
|
+
);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const algorithm = challenge?.parameters?.algorithm;
|
|
274
|
+
const deriveKey = await getDeriveKey(algorithm);
|
|
275
|
+
const result = await verifySolution({
|
|
276
|
+
challenge,
|
|
277
|
+
solution,
|
|
278
|
+
deriveKey,
|
|
279
|
+
hmacSignatureSecret: hmacSecret,
|
|
280
|
+
hmacKeySignatureSecret: hmacKeySecret,
|
|
281
|
+
});
|
|
282
|
+
console.log(JSON.stringify(result, null, 2));
|
|
283
|
+
if (!result.verified) {
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
32
287
|
} catch (err) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
288
|
+
console.error(`Error: ${err.message}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
package/dist/cjs/v2/pow.js
CHANGED
|
@@ -89,17 +89,18 @@ async function solveChallenge(options) {
|
|
|
89
89
|
const password = new PasswordBuffer(nonceBuf, counterMode);
|
|
90
90
|
const start = performance.now();
|
|
91
91
|
let counter = counterStart;
|
|
92
|
+
let iterations = 0;
|
|
92
93
|
let derivedKeyHex = '';
|
|
93
94
|
let lastYield = start;
|
|
94
95
|
while (true) {
|
|
95
96
|
// Check for abort signal or timeout every 10 iterations.
|
|
96
97
|
if (controller?.signal.aborted ||
|
|
97
|
-
(timeout &&
|
|
98
|
+
(timeout && iterations % 10 === 0 && performance.now() - start > timeout)) {
|
|
98
99
|
return null;
|
|
99
100
|
}
|
|
100
101
|
const { derivedKey } = await deriveKey(challenge.parameters, saltBuf, password.setCounter(counter));
|
|
101
102
|
// Yield to the event loop periodically.
|
|
102
|
-
if (
|
|
103
|
+
if (iterations % 10 === 0 && performance.now() - lastYield > 200) {
|
|
103
104
|
await (0, helpers_js_1.delay)(0);
|
|
104
105
|
lastYield = performance.now();
|
|
105
106
|
}
|
|
@@ -111,6 +112,7 @@ async function solveChallenge(options) {
|
|
|
111
112
|
break;
|
|
112
113
|
}
|
|
113
114
|
counter = counter + counterStep;
|
|
115
|
+
iterations = iterations + 1;
|
|
114
116
|
}
|
|
115
117
|
return {
|
|
116
118
|
counter,
|
package/dist/esm/v2/pow.js
CHANGED
|
@@ -84,17 +84,18 @@ export async function solveChallenge(options) {
|
|
|
84
84
|
const password = new PasswordBuffer(nonceBuf, counterMode);
|
|
85
85
|
const start = performance.now();
|
|
86
86
|
let counter = counterStart;
|
|
87
|
+
let iterations = 0;
|
|
87
88
|
let derivedKeyHex = '';
|
|
88
89
|
let lastYield = start;
|
|
89
90
|
while (true) {
|
|
90
91
|
// Check for abort signal or timeout every 10 iterations.
|
|
91
92
|
if (controller?.signal.aborted ||
|
|
92
|
-
(timeout &&
|
|
93
|
+
(timeout && iterations % 10 === 0 && performance.now() - start > timeout)) {
|
|
93
94
|
return null;
|
|
94
95
|
}
|
|
95
96
|
const { derivedKey } = await deriveKey(challenge.parameters, saltBuf, password.setCounter(counter));
|
|
96
97
|
// Yield to the event loop periodically.
|
|
97
|
-
if (
|
|
98
|
+
if (iterations % 10 === 0 && performance.now() - lastYield > 200) {
|
|
98
99
|
await delay(0);
|
|
99
100
|
lastYield = performance.now();
|
|
100
101
|
}
|
|
@@ -106,6 +107,7 @@ export async function solveChallenge(options) {
|
|
|
106
107
|
break;
|
|
107
108
|
}
|
|
108
109
|
counter = counter + counterStep;
|
|
110
|
+
iterations = iterations + 1;
|
|
109
111
|
}
|
|
110
112
|
return {
|
|
111
113
|
counter,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/v1/helpers.ts","../src/v1/index.ts","../src/v1/types.ts","../src/v1/worker.ts","../src/v2/capped-map.ts","../src/v2/helpers.ts","../src/v2/index.ts","../src/v2/obfuscation.ts","../src/v2/pow.ts","../src/v2/server-signature.ts","../src/v2/types.ts","../src/v2/algorithms/argon2id.ts","../src/v2/algorithms/pbkdf2.ts","../src/v2/algorithms/scrypt.ts","../src/v2/algorithms/sha.ts","../src/v2/algorithms/web/pbkdf2.ts","../src/v2/algorithms/web/sha.ts","../src/v2/frameworks/express.ts","../src/v2/frameworks/fastify.ts","../src/v2/frameworks/h3.ts","../src/v2/frameworks/hono.ts","../src/v2/frameworks/nestjs.ts","../src/v2/frameworks/nextjs.ts","../src/v2/frameworks/shared.ts","../src/v2/frameworks/sveltekit.ts","../src/v2/frameworks/types.ts","../src/v2/workers/argon2id.ts","../src/v2/workers/pbkdf2.ts","../src/v2/workers/scrypt.ts","../src/v2/workers/sha.ts","../src/v2/workers/shared.ts"],"version":"5.9.3"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/v1/helpers.ts","../src/v1/index.ts","../src/v1/types.ts","../src/v1/worker.ts","../src/v2/capped-map.ts","../src/v2/helpers.ts","../src/v2/index.ts","../src/v2/obfuscation.ts","../src/v2/pow.ts","../src/v2/server-signature.ts","../src/v2/types.ts","../src/v2/algorithms/argon2id.ts","../src/v2/algorithms/pbkdf2.ts","../src/v2/algorithms/scrypt.ts","../src/v2/algorithms/sha.ts","../src/v2/algorithms/web/pbkdf2.ts","../src/v2/algorithms/web/sha.ts","../src/v2/frameworks/express.ts","../src/v2/frameworks/fastify.ts","../src/v2/frameworks/h3.ts","../src/v2/frameworks/hono.ts","../src/v2/frameworks/nestjs.ts","../src/v2/frameworks/nextjs.ts","../src/v2/frameworks/shared.ts","../src/v2/frameworks/sveltekit.ts","../src/v2/frameworks/types.ts","../src/v2/workers/argon2id.ts","../src/v2/workers/pbkdf2.ts","../src/v2/workers/scrypt.ts","../src/v2/workers/sha.ts","../src/v2/workers/shared.ts"],"version":"5.9.3"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "altcha-lib",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "A lightweight library for creating and verifying ALTCHA challenges on the server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Daniel Regeci",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"fastify": "^5.8.2",
|
|
115
115
|
"globals": "^17.4.0",
|
|
116
116
|
"h3": "^2.0.1-rc.18",
|
|
117
|
-
"hono": "^4.12.
|
|
117
|
+
"hono": "^4.12.12",
|
|
118
118
|
"husky": "^9.1.7",
|
|
119
119
|
"prettier": "^3.8.1",
|
|
120
120
|
"rimraf": "^6.1.3",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["../../src/v1/helpers.ts","../../src/v1/index.ts","../../src/v1/types.ts","../../src/v1/worker.ts","../../src/v2/capped-map.ts","../../src/v2/helpers.ts","../../src/v2/index.ts","../../src/v2/obfuscation.ts","../../src/v2/pow.ts","../../src/v2/server-signature.ts","../../src/v2/types.ts","../../src/v2/algorithms/argon2id.ts","../../src/v2/algorithms/pbkdf2.ts","../../src/v2/algorithms/scrypt.ts","../../src/v2/algorithms/sha.ts","../../src/v2/algorithms/web/pbkdf2.ts","../../src/v2/algorithms/web/sha.ts","../../src/v2/frameworks/express.ts","../../src/v2/frameworks/fastify.ts","../../src/v2/frameworks/h3.ts","../../src/v2/frameworks/hono.ts","../../src/v2/frameworks/nestjs.ts","../../src/v2/frameworks/nextjs.ts","../../src/v2/frameworks/shared.ts","../../src/v2/frameworks/sveltekit.ts","../../src/v2/frameworks/types.ts","../../src/v2/workers/argon2id.ts","../../src/v2/workers/pbkdf2.ts","../../src/v2/workers/scrypt.ts","../../src/v2/workers/sha.ts","../../src/v2/workers/shared.ts"],"version":"5.9.3"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["../../src/v1/helpers.ts","../../src/v1/index.ts","../../src/v1/types.ts","../../src/v1/worker.ts","../../src/v2/capped-map.ts","../../src/v2/helpers.ts","../../src/v2/index.ts","../../src/v2/obfuscation.ts","../../src/v2/pow.ts","../../src/v2/server-signature.ts","../../src/v2/types.ts","../../src/v2/algorithms/argon2id.ts","../../src/v2/algorithms/pbkdf2.ts","../../src/v2/algorithms/scrypt.ts","../../src/v2/algorithms/sha.ts","../../src/v2/algorithms/web/pbkdf2.ts","../../src/v2/algorithms/web/sha.ts","../../src/v2/frameworks/express.ts","../../src/v2/frameworks/fastify.ts","../../src/v2/frameworks/h3.ts","../../src/v2/frameworks/hono.ts","../../src/v2/frameworks/nestjs.ts","../../src/v2/frameworks/nextjs.ts","../../src/v2/frameworks/shared.ts","../../src/v2/frameworks/sveltekit.ts","../../src/v2/frameworks/types.ts","../../src/v2/workers/argon2id.ts","../../src/v2/workers/pbkdf2.ts","../../src/v2/workers/scrypt.ts","../../src/v2/workers/sha.ts","../../src/v2/workers/shared.ts"],"version":"5.9.3"}
|