i18ntk 2.3.8 → 2.5.0

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.
@@ -1,520 +0,0 @@
1
- /**
2
- * Admin PIN Management System
3
- * Handles secure PIN creation, validation, and storage
4
- */
5
-
6
- const crypto = require('crypto');
7
- const fs = require('fs');
8
- const path = require('path');
9
- const { getGlobalReadline, ask } = require('./cli');
10
- const { promptPin: rawPromptPin, promptPinConfirm } = require('./promptPin');
11
-
12
- // Lazy load i18n to prevent initialization race conditions
13
- let i18n;
14
- function getI18n() {
15
- if (!i18n) {
16
- try {
17
- i18n = require('./i18n-helper');
18
- } catch (error) {
19
- // Fallback to simple identity function if i18n fails to load
20
- console.warn('i18n-helper not available, using fallback messages');
21
- return { t: (key, params = {}) => key };
22
- }
23
- }
24
- return i18n;
25
- }
26
-
27
- // Use environment variables for configuration
28
- const SALT_LENGTH = 32;
29
- const KEY_LENGTH = 32;
30
- const MEMORY_COST = 2 ** 16; // 64MB
31
- const TIME_COST = 3;
32
- const PARALLELISM = 1;
33
- const ALGORITHM = 'argon2id';
34
-
35
- class AdminPinManager {
36
- constructor() {
37
- this.pinFile = path.join(__dirname, '..', 'settings', 'admin-pin.json');
38
- this.algorithm = 'aes-256-gcm';
39
- this.keyLength = 32;
40
- this.ivLength = 16;
41
- this.tagLength = 16;
42
-
43
- // Session management
44
- this.isAuthenticated = false;
45
- this.sessionTimeout = 30 * 60 * 1000; // 30 minutes in milliseconds
46
- this.sessionTimer = null;
47
- this.lastActivity = null;
48
- }
49
-
50
- /**
51
- * Generate a random key for encryption (AES-256-GCM)
52
- * Returns a 32-byte (256-bit) key for AES-256-GCM
53
- */
54
- generateKey() {
55
- return crypto.randomBytes(32);
56
- }
57
-
58
- /**
59
- * Generate secure random IV for AES-256-GCM
60
- * GCM requires 96-bit (12-byte) IV for optimal security
61
- */
62
- generateIV() {
63
- return crypto.randomBytes(12);
64
- }
65
-
66
- /**
67
- * Encrypt the PIN using AES-256-GCM with proper authentication
68
- */
69
- encryptPin(pin, key) {
70
- const iv = this.generateIV();
71
- const cipher = crypto.createCipherGCM('aes-256-gcm', key);
72
- cipher.setAAD(Buffer.from('admin-pin-v1')); // Additional authenticated data
73
-
74
- let encrypted = cipher.update(pin, 'utf8', 'hex');
75
- encrypted += cipher.final('hex');
76
-
77
- const authTag = cipher.getAuthTag();
78
-
79
- return {
80
- encrypted,
81
- iv: iv.toString('hex'),
82
- authTag: authTag.toString('hex')
83
- };
84
- }
85
-
86
- /**
87
- * Decrypt the PIN using AES-256-GCM with authentication verification
88
- */
89
- decryptPin(encryptedData, key) {
90
- try {
91
- const iv = Buffer.from(encryptedData.iv, 'hex');
92
- const authTag = Buffer.from(encryptedData.authTag || encryptedData.tag, 'hex');
93
-
94
- const decipher = crypto.createDecipherGCM('aes-256-gcm', key);
95
- decipher.setAuthTag(authTag);
96
- decipher.setAAD(Buffer.from('admin-pin-v1'));
97
-
98
- let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
99
- decrypted += decipher.final('utf8');
100
-
101
- return decrypted;
102
- } catch (error) {
103
- return null;
104
- }
105
- }
106
-
107
- /**
108
- * Hash PIN using secure password hashing
109
- * Uses crypto.scrypt as a secure alternative to argon2
110
- */
111
- async hashPin(pin, salt = null) {
112
- if (!salt) {
113
- salt = crypto.randomBytes(32);
114
- } else if (typeof salt === 'string') {
115
- salt = Buffer.from(salt, 'hex');
116
- }
117
-
118
- try {
119
- // Use scrypt (Node.js built-in, more secure than pbkdf2)
120
- const hash = crypto.scryptSync(pin, salt, 32, {
121
- N: 16384, // CPU/memory cost parameter
122
- r: 8, // block size parameter
123
- p: 1 // parallelization parameter
124
- });
125
- return {
126
- hash: hash.toString('hex'),
127
- salt: salt.toString('hex'),
128
- algorithm: 'scrypt'
129
- };
130
- } catch (error) {
131
- // Fallback to pbkdf2
132
- const hash = crypto.pbkdf2Sync(pin, salt, 100000, 32, 'sha256');
133
- return {
134
- hash: hash.toString('hex'),
135
- salt: salt.toString('hex'),
136
- algorithm: 'pbkdf2'
137
- };
138
- }
139
- }
140
-
141
- /**
142
- * Check if PIN is weak/common
143
- */
144
- isWeakPin(pin) {
145
- const weakPins = [
146
- '1234', '0000', '1111', '2222', '3333', '4444', '5555',
147
- '6666', '7777', '8888', '9999', '123456', '654321',
148
- '000000', '111111', '121212', '112233', '12345',
149
- ];
150
-
151
- return weakPins.includes(pin) ||
152
- /^(.)\1+$/.test(pin); // All same characters
153
- }
154
-
155
- /**
156
- * Set up a new admin PIN with security checks
157
- */
158
- async setupPin(externalRl = null) {
159
- // Use shared readline interface
160
- const hadGlobal = !!global.activeReadlineInterface;
161
- const rl = externalRl || getGlobalReadline();
162
- const shouldCloseRL = !externalRl && !hadGlobal;
163
-
164
- try {
165
- const i18nHelper = getI18n();
166
- console.log('\n' + i18nHelper.t('adminPin.setup_title'));
167
- console.log(i18nHelper.t('adminPin.setup_separator'));
168
- console.log(i18nHelper.t('adminPin.setup_description'));
169
- console.log(i18nHelper.t('adminPin.required_for_title'));
170
- console.log(i18nHelper.t('adminPin.required_for_1'));
171
- console.log(i18nHelper.t('adminPin.required_for_2'));
172
- console.log(i18nHelper.t('adminPin.required_for_3'));
173
- console.log(i18nHelper.t('adminPin.required_for_4'));
174
- console.log('\n' + i18nHelper.t('adminPin.setup_note'));
175
- console.log(i18nHelper.t('adminPin.setup_digits_only'));
176
-
177
- const pin = await promptPinConfirm(rl, i18nHelper.t('adminPin.enter_new_pin'), i18n.t('adminPin.confirm_pin'));
178
-
179
- if (!this.validatePin(pin)) {
180
- console.log(i18nHelper.t('adminPin.invalid_pin_length'));
181
- console.log(i18nHelper.t('adminPin.invalid_pin_example'));
182
- if (shouldCloseRL) rl.close();
183
- return false;
184
- }
185
-
186
- if (this.isWeakPin(pin)) {
187
- console.log(i18nHelper.t('adminPin.weak_pin_warning'));
188
- console.log(i18nHelper.t('adminPin.weak_pin_suggestion'));
189
- const proceed = await new Promise(resolve => {
190
- rl.question(i18nHelper.t('adminPin.use_anyway_prompt'), resolve);
191
- });
192
- if (proceed.toLowerCase() !== 'yes') {
193
- if (shouldCloseRL) rl.close();
194
- return false;
195
- }
196
- }
197
-
198
- // Generate encryption key and encrypt PIN
199
- const key = this.generateKey();
200
- const encryptedPin = this.encryptPin(pin, key);
201
- const hashedPin = await this.hashPin(pin);
202
-
203
- // Store encrypted data
204
- const pinData = {
205
- hash: hashedPin.hash,
206
- salt: hashedPin.salt,
207
- algorithm: hashedPin.algorithm,
208
- encrypted: encryptedPin,
209
- key: key.toString('hex'),
210
- created: new Date().toISOString(),
211
- lastChanged: new Date().toISOString(),
212
- attempts: 0,
213
- locked: false
214
- };
215
-
216
- // Ensure settings directory exists
217
- const settingsDir = path.dirname(this.pinFile);
218
- if (!SecurityUtils.safeExistsSync(settingsDir)) {
219
- fs.mkdirSync(settingsDir, { recursive: true });
220
- }
221
-
222
- SecurityUtils.safeWriteFileSync(this.pinFile, JSON.stringify(pinData, null, 2));
223
-
224
- const i18n = getI18n();
225
- console.log(i18n.t('adminPin.setup_success'));
226
- console.log(i18n.t('adminPin.setup_warning'));
227
-
228
- if (shouldCloseRL) rl.close();
229
- return true;
230
-
231
- } catch (error) {
232
- const i18n = getI18n();
233
- console.error(i18n.t('adminPin.setup_error'), error.message);
234
- if (shouldCloseRL) rl.close();
235
- return false;
236
- }
237
- }
238
-
239
- /**
240
- * Prompt for PIN with configurable display mode
241
- */
242
- promptPin(rl, message, hideInput = false) {
243
- return hideInput ? rawPromptPin({ rl, label: message }) : ask(message);
244
- }
245
-
246
- /**
247
- * Validate PIN format
248
- */
249
- validatePin(pin) {
250
- return /^\d{4,6}$/.test(pin);
251
- }
252
-
253
- /**
254
- * Check if PIN is set
255
- */
256
- isPinSet() {
257
- return SecurityUtils.safeExistsSync(this.pinFile);
258
- }
259
-
260
- /**
261
- * Start authentication session
262
- */
263
- startSession() {
264
- this.isAuthenticated = true;
265
- this.lastActivity = Date.now();
266
-
267
- // Clear existing timer
268
- if (this.sessionTimer) {
269
- clearTimeout(this.sessionTimer);
270
- }
271
-
272
- // Set new timeout
273
- this.sessionTimer = setTimeout(() => {
274
- this.endSession();
275
- console.log(i18n.t('adminPin.session_expired'));
276
- }, this.sessionTimeout);
277
- }
278
-
279
- /**
280
- * End authentication session
281
- */
282
- endSession() {
283
- this.isAuthenticated = false;
284
- this.lastActivity = null;
285
-
286
- if (this.sessionTimer) {
287
- clearTimeout(this.sessionTimer);
288
- this.sessionTimer = null;
289
- }
290
- }
291
-
292
- /**
293
- * Check if currently authenticated
294
- */
295
- isCurrentlyAuthenticated() {
296
- if (!this.isAuthenticated) {
297
- return false;
298
- }
299
-
300
- // Check if session has expired
301
- if (this.lastActivity && (Date.now() - this.lastActivity) > this.sessionTimeout) {
302
- this.endSession();
303
- return false;
304
- }
305
-
306
- // Update last activity
307
- this.lastActivity = Date.now();
308
- return true;
309
- }
310
-
311
- /**
312
- * Require authentication (with session support)
313
- */
314
- async requireAuth(forceSetup = false) {
315
- // Check if already authenticated in current session
316
- if (this.isCurrentlyAuthenticated()) {
317
- return true;
318
- }
319
-
320
- // Need to authenticate
321
- const authenticated = await this.verifyPin(forceSetup);
322
- if (authenticated) {
323
- this.startSession();
324
- }
325
-
326
- return authenticated;
327
- }
328
-
329
- /**
330
- * Constant-time comparison for PIN verification
331
- * Prevents timing attacks
332
- */
333
- constantTimeCompare(a, b) {
334
- if (typeof a !== 'string' || typeof b !== 'string') {
335
- return false;
336
- }
337
- if (a.length !== b.length) {
338
- return false;
339
- }
340
-
341
- try {
342
- return crypto.timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
343
- } catch (error) {
344
- // Fallback to manual constant-time comparison
345
- let result = 0;
346
- for (let i = 0; i < a.length; i++) {
347
- result |= a.charCodeAt(i) ^ b.charCodeAt(i);
348
- }
349
- return result === 0;
350
- }
351
- }
352
-
353
- /**
354
- * Verify admin PIN with secure hashing and constant-time comparison
355
- */
356
- async verifyPin(forceSetup = false, externalRl = null) {
357
- if (!this.isPinSet()) {
358
- if (forceSetup) {
359
- const i18n = getI18n();
360
- console.log(i18n.t('adminPin.no_pin_set_setting_up'));
361
- return await this.setupPin(externalRl);
362
- } else {
363
- const i18n = getI18n();
364
- console.log(i18n.t('adminPin.no_pin_configured_access_denied'));
365
- console.log(i18n.t('adminPin.use_admin_settings_to_set_pin'));
366
- return false;
367
- }
368
- }
369
-
370
- // Use shared readline interface if available
371
- const hadGlobal = !!global.activeReadlineInterface;
372
- const rl = externalRl || getGlobalReadline();
373
- const shouldCloseRL = !externalRl && !hadGlobal;
374
-
375
- try {
376
- const pinData = JSON.parse(SecurityUtils.safeReadFileSync(this.pinFile, path.dirname(this.pinFile), 'utf8'));
377
-
378
- if (pinData.locked) {
379
- const i18n = getI18n();
380
- console.log(i18n.t('adminPin.locked_out'));
381
- console.log(i18n.t('adminPin.wait_before_retry'));
382
- if (shouldCloseRL) rl.close();
383
- return false;
384
- }
385
-
386
- const enteredPin = await this.promptPin(rl, getI18n().t('adminCli.enterPin'), true);
387
-
388
- // Recompute hash with stored salt
389
- const salt = Buffer.from(pinData.salt, 'hex');
390
- let computedHash;
391
-
392
- if (pinData.algorithm === 'scrypt') {
393
- computedHash = crypto.scryptSync(enteredPin, salt, 32, {
394
- N: 16384,
395
- r: 8,
396
- p: 1
397
- });
398
- } else {
399
- computedHash = crypto.pbkdf2Sync(enteredPin, salt, 100000, 32, 'sha256');
400
- }
401
-
402
- const computedHashHex = computedHash.toString('hex');
403
-
404
- // Use constant-time comparison
405
- if (this.constantTimeCompare(computedHashHex, pinData.hash)) {
406
- // Reset attempts on successful login
407
- pinData.attempts = 0;
408
- SecurityUtils.safeWriteFileSync(this.pinFile, JSON.stringify(pinData, null, 2));
409
-
410
- const i18n = getI18n();
411
- console.log(i18n.t('adminPin.access_granted'));
412
- if (shouldCloseRL) rl.close();
413
- return true;
414
- } else {
415
- pinData.attempts = (pinData.attempts || 0) + 1;
416
-
417
- if (pinData.attempts >= 3) {
418
- pinData.locked = true;
419
- setTimeout(() => {
420
- pinData.locked = false;
421
- pinData.attempts = 0;
422
- SecurityUtils.safeWriteFileSync(this.pinFile, JSON.stringify(pinData, null, 2));
423
- }, 5 * 60 * 1000); // 5 minutes
424
- }
425
-
426
- SecurityUtils.safeWriteFileSync(this.pinFile, JSON.stringify(pinData, null, 2));
427
-
428
- const i18n = getI18n();
429
- console.log(i18n.t('adminPin.incorrect_pin', { attempts: 3 - pinData.attempts }));
430
- if (shouldCloseRL) rl.close();
431
- return false;
432
- }
433
-
434
- } catch (error) {
435
- const i18n = getI18n();
436
- console.error(i18n.t('adminPin.verify_pin_error'), error.message);
437
- if (shouldCloseRL) rl.close();
438
- return false;
439
- }
440
- }
441
-
442
- /**
443
- * Prompt user for optional PIN setup
444
- */
445
- async promptOptionalSetup() {
446
- if (this.isPinSet()) {
447
- console.log(i18n.t('adminPin.already_configured'));
448
- return true;
449
- }
450
-
451
- // Use shared readline interface if available
452
- const hadGlobal = !!global.activeReadlineInterface;
453
- const rl = getGlobalReadline();
454
- const shouldCloseRL = !hadGlobal;
455
-
456
- try {
457
- const i18n = getI18n();
458
- console.log(i18n.t('adminPin.optional_setup_title'));
459
- console.log(i18n.t('adminPin.setup_separator'));
460
- console.log(i18n.t('adminPin.optional_setup_description'));
461
- console.log(i18n.t('adminPin.required_for_1'));
462
- console.log(i18n.t('adminPin.required_for_2'));
463
- console.log(i18n.t('adminPin.required_for_3'));
464
- console.log(i18n.t('adminPin.required_for_4'));
465
- console.log('');
466
-
467
- const response = await new Promise(resolve => {
468
- rl.question(getI18n().t('adminPin.setup_prompt'), resolve);
469
- });
470
-
471
- if (shouldCloseRL) rl.close();
472
-
473
- if (response.toLowerCase() === 'y' || response.toLowerCase() === 'yes') {
474
- return await this.setupPin();
475
- } else {
476
- console.log(getI18n().t('adminPin.skipping_setup'));
477
- return false;
478
- }
479
- } catch (error) {
480
- if (shouldCloseRL) rl.close();
481
- console.error(getI18n().t('adminPin.setup_prompt_error'), error.message);
482
- return false;
483
- }
484
- }
485
-
486
- /**
487
- * Get PIN display (masked)
488
- */
489
- getPinDisplay() {
490
- if (!this.isPinSet()) {
491
- return i18n.t('adminPin.not_set');
492
- }
493
-
494
- try {
495
- const pinData = JSON.parse(SecurityUtils.safeReadFileSync(this.pinFile, path.dirname(this.pinFile), 'utf8'));
496
- const key = Buffer.from(pinData.key, 'hex');
497
- const decryptedPin = this.decryptPin(pinData.encrypted, key);
498
-
499
- if (decryptedPin) {
500
- return '*'.repeat(decryptedPin.length);
501
- }
502
- } catch (error) {
503
- // Ignore errors, return default
504
- }
505
-
506
- return i18n.t('adminPin.pin_display_mask');
507
- }
508
-
509
- /**
510
- * Reset PIN
511
- */
512
- async resetPin() {
513
- if (SecurityUtils.safeExistsSync(this.pinFile)) {
514
- fs.unlinkSync(this.pinFile);
515
- }
516
- return await this.setupPin();
517
- }
518
- }
519
-
520
- module.exports = AdminPinManager;
@@ -1,40 +0,0 @@
1
- class ArgumentParser {
2
- constructor() {
3
- this.args = {
4
- _: []
5
- };
6
- this.parse(process.argv.slice(2));
7
- }
8
-
9
- parse(argv) {
10
- let currentOption = null;
11
-
12
- for (const arg of argv) {
13
- if (arg.startsWith('--')) {
14
- // Handle --option=value or --option value
15
- if (arg.includes('=')) {
16
- const [key, value] = arg.split('=');
17
- this.args[key.slice(2)] = value;
18
- } else {
19
- currentOption = arg.slice(2);
20
- this.args[currentOption] = true;
21
- }
22
- } else if (arg.startsWith('-')) {
23
- // Handle short options
24
- currentOption = arg.slice(1);
25
- this.args[currentOption] = true;
26
- } else if (currentOption) {
27
- // Handle option value
28
- this.args[currentOption] = arg;
29
- currentOption = null;
30
- } else {
31
- // Handle positional arguments
32
- this.args._.push(arg);
33
- }
34
- }
35
-
36
- return this.args;
37
- }
38
- }
39
-
40
- module.exports = ArgumentParser;