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