i18ntk 2.4.0 → 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.
- package/README.md +6 -5
- package/main/manage/commands/FixerCommand.js +97 -97
- package/main/manage/managers/DebugMenu.js +10 -9
- package/package.json +61 -32
- package/runtime/index.js +14 -8
- package/utils/admin-auth.js +594 -576
- package/utils/config-manager.js +72 -72
- package/utils/env-manager.js +117 -26
- package/utils/i18n-helper.js +50 -49
- package/utils/json-output.js +13 -12
- package/utils/logger.js +7 -6
- package/utils/prompt-helper.js +44 -41
- package/utils/secure-errors.js +156 -154
- package/utils/security.js +235 -233
- package/utils/setup-enforcer.js +110 -109
- package/utils/terminal-icons.js +164 -163
- package/settings/i18ntk-config.json +0 -283
- package/utils/admin-pin.js +0 -520
- package/utils/arg-parser.js +0 -40
- package/utils/cli-args.js +0 -210
- package/utils/mini-commander.js +0 -179
- package/utils/missing-key-validator.js +0 -858
- package/utils/path-utils.js +0 -33
- package/utils/performance-optimizer.js +0 -246
- package/utils/prompt-new.js +0 -55
- package/utils/promptPin.js +0 -76
- package/utils/safe-json.js +0 -40
- package/utils/secure-backup.js +0 -340
- package/utils/security-check-improved.js +0 -393
- package/utils/security-config.js +0 -239
- package/utils/setup-validator.js +0 -717
- package/utils/ultra-performance-optimizer.js +0 -352
package/utils/admin-auth.js
CHANGED
|
@@ -1,576 +1,594 @@
|
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
this.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
+
/**
|
|
192
|
+
* Verify PIN
|
|
193
|
+
*/
|
|
194
|
+
async verifyPin(pin) {
|
|
195
|
+
try {
|
|
196
|
+
const config = await this.loadConfig();
|
|
197
|
+
if (!config || !config.enabled) {
|
|
198
|
+
return true; // No authentication required if not enabled
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for lockout
|
|
202
|
+
const clientId = 'local'; // In a real app, this would be client IP or session ID
|
|
203
|
+
if (this.isLockedOut(clientId)) {
|
|
204
|
+
SecurityUtils.logSecurityEvent(
|
|
205
|
+
'admin_auth_lockout',
|
|
206
|
+
'warning',
|
|
207
|
+
{ message: 'Authentication attempt during lockout period' }
|
|
208
|
+
);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Validate PIN format
|
|
213
|
+
if (!/^\d{4,6}$/.test(pin)) {
|
|
214
|
+
this.recordFailedAttempt(clientId);
|
|
215
|
+
SecurityUtils.logSecurityEvent(
|
|
216
|
+
'admin_auth_invalid_format',
|
|
217
|
+
'warning',
|
|
218
|
+
{ message: 'Invalid PIN format attempted' }
|
|
219
|
+
);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Verify PIN
|
|
224
|
+
const pinHash = this.hashPin(pin, config.salt);
|
|
225
|
+
const isValid = this.timingSafeHexEqual(pinHash, config.pinHash);
|
|
226
|
+
|
|
227
|
+
if (isValid) {
|
|
228
|
+
this.clearFailedAttempts(clientId);
|
|
229
|
+
SecurityUtils.logSecurityEvent(
|
|
230
|
+
'admin_auth_success',
|
|
231
|
+
'info',
|
|
232
|
+
{ message: 'Admin authentication successful' }
|
|
233
|
+
);
|
|
234
|
+
return true;
|
|
235
|
+
} else {
|
|
236
|
+
this.recordFailedAttempt(clientId);
|
|
237
|
+
SecurityUtils.logSecurityEvent(
|
|
238
|
+
'admin_auth_failure',
|
|
239
|
+
'warning',
|
|
240
|
+
{ message: 'Admin authentication failed' }
|
|
241
|
+
);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
SecurityUtils.logSecurityEvent(
|
|
246
|
+
'admin_auth_error',
|
|
247
|
+
'error',
|
|
248
|
+
{ message: `Authentication error: ${error.message}` }
|
|
249
|
+
);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if admin PIN is configured
|
|
256
|
+
*/
|
|
257
|
+
async isPinConfigured() {
|
|
258
|
+
const config = await this.loadConfig();
|
|
259
|
+
return config && config.enabled && config.pinHash;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if authentication is required
|
|
264
|
+
*/
|
|
265
|
+
async isAuthRequired() {
|
|
266
|
+
// Check if admin PIN is enabled in settings
|
|
267
|
+
const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
|
|
268
|
+
if (!(settings.security?.adminPinEnabled)) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const config = await this.loadConfig();
|
|
273
|
+
return config && config.enabled;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if authentication is required for a specific script
|
|
278
|
+
*/
|
|
279
|
+
async isAuthRequiredForScript(scriptName) {
|
|
280
|
+
// Check if admin PIN is enabled globally
|
|
281
|
+
const globalSettings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
|
|
282
|
+
if (!(globalSettings.security?.adminPinEnabled)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check if admin PIN is actually configured
|
|
287
|
+
const config = await this.loadConfig();
|
|
288
|
+
if (!config || !config.enabled || !config.pinHash) {
|
|
289
|
+
return false; // Don't require PIN if admin PIN is not configured
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check if PIN protection is enabled
|
|
293
|
+
const pinProtection = globalSettings.security?.pinProtection;
|
|
294
|
+
if (!pinProtection || !pinProtection.enabled) {
|
|
295
|
+
return false; // Don't require PIN if protection is disabled
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check if this specific script requires protection
|
|
299
|
+
const protectedScripts = pinProtection.protectedScripts || {};
|
|
300
|
+
return protectedScripts[scriptName] !== false; // Default to true if not explicitly set
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Setup process handlers for session cleanup
|
|
305
|
+
*/
|
|
306
|
+
setupProcessHandlers() {
|
|
307
|
+
const cleanup = () => {
|
|
308
|
+
this.clearCurrentSession();
|
|
309
|
+
if (this.cleanupInterval) {
|
|
310
|
+
clearInterval(this.cleanupInterval);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Handle various exit scenarios
|
|
315
|
+
process.on('exit', cleanup);
|
|
316
|
+
process.on('SIGINT', () => {
|
|
317
|
+
cleanup();
|
|
318
|
+
process.exit(0);
|
|
319
|
+
});
|
|
320
|
+
process.on('SIGTERM', () => {
|
|
321
|
+
cleanup();
|
|
322
|
+
process.exit(0);
|
|
323
|
+
});
|
|
324
|
+
process.on('uncaughtException', (error) => {
|
|
325
|
+
SecurityUtils.logSecurityEvent('uncaught_exception', 'error', error.message);
|
|
326
|
+
cleanup();
|
|
327
|
+
process.exit(1);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Create a new authenticated session
|
|
333
|
+
*/
|
|
334
|
+
async createSession(sessionId = null) {
|
|
335
|
+
if (!sessionId) {
|
|
336
|
+
sessionId = this.generateSessionId();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const session = {
|
|
340
|
+
id: sessionId,
|
|
341
|
+
created: new Date().toISOString(),
|
|
342
|
+
lastActivity: new Date().toISOString(),
|
|
343
|
+
expires: new Date(Date.now() + this.sessionTimeout).toISOString()
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
this.activeSessions.set(sessionId, session);
|
|
347
|
+
this.currentSession = session;
|
|
348
|
+
this.sessionStartTime = new Date();
|
|
349
|
+
|
|
350
|
+
SecurityUtils.logSecurityEvent(
|
|
351
|
+
'session_created',
|
|
352
|
+
'info',
|
|
353
|
+
{ message: `Session ${sessionId} created` }
|
|
354
|
+
);
|
|
355
|
+
return sessionId;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Validate current session
|
|
360
|
+
*/
|
|
361
|
+
async validateSession(sessionId) {
|
|
362
|
+
if (!sessionId || !this.currentSession) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (sessionId !== this.currentSession.id) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const session = this.activeSessions.get(sessionId);
|
|
371
|
+
if (!session) {
|
|
372
|
+
this.clearCurrentSession();
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const now = new Date();
|
|
377
|
+
const expires = new Date(session.expires);
|
|
378
|
+
|
|
379
|
+
if (now > expires) {
|
|
380
|
+
this.activeSessions.delete(sessionId);
|
|
381
|
+
this.clearCurrentSession();
|
|
382
|
+
SecurityUtils.logSecurityEvent(
|
|
383
|
+
'session_expired',
|
|
384
|
+
'info',
|
|
385
|
+
{ message: `Session ${sessionId} expired` }
|
|
386
|
+
);
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Update last activity
|
|
391
|
+
session.lastActivity = now.toISOString();
|
|
392
|
+
session.expires = new Date(now.getTime() + this.sessionTimeout).toISOString();
|
|
393
|
+
this.activeSessions.set(sessionId, session);
|
|
394
|
+
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Clear current session
|
|
400
|
+
*/
|
|
401
|
+
clearCurrentSession() {
|
|
402
|
+
if (this.currentSession) {
|
|
403
|
+
this.activeSessions.delete(this.currentSession.id);
|
|
404
|
+
SecurityUtils.logSecurityEvent(
|
|
405
|
+
'session_cleared',
|
|
406
|
+
'info',
|
|
407
|
+
{ message: `Session ${this.currentSession.id} cleared` }
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
this.currentSession = null;
|
|
411
|
+
this.sessionStartTime = null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Check if currently authenticated
|
|
416
|
+
*/
|
|
417
|
+
isCurrentlyAuthenticated() {
|
|
418
|
+
return this.currentSession !== null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get current session info
|
|
423
|
+
*/
|
|
424
|
+
getCurrentSessionInfo() {
|
|
425
|
+
if (!this.currentSession) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
sessionId: this.currentSession.id,
|
|
431
|
+
started: this.sessionStartTime,
|
|
432
|
+
expires: new Date(this.currentSession.expires),
|
|
433
|
+
duration: Date.now() - this.sessionStartTime.getTime()
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Generate secure session ID
|
|
439
|
+
*/
|
|
440
|
+
generateSessionId() {
|
|
441
|
+
return crypto.randomBytes(16).toString('hex');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Disable admin authentication (completely removes PIN)
|
|
446
|
+
*/
|
|
447
|
+
async disableAuth() {
|
|
448
|
+
try {
|
|
449
|
+
const config = await this.loadConfig();
|
|
450
|
+
if (config) {
|
|
451
|
+
config.enabled = false;
|
|
452
|
+
config.pinHash = null;
|
|
453
|
+
config.salt = null;
|
|
454
|
+
config.lastModified = new Date().toISOString();
|
|
455
|
+
const success = await this.saveConfig(config);
|
|
456
|
+
if (success) {
|
|
457
|
+
SecurityUtils.logSecurityEvent(
|
|
458
|
+
'admin_auth_disabled',
|
|
459
|
+
'info',
|
|
460
|
+
{ message: 'Admin authentication disabled' }
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return success;
|
|
464
|
+
}
|
|
465
|
+
return true;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
SecurityUtils.logSecurityEvent(
|
|
468
|
+
'admin_auth_disable_error',
|
|
469
|
+
'error',
|
|
470
|
+
{ message: `Failed to disable auth: ${error.message}` }
|
|
471
|
+
);
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Disable PIN protection (keeps PIN for future re-enable)
|
|
478
|
+
*/
|
|
479
|
+
async disablePinProtection() {
|
|
480
|
+
try {
|
|
481
|
+
const config = await this.loadConfig();
|
|
482
|
+
if (config) {
|
|
483
|
+
config.enabled = false;
|
|
484
|
+
config.lastModified = new Date().toISOString();
|
|
485
|
+
const success = await this.saveConfig(config);
|
|
486
|
+
if (success) {
|
|
487
|
+
SecurityUtils.logSecurityEvent('pin_protection_disabled', 'info', 'PIN protection disabled (PIN retained)');
|
|
488
|
+
}
|
|
489
|
+
return success;
|
|
490
|
+
}
|
|
491
|
+
return true;
|
|
492
|
+
} catch (error) {
|
|
493
|
+
SecurityUtils.logSecurityEvent('pin_protection_disable_error', 'error', `Failed to disable PIN protection: ${error.message}`);
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Enable PIN protection (requires PIN to be already set)
|
|
500
|
+
*/
|
|
501
|
+
async enablePinProtection() {
|
|
502
|
+
try {
|
|
503
|
+
const config = await this.loadConfig();
|
|
504
|
+
if (config && config.pinHash) {
|
|
505
|
+
config.enabled = true;
|
|
506
|
+
config.lastModified = new Date().toISOString();
|
|
507
|
+
const success = await this.saveConfig(config);
|
|
508
|
+
if (success) {
|
|
509
|
+
SecurityUtils.logSecurityEvent('pin_protection_enabled', 'info', 'PIN protection enabled');
|
|
510
|
+
}
|
|
511
|
+
return success;
|
|
512
|
+
}
|
|
513
|
+
return false;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
SecurityUtils.logSecurityEvent('pin_protection_enable_error', 'error', `Failed to enable PIN protection: ${error.message}`);
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Record failed authentication attempt
|
|
522
|
+
*/
|
|
523
|
+
recordFailedAttempt(clientId) {
|
|
524
|
+
const now = Date.now();
|
|
525
|
+
const attempts = this.failedAttempts.get(clientId) || [];
|
|
526
|
+
|
|
527
|
+
// Remove old attempts (older than lockout duration)
|
|
528
|
+
const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
|
|
529
|
+
recentAttempts.push(now);
|
|
530
|
+
|
|
531
|
+
this.failedAttempts.set(clientId, recentAttempts);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Clear failed attempts for client
|
|
536
|
+
*/
|
|
537
|
+
clearFailedAttempts(clientId) {
|
|
538
|
+
this.failedAttempts.delete(clientId);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Check if client is locked out
|
|
543
|
+
*/
|
|
544
|
+
isLockedOut(clientId) {
|
|
545
|
+
const attempts = this.failedAttempts.get(clientId) || [];
|
|
546
|
+
const now = Date.now();
|
|
547
|
+
|
|
548
|
+
// Remove old attempts
|
|
549
|
+
const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
|
|
550
|
+
this.failedAttempts.set(clientId, recentAttempts);
|
|
551
|
+
|
|
552
|
+
return recentAttempts.length >= this.maxAttempts;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Destroy session
|
|
557
|
+
*/
|
|
558
|
+
destroySession(sessionId) {
|
|
559
|
+
const deleted = this.activeSessions.delete(sessionId);
|
|
560
|
+
if (deleted) {
|
|
561
|
+
SecurityUtils.logSecurityEvent('admin_session_destroyed', 'info', 'Admin session destroyed');
|
|
562
|
+
}
|
|
563
|
+
return deleted;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Clean up expired sessions
|
|
568
|
+
*/
|
|
569
|
+
cleanupExpiredSessions() {
|
|
570
|
+
const now = Date.now();
|
|
571
|
+
let cleaned = 0;
|
|
572
|
+
|
|
573
|
+
for (const [sessionId, session] of this.activeSessions.entries()) {
|
|
574
|
+
const expiresAt = session.expiresAt || new Date(session.expires).getTime();
|
|
575
|
+
if (!Number.isFinite(expiresAt) || now > expiresAt) {
|
|
576
|
+
this.activeSessions.delete(sessionId);
|
|
577
|
+
cleaned++;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (cleaned > 0) {
|
|
582
|
+
SecurityUtils.logSecurityEvent('admin_sessions_cleaned', 'info', `Cleaned up ${cleaned} expired sessions`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Clean up expired sessions (alias for backward compatibility)
|
|
588
|
+
*/
|
|
589
|
+
cleanupSessions() {
|
|
590
|
+
return this.cleanupExpiredSessions();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
module.exports = AdminAuth;
|