@angular-helpers/security 21.0.3 → 21.1.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.es.md CHANGED
@@ -15,6 +15,13 @@ Paquete de seguridad para aplicaciones Angular que previene ataques comunes como
15
15
  - **Análisis de Complejidad**: Detecta patrones peligrosos antes de la ejecución.
16
16
  - **Modo Seguro**: Solo permite patrones verificados como seguros.
17
17
 
18
+ ### **Web Crypto API**
19
+
20
+ - **Cifrado/Descifrado**: Soporte AES-GCM para manejo seguro de datos
21
+ - **Hashing**: SHA-256 y otros algoritmos
22
+ - **Gestión de Claves**: Generar, importar y exportar claves criptográficas
23
+ - **Aleatorio Seguro**: Valores aleatorios criptográficamente seguros
24
+
18
25
  ### **Patrón Builder**
19
26
 
20
27
  - **API Fluida**: Construye expresiones regulares de forma intuitiva.
@@ -93,6 +100,51 @@ const result = await RegexSecurityService.builder()
93
100
  .execute('12345', this.securityService);
94
101
  ```
95
102
 
103
+ ### **WebCryptoService**
104
+
105
+ ```typescript
106
+ import { WebCryptoService } from '@angular-helpers/security';
107
+
108
+ export class SecureStorageComponent {
109
+ private cryptoService = inject(WebCryptoService);
110
+
111
+ async hashPassword(password: string): Promise<string> {
112
+ return await this.cryptoService.hash(password, 'SHA-256');
113
+ }
114
+
115
+ async encryptData(
116
+ data: string,
117
+ ): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array; key: CryptoKey }> {
118
+ const key = await this.cryptoService.generateAesKey(256);
119
+ const { ciphertext, iv } = await this.cryptoService.encryptAes(key, data);
120
+ return { ciphertext, iv, key };
121
+ }
122
+
123
+ async decryptData(ciphertext: ArrayBuffer, iv: Uint8Array, key: CryptoKey): Promise<string> {
124
+ return await this.cryptoService.decryptAes(key, ciphertext, iv);
125
+ }
126
+
127
+ async exportKeyForStorage(key: CryptoKey): Promise<JsonWebKey> {
128
+ return await this.cryptoService.exportKey(key);
129
+ }
130
+
131
+ async importKeyFromStorage(jwk: JsonWebKey): Promise<CryptoKey> {
132
+ return await this.cryptoService.importAesKey(jwk);
133
+ }
134
+
135
+ generateSecureToken(length: number = 32): string {
136
+ const bytes = this.cryptoService.generateRandomBytes(length);
137
+ return Array.from(bytes)
138
+ .map((b) => b.toString(16).padStart(2, '0'))
139
+ .join('');
140
+ }
141
+
142
+ generateUUID(): string {
143
+ return this.cryptoService.randomUUID();
144
+ }
145
+ }
146
+ ```
147
+
96
148
  ## 📊 Niveles de Riesgo
97
149
 
98
150
  | Nivel | Descripción | Acción |
package/README.md CHANGED
@@ -15,6 +15,36 @@ Security package for Angular applications that prevents common attacks like ReDo
15
15
  - **Complexity Analysis**: Detects dangerous patterns before execution.
16
16
  - **Safe Mode**: Only allows patterns verified as safe.
17
17
 
18
+ ### **Web Crypto API**
19
+
20
+ - **Encryption/Decryption**: AES-GCM support for secure data handling
21
+ - **Hashing**: SHA-256 and other algorithms
22
+ - **HMAC Signing**: HMAC-SHA-256/384/512 for message authentication
23
+ - **Key Management**: Generate, import, and export cryptographic keys
24
+ - **Secure Random**: Cryptographically secure random values
25
+ - **UUID Generation**: RFC4122 v4 UUIDs
26
+
27
+ ### **Secure Storage**
28
+
29
+ - **Transparent Encryption**: AES-GCM encrypted localStorage/sessionStorage
30
+ - **Ephemeral Mode**: In-memory keys for single-session security
31
+ - **Passphrase Mode**: PBKDF2-derived keys for cross-session persistence
32
+ - **Namespace Isolation**: Organize stored data with prefixes
33
+
34
+ ### **Input Sanitization**
35
+
36
+ - **XSS Prevention**: Strip dangerous tags and attributes from HTML
37
+ - **URL Validation**: Allow only http/https schemes
38
+ - **HTML Escaping**: Safe interpolation of user content
39
+ - **JSON Safety**: Safe parsing without eval
40
+
41
+ ### **Password Strength**
42
+
43
+ - **Entropy-Based Scoring**: 0-4 score with labeled strength levels
44
+ - **Pattern Detection**: Detects sequences, repetitions, keyboard walks
45
+ - **Common Password Check**: Blocks frequently used passwords
46
+ - **Feedback Messages**: Actionable improvement suggestions
47
+
18
48
  ### **Builder Pattern**
19
49
 
20
50
  - **Fluent API**: Intuitively build regular expressions.
@@ -37,7 +67,16 @@ import { provideSecurity } from '@angular-helpers/security';
37
67
  bootstrapApplication(AppComponent, {
38
68
  providers: [
39
69
  provideSecurity({
70
+ // Core services (enabled by default)
40
71
  enableRegexSecurity: true,
72
+ enableWebCrypto: true,
73
+
74
+ // New services (opt-in, disabled by default)
75
+ enableSecureStorage: true,
76
+ enableInputSanitizer: true,
77
+ enablePasswordStrength: true,
78
+
79
+ // Global settings
41
80
  defaultTimeout: 5000,
42
81
  safeMode: false,
43
82
  }),
@@ -45,6 +84,27 @@ bootstrapApplication(AppComponent, {
45
84
  });
46
85
  ```
47
86
 
87
+ ### **Individual Providers**
88
+
89
+ ```typescript
90
+ import {
91
+ provideRegexSecurity,
92
+ provideWebCrypto,
93
+ provideSecureStorage,
94
+ provideInputSanitizer,
95
+ providePasswordStrength,
96
+ } from '@angular-helpers/security';
97
+
98
+ // Use only the services you need
99
+ bootstrapApplication(AppComponent, {
100
+ providers: [
101
+ provideSecureStorage({ storage: 'session', pbkdf2Iterations: 600_000 }),
102
+ provideInputSanitizer({ allowedTags: ['b', 'i', 'em', 'strong'] }),
103
+ providePasswordStrength(),
104
+ ],
105
+ });
106
+ ```
107
+
48
108
  ### **Service Injection**
49
109
 
50
110
  ```typescript
@@ -163,6 +223,154 @@ export class FormValidationComponent {
163
223
  }
164
224
  ```
165
225
 
226
+ ### **WebCryptoService**
227
+
228
+ ```typescript
229
+ import { WebCryptoService } from '@angular-helpers/security';
230
+
231
+ export class SecureStorageComponent {
232
+ private cryptoService = inject(WebCryptoService);
233
+
234
+ async hashPassword(password: string): Promise<string> {
235
+ return await this.cryptoService.hash(password, 'SHA-256');
236
+ }
237
+
238
+ async encryptData(
239
+ data: string,
240
+ ): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array; key: CryptoKey }> {
241
+ const key = await this.cryptoService.generateAesKey(256);
242
+ const { ciphertext, iv } = await this.cryptoService.encryptAes(key, data);
243
+ return { ciphertext, iv, key };
244
+ }
245
+
246
+ async decryptData(ciphertext: ArrayBuffer, iv: Uint8Array, key: CryptoKey): Promise<string> {
247
+ return await this.cryptoService.decryptAes(key, ciphertext, iv);
248
+ }
249
+
250
+ async exportKeyForStorage(key: CryptoKey): Promise<JsonWebKey> {
251
+ return await this.cryptoService.exportKey(key);
252
+ }
253
+
254
+ async importKeyFromStorage(jwk: JsonWebKey): Promise<CryptoKey> {
255
+ return await this.cryptoService.importAesKey(jwk);
256
+ }
257
+
258
+ generateSecureToken(length: number = 32): string {
259
+ const bytes = this.cryptoService.generateRandomBytes(length);
260
+ return Array.from(bytes)
261
+ .map((b) => b.toString(16).padStart(2, '0'))
262
+ .join('');
263
+ }
264
+
265
+ generateUUID(): string {
266
+ return this.cryptoService.randomUUID();
267
+ }
268
+
269
+ async signAndVerify(data: string): Promise<boolean> {
270
+ // Generate HMAC key for SHA-256
271
+ const key = await this.cryptoService.generateHmacKey('HMAC-SHA-256');
272
+
273
+ // Sign the data
274
+ const signature = await this.cryptoService.sign(key, data);
275
+
276
+ // Verify the signature
277
+ return await this.cryptoService.verify(key, data, signature);
278
+ }
279
+ }
280
+ ```
281
+
282
+ ### **SecureStorageService**
283
+
284
+ ```typescript
285
+ import { SecureStorageService } from '@angular-helpers/security';
286
+
287
+ export class UserSettingsComponent {
288
+ private storage = inject(SecureStorageService);
289
+
290
+ async saveUserToken(token: string): Promise<void> {
291
+ // Ephemeral mode (default): data survives only this session
292
+ await this.storage.set('authToken', { token, createdAt: Date.now() });
293
+ }
294
+
295
+ async getUserToken(): Promise<{ token: string; createdAt: number } | null> {
296
+ return await this.storage.get<{ token: string; createdAt: number }>('authToken');
297
+ }
298
+
299
+ async initWithPassphrase(passphrase: string): Promise<void> {
300
+ // Passphrase mode: data survives page reloads
301
+ await this.storage.initWithPassphrase(passphrase);
302
+ }
303
+
304
+ async saveWithNamespace(userId: string, data: unknown): Promise<void> {
305
+ // Namespace isolation
306
+ await this.storage.set('profile', data, `user:${userId}`);
307
+ }
308
+
309
+ clearUserData(userId: string): void {
310
+ // Clear only this user's data
311
+ this.storage.clear(`user:${userId}`);
312
+ }
313
+ }
314
+ ```
315
+
316
+ ### **InputSanitizerService**
317
+
318
+ ```typescript
319
+ import { InputSanitizerService } from '@angular-helpers/security';
320
+
321
+ export class CommentComponent {
322
+ private sanitizer = inject(InputSanitizerService);
323
+
324
+ sanitizeUserComment(html: string): string {
325
+ // Strip dangerous tags, keep safe ones (b, i, em, a, etc.)
326
+ return this.sanitizer.sanitizeHtml(html);
327
+ // Example: '<b>Hello</b><script>alert(1)</script>' → '<b>Hello</b>'
328
+ }
329
+
330
+ validateUserLink(url: string): string | null {
331
+ // Only allow http/https URLs
332
+ return this.sanitizer.sanitizeUrl(url);
333
+ // Example: 'javascript:alert(1)' → null
334
+ // Example: 'https://example.com' → 'https://example.com/'
335
+ }
336
+
337
+ escapeForDisplay(text: string): string {
338
+ // Safe for HTML text nodes
339
+ return this.sanitizer.escapeHtml(text);
340
+ // Example: '<b>hello</b>' → '&lt;b&gt;hello&lt;/b&gt;'
341
+ }
342
+
343
+ parseUserJson(json: string): unknown | null {
344
+ // Safe JSON parsing without eval
345
+ return this.sanitizer.sanitizeJson(json);
346
+ }
347
+ }
348
+ ```
349
+
350
+ ### **PasswordStrengthService**
351
+
352
+ ```typescript
353
+ import { PasswordStrengthService } from '@angular-helpers/security';
354
+
355
+ export class RegistrationComponent {
356
+ private passwordStrength = inject(PasswordStrengthService);
357
+
358
+ checkPasswordStrength(password: string): void {
359
+ const result = this.passwordStrength.assess(password);
360
+
361
+ console.log(`Score: ${result.score}/4`); // 0-4
362
+ console.log(`Label: ${result.label}`); // 'very-weak' to 'very-strong'
363
+ console.log(`Entropy: ${result.entropy} bits`); // calculated entropy
364
+ console.log('Feedback:', result.feedback); // improvement suggestions
365
+
366
+ // Example results:
367
+ // 'password' → score: 0, label: 'very-weak', feedback: ['This is a commonly used password']
368
+ // 'P@ssw0rd!' → score: 2, label: 'fair', feedback: ['Avoid keyboard patterns']
369
+ // 'xK#9mZ$vLq2@rBnT7' → score: 4, label: 'very-strong', feedback: []
370
+ }
371
+ }
372
+ ```
373
+
166
374
  ## 🔧 Advanced Configuration
167
375
 
168
376
  ### **Security Options**
@@ -1,5 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, Injectable, makeEnvironmentProviders } from '@angular/core';
2
+ import { inject, DestroyRef, Injectable, PLATFORM_ID, InjectionToken, makeEnvironmentProviders } from '@angular/core';
3
+ import { isPlatformBrowser } from '@angular/common';
3
4
 
4
5
  /**
5
6
  * Security service for regular expressions that prevents ReDoS
@@ -411,8 +412,614 @@ class RegexSecurityBuilder {
411
412
  }
412
413
  }
413
414
 
415
+ class WebCryptoService {
416
+ platformId = inject(PLATFORM_ID);
417
+ isSupported() {
418
+ return isPlatformBrowser(this.platformId) && 'crypto' in window && 'subtle' in crypto;
419
+ }
420
+ get subtle() {
421
+ if (!this.isSupported()) {
422
+ throw new Error('Web Crypto API not supported in this environment');
423
+ }
424
+ return crypto.subtle;
425
+ }
426
+ ensureSecureContext() {
427
+ if (!window.isSecureContext) {
428
+ throw new Error('Web Crypto API requires a secure context (HTTPS)');
429
+ }
430
+ }
431
+ async hash(data, algorithm = 'SHA-256') {
432
+ this.ensureSecureContext();
433
+ const buffer = typeof data === 'string' ? new TextEncoder().encode(data) : data;
434
+ const hashBuffer = await this.subtle.digest(algorithm, buffer);
435
+ return this.bufferToHex(hashBuffer);
436
+ }
437
+ async generateAesKey(length = 256) {
438
+ this.ensureSecureContext();
439
+ return this.subtle.generateKey({ name: 'AES-GCM', length }, true, ['encrypt', 'decrypt']);
440
+ }
441
+ async encryptAes(key, data) {
442
+ this.ensureSecureContext();
443
+ const buffer = typeof data === 'string' ? new TextEncoder().encode(data) : data;
444
+ const iv = crypto.getRandomValues(new Uint8Array(12));
445
+ const ciphertext = await this.subtle.encrypt({ name: 'AES-GCM', iv }, key, buffer);
446
+ return { ciphertext, iv };
447
+ }
448
+ async decryptAes(key, ciphertext, iv) {
449
+ this.ensureSecureContext();
450
+ const decrypted = await this.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
451
+ return new TextDecoder().decode(decrypted);
452
+ }
453
+ async exportKey(key) {
454
+ this.ensureSecureContext();
455
+ return this.subtle.exportKey('jwk', key);
456
+ }
457
+ async importAesKey(jwk) {
458
+ this.ensureSecureContext();
459
+ return this.subtle.importKey('jwk', jwk, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt']);
460
+ }
461
+ generateRandomBytes(length) {
462
+ if (!this.isSupported()) {
463
+ throw new Error('Web Crypto API not supported');
464
+ }
465
+ return crypto.getRandomValues(new Uint8Array(length));
466
+ }
467
+ randomUUID() {
468
+ if (!this.isSupported()) {
469
+ throw new Error('Web Crypto API not supported');
470
+ }
471
+ return crypto.randomUUID();
472
+ }
473
+ async generateHmacKey(algorithm = 'HMAC-SHA-256') {
474
+ this.ensureSecureContext();
475
+ return this.subtle.generateKey({ name: 'HMAC', hash: { name: this.hmacHashName(algorithm) } }, true, ['sign', 'verify']);
476
+ }
477
+ /**
478
+ * Signs data with an HMAC key. Returns a hex-encoded signature.
479
+ */
480
+ async sign(key, data) {
481
+ this.ensureSecureContext();
482
+ const buffer = typeof data === 'string' ? new TextEncoder().encode(data) : data;
483
+ const signature = await this.subtle.sign('HMAC', key, buffer);
484
+ return this.bufferToHex(signature);
485
+ }
486
+ /**
487
+ * Verifies an HMAC signature (hex-encoded). Returns false for malformed input — never throws.
488
+ */
489
+ async verify(key, data, signature) {
490
+ this.ensureSecureContext();
491
+ let sigBytes;
492
+ try {
493
+ sigBytes = this.hexToBytes(signature);
494
+ }
495
+ catch {
496
+ return false;
497
+ }
498
+ try {
499
+ const buffer = typeof data === 'string' ? new TextEncoder().encode(data) : data;
500
+ return await this.subtle.verify('HMAC', key, sigBytes, buffer);
501
+ }
502
+ catch {
503
+ return false;
504
+ }
505
+ }
506
+ async importHmacKey(jwk, algorithm = 'HMAC-SHA-256') {
507
+ this.ensureSecureContext();
508
+ return this.subtle.importKey('jwk', jwk, { name: 'HMAC', hash: { name: this.hmacHashName(algorithm) } }, true, ['sign', 'verify']);
509
+ }
510
+ bufferToHex(buffer) {
511
+ return Array.from(new Uint8Array(buffer))
512
+ .map((b) => b.toString(16).padStart(2, '0'))
513
+ .join('');
514
+ }
515
+ hexToBytes(hex) {
516
+ if (hex.length % 2 !== 0)
517
+ throw new Error('Invalid hex string');
518
+ const bytes = new Uint8Array(new ArrayBuffer(hex.length / 2));
519
+ for (let i = 0; i < hex.length; i += 2) {
520
+ const byte = parseInt(hex.substring(i, i + 2), 16);
521
+ if (isNaN(byte))
522
+ throw new Error('Invalid hex string');
523
+ bytes[i / 2] = byte;
524
+ }
525
+ return bytes;
526
+ }
527
+ hmacHashName(algorithm) {
528
+ return algorithm.replace('HMAC-', '');
529
+ }
530
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebCryptoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
531
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebCryptoService });
532
+ }
533
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebCryptoService, decorators: [{
534
+ type: Injectable
535
+ }] });
536
+
537
+ const SECURE_STORAGE_CONFIG = new InjectionToken('SECURE_STORAGE_CONFIG');
538
+ const SALT_STORAGE_KEY = '__ss_salt__';
539
+ const DEFAULT_ITERATIONS = 600_000;
540
+ /**
541
+ * Service for transparent AES-GCM encrypted storage on top of localStorage/sessionStorage.
542
+ *
543
+ * Two key modes are supported:
544
+ * - **Ephemeral** (default): a CryptoKey is generated in memory per service instance.
545
+ * Data is unrecoverable after page reload or service re-creation.
546
+ * - **Passphrase-derived**: call `initWithPassphrase(passphrase)` to derive a stable key
547
+ * via PBKDF2. Data survives page reloads as long as the same passphrase is used.
548
+ *
549
+ * @example
550
+ * // Ephemeral mode
551
+ * await storage.set('token', { value: 'abc' });
552
+ * const token = await storage.get<{ value: string }>('token');
553
+ *
554
+ * @example
555
+ * // Passphrase mode
556
+ * await storage.initWithPassphrase('my-secret');
557
+ * await storage.set('user', { id: 1 }, 'auth');
558
+ * const user = await storage.get<{ id: number }>('user', 'auth');
559
+ */
560
+ class SecureStorageService {
561
+ platformId = inject(PLATFORM_ID);
562
+ storageConfig;
563
+ activeKey = null;
564
+ constructor() {
565
+ const config = inject(SECURE_STORAGE_CONFIG, { optional: true }) ?? {};
566
+ this.storageConfig = {
567
+ storage: config.storage ?? 'local',
568
+ pbkdf2Iterations: config.pbkdf2Iterations ?? DEFAULT_ITERATIONS,
569
+ };
570
+ }
571
+ isSupported() {
572
+ return (isPlatformBrowser(this.platformId) &&
573
+ 'crypto' in window &&
574
+ 'subtle' in crypto &&
575
+ 'localStorage' in window);
576
+ }
577
+ /**
578
+ * Initializes the service with a passphrase-derived key (PBKDF2 + AES-GCM).
579
+ * The salt is automatically persisted in storage on first call and reused on subsequent calls.
580
+ * Calling this again replaces the active key.
581
+ *
582
+ * @param passphrase Secret passphrase for key derivation.
583
+ * @param explicitSalt Optional base64 salt. When provided, the stored salt is ignored.
584
+ */
585
+ async initWithPassphrase(passphrase, explicitSalt) {
586
+ this.assertSupported();
587
+ let salt;
588
+ if (explicitSalt) {
589
+ salt = this.base64ToBytes(explicitSalt);
590
+ }
591
+ else {
592
+ const stored = this.nativeStorage.getItem(SALT_STORAGE_KEY);
593
+ if (stored) {
594
+ salt = this.base64ToBytes(stored);
595
+ }
596
+ else {
597
+ salt = crypto.getRandomValues(new Uint8Array(new ArrayBuffer(16)));
598
+ this.nativeStorage.setItem(SALT_STORAGE_KEY, this.bytesToBase64(salt));
599
+ }
600
+ }
601
+ const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(passphrase), 'PBKDF2', false, ['deriveKey']);
602
+ this.activeKey = await crypto.subtle.deriveKey({
603
+ name: 'PBKDF2',
604
+ salt,
605
+ iterations: this.storageConfig.pbkdf2Iterations,
606
+ hash: 'SHA-256',
607
+ }, keyMaterial, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
608
+ }
609
+ /**
610
+ * Encrypts and stores a value.
611
+ * A fresh random IV is generated for every write.
612
+ *
613
+ * @throws {TypeError} When `value` is `undefined`.
614
+ * @throws {DOMException} When storage quota is exceeded.
615
+ */
616
+ async set(key, value, namespace) {
617
+ this.assertSupported();
618
+ if (value === undefined) {
619
+ throw new TypeError('Cannot store undefined value in SecureStorageService');
620
+ }
621
+ const cryptoKey = await this.ensureKey();
622
+ const iv = crypto.getRandomValues(new Uint8Array(new ArrayBuffer(12)));
623
+ const plaintext = new TextEncoder().encode(JSON.stringify(value));
624
+ const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, plaintext);
625
+ const entry = {
626
+ iv: this.bytesToBase64(iv),
627
+ ct: this.bytesToBase64(ciphertext),
628
+ };
629
+ this.nativeStorage.setItem(this.buildStorageKey(key, namespace), JSON.stringify(entry));
630
+ }
631
+ /**
632
+ * Decrypts and returns a stored value.
633
+ * Returns `null` if the key does not exist, was written without encryption,
634
+ * or the ciphertext is corrupted.
635
+ */
636
+ async get(key, namespace) {
637
+ this.assertSupported();
638
+ const raw = this.nativeStorage.getItem(this.buildStorageKey(key, namespace));
639
+ if (!raw)
640
+ return null;
641
+ let entry;
642
+ try {
643
+ const parsed = JSON.parse(raw);
644
+ if (!parsed || typeof parsed !== 'object' || !('iv' in parsed) || !('ct' in parsed)) {
645
+ return null;
646
+ }
647
+ entry = parsed;
648
+ }
649
+ catch {
650
+ return null;
651
+ }
652
+ try {
653
+ const cryptoKey = await this.ensureKey();
654
+ const iv = this.base64ToBytes(entry.iv);
655
+ const ciphertext = this.base64ToBytes(entry.ct);
656
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, ciphertext);
657
+ return JSON.parse(new TextDecoder().decode(decrypted));
658
+ }
659
+ catch {
660
+ return null;
661
+ }
662
+ }
663
+ /**
664
+ * Removes a single entry from storage.
665
+ */
666
+ remove(key, namespace) {
667
+ this.assertSupported();
668
+ this.nativeStorage.removeItem(this.buildStorageKey(key, namespace));
669
+ }
670
+ /**
671
+ * Clears all entries belonging to a namespace.
672
+ * When called without arguments, clears the entire storage target.
673
+ */
674
+ clear(namespace) {
675
+ this.assertSupported();
676
+ if (!namespace) {
677
+ this.nativeStorage.clear();
678
+ return;
679
+ }
680
+ const prefix = `${namespace}:`;
681
+ const keysToRemove = [];
682
+ for (let i = 0; i < this.nativeStorage.length; i++) {
683
+ const k = this.nativeStorage.key(i);
684
+ if (k?.startsWith(prefix)) {
685
+ keysToRemove.push(k);
686
+ }
687
+ }
688
+ keysToRemove.forEach((k) => this.nativeStorage.removeItem(k));
689
+ }
690
+ get nativeStorage() {
691
+ return this.storageConfig.storage === 'session' ? sessionStorage : localStorage;
692
+ }
693
+ buildStorageKey(key, namespace) {
694
+ return namespace ? `${namespace}:${key}` : key;
695
+ }
696
+ async ensureKey() {
697
+ if (this.activeKey)
698
+ return this.activeKey;
699
+ this.activeKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
700
+ 'encrypt',
701
+ 'decrypt',
702
+ ]);
703
+ return this.activeKey;
704
+ }
705
+ assertSupported() {
706
+ if (!this.isSupported()) {
707
+ throw new Error('SecureStorageService is not supported in this environment (requires browser + Web Crypto API)');
708
+ }
709
+ }
710
+ bytesToBase64(buffer) {
711
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
712
+ let binary = '';
713
+ for (let i = 0; i < bytes.length; i++) {
714
+ binary += String.fromCharCode(bytes[i]);
715
+ }
716
+ return btoa(binary);
717
+ }
718
+ base64ToBytes(base64) {
719
+ return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
720
+ }
721
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SecureStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
722
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SecureStorageService });
723
+ }
724
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SecureStorageService, decorators: [{
725
+ type: Injectable
726
+ }], ctorParameters: () => [] });
727
+
728
+ const SANITIZER_CONFIG = new InjectionToken('SANITIZER_CONFIG');
729
+ const DEFAULT_ALLOWED_TAGS = [
730
+ 'b',
731
+ 'i',
732
+ 'em',
733
+ 'strong',
734
+ 'a',
735
+ 'p',
736
+ 'br',
737
+ 'ul',
738
+ 'ol',
739
+ 'li',
740
+ 'span',
741
+ ];
742
+ const DEFAULT_ALLOWED_ATTRIBUTES = {
743
+ a: ['href'],
744
+ };
745
+ const SAFE_URL_SCHEMES = ['http:', 'https:'];
746
+ /**
747
+ * Service for structured input sanitization to defend against XSS, URL injection, and unsafe HTML.
748
+ *
749
+ * This service is defense-in-depth and DOES NOT replace a Content Security Policy (CSP).
750
+ * Always configure a proper CSP alongside using this service.
751
+ *
752
+ * @example
753
+ * const clean = sanitizer.sanitizeHtml('<b>Hello</b><script>alert(1)</script>');
754
+ * // → '<b>Hello</b>'
755
+ *
756
+ * @example
757
+ * const url = sanitizer.sanitizeUrl('javascript:alert(1)');
758
+ * // → null
759
+ */
760
+ class InputSanitizerService {
761
+ platformId = inject(PLATFORM_ID);
762
+ allowedTags;
763
+ allowedAttributes;
764
+ constructor() {
765
+ const config = inject(SANITIZER_CONFIG, { optional: true }) ?? {};
766
+ this.allowedTags = new Set(config.allowedTags ?? DEFAULT_ALLOWED_TAGS);
767
+ this.allowedAttributes = config.allowedAttributes ?? DEFAULT_ALLOWED_ATTRIBUTES;
768
+ }
769
+ isSupported() {
770
+ return isPlatformBrowser(this.platformId) && typeof DOMParser !== 'undefined';
771
+ }
772
+ /**
773
+ * Parses and sanitizes an HTML string, keeping only allowed tags and attributes.
774
+ * Script execution is prevented — parsing is done via DOMParser, not innerHTML assignment.
775
+ *
776
+ * @throws {Error} When called in a non-browser environment.
777
+ */
778
+ sanitizeHtml(input) {
779
+ if (!this.isSupported()) {
780
+ throw new Error('sanitizeHtml requires a browser environment (DOMParser unavailable)');
781
+ }
782
+ if (!input)
783
+ return '';
784
+ const parser = new DOMParser();
785
+ const doc = parser.parseFromString(input, 'text/html');
786
+ this.processNode(doc.body);
787
+ return doc.body.innerHTML;
788
+ }
789
+ /**
790
+ * Validates and normalizes a URL string.
791
+ * Returns the normalized URL only for `http:` and `https:` schemes.
792
+ * Returns `null` for `javascript:`, `data:`, `vbscript:`, relative URLs, or malformed input.
793
+ */
794
+ sanitizeUrl(input) {
795
+ if (!input)
796
+ return null;
797
+ try {
798
+ const url = new URL(input);
799
+ return SAFE_URL_SCHEMES.includes(url.protocol) ? url.toString() : null;
800
+ }
801
+ catch {
802
+ return null;
803
+ }
804
+ }
805
+ /**
806
+ * Escapes HTML special characters for safe text interpolation.
807
+ * Use this when inserting user content into HTML text nodes or attributes.
808
+ */
809
+ escapeHtml(input) {
810
+ if (!input)
811
+ return '';
812
+ return input
813
+ .replace(/&/g, '&amp;')
814
+ .replace(/</g, '&lt;')
815
+ .replace(/>/g, '&gt;')
816
+ .replace(/"/g, '&quot;')
817
+ .replace(/'/g, '&#x27;');
818
+ }
819
+ /**
820
+ * Safely parses a JSON string. Returns the parsed value on success, `null` on any error.
821
+ * Does NOT use `eval` or `Function` — uses JSON.parse only.
822
+ */
823
+ sanitizeJson(input) {
824
+ if (!input)
825
+ return null;
826
+ try {
827
+ return JSON.parse(input);
828
+ }
829
+ catch {
830
+ return null;
831
+ }
832
+ }
833
+ processNode(node) {
834
+ const children = Array.from(node.childNodes);
835
+ for (const child of children) {
836
+ if (child.nodeType !== Node.ELEMENT_NODE)
837
+ continue;
838
+ const element = child;
839
+ const tagName = element.tagName.toLowerCase();
840
+ if (!this.allowedTags.has(tagName)) {
841
+ const text = element.textContent ?? '';
842
+ node.replaceChild(document.createTextNode(text), element);
843
+ continue;
844
+ }
845
+ this.sanitizeAttributes(element, tagName);
846
+ this.processNode(element);
847
+ }
848
+ }
849
+ sanitizeAttributes(element, tagName) {
850
+ const attrsToRemove = [];
851
+ const allowed = this.allowedAttributes[tagName] ?? [];
852
+ for (let i = 0; i < element.attributes.length; i++) {
853
+ const attr = element.attributes[i];
854
+ if (attr.name.startsWith('on')) {
855
+ attrsToRemove.push(attr.name);
856
+ continue;
857
+ }
858
+ if (!allowed.includes(attr.name)) {
859
+ attrsToRemove.push(attr.name);
860
+ continue;
861
+ }
862
+ if (attr.name === 'href' && this.sanitizeUrl(attr.value) === null) {
863
+ attrsToRemove.push(attr.name);
864
+ }
865
+ }
866
+ attrsToRemove.forEach((name) => element.removeAttribute(name));
867
+ }
868
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: InputSanitizerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
869
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: InputSanitizerService });
870
+ }
871
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: InputSanitizerService, decorators: [{
872
+ type: Injectable
873
+ }], ctorParameters: () => [] });
874
+
875
+ const COMMON_PASSWORDS = new Set([
876
+ 'password',
877
+ '123456',
878
+ 'qwerty',
879
+ 'letmein',
880
+ 'admin',
881
+ 'welcome',
882
+ '111111',
883
+ 'abc123',
884
+ 'monkey',
885
+ 'master',
886
+ 'login',
887
+ 'pass',
888
+ ]);
889
+ const ALPHA_SEQUENCES = 'abcdefghijklmnopqrstuvwxyz';
890
+ const DIGIT_SEQUENCES = '0123456789';
891
+ const KEYBOARD_ROWS = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'];
892
+ const SCORE_LABELS = {
893
+ 0: 'very-weak',
894
+ 1: 'weak',
895
+ 2: 'fair',
896
+ 3: 'strong',
897
+ 4: 'very-strong',
898
+ };
899
+ /**
900
+ * Service for entropy-based password strength evaluation.
901
+ * All methods are synchronous and side-effect free — safely wrappable in Angular `computed()`.
902
+ *
903
+ * Score thresholds (bits of entropy):
904
+ * - 0 (very-weak): < 28 bits
905
+ * - 1 (weak): 28–35 bits
906
+ * - 2 (fair): 36–49 bits
907
+ * - 3 (strong): 50–69 bits
908
+ * - 4 (very-strong): ≥ 70 bits
909
+ *
910
+ * @example
911
+ * const result = passwordStrength.assess('P@ssw0rd!');
912
+ * console.log(result.score); // 2
913
+ * console.log(result.label); // 'fair'
914
+ * console.log(result.entropy); // ~42.5
915
+ */
916
+ class PasswordStrengthService {
917
+ /**
918
+ * Evaluates the strength of a password.
919
+ * Never throws — returns score 0 for empty or null-like input.
920
+ */
921
+ assess(password) {
922
+ if (!password) {
923
+ return {
924
+ score: 0,
925
+ label: 'very-weak',
926
+ entropy: 0,
927
+ feedback: ['Password cannot be empty'],
928
+ };
929
+ }
930
+ const feedback = [];
931
+ const chars = [...password];
932
+ const length = chars.length;
933
+ const hasLower = /[a-z]/.test(password);
934
+ const hasUpper = /[A-Z]/.test(password);
935
+ const hasDigit = /[0-9]/.test(password);
936
+ const hasSymbol = /[!@#$%^&*()\-_=+[\]{}|;:'",.<>/?\\`~]/.test(password);
937
+ const hasExtended = chars.some((c) => c.codePointAt(0) > 127);
938
+ let poolSize = 0;
939
+ if (hasLower)
940
+ poolSize += 26;
941
+ if (hasUpper)
942
+ poolSize += 26;
943
+ if (hasDigit)
944
+ poolSize += 10;
945
+ if (hasSymbol)
946
+ poolSize += 32;
947
+ if (hasExtended)
948
+ poolSize += 64;
949
+ if (poolSize === 0)
950
+ poolSize = 26;
951
+ let entropy = length * Math.log2(poolSize);
952
+ const hasAlphaSeq = this.containsSequence(password.toLowerCase(), ALPHA_SEQUENCES, 3);
953
+ const hasDigitSeq = this.containsSequence(password, DIGIT_SEQUENCES, 3);
954
+ const hasRepeat = /(.)\1{2,}/.test(password);
955
+ const hasKeyboard = KEYBOARD_ROWS.some((row) => this.containsSequence(password.toLowerCase(), row, 4));
956
+ if (hasAlphaSeq || hasDigitSeq) {
957
+ entropy *= 0.8;
958
+ feedback.push('Avoid predictable sequences (abc, 123)');
959
+ }
960
+ if (hasRepeat) {
961
+ entropy *= 0.9;
962
+ feedback.push('Avoid repeated characters');
963
+ }
964
+ if (hasKeyboard) {
965
+ entropy *= 0.85;
966
+ feedback.push('Avoid keyboard patterns (qwerty, asdf)');
967
+ }
968
+ if (length < 8) {
969
+ feedback.push('Use at least 8 characters');
970
+ }
971
+ if (!hasUpper)
972
+ feedback.push('Add uppercase letters');
973
+ if (!hasDigit)
974
+ feedback.push('Add numbers');
975
+ if (!hasSymbol)
976
+ feedback.push('Add special characters');
977
+ const isCommon = COMMON_PASSWORDS.has(password.toLowerCase());
978
+ let score = this.entropyToScore(entropy);
979
+ if (isCommon) {
980
+ score = Math.min(score, 1);
981
+ if (!feedback.includes('Use at least 8 characters')) {
982
+ feedback.push('This is a commonly used password');
983
+ }
984
+ }
985
+ return {
986
+ score,
987
+ label: SCORE_LABELS[score],
988
+ entropy: Math.round(entropy * 100) / 100,
989
+ feedback,
990
+ };
991
+ }
992
+ entropyToScore(entropy) {
993
+ if (entropy < 28)
994
+ return 0;
995
+ if (entropy < 36)
996
+ return 1;
997
+ if (entropy < 50)
998
+ return 2;
999
+ if (entropy < 70)
1000
+ return 3;
1001
+ return 4;
1002
+ }
1003
+ containsSequence(input, sequence, minLength) {
1004
+ for (let i = 0; i <= sequence.length - minLength; i++) {
1005
+ if (input.includes(sequence.substring(i, i + minLength)))
1006
+ return true;
1007
+ }
1008
+ return false;
1009
+ }
1010
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PasswordStrengthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1011
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PasswordStrengthService });
1012
+ }
1013
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PasswordStrengthService, decorators: [{
1014
+ type: Injectable
1015
+ }] });
1016
+
414
1017
  const defaultSecurityConfig = {
415
1018
  enableRegexSecurity: true,
1019
+ enableWebCrypto: true,
1020
+ enableSecureStorage: false,
1021
+ enableInputSanitizer: false,
1022
+ enablePasswordStrength: false,
416
1023
  defaultTimeout: 5000,
417
1024
  safeMode: false,
418
1025
  };
@@ -422,14 +1029,44 @@ function provideSecurity(config = {}) {
422
1029
  if (mergedConfig.enableRegexSecurity) {
423
1030
  providers.push(RegexSecurityService);
424
1031
  }
1032
+ if (mergedConfig.enableWebCrypto) {
1033
+ providers.push(WebCryptoService);
1034
+ }
1035
+ if (mergedConfig.enableSecureStorage) {
1036
+ providers.push(SecureStorageService);
1037
+ }
1038
+ if (mergedConfig.enableInputSanitizer) {
1039
+ providers.push(InputSanitizerService);
1040
+ }
1041
+ if (mergedConfig.enablePasswordStrength) {
1042
+ providers.push(PasswordStrengthService);
1043
+ }
425
1044
  return makeEnvironmentProviders(providers);
426
1045
  }
427
1046
  function provideRegexSecurity() {
428
1047
  return makeEnvironmentProviders([RegexSecurityService]);
429
1048
  }
1049
+ function provideWebCrypto() {
1050
+ return makeEnvironmentProviders([WebCryptoService]);
1051
+ }
1052
+ function provideSecureStorage(config) {
1053
+ return makeEnvironmentProviders([
1054
+ SecureStorageService,
1055
+ ...(config ? [{ provide: SECURE_STORAGE_CONFIG, useValue: config }] : []),
1056
+ ]);
1057
+ }
1058
+ function provideInputSanitizer(config) {
1059
+ return makeEnvironmentProviders([
1060
+ InputSanitizerService,
1061
+ ...(config ? [{ provide: SANITIZER_CONFIG, useValue: config }] : []),
1062
+ ]);
1063
+ }
1064
+ function providePasswordStrength() {
1065
+ return makeEnvironmentProviders([PasswordStrengthService]);
1066
+ }
430
1067
 
431
1068
  /**
432
1069
  * Generated bundle index. Do not edit.
433
1070
  */
434
1071
 
435
- export { RegexSecurityBuilder, RegexSecurityService, defaultSecurityConfig, provideRegexSecurity, provideSecurity };
1072
+ export { InputSanitizerService, PasswordStrengthService, RegexSecurityBuilder, RegexSecurityService, SANITIZER_CONFIG, SECURE_STORAGE_CONFIG, SecureStorageService, WebCryptoService, defaultSecurityConfig, provideInputSanitizer, providePasswordStrength, provideRegexSecurity, provideSecureStorage, provideSecurity, provideWebCrypto };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/security",
3
- "version": "21.0.3",
3
+ "version": "21.1.0",
4
4
  "description": "Angular security helpers for preventing ReDoS and other security vulnerabilities",
5
5
  "keywords": [
6
6
  "angular",
@@ -9,18 +9,25 @@
9
9
  "redos",
10
10
  "prevention",
11
11
  "web-worker",
12
- "builder-pattern"
12
+ "builder-pattern",
13
+ "encryption",
14
+ "web-crypto",
15
+ "hmac",
16
+ "xss",
17
+ "sanitization",
18
+ "password-strength",
19
+ "secure-storage"
13
20
  ],
14
21
  "author": "Angular Helpers Team",
15
22
  "license": "MIT",
16
23
  "repository": {
17
24
  "type": "git",
18
- "url": "https://github.com/angular-helpers/security"
25
+ "url": "https://github.com/Gaspar1992/angular-helpers"
19
26
  },
20
27
  "bugs": {
21
- "url": "https://github.com/angular-helpers/security/issues"
28
+ "url": "https://github.com/Gaspar1992/angular-helpers/issues"
22
29
  },
23
- "homepage": "https://github.com/angular-helpers/security#readme",
30
+ "homepage": "https://gaspar1992.github.io/angular-helpers/docs/security",
24
31
  "publishConfig": {
25
32
  "access": "public"
26
33
  },
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EnvironmentProviders } from '@angular/core';
2
+ import { InjectionToken, EnvironmentProviders } from '@angular/core';
3
3
 
4
4
  interface RegexSecurityConfig {
5
5
  timeout?: number;
@@ -151,14 +151,222 @@ declare class RegexSecurityBuilder {
151
151
  execute(text: string, service: RegexSecurityService): Promise<RegexTestResult>;
152
152
  }
153
153
 
154
+ type HashAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
155
+ type HmacAlgorithm = 'HMAC-SHA-256' | 'HMAC-SHA-384' | 'HMAC-SHA-512';
156
+ type AesKeyLength = 128 | 192 | 256;
157
+ interface AesEncryptResult {
158
+ ciphertext: ArrayBuffer;
159
+ iv: Uint8Array;
160
+ }
161
+ declare class WebCryptoService {
162
+ private readonly platformId;
163
+ isSupported(): boolean;
164
+ private get subtle();
165
+ private ensureSecureContext;
166
+ hash(data: string | ArrayBuffer, algorithm?: HashAlgorithm): Promise<string>;
167
+ generateAesKey(length?: AesKeyLength): Promise<CryptoKey>;
168
+ encryptAes(key: CryptoKey, data: string | ArrayBuffer): Promise<AesEncryptResult>;
169
+ decryptAes(key: CryptoKey, ciphertext: ArrayBuffer, iv: Uint8Array<ArrayBuffer>): Promise<string>;
170
+ exportKey(key: CryptoKey): Promise<JsonWebKey>;
171
+ importAesKey(jwk: JsonWebKey): Promise<CryptoKey>;
172
+ generateRandomBytes(length: number): Uint8Array;
173
+ randomUUID(): string;
174
+ generateHmacKey(algorithm?: HmacAlgorithm): Promise<CryptoKey>;
175
+ /**
176
+ * Signs data with an HMAC key. Returns a hex-encoded signature.
177
+ */
178
+ sign(key: CryptoKey, data: string | ArrayBuffer): Promise<string>;
179
+ /**
180
+ * Verifies an HMAC signature (hex-encoded). Returns false for malformed input — never throws.
181
+ */
182
+ verify(key: CryptoKey, data: string | ArrayBuffer, signature: string): Promise<boolean>;
183
+ importHmacKey(jwk: JsonWebKey, algorithm?: HmacAlgorithm): Promise<CryptoKey>;
184
+ private bufferToHex;
185
+ private hexToBytes;
186
+ private hmacHashName;
187
+ static ɵfac: i0.ɵɵFactoryDeclaration<WebCryptoService, never>;
188
+ static ɵprov: i0.ɵɵInjectableDeclaration<WebCryptoService>;
189
+ }
190
+
191
+ type StorageTarget = 'local' | 'session';
192
+ interface SecureStorageConfig {
193
+ storage?: StorageTarget;
194
+ pbkdf2Iterations?: number;
195
+ }
196
+ declare const SECURE_STORAGE_CONFIG: InjectionToken<SecureStorageConfig>;
197
+ /**
198
+ * Service for transparent AES-GCM encrypted storage on top of localStorage/sessionStorage.
199
+ *
200
+ * Two key modes are supported:
201
+ * - **Ephemeral** (default): a CryptoKey is generated in memory per service instance.
202
+ * Data is unrecoverable after page reload or service re-creation.
203
+ * - **Passphrase-derived**: call `initWithPassphrase(passphrase)` to derive a stable key
204
+ * via PBKDF2. Data survives page reloads as long as the same passphrase is used.
205
+ *
206
+ * @example
207
+ * // Ephemeral mode
208
+ * await storage.set('token', { value: 'abc' });
209
+ * const token = await storage.get<{ value: string }>('token');
210
+ *
211
+ * @example
212
+ * // Passphrase mode
213
+ * await storage.initWithPassphrase('my-secret');
214
+ * await storage.set('user', { id: 1 }, 'auth');
215
+ * const user = await storage.get<{ id: number }>('user', 'auth');
216
+ */
217
+ declare class SecureStorageService {
218
+ private readonly platformId;
219
+ private readonly storageConfig;
220
+ private activeKey;
221
+ constructor();
222
+ isSupported(): boolean;
223
+ /**
224
+ * Initializes the service with a passphrase-derived key (PBKDF2 + AES-GCM).
225
+ * The salt is automatically persisted in storage on first call and reused on subsequent calls.
226
+ * Calling this again replaces the active key.
227
+ *
228
+ * @param passphrase Secret passphrase for key derivation.
229
+ * @param explicitSalt Optional base64 salt. When provided, the stored salt is ignored.
230
+ */
231
+ initWithPassphrase(passphrase: string, explicitSalt?: string): Promise<void>;
232
+ /**
233
+ * Encrypts and stores a value.
234
+ * A fresh random IV is generated for every write.
235
+ *
236
+ * @throws {TypeError} When `value` is `undefined`.
237
+ * @throws {DOMException} When storage quota is exceeded.
238
+ */
239
+ set<T>(key: string, value: T, namespace?: string): Promise<void>;
240
+ /**
241
+ * Decrypts and returns a stored value.
242
+ * Returns `null` if the key does not exist, was written without encryption,
243
+ * or the ciphertext is corrupted.
244
+ */
245
+ get<T>(key: string, namespace?: string): Promise<T | null>;
246
+ /**
247
+ * Removes a single entry from storage.
248
+ */
249
+ remove(key: string, namespace?: string): void;
250
+ /**
251
+ * Clears all entries belonging to a namespace.
252
+ * When called without arguments, clears the entire storage target.
253
+ */
254
+ clear(namespace?: string): void;
255
+ private get nativeStorage();
256
+ private buildStorageKey;
257
+ private ensureKey;
258
+ private assertSupported;
259
+ private bytesToBase64;
260
+ private base64ToBytes;
261
+ static ɵfac: i0.ɵɵFactoryDeclaration<SecureStorageService, never>;
262
+ static ɵprov: i0.ɵɵInjectableDeclaration<SecureStorageService>;
263
+ }
264
+
265
+ interface SanitizerConfig {
266
+ allowedTags?: string[];
267
+ allowedAttributes?: Record<string, string[]>;
268
+ }
269
+ declare const SANITIZER_CONFIG: InjectionToken<SanitizerConfig>;
270
+ /**
271
+ * Service for structured input sanitization to defend against XSS, URL injection, and unsafe HTML.
272
+ *
273
+ * This service is defense-in-depth and DOES NOT replace a Content Security Policy (CSP).
274
+ * Always configure a proper CSP alongside using this service.
275
+ *
276
+ * @example
277
+ * const clean = sanitizer.sanitizeHtml('<b>Hello</b><script>alert(1)</script>');
278
+ * // → '<b>Hello</b>'
279
+ *
280
+ * @example
281
+ * const url = sanitizer.sanitizeUrl('javascript:alert(1)');
282
+ * // → null
283
+ */
284
+ declare class InputSanitizerService {
285
+ private readonly platformId;
286
+ private readonly allowedTags;
287
+ private readonly allowedAttributes;
288
+ constructor();
289
+ isSupported(): boolean;
290
+ /**
291
+ * Parses and sanitizes an HTML string, keeping only allowed tags and attributes.
292
+ * Script execution is prevented — parsing is done via DOMParser, not innerHTML assignment.
293
+ *
294
+ * @throws {Error} When called in a non-browser environment.
295
+ */
296
+ sanitizeHtml(input: string): string;
297
+ /**
298
+ * Validates and normalizes a URL string.
299
+ * Returns the normalized URL only for `http:` and `https:` schemes.
300
+ * Returns `null` for `javascript:`, `data:`, `vbscript:`, relative URLs, or malformed input.
301
+ */
302
+ sanitizeUrl(input: string): string | null;
303
+ /**
304
+ * Escapes HTML special characters for safe text interpolation.
305
+ * Use this when inserting user content into HTML text nodes or attributes.
306
+ */
307
+ escapeHtml(input: string): string;
308
+ /**
309
+ * Safely parses a JSON string. Returns the parsed value on success, `null` on any error.
310
+ * Does NOT use `eval` or `Function` — uses JSON.parse only.
311
+ */
312
+ sanitizeJson(input: string): unknown | null;
313
+ private processNode;
314
+ private sanitizeAttributes;
315
+ static ɵfac: i0.ɵɵFactoryDeclaration<InputSanitizerService, never>;
316
+ static ɵprov: i0.ɵɵInjectableDeclaration<InputSanitizerService>;
317
+ }
318
+
319
+ interface PasswordStrengthResult {
320
+ score: 0 | 1 | 2 | 3 | 4;
321
+ label: 'very-weak' | 'weak' | 'fair' | 'strong' | 'very-strong';
322
+ entropy: number;
323
+ feedback: string[];
324
+ }
325
+ /**
326
+ * Service for entropy-based password strength evaluation.
327
+ * All methods are synchronous and side-effect free — safely wrappable in Angular `computed()`.
328
+ *
329
+ * Score thresholds (bits of entropy):
330
+ * - 0 (very-weak): < 28 bits
331
+ * - 1 (weak): 28–35 bits
332
+ * - 2 (fair): 36–49 bits
333
+ * - 3 (strong): 50–69 bits
334
+ * - 4 (very-strong): ≥ 70 bits
335
+ *
336
+ * @example
337
+ * const result = passwordStrength.assess('P@ssw0rd!');
338
+ * console.log(result.score); // 2
339
+ * console.log(result.label); // 'fair'
340
+ * console.log(result.entropy); // ~42.5
341
+ */
342
+ declare class PasswordStrengthService {
343
+ /**
344
+ * Evaluates the strength of a password.
345
+ * Never throws — returns score 0 for empty or null-like input.
346
+ */
347
+ assess(password: string): PasswordStrengthResult;
348
+ private entropyToScore;
349
+ private containsSequence;
350
+ static ɵfac: i0.ɵɵFactoryDeclaration<PasswordStrengthService, never>;
351
+ static ɵprov: i0.ɵɵInjectableDeclaration<PasswordStrengthService>;
352
+ }
353
+
154
354
  interface SecurityConfig {
155
355
  enableRegexSecurity?: boolean;
356
+ enableWebCrypto?: boolean;
357
+ enableSecureStorage?: boolean;
358
+ enableInputSanitizer?: boolean;
359
+ enablePasswordStrength?: boolean;
156
360
  defaultTimeout?: number;
157
361
  safeMode?: boolean;
158
362
  }
159
363
  declare const defaultSecurityConfig: SecurityConfig;
160
364
  declare function provideSecurity(config?: SecurityConfig): EnvironmentProviders;
161
365
  declare function provideRegexSecurity(): EnvironmentProviders;
366
+ declare function provideWebCrypto(): EnvironmentProviders;
367
+ declare function provideSecureStorage(config?: SecureStorageConfig): EnvironmentProviders;
368
+ declare function provideInputSanitizer(config?: SanitizerConfig): EnvironmentProviders;
369
+ declare function providePasswordStrength(): EnvironmentProviders;
162
370
 
163
- export { RegexSecurityBuilder, RegexSecurityService, defaultSecurityConfig, provideRegexSecurity, provideSecurity };
164
- export type { RegexBuilderOptions, RegexSecurityConfig, RegexSecurityResult, RegexTestResult, SecurityConfig };
371
+ export { InputSanitizerService, PasswordStrengthService, RegexSecurityBuilder, RegexSecurityService, SANITIZER_CONFIG, SECURE_STORAGE_CONFIG, SecureStorageService, WebCryptoService, defaultSecurityConfig, provideInputSanitizer, providePasswordStrength, provideRegexSecurity, provideSecureStorage, provideSecurity, provideWebCrypto };
372
+ export type { AesEncryptResult, AesKeyLength, HashAlgorithm, HmacAlgorithm, PasswordStrengthResult, RegexBuilderOptions, RegexSecurityConfig, RegexSecurityResult, RegexTestResult, SanitizerConfig, SecureStorageConfig, SecurityConfig, StorageTarget };