altcha-lib 2.0.0 → 2.0.1

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 CHANGED
@@ -20,6 +20,7 @@ If your framework is not listed, see the [Advanced Usage](/docs/advanced-usage.m
20
20
 
21
21
  - Advanced Usage: [`/docs/advanced-usage.md`](/docs/advanced-usage.md)
22
22
  - Algorithms: [`/docs/algorithms.md`](/docs/algorithms.md)
23
+ - CLI Usage: [`/docs/cli.md`](/docs/cli.md)
23
24
  - Configuration Options: [`/docs/configuration-options.md`](/docs/configuration-options.md)
24
25
  - Obfuscation: [`/docs/obfuscation.md`](/docs/obfuscation.md)
25
26
  - Store: [`/docs/store.md`](/docs/store.md)
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
- if (!command || !['obfuscate', 'deobfuscate'].includes(command)) {
8
- console.error('Usage: altcha-lib <obfuscate|deobfuscate> [data]');
9
- process.exit(1);
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
- return new Promise((resolve, reject) => {
14
- let buf = '';
15
- process.stdin.setEncoding('utf-8');
16
- process.stdin.on('data', (chunk) => buf += chunk);
17
- process.stdin.on('end', () => resolve(buf.trim()));
18
- process.stdin.on('error', reject);
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
- try {
23
- const data = rest.length ? rest.join(' ') : await readStdin();
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
- if (!data) {
26
- console.error(`Error: No data provided for ${command}`);
27
- process.exit(1);
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
- const result = command === 'obfuscate' ? await obfuscate(data) : await deobfuscate(data);
31
- console.log(result);
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
- console.error(`Error during ${command}: ${err.message}`);
34
- process.exit(1);
35
- }
288
+ console.error(`Error: ${err.message}`);
289
+ process.exit(1);
290
+ }
@@ -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 && counter % 10 === 0 && performance.now() - start > 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 (counter % 10 === 0 && performance.now() - lastYield > 200) {
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,
@@ -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 && counter % 10 === 0 && performance.now() - start > 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 (counter % 10 === 0 && performance.now() - lastYield > 200) {
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.0",
3
+ "version": "2.0.1",
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.5",
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"}