i18ntk 2.4.0 → 2.5.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.
@@ -1,576 +1,655 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const crypto = require('crypto');
4
- const SecurityUtils = require('./security');
5
- const configManager = require('./config-manager');
6
-
7
- /**
8
- * Admin Authentication Module
9
- * Provides secure PIN-based authentication for administrative operations
10
- */
11
- class AdminAuth {
12
- constructor() {
13
- const packageRoot = path.resolve(__dirname, '..');
14
- this.configPath = path.join(packageRoot, '.i18n-admin-config.json');
15
-
16
- // Get settings from config manager
17
- const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
18
- const securitySettings = settings.security || {};
19
- this.sessionTimeout = (securitySettings.sessionTimeout || 30) * 60 * 1000; // Convert minutes to milliseconds
20
- this.maxAttempts = securitySettings.maxFailedAttempts || 3;
21
- this.lockoutDuration = (securitySettings.lockoutDuration || 15) * 60 * 1000; // Convert minutes to milliseconds
22
- this.keepAuthenticatedUntilExit = securitySettings.keepAuthenticatedUntilExit !== false;
23
-
24
- this.activeSessions = new Map();
25
- this.failedAttempts = new Map();
26
- this.lockouts = new Map();
27
- this.currentSession = null;
28
- this.sessionStartTime = null;
29
-
30
- // Clean up expired sessions every 5 minutes
31
- this.cleanupInterval = setInterval(this.cleanupExpiredSessions.bind(this), 5 * 60 * 1000);
32
-
33
- // Handle process exit to ensure session cleanup
34
- this.setupProcessHandlers();
35
- }
36
-
37
- /**
38
- * Initialize admin authentication system
39
- */
40
- async initialize() {
41
- try {
42
- if (!SecurityUtils.safeExistsSync(this.configPath)) {
43
- // Create default config if it doesn't exist
44
- const defaultConfig = {
45
- enabled: false,
46
- pinHash: null,
47
- salt: null,
48
- createdAt: new Date().toISOString(),
49
- lastModified: new Date().toISOString()
50
- };
51
- await this.saveConfig(defaultConfig);
52
- }
53
-
54
- SecurityUtils.logSecurityEvent(
55
- 'admin_auth_initialized',
56
- 'info',
57
- { message: 'Admin authentication system initialized' }
58
- );
59
- return true;
60
- } catch (error) {
61
- SecurityUtils.logSecurityEvent(
62
- 'admin_auth_init_error',
63
- 'error',
64
- { message: `Failed to initialize admin auth: ${error.message}` }
65
- );
66
- return false;
67
- }
68
- }
69
-
70
- /**
71
- * Cleanup resources and stop intervals
72
- */
73
- async cleanup() {
74
- if (this.cleanupInterval) {
75
- clearInterval(this.cleanupInterval);
76
- this.cleanupInterval = null;
77
- }
78
- }
79
-
80
- /**
81
- * Load admin configuration
82
- */
83
- async loadConfig() {
84
- try {
85
- if (!SecurityUtils.safeExistsSync(this.configPath)) {
86
- return null;
87
- }
88
-
89
- const content = await fs.promises.readFile(this.configPath, 'utf8');
90
- return SecurityUtils.safeParseJSON(content);
91
- } catch (error) {
92
- SecurityUtils.logSecurityEvent(
93
- 'admin_config_load_error',
94
- 'error',
95
- { message: `Failed to load admin config: ${error.message}` }
96
- );
97
- return null;
98
- }
99
- }
100
-
101
- /**
102
- * Save admin configuration
103
- */
104
- async saveConfig(config) {
105
- try {
106
- const content = JSON.stringify(config, null, 2);
107
- await fs.promises.writeFile(this.configPath, content, { mode: 0o600 }); // Restrict permissions
108
- SecurityUtils.logSecurityEvent(
109
- 'admin_config_saved',
110
- 'info',
111
- { message: 'Admin configuration saved' }
112
- );
113
- return true;
114
- } catch (error) {
115
- SecurityUtils.logSecurityEvent(
116
- 'admin_config_save_error',
117
- 'error',
118
- { message: `Failed to save admin config: ${error.message}` }
119
- );
120
- return false;
121
- }
122
- }
123
-
124
- /**
125
- * Set up admin PIN
126
- */
127
- async setupPin(pin) {
128
- try {
129
- // Validate PIN format (4-6 digits)
130
- if (!/^\d{4,6}$/.test(pin)) {
131
- throw new Error('PIN must be 4-6 digits');
132
- }
133
-
134
- // Generate salt and hash
135
- const salt = crypto.randomBytes(32).toString('hex');
136
- const pinHash = this.hashPin(pin, salt);
137
-
138
- const config = {
139
- enabled: true,
140
- pinHash,
141
- salt,
142
- createdAt: new Date().toISOString(),
143
- lastModified: new Date().toISOString()
144
- };
145
-
146
- const success = await this.saveConfig(config);
147
- if (success) {
148
- // Reset failed attempts on successful PIN setup
149
- this.failedAttempts.clear();
150
- SecurityUtils.logSecurityEvent(
151
- 'admin_pin_setup',
152
- 'info',
153
- { message: 'Admin PIN configured successfully' }
154
- );
155
- }
156
- return success;
157
- } catch (error) {
158
- SecurityUtils.logSecurityEvent(
159
- 'admin_pin_setup_error',
160
- 'error',
161
- { message: `Failed to setup PIN: ${error.message}` }
162
- );
163
- return false;
164
- }
165
- }
166
-
167
- /**
168
- * Hash PIN with salt
169
- */
170
- hashPin(pin, salt) {
171
- return crypto.pbkdf2Sync(pin, salt, 100000, 64, 'sha512').toString('hex');
172
- }
173
-
174
- /**
175
- * Verify PIN
176
- */
177
- async verifyPin(pin) {
178
- try {
179
- const config = await this.loadConfig();
180
- if (!config || !config.enabled) {
181
- return true; // No authentication required if not enabled
182
- }
183
-
184
- // Check for lockout
185
- const clientId = 'local'; // In a real app, this would be client IP or session ID
186
- if (this.isLockedOut(clientId)) {
187
- SecurityUtils.logSecurityEvent(
188
- 'admin_auth_lockout',
189
- 'warning',
190
- { message: 'Authentication attempt during lockout period' }
191
- );
192
- return false;
193
- }
194
-
195
- // Validate PIN format
196
- if (!/^\d{4,6}$/.test(pin)) {
197
- this.recordFailedAttempt(clientId);
198
- SecurityUtils.logSecurityEvent(
199
- 'admin_auth_invalid_format',
200
- 'warning',
201
- { message: 'Invalid PIN format attempted' }
202
- );
203
- return false;
204
- }
205
-
206
- // Verify PIN
207
- const pinHash = this.hashPin(pin, config.salt);
208
- const isValid = pinHash === config.pinHash;
209
-
210
- if (isValid) {
211
- this.clearFailedAttempts(clientId);
212
- SecurityUtils.logSecurityEvent(
213
- 'admin_auth_success',
214
- 'info',
215
- { message: 'Admin authentication successful' }
216
- );
217
- return true;
218
- } else {
219
- this.recordFailedAttempt(clientId);
220
- SecurityUtils.logSecurityEvent(
221
- 'admin_auth_failure',
222
- 'warning',
223
- { message: 'Admin authentication failed' }
224
- );
225
- return false;
226
- }
227
- } catch (error) {
228
- SecurityUtils.logSecurityEvent(
229
- 'admin_auth_error',
230
- 'error',
231
- { message: `Authentication error: ${error.message}` }
232
- );
233
- return false;
234
- }
235
- }
236
-
237
- /**
238
- * Check if admin PIN is configured
239
- */
240
- async isPinConfigured() {
241
- const config = await this.loadConfig();
242
- return config && config.enabled && config.pinHash;
243
- }
244
-
245
- /**
246
- * Check if authentication is required
247
- */
248
- async isAuthRequired() {
249
- // Check if admin PIN is enabled in settings
250
- const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
251
- if (!(settings.security?.adminPinEnabled)) {
252
- return false;
253
- }
254
-
255
- const config = await this.loadConfig();
256
- return config && config.enabled;
257
- }
258
-
259
- /**
260
- * Check if authentication is required for a specific script
261
- */
262
- async isAuthRequiredForScript(scriptName) {
263
- // Check if admin PIN is enabled globally
264
- const globalSettings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
265
- if (!(globalSettings.security?.adminPinEnabled)) {
266
- return false;
267
- }
268
-
269
- // Check if admin PIN is actually configured
270
- const config = await this.loadConfig();
271
- if (!config || !config.enabled || !config.pinHash) {
272
- return false; // Don't require PIN if admin PIN is not configured
273
- }
274
-
275
- // Check if PIN protection is enabled
276
- const pinProtection = globalSettings.security?.pinProtection;
277
- if (!pinProtection || !pinProtection.enabled) {
278
- return false; // Don't require PIN if protection is disabled
279
- }
280
-
281
- // Check if this specific script requires protection
282
- const protectedScripts = pinProtection.protectedScripts || {};
283
- return protectedScripts[scriptName] !== false; // Default to true if not explicitly set
284
- }
285
-
286
- /**
287
- * Setup process handlers for session cleanup
288
- */
289
- setupProcessHandlers() {
290
- const cleanup = () => {
291
- this.clearCurrentSession();
292
- if (this.cleanupInterval) {
293
- clearInterval(this.cleanupInterval);
294
- }
295
- };
296
-
297
- // Handle various exit scenarios
298
- process.on('exit', cleanup);
299
- process.on('SIGINT', () => {
300
- cleanup();
301
- process.exit(0);
302
- });
303
- process.on('SIGTERM', () => {
304
- cleanup();
305
- process.exit(0);
306
- });
307
- process.on('uncaughtException', (error) => {
308
- SecurityUtils.logSecurityEvent('uncaught_exception', 'error', error.message);
309
- cleanup();
310
- process.exit(1);
311
- });
312
- }
313
-
314
- /**
315
- * Create a new authenticated session
316
- */
317
- async createSession(sessionId = null) {
318
- if (!sessionId) {
319
- sessionId = this.generateSessionId();
320
- }
321
-
322
- const session = {
323
- id: sessionId,
324
- created: new Date().toISOString(),
325
- lastActivity: new Date().toISOString(),
326
- expires: new Date(Date.now() + this.sessionTimeout).toISOString()
327
- };
328
-
329
- this.activeSessions.set(sessionId, session);
330
- this.currentSession = session;
331
- this.sessionStartTime = new Date();
332
-
333
- SecurityUtils.logSecurityEvent(
334
- 'session_created',
335
- 'info',
336
- { message: `Session ${sessionId} created` }
337
- );
338
- return sessionId;
339
- }
340
-
341
- /**
342
- * Validate current session
343
- */
344
- async validateSession(sessionId) {
345
- if (!sessionId || !this.currentSession) {
346
- return false;
347
- }
348
-
349
- if (sessionId !== this.currentSession.id) {
350
- return false;
351
- }
352
-
353
- const session = this.activeSessions.get(sessionId);
354
- if (!session) {
355
- this.clearCurrentSession();
356
- return false;
357
- }
358
-
359
- const now = new Date();
360
- const expires = new Date(session.expires);
361
-
362
- if (now > expires) {
363
- this.activeSessions.delete(sessionId);
364
- this.clearCurrentSession();
365
- SecurityUtils.logSecurityEvent(
366
- 'session_expired',
367
- 'info',
368
- { message: `Session ${sessionId} expired` }
369
- );
370
- return false;
371
- }
372
-
373
- // Update last activity
374
- session.lastActivity = now.toISOString();
375
- session.expires = new Date(now.getTime() + this.sessionTimeout).toISOString();
376
- this.activeSessions.set(sessionId, session);
377
-
378
- return true;
379
- }
380
-
381
- /**
382
- * Clear current session
383
- */
384
- clearCurrentSession() {
385
- if (this.currentSession) {
386
- this.activeSessions.delete(this.currentSession.id);
387
- SecurityUtils.logSecurityEvent(
388
- 'session_cleared',
389
- 'info',
390
- { message: `Session ${this.currentSession.id} cleared` }
391
- );
392
- }
393
- this.currentSession = null;
394
- this.sessionStartTime = null;
395
- }
396
-
397
- /**
398
- * Check if currently authenticated
399
- */
400
- isCurrentlyAuthenticated() {
401
- return this.currentSession !== null;
402
- }
403
-
404
- /**
405
- * Get current session info
406
- */
407
- getCurrentSessionInfo() {
408
- if (!this.currentSession) {
409
- return null;
410
- }
411
-
412
- return {
413
- sessionId: this.currentSession.id,
414
- started: this.sessionStartTime,
415
- expires: new Date(this.currentSession.expires),
416
- duration: Date.now() - this.sessionStartTime.getTime()
417
- };
418
- }
419
-
420
- /**
421
- * Generate secure session ID
422
- */
423
- generateSessionId() {
424
- return crypto.randomBytes(16).toString('hex');
425
- }
426
-
427
- /**
428
- * Disable admin authentication (completely removes PIN)
429
- */
430
- async disableAuth() {
431
- try {
432
- const config = await this.loadConfig();
433
- if (config) {
434
- config.enabled = false;
435
- config.pinHash = null;
436
- config.salt = null;
437
- config.lastModified = new Date().toISOString();
438
- const success = await this.saveConfig(config);
439
- if (success) {
440
- SecurityUtils.logSecurityEvent(
441
- 'admin_auth_disabled',
442
- 'info',
443
- { message: 'Admin authentication disabled' }
444
- );
445
- }
446
- return success;
447
- }
448
- return true;
449
- } catch (error) {
450
- SecurityUtils.logSecurityEvent(
451
- 'admin_auth_disable_error',
452
- 'error',
453
- { message: `Failed to disable auth: ${error.message}` }
454
- );
455
- return false;
456
- }
457
- }
458
-
459
- /**
460
- * Disable PIN protection (keeps PIN for future re-enable)
461
- */
462
- async disablePinProtection() {
463
- try {
464
- const config = await this.loadConfig();
465
- if (config) {
466
- config.enabled = false;
467
- config.lastModified = new Date().toISOString();
468
- const success = await this.saveConfig(config);
469
- if (success) {
470
- SecurityUtils.logSecurityEvent('pin_protection_disabled', 'info', 'PIN protection disabled (PIN retained)');
471
- }
472
- return success;
473
- }
474
- return true;
475
- } catch (error) {
476
- SecurityUtils.logSecurityEvent('pin_protection_disable_error', 'error', `Failed to disable PIN protection: ${error.message}`);
477
- return false;
478
- }
479
- }
480
-
481
- /**
482
- * Enable PIN protection (requires PIN to be already set)
483
- */
484
- async enablePinProtection() {
485
- try {
486
- const config = await this.loadConfig();
487
- if (config && config.pinHash) {
488
- config.enabled = true;
489
- config.lastModified = new Date().toISOString();
490
- const success = await this.saveConfig(config);
491
- if (success) {
492
- SecurityUtils.logSecurityEvent('pin_protection_enabled', 'info', 'PIN protection enabled');
493
- }
494
- return success;
495
- }
496
- return false;
497
- } catch (error) {
498
- SecurityUtils.logSecurityEvent('pin_protection_enable_error', 'error', `Failed to enable PIN protection: ${error.message}`);
499
- return false;
500
- }
501
- }
502
-
503
- /**
504
- * Record failed authentication attempt
505
- */
506
- recordFailedAttempt(clientId) {
507
- const now = Date.now();
508
- const attempts = this.failedAttempts.get(clientId) || [];
509
-
510
- // Remove old attempts (older than lockout duration)
511
- const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
512
- recentAttempts.push(now);
513
-
514
- this.failedAttempts.set(clientId, recentAttempts);
515
- }
516
-
517
- /**
518
- * Clear failed attempts for client
519
- */
520
- clearFailedAttempts(clientId) {
521
- this.failedAttempts.delete(clientId);
522
- }
523
-
524
- /**
525
- * Check if client is locked out
526
- */
527
- isLockedOut(clientId) {
528
- const attempts = this.failedAttempts.get(clientId) || [];
529
- const now = Date.now();
530
-
531
- // Remove old attempts
532
- const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
533
- this.failedAttempts.set(clientId, recentAttempts);
534
-
535
- return recentAttempts.length >= this.maxAttempts;
536
- }
537
-
538
- /**
539
- * Destroy session
540
- */
541
- destroySession(sessionId) {
542
- const deleted = this.activeSessions.delete(sessionId);
543
- if (deleted) {
544
- SecurityUtils.logSecurityEvent('admin_session_destroyed', 'info', 'Admin session destroyed');
545
- }
546
- return deleted;
547
- }
548
-
549
- /**
550
- * Clean up expired sessions
551
- */
552
- cleanupExpiredSessions() {
553
- const now = Date.now();
554
- let cleaned = 0;
555
-
556
- for (const [sessionId, session] of this.activeSessions.entries()) {
557
- if (now > session.expiresAt) {
558
- this.activeSessions.delete(sessionId);
559
- cleaned++;
560
- }
561
- }
562
-
563
- if (cleaned > 0) {
564
- SecurityUtils.logSecurityEvent('admin_sessions_cleaned', 'info', `Cleaned up ${cleaned} expired sessions`);
565
- }
566
- }
567
-
568
- /**
569
- * Clean up expired sessions (alias for backward compatibility)
570
- */
571
- cleanupSessions() {
572
- return this.cleanupExpiredSessions();
573
- }
574
- }
575
-
576
- module.exports = AdminAuth;
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const SecurityUtils = require('./security');
5
+ const configManager = require('./config-manager');
6
+
7
+ /**
8
+ * Admin Authentication Module
9
+ * Provides secure PIN-based authentication for administrative operations
10
+ */
11
+ class AdminAuth {
12
+ constructor() {
13
+ const packageRoot = path.resolve(__dirname, '..');
14
+ this.configPath = path.join(packageRoot, '.i18n-admin-config.json');
15
+
16
+ // Get settings from config manager
17
+ const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
18
+ const securitySettings = settings.security || {};
19
+ this.sessionTimeout = (securitySettings.sessionTimeout || 30) * 60 * 1000; // Convert minutes to milliseconds
20
+ this.maxAttempts = securitySettings.maxFailedAttempts || 3;
21
+ this.lockoutDuration = (securitySettings.lockoutDuration || 15) * 60 * 1000; // Convert minutes to milliseconds
22
+ this.keepAuthenticatedUntilExit = securitySettings.keepAuthenticatedUntilExit !== false;
23
+
24
+ this.activeSessions = new Map();
25
+ this.failedAttempts = new Map();
26
+ this.lockouts = new Map();
27
+ this.currentSession = null;
28
+ this.sessionStartTime = null;
29
+
30
+ // Clean up expired sessions every 5 minutes
31
+ this.cleanupInterval = setInterval(this.cleanupExpiredSessions.bind(this), 5 * 60 * 1000);
32
+ if (typeof this.cleanupInterval.unref === 'function') {
33
+ this.cleanupInterval.unref();
34
+ }
35
+
36
+ // Handle process exit to ensure session cleanup
37
+ this.setupProcessHandlers();
38
+ }
39
+
40
+ /**
41
+ * Initialize admin authentication system
42
+ */
43
+ async initialize() {
44
+ try {
45
+ if (!SecurityUtils.safeExistsSync(this.configPath)) {
46
+ // Create default config if it doesn't exist
47
+ const defaultConfig = {
48
+ enabled: false,
49
+ pinHash: null,
50
+ salt: null,
51
+ createdAt: new Date().toISOString(),
52
+ lastModified: new Date().toISOString()
53
+ };
54
+ await this.saveConfig(defaultConfig);
55
+ }
56
+
57
+ SecurityUtils.logSecurityEvent(
58
+ 'admin_auth_initialized',
59
+ 'info',
60
+ { message: 'Admin authentication system initialized' }
61
+ );
62
+ return true;
63
+ } catch (error) {
64
+ SecurityUtils.logSecurityEvent(
65
+ 'admin_auth_init_error',
66
+ 'error',
67
+ { message: `Failed to initialize admin auth: ${error.message}` }
68
+ );
69
+ return false;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Cleanup resources and stop intervals
75
+ */
76
+ async cleanup() {
77
+ if (this.cleanupInterval) {
78
+ clearInterval(this.cleanupInterval);
79
+ this.cleanupInterval = null;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Load admin configuration
85
+ */
86
+ async loadConfig() {
87
+ try {
88
+ if (!SecurityUtils.safeExistsSync(this.configPath)) {
89
+ return null;
90
+ }
91
+
92
+ const content = await fs.promises.readFile(this.configPath, 'utf8');
93
+ return SecurityUtils.safeParseJSON(content);
94
+ } catch (error) {
95
+ SecurityUtils.logSecurityEvent(
96
+ 'admin_config_load_error',
97
+ 'error',
98
+ { message: `Failed to load admin config: ${error.message}` }
99
+ );
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Save admin configuration
106
+ */
107
+ async saveConfig(config) {
108
+ try {
109
+ const content = JSON.stringify(config, null, 2);
110
+ await fs.promises.writeFile(this.configPath, content, { mode: 0o600 }); // Restrict permissions
111
+ SecurityUtils.logSecurityEvent(
112
+ 'admin_config_saved',
113
+ 'info',
114
+ { message: 'Admin configuration saved' }
115
+ );
116
+ return true;
117
+ } catch (error) {
118
+ SecurityUtils.logSecurityEvent(
119
+ 'admin_config_save_error',
120
+ 'error',
121
+ { message: `Failed to save admin config: ${error.message}` }
122
+ );
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Set up admin PIN
129
+ */
130
+ async setupPin(pin) {
131
+ try {
132
+ // Validate PIN format (4-6 digits)
133
+ if (!/^\d{4,6}$/.test(pin)) {
134
+ throw new Error('PIN must be 4-6 digits');
135
+ }
136
+
137
+ // Generate salt and hash
138
+ const salt = crypto.randomBytes(32).toString('hex');
139
+ const pinHash = this.hashPin(pin, salt);
140
+
141
+ const config = {
142
+ enabled: true,
143
+ pinHash,
144
+ salt,
145
+ createdAt: new Date().toISOString(),
146
+ lastModified: new Date().toISOString()
147
+ };
148
+
149
+ const success = await this.saveConfig(config);
150
+ if (success) {
151
+ // Reset failed attempts on successful PIN setup
152
+ this.failedAttempts.clear();
153
+ SecurityUtils.logSecurityEvent(
154
+ 'admin_pin_setup',
155
+ 'info',
156
+ { message: 'Admin PIN configured successfully' }
157
+ );
158
+ }
159
+ return success;
160
+ } catch (error) {
161
+ SecurityUtils.logSecurityEvent(
162
+ 'admin_pin_setup_error',
163
+ 'error',
164
+ { message: `Failed to setup PIN: ${error.message}` }
165
+ );
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Hash PIN with salt
172
+ */
173
+ hashPin(pin, salt) {
174
+ return crypto.pbkdf2Sync(pin, salt, 100000, 64, 'sha512').toString('hex');
175
+ }
176
+
177
+ timingSafeHexEqual(leftHex, rightHex) {
178
+ if (typeof leftHex !== 'string' || typeof rightHex !== 'string') {
179
+ return false;
180
+ }
181
+
182
+ const left = Buffer.from(leftHex, 'hex');
183
+ const right = Buffer.from(rightHex, 'hex');
184
+ if (left.length === 0 || left.length !== right.length) {
185
+ return false;
186
+ }
187
+
188
+ return crypto.timingSafeEqual(left, right);
189
+ }
190
+
191
+ hasUsablePinConfig(config) {
192
+ return Boolean(
193
+ config &&
194
+ config.enabled === true &&
195
+ typeof config.pinHash === 'string' &&
196
+ config.pinHash.length > 0 &&
197
+ typeof config.salt === 'string' &&
198
+ config.salt.length > 0
199
+ );
200
+ }
201
+
202
+ logMissingPinConfig(context) {
203
+ SecurityUtils.logSecurityEvent(
204
+ 'admin_auth_config_invalid',
205
+ 'warning',
206
+ { message: `Admin PIN is required but not usable during ${context}` }
207
+ );
208
+ }
209
+
210
+ getSessionExpiryTime(session) {
211
+ if (!session || typeof session !== 'object') {
212
+ return NaN;
213
+ }
214
+
215
+ if (Number.isFinite(session.expiresAt)) {
216
+ return session.expiresAt;
217
+ }
218
+
219
+ if (typeof session.expiresAt === 'string') {
220
+ const parsedExpiresAt = Date.parse(session.expiresAt);
221
+ if (Number.isFinite(parsedExpiresAt)) {
222
+ return parsedExpiresAt;
223
+ }
224
+ }
225
+
226
+ if (typeof session.expires === 'string') {
227
+ const parsedExpires = Date.parse(session.expires);
228
+ if (Number.isFinite(parsedExpires)) {
229
+ return parsedExpires;
230
+ }
231
+ }
232
+
233
+ return NaN;
234
+ }
235
+
236
+ /**
237
+ * Verify PIN
238
+ */
239
+ async verifyPin(pin) {
240
+ try {
241
+ const config = await this.loadConfig();
242
+ if (!this.hasUsablePinConfig(config)) {
243
+ this.logMissingPinConfig('PIN verification');
244
+ return false;
245
+ }
246
+
247
+ // Check for lockout
248
+ const clientId = 'local'; // In a real app, this would be client IP or session ID
249
+ if (this.isLockedOut(clientId)) {
250
+ SecurityUtils.logSecurityEvent(
251
+ 'admin_auth_lockout',
252
+ 'warning',
253
+ { message: 'Authentication attempt during lockout period' }
254
+ );
255
+ return false;
256
+ }
257
+
258
+ // Validate PIN format
259
+ if (!/^\d{4,6}$/.test(pin)) {
260
+ this.recordFailedAttempt(clientId);
261
+ SecurityUtils.logSecurityEvent(
262
+ 'admin_auth_invalid_format',
263
+ 'warning',
264
+ { message: 'Invalid PIN format attempted' }
265
+ );
266
+ return false;
267
+ }
268
+
269
+ // Verify PIN
270
+ const pinHash = this.hashPin(pin, config.salt);
271
+ const isValid = this.timingSafeHexEqual(pinHash, config.pinHash);
272
+
273
+ if (isValid) {
274
+ this.clearFailedAttempts(clientId);
275
+ SecurityUtils.logSecurityEvent(
276
+ 'admin_auth_success',
277
+ 'info',
278
+ { message: 'Admin authentication successful' }
279
+ );
280
+ return true;
281
+ } else {
282
+ this.recordFailedAttempt(clientId);
283
+ SecurityUtils.logSecurityEvent(
284
+ 'admin_auth_failure',
285
+ 'warning',
286
+ { message: 'Admin authentication failed' }
287
+ );
288
+ return false;
289
+ }
290
+ } catch (error) {
291
+ SecurityUtils.logSecurityEvent(
292
+ 'admin_auth_error',
293
+ 'error',
294
+ { message: `Authentication error: ${error.message}` }
295
+ );
296
+ return false;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Check if admin PIN is configured
302
+ */
303
+ async isPinConfigured() {
304
+ const config = await this.loadConfig();
305
+ return this.hasUsablePinConfig(config);
306
+ }
307
+
308
+ /**
309
+ * Check if authentication is required
310
+ */
311
+ async isAuthRequired() {
312
+ // Check if admin PIN is enabled in settings
313
+ const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
314
+ if (!(settings.security?.adminPinEnabled)) {
315
+ return false;
316
+ }
317
+
318
+ const config = await this.loadConfig();
319
+ if (!this.hasUsablePinConfig(config)) {
320
+ this.logMissingPinConfig('auth-required check');
321
+ }
322
+ return true;
323
+ }
324
+
325
+ /**
326
+ * Check if authentication is required for a specific script
327
+ */
328
+ async isAuthRequiredForScript(scriptName) {
329
+ // Check if admin PIN is enabled globally
330
+ const globalSettings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
331
+ if (!(globalSettings.security?.adminPinEnabled)) {
332
+ return false;
333
+ }
334
+
335
+ // Check if PIN protection is enabled
336
+ const pinProtection = globalSettings.security?.pinProtection;
337
+ if (!pinProtection || !pinProtection.enabled) {
338
+ return false; // Don't require PIN if protection is disabled
339
+ }
340
+
341
+ // Check if this specific script requires protection
342
+ const protectedScripts = pinProtection.protectedScripts || {};
343
+ if (protectedScripts[scriptName] === false) {
344
+ return false;
345
+ }
346
+
347
+ const config = await this.loadConfig();
348
+ if (!this.hasUsablePinConfig(config)) {
349
+ this.logMissingPinConfig(`script auth-required check for ${scriptName}`);
350
+ }
351
+
352
+ return true; // Default to true if not explicitly set
353
+ }
354
+
355
+ /**
356
+ * Setup process handlers for session cleanup
357
+ */
358
+ setupProcessHandlers() {
359
+ const cleanup = () => {
360
+ this.clearCurrentSession();
361
+ if (this.cleanupInterval) {
362
+ clearInterval(this.cleanupInterval);
363
+ }
364
+ };
365
+
366
+ // Handle various exit scenarios
367
+ process.on('exit', cleanup);
368
+ process.on('SIGINT', () => {
369
+ cleanup();
370
+ process.exit(0);
371
+ });
372
+ process.on('SIGTERM', () => {
373
+ cleanup();
374
+ process.exit(0);
375
+ });
376
+ process.on('uncaughtException', (error) => {
377
+ SecurityUtils.logSecurityEvent('uncaught_exception', 'error', error.message);
378
+ cleanup();
379
+ process.exit(1);
380
+ });
381
+ }
382
+
383
+ /**
384
+ * Create a new authenticated session
385
+ */
386
+ async createSession(sessionId = null) {
387
+ if (!sessionId) {
388
+ sessionId = this.generateSessionId();
389
+ }
390
+
391
+ const now = Date.now();
392
+ const expiresAt = now + this.sessionTimeout;
393
+ const session = {
394
+ id: sessionId,
395
+ created: new Date(now).toISOString(),
396
+ lastActivity: new Date(now).toISOString(),
397
+ expires: new Date(expiresAt).toISOString(),
398
+ expiresAt
399
+ };
400
+
401
+ this.activeSessions.set(sessionId, session);
402
+ this.currentSession = session;
403
+ this.sessionStartTime = new Date();
404
+
405
+ SecurityUtils.logSecurityEvent(
406
+ 'session_created',
407
+ 'info',
408
+ { message: `Session ${sessionId} created` }
409
+ );
410
+ return sessionId;
411
+ }
412
+
413
+ /**
414
+ * Validate current session
415
+ */
416
+ async validateSession(sessionId) {
417
+ if (!sessionId || !this.currentSession) {
418
+ return false;
419
+ }
420
+
421
+ if (sessionId !== this.currentSession.id) {
422
+ return false;
423
+ }
424
+
425
+ const session = this.activeSessions.get(sessionId);
426
+ if (!session) {
427
+ this.clearCurrentSession();
428
+ return false;
429
+ }
430
+
431
+ const now = Date.now();
432
+ const expiresAt = this.getSessionExpiryTime(session);
433
+
434
+ if (!Number.isFinite(expiresAt) || now > expiresAt) {
435
+ this.activeSessions.delete(sessionId);
436
+ this.clearCurrentSession();
437
+ SecurityUtils.logSecurityEvent(
438
+ 'session_expired',
439
+ 'info',
440
+ { message: `Session ${sessionId} expired` }
441
+ );
442
+ return false;
443
+ }
444
+
445
+ // Update last activity
446
+ const nextExpiresAt = now + this.sessionTimeout;
447
+ session.lastActivity = new Date(now).toISOString();
448
+ session.expires = new Date(nextExpiresAt).toISOString();
449
+ session.expiresAt = nextExpiresAt;
450
+ this.activeSessions.set(sessionId, session);
451
+
452
+ return true;
453
+ }
454
+
455
+ /**
456
+ * Clear current session
457
+ */
458
+ clearCurrentSession() {
459
+ if (this.currentSession) {
460
+ this.activeSessions.delete(this.currentSession.id);
461
+ SecurityUtils.logSecurityEvent(
462
+ 'session_cleared',
463
+ 'info',
464
+ { message: `Session ${this.currentSession.id} cleared` }
465
+ );
466
+ }
467
+ this.currentSession = null;
468
+ this.sessionStartTime = null;
469
+ }
470
+
471
+ /**
472
+ * Check if currently authenticated
473
+ */
474
+ isCurrentlyAuthenticated() {
475
+ return this.currentSession !== null;
476
+ }
477
+
478
+ /**
479
+ * Get current session info
480
+ */
481
+ getCurrentSessionInfo() {
482
+ if (!this.currentSession) {
483
+ return null;
484
+ }
485
+
486
+ return {
487
+ sessionId: this.currentSession.id,
488
+ started: this.sessionStartTime,
489
+ expires: new Date(this.currentSession.expires),
490
+ duration: Date.now() - this.sessionStartTime.getTime()
491
+ };
492
+ }
493
+
494
+ /**
495
+ * Generate secure session ID
496
+ */
497
+ generateSessionId() {
498
+ return crypto.randomBytes(16).toString('hex');
499
+ }
500
+
501
+ /**
502
+ * Disable admin authentication (completely removes PIN)
503
+ */
504
+ async disableAuth() {
505
+ try {
506
+ const config = await this.loadConfig();
507
+ if (config) {
508
+ config.enabled = false;
509
+ config.pinHash = null;
510
+ config.salt = null;
511
+ config.lastModified = new Date().toISOString();
512
+ const success = await this.saveConfig(config);
513
+ if (success) {
514
+ SecurityUtils.logSecurityEvent(
515
+ 'admin_auth_disabled',
516
+ 'info',
517
+ { message: 'Admin authentication disabled' }
518
+ );
519
+ }
520
+ return success;
521
+ }
522
+ return true;
523
+ } catch (error) {
524
+ SecurityUtils.logSecurityEvent(
525
+ 'admin_auth_disable_error',
526
+ 'error',
527
+ { message: `Failed to disable auth: ${error.message}` }
528
+ );
529
+ return false;
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Disable PIN protection (keeps PIN for future re-enable)
535
+ */
536
+ async disablePinProtection() {
537
+ try {
538
+ const config = await this.loadConfig();
539
+ if (config) {
540
+ config.enabled = false;
541
+ config.lastModified = new Date().toISOString();
542
+ const success = await this.saveConfig(config);
543
+ if (success) {
544
+ SecurityUtils.logSecurityEvent('pin_protection_disabled', 'info', 'PIN protection disabled (PIN retained)');
545
+ }
546
+ return success;
547
+ }
548
+ return true;
549
+ } catch (error) {
550
+ SecurityUtils.logSecurityEvent('pin_protection_disable_error', 'error', `Failed to disable PIN protection: ${error.message}`);
551
+ return false;
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Enable PIN protection (requires PIN to be already set)
557
+ */
558
+ async enablePinProtection() {
559
+ try {
560
+ const config = await this.loadConfig();
561
+ if (config && config.pinHash) {
562
+ config.enabled = true;
563
+ config.lastModified = new Date().toISOString();
564
+ const success = await this.saveConfig(config);
565
+ if (success) {
566
+ SecurityUtils.logSecurityEvent('pin_protection_enabled', 'info', 'PIN protection enabled');
567
+ }
568
+ return success;
569
+ }
570
+ return false;
571
+ } catch (error) {
572
+ SecurityUtils.logSecurityEvent('pin_protection_enable_error', 'error', `Failed to enable PIN protection: ${error.message}`);
573
+ return false;
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Record failed authentication attempt
579
+ */
580
+ recordFailedAttempt(clientId) {
581
+ const now = Date.now();
582
+ const attempts = this.failedAttempts.get(clientId) || [];
583
+
584
+ // Remove old attempts (older than lockout duration)
585
+ const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
586
+ recentAttempts.push(now);
587
+
588
+ this.failedAttempts.set(clientId, recentAttempts);
589
+ }
590
+
591
+ /**
592
+ * Clear failed attempts for client
593
+ */
594
+ clearFailedAttempts(clientId) {
595
+ this.failedAttempts.delete(clientId);
596
+ }
597
+
598
+ /**
599
+ * Check if client is locked out
600
+ */
601
+ isLockedOut(clientId) {
602
+ const attempts = this.failedAttempts.get(clientId) || [];
603
+ const now = Date.now();
604
+
605
+ // Remove old attempts
606
+ const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
607
+ this.failedAttempts.set(clientId, recentAttempts);
608
+
609
+ return recentAttempts.length >= this.maxAttempts;
610
+ }
611
+
612
+ /**
613
+ * Destroy session
614
+ */
615
+ destroySession(sessionId) {
616
+ const deleted = this.activeSessions.delete(sessionId);
617
+ if (deleted) {
618
+ SecurityUtils.logSecurityEvent('admin_session_destroyed', 'info', 'Admin session destroyed');
619
+ }
620
+ return deleted;
621
+ }
622
+
623
+ /**
624
+ * Clean up expired sessions
625
+ */
626
+ cleanupExpiredSessions() {
627
+ const now = Date.now();
628
+ let cleaned = 0;
629
+
630
+ for (const [sessionId, session] of this.activeSessions.entries()) {
631
+ const expiresAt = this.getSessionExpiryTime(session);
632
+ if (!Number.isFinite(expiresAt) || now > expiresAt) {
633
+ this.activeSessions.delete(sessionId);
634
+ if (this.currentSession && this.currentSession.id === sessionId) {
635
+ this.currentSession = null;
636
+ this.sessionStartTime = null;
637
+ }
638
+ cleaned++;
639
+ }
640
+ }
641
+
642
+ if (cleaned > 0) {
643
+ SecurityUtils.logSecurityEvent('admin_sessions_cleaned', 'info', `Cleaned up ${cleaned} expired sessions`);
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Clean up expired sessions (alias for backward compatibility)
649
+ */
650
+ cleanupSessions() {
651
+ return this.cleanupExpiredSessions();
652
+ }
653
+ }
654
+
655
+ module.exports = AdminAuth;