@omindu/yaksha 1.0.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.
@@ -0,0 +1,441 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Yaksha Authentication System
5
+ * Supports multiple authentication methods: password, token, certificate
6
+ */
7
+
8
+ const crypto = require('crypto');
9
+ const EventEmitter = require('events');
10
+
11
+ // Authentication methods
12
+ const AUTH_METHODS = {
13
+ PASSWORD: 'password',
14
+ TOKEN: 'token',
15
+ CERTIFICATE: 'certificate'
16
+ };
17
+
18
+ // Authentication states
19
+ const AUTH_STATES = {
20
+ PENDING: 'pending',
21
+ AUTHENTICATED: 'authenticated',
22
+ FAILED: 'failed',
23
+ LOCKED: 'locked'
24
+ };
25
+
26
+ class Authentication extends EventEmitter {
27
+ constructor(method = 'token', options = {}) {
28
+ super();
29
+
30
+ this.method = method;
31
+ this.maxAttempts = options.maxAttempts || 10;
32
+ this.lockoutDuration = options.lockoutDuration || 300000; // 5 minutes
33
+ this.rateLimitWindow = options.rateLimitWindow || 60000; // 1 minute
34
+ this.maxAttemptsPerWindow = options.maxAttemptsPerWindow || 5;
35
+ this.sessionTimeout = options.sessionTimeout || 3600000; // 1 hour
36
+ this.tokenRotationInterval = options.tokenRotationInterval || 86400000; // 24 hours
37
+
38
+ // Storage for credentials and sessions
39
+ this.credentials = new Map(); // username/id -> credential data
40
+ this.sessions = new Map(); // sessionId -> session data
41
+ this.attempts = new Map(); // identifier -> attempt data
42
+ this.lockouts = new Map(); // identifier -> lockout timestamp
43
+ }
44
+
45
+ /**
46
+ * Hash password using PBKDF2
47
+ */
48
+ _hashPassword(password, salt) {
49
+ salt = salt || crypto.randomBytes(32);
50
+ const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512');
51
+ return { hash, salt };
52
+ }
53
+
54
+ /**
55
+ * Verify password (constant-time comparison)
56
+ */
57
+ _verifyPassword(password, storedHash, salt) {
58
+ const { hash } = this._hashPassword(password, salt);
59
+ return crypto.timingSafeEqual(hash, storedHash);
60
+ }
61
+
62
+ /**
63
+ * Generate random token
64
+ */
65
+ _generateToken() {
66
+ return crypto.randomBytes(32).toString('hex');
67
+ }
68
+
69
+ /**
70
+ * Generate challenge for challenge-response authentication
71
+ */
72
+ _generateChallenge() {
73
+ return crypto.randomBytes(32);
74
+ }
75
+
76
+ /**
77
+ * Verify TOTP (Time-based One-Time Password) for 2FA
78
+ */
79
+ _verifyTOTP(token, secret, window = 1) {
80
+ // Simplified TOTP verification
81
+ // In production, use a proper TOTP library
82
+ const time = Math.floor(Date.now() / 30000); // 30-second window
83
+
84
+ for (let i = -window; i <= window; i++) {
85
+ const expectedToken = this._generateTOTP(secret, time + i);
86
+ if (token === expectedToken) {
87
+ return true;
88
+ }
89
+ }
90
+
91
+ return false;
92
+ }
93
+
94
+ /**
95
+ * Generate TOTP token
96
+ */
97
+ _generateTOTP(secret, time) {
98
+ // Simplified TOTP generation
99
+ const hmac = crypto.createHmac('sha1', Buffer.from(secret, 'hex'));
100
+ hmac.update(Buffer.from([0, 0, 0, 0, 0, 0, 0, time]));
101
+ const hash = hmac.digest();
102
+
103
+ const offset = hash[hash.length - 1] & 0xf;
104
+ const code = ((hash[offset] & 0x7f) << 24) |
105
+ ((hash[offset + 1] & 0xff) << 16) |
106
+ ((hash[offset + 2] & 0xff) << 8) |
107
+ (hash[offset + 3] & 0xff);
108
+
109
+ return String(code % 1000000).padStart(6, '0');
110
+ }
111
+
112
+ /**
113
+ * Check if identifier is rate limited
114
+ */
115
+ _isRateLimited(identifier) {
116
+ const now = Date.now();
117
+ const attemptData = this.attempts.get(identifier);
118
+
119
+ if (!attemptData) {
120
+ return false;
121
+ }
122
+
123
+ // Remove old attempts outside the window
124
+ attemptData.timestamps = attemptData.timestamps.filter(
125
+ ts => now - ts < this.rateLimitWindow
126
+ );
127
+
128
+ if (attemptData.timestamps.length >= this.maxAttemptsPerWindow) {
129
+ return true;
130
+ }
131
+
132
+ return false;
133
+ }
134
+
135
+ /**
136
+ * Check if identifier is locked out
137
+ */
138
+ _isLockedOut(identifier) {
139
+ const lockoutTime = this.lockouts.get(identifier);
140
+ if (!lockoutTime) {
141
+ return false;
142
+ }
143
+
144
+ const now = Date.now();
145
+ if (now - lockoutTime > this.lockoutDuration) {
146
+ this.lockouts.delete(identifier);
147
+ return false;
148
+ }
149
+
150
+ return true;
151
+ }
152
+
153
+ /**
154
+ * Record authentication attempt
155
+ */
156
+ _recordAttempt(identifier, success) {
157
+ const now = Date.now();
158
+
159
+ if (!this.attempts.has(identifier)) {
160
+ this.attempts.set(identifier, {
161
+ total: 0,
162
+ failed: 0,
163
+ timestamps: []
164
+ });
165
+ }
166
+
167
+ const attemptData = this.attempts.get(identifier);
168
+ attemptData.total++;
169
+ attemptData.timestamps.push(now);
170
+
171
+ if (!success) {
172
+ attemptData.failed++;
173
+
174
+ // Check if should lock out
175
+ if (attemptData.failed >= this.maxAttempts) {
176
+ this.lockouts.set(identifier, now);
177
+ this.emit('lockout', identifier);
178
+ }
179
+ } else {
180
+ // Reset on successful authentication
181
+ attemptData.failed = 0;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Register user with password
187
+ */
188
+ registerPassword(username, password) {
189
+ if (this.method !== AUTH_METHODS.PASSWORD) {
190
+ throw new Error('Authentication method is not password');
191
+ }
192
+
193
+ const { hash, salt } = this._hashPassword(password);
194
+
195
+ this.credentials.set(username, {
196
+ method: AUTH_METHODS.PASSWORD,
197
+ hash,
198
+ salt,
199
+ createdAt: Date.now()
200
+ });
201
+
202
+ return true;
203
+ }
204
+
205
+ /**
206
+ * Register user with token
207
+ */
208
+ registerToken(identifier, token) {
209
+ if (this.method !== AUTH_METHODS.TOKEN) {
210
+ throw new Error('Authentication method is not token');
211
+ }
212
+
213
+ token = token || this._generateToken();
214
+
215
+ this.credentials.set(identifier, {
216
+ method: AUTH_METHODS.TOKEN,
217
+ token,
218
+ createdAt: Date.now(),
219
+ expiresAt: Date.now() + this.tokenRotationInterval
220
+ });
221
+
222
+ return token;
223
+ }
224
+
225
+ /**
226
+ * Register user with certificate
227
+ */
228
+ registerCertificate(identifier, certificate, options = {}) {
229
+ if (this.method !== AUTH_METHODS.CERTIFICATE) {
230
+ throw new Error('Authentication method is not certificate');
231
+ }
232
+
233
+ this.credentials.set(identifier, {
234
+ method: AUTH_METHODS.CERTIFICATE,
235
+ certificate,
236
+ twoFactorSecret: options.twoFactorSecret || null,
237
+ createdAt: Date.now()
238
+ });
239
+
240
+ return true;
241
+ }
242
+
243
+ /**
244
+ * Authenticate with password
245
+ */
246
+ authenticatePassword(username, password) {
247
+ if (this._isLockedOut(username)) {
248
+ return { success: false, state: AUTH_STATES.LOCKED, reason: 'Account locked' };
249
+ }
250
+
251
+ if (this._isRateLimited(username)) {
252
+ return { success: false, state: AUTH_STATES.FAILED, reason: 'Rate limit exceeded' };
253
+ }
254
+
255
+ const credential = this.credentials.get(username);
256
+
257
+ if (!credential || credential.method !== AUTH_METHODS.PASSWORD) {
258
+ this._recordAttempt(username, false);
259
+ return { success: false, state: AUTH_STATES.FAILED, reason: 'Invalid credentials' };
260
+ }
261
+
262
+ const isValid = this._verifyPassword(password, credential.hash, credential.salt);
263
+ this._recordAttempt(username, isValid);
264
+
265
+ if (!isValid) {
266
+ return { success: false, state: AUTH_STATES.FAILED, reason: 'Invalid credentials' };
267
+ }
268
+
269
+ // Create session
270
+ const sessionId = this._generateToken();
271
+ this.sessions.set(sessionId, {
272
+ identifier: username,
273
+ createdAt: Date.now(),
274
+ expiresAt: Date.now() + this.sessionTimeout
275
+ });
276
+
277
+ this.emit('authenticated', username, sessionId);
278
+
279
+ return {
280
+ success: true,
281
+ state: AUTH_STATES.AUTHENTICATED,
282
+ sessionId,
283
+ expiresAt: Date.now() + this.sessionTimeout
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Authenticate with token
289
+ */
290
+ authenticateToken(identifier, token) {
291
+ if (this._isLockedOut(identifier)) {
292
+ return { success: false, state: AUTH_STATES.LOCKED, reason: 'Account locked' };
293
+ }
294
+
295
+ if (this._isRateLimited(identifier)) {
296
+ return { success: false, state: AUTH_STATES.FAILED, reason: 'Rate limit exceeded' };
297
+ }
298
+
299
+ const credential = this.credentials.get(identifier);
300
+
301
+ if (!credential || credential.method !== AUTH_METHODS.TOKEN) {
302
+ this._recordAttempt(identifier, false);
303
+ return { success: false, state: AUTH_STATES.FAILED, reason: 'Invalid token' };
304
+ }
305
+
306
+ // Check token expiration
307
+ if (credential.expiresAt && Date.now() > credential.expiresAt) {
308
+ return { success: false, state: AUTH_STATES.FAILED, reason: 'Token expired' };
309
+ }
310
+
311
+ // Constant-time comparison
312
+ const isValid = crypto.timingSafeEqual(
313
+ Buffer.from(token),
314
+ Buffer.from(credential.token)
315
+ );
316
+
317
+ this._recordAttempt(identifier, isValid);
318
+
319
+ if (!isValid) {
320
+ return { success: false, state: AUTH_STATES.FAILED, reason: 'Invalid token' };
321
+ }
322
+
323
+ // Create session
324
+ const sessionId = this._generateToken();
325
+ this.sessions.set(sessionId, {
326
+ identifier,
327
+ createdAt: Date.now(),
328
+ expiresAt: Date.now() + this.sessionTimeout
329
+ });
330
+
331
+ this.emit('authenticated', identifier, sessionId);
332
+
333
+ return {
334
+ success: true,
335
+ state: AUTH_STATES.AUTHENTICATED,
336
+ sessionId,
337
+ expiresAt: Date.now() + this.sessionTimeout
338
+ };
339
+ }
340
+
341
+ /**
342
+ * Authenticate with certificate
343
+ */
344
+ authenticateCertificate(identifier, certificate, totpToken = null) {
345
+ if (this._isLockedOut(identifier)) {
346
+ return { success: false, state: AUTH_STATES.LOCKED, reason: 'Account locked' };
347
+ }
348
+
349
+ const credential = this.credentials.get(identifier);
350
+
351
+ if (!credential || credential.method !== AUTH_METHODS.CERTIFICATE) {
352
+ this._recordAttempt(identifier, false);
353
+ return { success: false, state: AUTH_STATES.FAILED, reason: 'Invalid certificate' };
354
+ }
355
+
356
+ // In production, perform proper certificate validation
357
+ const isValid = certificate === credential.certificate;
358
+
359
+ if (!isValid) {
360
+ this._recordAttempt(identifier, false);
361
+ return { success: false, state: AUTH_STATES.FAILED, reason: 'Invalid certificate' };
362
+ }
363
+
364
+ // Check 2FA if enabled
365
+ if (credential.twoFactorSecret) {
366
+ if (!totpToken || !this._verifyTOTP(totpToken, credential.twoFactorSecret)) {
367
+ this._recordAttempt(identifier, false);
368
+ return { success: false, state: AUTH_STATES.FAILED, reason: '2FA verification failed' };
369
+ }
370
+ }
371
+
372
+ this._recordAttempt(identifier, true);
373
+
374
+ // Create session
375
+ const sessionId = this._generateToken();
376
+ this.sessions.set(sessionId, {
377
+ identifier,
378
+ createdAt: Date.now(),
379
+ expiresAt: Date.now() + this.sessionTimeout
380
+ });
381
+
382
+ this.emit('authenticated', identifier, sessionId);
383
+
384
+ return {
385
+ success: true,
386
+ state: AUTH_STATES.AUTHENTICATED,
387
+ sessionId,
388
+ expiresAt: Date.now() + this.sessionTimeout
389
+ };
390
+ }
391
+
392
+ /**
393
+ * Validate session
394
+ */
395
+ validateSession(sessionId) {
396
+ const session = this.sessions.get(sessionId);
397
+
398
+ if (!session) {
399
+ return { valid: false, reason: 'Invalid session' };
400
+ }
401
+
402
+ if (Date.now() > session.expiresAt) {
403
+ this.sessions.delete(sessionId);
404
+ return { valid: false, reason: 'Session expired' };
405
+ }
406
+
407
+ return {
408
+ valid: true,
409
+ identifier: session.identifier,
410
+ expiresAt: session.expiresAt
411
+ };
412
+ }
413
+
414
+ /**
415
+ * Revoke session
416
+ */
417
+ revokeSession(sessionId) {
418
+ return this.sessions.delete(sessionId);
419
+ }
420
+
421
+ /**
422
+ * Cleanup expired sessions
423
+ */
424
+ cleanupSessions() {
425
+ const now = Date.now();
426
+ let cleaned = 0;
427
+
428
+ for (const [sessionId, session] of this.sessions.entries()) {
429
+ if (now > session.expiresAt) {
430
+ this.sessions.delete(sessionId);
431
+ cleaned++;
432
+ }
433
+ }
434
+
435
+ return cleaned;
436
+ }
437
+ }
438
+
439
+ module.exports = Authentication;
440
+ module.exports.AUTH_METHODS = AUTH_METHODS;
441
+ module.exports.AUTH_STATES = AUTH_STATES;