@sparkleideas/security 3.0.0-alpha.10

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.
Files changed (34) hide show
  1. package/README.md +234 -0
  2. package/__tests__/acceptance/security-compliance.test.ts +674 -0
  3. package/__tests__/credential-generator.test.ts +310 -0
  4. package/__tests__/fixtures/configurations.ts +419 -0
  5. package/__tests__/fixtures/index.ts +21 -0
  6. package/__tests__/helpers/create-mock.ts +469 -0
  7. package/__tests__/helpers/index.ts +32 -0
  8. package/__tests__/input-validator.test.ts +381 -0
  9. package/__tests__/integration/security-flow.test.ts +606 -0
  10. package/__tests__/password-hasher.test.ts +239 -0
  11. package/__tests__/path-validator.test.ts +302 -0
  12. package/__tests__/safe-executor.test.ts +292 -0
  13. package/__tests__/token-generator.test.ts +371 -0
  14. package/__tests__/unit/credential-generator.test.ts +182 -0
  15. package/__tests__/unit/password-hasher.test.ts +359 -0
  16. package/__tests__/unit/path-validator.test.ts +509 -0
  17. package/__tests__/unit/safe-executor.test.ts +667 -0
  18. package/__tests__/unit/token-generator.test.ts +310 -0
  19. package/package.json +28 -0
  20. package/src/CVE-REMEDIATION.ts +251 -0
  21. package/src/application/index.ts +10 -0
  22. package/src/application/services/security-application-service.ts +193 -0
  23. package/src/credential-generator.ts +368 -0
  24. package/src/domain/entities/security-context.ts +173 -0
  25. package/src/domain/index.ts +17 -0
  26. package/src/domain/services/security-domain-service.ts +296 -0
  27. package/src/index.ts +271 -0
  28. package/src/input-validator.ts +466 -0
  29. package/src/password-hasher.ts +270 -0
  30. package/src/path-validator.ts +525 -0
  31. package/src/safe-executor.ts +525 -0
  32. package/src/token-generator.ts +463 -0
  33. package/tmp.json +0 -0
  34. package/tsconfig.json +9 -0
@@ -0,0 +1,463 @@
1
+ /**
2
+ * Token Generator - Secure Token Generation
3
+ *
4
+ * Provides cryptographically secure token generation for:
5
+ * - JWT tokens
6
+ * - Session tokens
7
+ * - CSRF tokens
8
+ * - API tokens
9
+ * - Verification codes
10
+ *
11
+ * Security Properties:
12
+ * - Uses crypto.randomBytes for all randomness
13
+ * - Configurable entropy levels
14
+ * - Timing-safe comparison
15
+ * - Token expiration handling
16
+ *
17
+ * @module v3/security/token-generator
18
+ */
19
+
20
+ import { randomBytes, createHmac, timingSafeEqual } from 'crypto';
21
+
22
+ export interface TokenConfig {
23
+ /**
24
+ * Default token length in bytes.
25
+ * Default: 32 (256 bits)
26
+ */
27
+ defaultLength?: number;
28
+
29
+ /**
30
+ * Token encoding format.
31
+ * Default: 'base64url'
32
+ */
33
+ encoding?: 'hex' | 'base64' | 'base64url';
34
+
35
+ /**
36
+ * HMAC secret for signed tokens.
37
+ */
38
+ hmacSecret?: string;
39
+
40
+ /**
41
+ * Default expiration time in seconds.
42
+ * Default: 3600 (1 hour)
43
+ */
44
+ defaultExpiration?: number;
45
+ }
46
+
47
+ export interface Token {
48
+ value: string;
49
+ createdAt: Date;
50
+ expiresAt: Date;
51
+ metadata?: Record<string, unknown>;
52
+ }
53
+
54
+ export interface SignedToken {
55
+ token: string;
56
+ signature: string;
57
+ combined: string;
58
+ createdAt: Date;
59
+ expiresAt: Date;
60
+ }
61
+
62
+ export interface VerificationCode {
63
+ code: string;
64
+ createdAt: Date;
65
+ expiresAt: Date;
66
+ attempts: number;
67
+ maxAttempts: number;
68
+ }
69
+
70
+ export class TokenGeneratorError extends Error {
71
+ constructor(
72
+ message: string,
73
+ public readonly code: string,
74
+ ) {
75
+ super(message);
76
+ this.name = 'TokenGeneratorError';
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Secure token generator.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const generator = new TokenGenerator({ hmacSecret: 'secret' });
86
+ *
87
+ * // Generate session token
88
+ * const session = generator.generateSessionToken();
89
+ *
90
+ * // Generate signed token
91
+ * const signed = generator.generateSignedToken({ userId: '123' });
92
+ *
93
+ * // Verify signed token
94
+ * const isValid = generator.verifySignedToken(signed.combined);
95
+ * ```
96
+ */
97
+ export class TokenGenerator {
98
+ private readonly config: Required<TokenConfig>;
99
+
100
+ constructor(config: TokenConfig = {}) {
101
+ this.config = {
102
+ defaultLength: config.defaultLength ?? 32,
103
+ encoding: config.encoding ?? 'base64url',
104
+ hmacSecret: config.hmacSecret ?? '',
105
+ defaultExpiration: config.defaultExpiration ?? 3600,
106
+ };
107
+
108
+ if (this.config.defaultLength < 16) {
109
+ throw new TokenGeneratorError(
110
+ 'Token length must be at least 16 bytes',
111
+ 'INVALID_LENGTH'
112
+ );
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Generates a random token.
118
+ *
119
+ * @param length - Token length in bytes
120
+ * @returns Random token string
121
+ */
122
+ generate(length?: number): string {
123
+ const len = length ?? this.config.defaultLength;
124
+ const buffer = randomBytes(len);
125
+ return this.encode(buffer);
126
+ }
127
+
128
+ /**
129
+ * Generates a token with expiration.
130
+ *
131
+ * @param expirationSeconds - Expiration time in seconds
132
+ * @param metadata - Optional metadata to attach
133
+ * @returns Token with expiration
134
+ */
135
+ generateWithExpiration(
136
+ expirationSeconds?: number,
137
+ metadata?: Record<string, unknown>
138
+ ): Token {
139
+ const expiration = expirationSeconds ?? this.config.defaultExpiration;
140
+ const now = new Date();
141
+ const expiresAt = new Date(now.getTime() + expiration * 1000);
142
+
143
+ return {
144
+ value: this.generate(),
145
+ createdAt: now,
146
+ expiresAt,
147
+ metadata,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Generates a session token (URL-safe).
153
+ *
154
+ * @param length - Token length in bytes (default: 32)
155
+ * @returns Session token
156
+ */
157
+ generateSessionToken(length = 32): Token {
158
+ return this.generateWithExpiration(this.config.defaultExpiration);
159
+ }
160
+
161
+ /**
162
+ * Generates a CSRF token.
163
+ *
164
+ * @returns CSRF token (shorter expiration)
165
+ */
166
+ generateCsrfToken(): Token {
167
+ return this.generateWithExpiration(1800); // 30 minutes
168
+ }
169
+
170
+ /**
171
+ * Generates an API token with prefix.
172
+ *
173
+ * @param prefix - Token prefix (e.g., 'cf_')
174
+ * @returns Prefixed API token
175
+ */
176
+ generateApiToken(prefix = 'cf_'): Token {
177
+ const tokenBody = this.generate(32);
178
+ const now = new Date();
179
+
180
+ return {
181
+ value: `${prefix}${tokenBody}`,
182
+ createdAt: now,
183
+ expiresAt: new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000), // 1 year
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Generates a numeric verification code.
189
+ *
190
+ * @param length - Number of digits (default: 6)
191
+ * @param expirationMinutes - Expiration in minutes (default: 10)
192
+ * @param maxAttempts - Maximum verification attempts (default: 3)
193
+ * @returns Verification code
194
+ */
195
+ generateVerificationCode(
196
+ length = 6,
197
+ expirationMinutes = 10,
198
+ maxAttempts = 3
199
+ ): VerificationCode {
200
+ const buffer = randomBytes(length);
201
+ let code = '';
202
+
203
+ for (let i = 0; i < length; i++) {
204
+ code += (buffer[i] % 10).toString();
205
+ }
206
+
207
+ const now = new Date();
208
+
209
+ return {
210
+ code,
211
+ createdAt: now,
212
+ expiresAt: new Date(now.getTime() + expirationMinutes * 60 * 1000),
213
+ attempts: 0,
214
+ maxAttempts,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Generates a signed token using HMAC.
220
+ *
221
+ * @param payload - Data to include in token
222
+ * @param expirationSeconds - Token expiration
223
+ * @returns Signed token
224
+ */
225
+ generateSignedToken(
226
+ payload: Record<string, unknown>,
227
+ expirationSeconds?: number
228
+ ): SignedToken {
229
+ if (!this.config.hmacSecret) {
230
+ throw new TokenGeneratorError(
231
+ 'HMAC secret required for signed tokens',
232
+ 'NO_SECRET'
233
+ );
234
+ }
235
+
236
+ const expiration = expirationSeconds ?? this.config.defaultExpiration;
237
+ const now = new Date();
238
+ const expiresAt = new Date(now.getTime() + expiration * 1000);
239
+
240
+ const tokenData = {
241
+ ...payload,
242
+ iat: now.getTime(),
243
+ exp: expiresAt.getTime(),
244
+ nonce: this.generate(8),
245
+ };
246
+
247
+ const token = Buffer.from(JSON.stringify(tokenData)).toString('base64url');
248
+ const signature = this.sign(token);
249
+
250
+ return {
251
+ token,
252
+ signature,
253
+ combined: `${token}.${signature}`,
254
+ createdAt: now,
255
+ expiresAt,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Verifies a signed token.
261
+ *
262
+ * @param combined - Combined token string (token.signature)
263
+ * @returns Decoded payload if valid, null otherwise
264
+ */
265
+ verifySignedToken(combined: string): Record<string, unknown> | null {
266
+ if (!this.config.hmacSecret) {
267
+ throw new TokenGeneratorError(
268
+ 'HMAC secret required for signed tokens',
269
+ 'NO_SECRET'
270
+ );
271
+ }
272
+
273
+ const parts = combined.split('.');
274
+ if (parts.length !== 2) {
275
+ return null;
276
+ }
277
+
278
+ const [token, signature] = parts;
279
+
280
+ // Verify signature
281
+ const expectedSignature = this.sign(token);
282
+
283
+ try {
284
+ const sigBuffer = Buffer.from(signature, 'base64url');
285
+ const expectedBuffer = Buffer.from(expectedSignature, 'base64url');
286
+
287
+ if (sigBuffer.length !== expectedBuffer.length) {
288
+ return null;
289
+ }
290
+
291
+ if (!timingSafeEqual(sigBuffer, expectedBuffer)) {
292
+ return null;
293
+ }
294
+ } catch {
295
+ return null;
296
+ }
297
+
298
+ // Decode and validate payload
299
+ try {
300
+ const payload = JSON.parse(Buffer.from(token, 'base64url').toString());
301
+
302
+ // Check expiration
303
+ if (payload.exp && payload.exp < Date.now()) {
304
+ return null;
305
+ }
306
+
307
+ return payload;
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Generates a refresh token pair.
315
+ *
316
+ * @returns Access and refresh tokens
317
+ */
318
+ generateTokenPair(): {
319
+ accessToken: Token;
320
+ refreshToken: Token;
321
+ } {
322
+ return {
323
+ accessToken: this.generateWithExpiration(900), // 15 minutes
324
+ refreshToken: this.generateWithExpiration(604800), // 7 days
325
+ };
326
+ }
327
+
328
+ /**
329
+ * Generates a password reset token.
330
+ *
331
+ * @returns Password reset token (short expiration)
332
+ */
333
+ generatePasswordResetToken(): Token {
334
+ return this.generateWithExpiration(1800); // 30 minutes
335
+ }
336
+
337
+ /**
338
+ * Generates an email verification token.
339
+ *
340
+ * @returns Email verification token
341
+ */
342
+ generateEmailVerificationToken(): Token {
343
+ return this.generateWithExpiration(86400); // 24 hours
344
+ }
345
+
346
+ /**
347
+ * Generates a unique request ID.
348
+ *
349
+ * @returns Request ID (shorter, for logging)
350
+ */
351
+ generateRequestId(): string {
352
+ return this.generate(8);
353
+ }
354
+
355
+ /**
356
+ * Generates a correlation ID for distributed tracing.
357
+ *
358
+ * @returns Correlation ID
359
+ */
360
+ generateCorrelationId(): string {
361
+ const timestamp = Date.now().toString(36);
362
+ const random = this.generate(8);
363
+ return `${timestamp}-${random}`;
364
+ }
365
+
366
+ /**
367
+ * Checks if a token has expired.
368
+ *
369
+ * @param token - Token to check
370
+ * @returns True if expired
371
+ */
372
+ isExpired(token: Token | VerificationCode): boolean {
373
+ return token.expiresAt < new Date();
374
+ }
375
+
376
+ /**
377
+ * Compares two tokens in constant time.
378
+ *
379
+ * @param a - First token
380
+ * @param b - Second token
381
+ * @returns True if equal
382
+ */
383
+ compare(a: string, b: string): boolean {
384
+ if (a.length !== b.length) {
385
+ return false;
386
+ }
387
+
388
+ try {
389
+ const bufferA = Buffer.from(a);
390
+ const bufferB = Buffer.from(b);
391
+ return timingSafeEqual(bufferA, bufferB);
392
+ } catch {
393
+ return false;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Signs data using HMAC-SHA256.
399
+ */
400
+ private sign(data: string): string {
401
+ return createHmac('sha256', this.config.hmacSecret)
402
+ .update(data)
403
+ .digest('base64url');
404
+ }
405
+
406
+ /**
407
+ * Encodes bytes according to configuration.
408
+ */
409
+ private encode(buffer: Buffer): string {
410
+ switch (this.config.encoding) {
411
+ case 'hex':
412
+ return buffer.toString('hex');
413
+ case 'base64':
414
+ return buffer.toString('base64');
415
+ case 'base64url':
416
+ default:
417
+ return buffer.toString('base64url');
418
+ }
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Factory function to create a production token generator.
424
+ *
425
+ * @param hmacSecret - HMAC secret for signed tokens
426
+ * @returns Configured TokenGenerator
427
+ */
428
+ export function createTokenGenerator(hmacSecret: string): TokenGenerator {
429
+ return new TokenGenerator({
430
+ hmacSecret,
431
+ defaultLength: 32,
432
+ encoding: 'base64url',
433
+ });
434
+ }
435
+
436
+ /**
437
+ * Singleton instance for quick token generation without configuration.
438
+ */
439
+ let defaultGenerator: TokenGenerator | null = null;
440
+
441
+ /**
442
+ * Gets or creates the default token generator.
443
+ * Note: Does not support signed tokens without configuration.
444
+ */
445
+ export function getDefaultGenerator(): TokenGenerator {
446
+ if (!defaultGenerator) {
447
+ defaultGenerator = new TokenGenerator();
448
+ }
449
+ return defaultGenerator;
450
+ }
451
+
452
+ /**
453
+ * Quick token generation functions.
454
+ */
455
+ export const quickGenerate = {
456
+ token: (length = 32) => getDefaultGenerator().generate(length),
457
+ sessionToken: () => getDefaultGenerator().generateSessionToken(),
458
+ csrfToken: () => getDefaultGenerator().generateCsrfToken(),
459
+ apiToken: (prefix = 'cf_') => getDefaultGenerator().generateApiToken(prefix),
460
+ verificationCode: (length = 6) => getDefaultGenerator().generateVerificationCode(length),
461
+ requestId: () => getDefaultGenerator().generateRequestId(),
462
+ correlationId: () => getDefaultGenerator().generateCorrelationId(),
463
+ };
package/tmp.json ADDED
File without changes
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }