@angular-helpers/security 21.0.4 → 21.2.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
@@ -4,7 +4,11 @@
4
4
 
5
5
  # Angular Security Helpers
6
6
 
7
- Paquete de seguridad para aplicaciones Angular que previene ataques comunes como ReDoS (Regular Expression Denial of Service) usando Web Workers para ejecución segura.
7
+ Paquete de seguridad para aplicaciones Angular que previene ataques comunes como ReDoS (Regular Expression Denial of Service), XSS, y proporciona utilidades criptográficas usando Web Workers y Web Crypto API para ejecución segura.
8
+
9
+ > **Versión**: 21.2.0 — [CHANGELOG](./CHANGELOG.md)
10
+
11
+ ---
8
12
 
9
13
  ## 🛡️ Características
10
14
 
@@ -18,9 +22,32 @@ Paquete de seguridad para aplicaciones Angular que previene ataques comunes como
18
22
  ### **Web Crypto API**
19
23
 
20
24
  - **Cifrado/Descifrado**: Soporte AES-GCM para manejo seguro de datos
25
+ - **Firmas HMAC**: Firmar y verificar datos con HMAC-SHA256/384/512
21
26
  - **Hashing**: SHA-256 y otros algoritmos
22
27
  - **Gestión de Claves**: Generar, importar y exportar claves criptográficas
23
28
  - **Aleatorio Seguro**: Valores aleatorios criptográficamente seguros
29
+ - **Generación UUID**: UUIDs RFC4122 v4
30
+
31
+ ### **Almacenamiento Seguro**
32
+
33
+ - **Cifrado Transparente**: Almacenamiento cifrado AES-GCM sobre localStorage/sessionStorage
34
+ - **Modo Efímero**: Claves en memoria para seguridad de sesión única
35
+ - **Modo Passphrase**: Claves derivadas PBKDF2 para persistencia entre sesiones
36
+ - **Aislamiento por Namespace**: Organiza datos almacenados con prefijos
37
+
38
+ ### **Sanitización de Input**
39
+
40
+ - **Prevención XSS**: Limpieza de HTML con lista de permitidos
41
+ - **Validación de URLs**: Esquemas de URL seguros
42
+ - **Escape de HTML**: Caracteres especiales para interpolación segura
43
+ - **JSON Seguro**: Parseo seguro sin eval
44
+
45
+ ### **Fuerza de Contraseña**
46
+
47
+ - **Puntuación basada en Entropía**: Calcular fuerza en bits de entropía
48
+ - **Detección de Contraseñas Comunes**: Bloquear contraseñas débiles conocidas
49
+ - **Detección de Patrones**: Detectar patrones de teclado y secuencias
50
+ - **Feedback Accionable**: Sugerencias específicas para mejorar
24
51
 
25
52
  ### **Patrón Builder**
26
53
 
@@ -44,7 +71,16 @@ import { provideSecurity } from '@angular-helpers/security';
44
71
  bootstrapApplication(AppComponent, {
45
72
  providers: [
46
73
  provideSecurity({
74
+ // Servicios core (habilitados por defecto)
47
75
  enableRegexSecurity: true,
76
+ enableWebCrypto: true,
77
+
78
+ // Nuevos servicios (opt-in, deshabilitados por defecto)
79
+ enableSecureStorage: true,
80
+ enableInputSanitizer: true,
81
+ enablePasswordStrength: true,
82
+
83
+ // Configuración global
48
84
  defaultTimeout: 5000,
49
85
  safeMode: false,
50
86
  }),
@@ -52,6 +88,27 @@ bootstrapApplication(AppComponent, {
52
88
  });
53
89
  ```
54
90
 
91
+ ### **Providers Individuales**
92
+
93
+ ```typescript
94
+ import {
95
+ provideRegexSecurity,
96
+ provideWebCrypto,
97
+ provideSecureStorage,
98
+ provideInputSanitizer,
99
+ providePasswordStrength,
100
+ } from '@angular-helpers/security';
101
+
102
+ // Usar solo los servicios que necesites
103
+ bootstrapApplication(AppComponent, {
104
+ providers: [
105
+ provideSecureStorage({ storage: 'session', pbkdf2Iterations: 600_000 }),
106
+ provideInputSanitizer({ allowedTags: ['b', 'i', 'em', 'strong'] }),
107
+ providePasswordStrength(),
108
+ ],
109
+ });
110
+ ```
111
+
55
112
  ### **Inyección de Servicios**
56
113
 
57
114
  ```typescript
@@ -142,6 +199,109 @@ export class SecureStorageComponent {
142
199
  generateUUID(): string {
143
200
  return this.cryptoService.randomUUID();
144
201
  }
202
+
203
+ async signAndVerify(data: string): Promise<boolean> {
204
+ // Generar clave HMAC para SHA-256
205
+ const key = await this.cryptoService.generateHmacKey('HMAC-SHA-256');
206
+
207
+ // Firmar los datos
208
+ const signature = await this.cryptoService.sign(key, data);
209
+
210
+ // Verificar la firma
211
+ return await this.cryptoService.verify(key, data, signature);
212
+ }
213
+ }
214
+ ```
215
+
216
+ ### **SecureStorageService — Almacenamiento Cifrado**
217
+
218
+ ```typescript
219
+ import { SecureStorageService } from '@angular-helpers/security';
220
+
221
+ export class UserSettingsComponent {
222
+ private storage = inject(SecureStorageService);
223
+
224
+ async saveUserToken(token: string): Promise<void> {
225
+ // Modo efímero (por defecto): datos sobreviven solo esta sesión
226
+ await this.storage.set('authToken', { token, createdAt: Date.now() });
227
+ }
228
+
229
+ async getUserToken(): Promise<{ token: string; createdAt: number } | null> {
230
+ return await this.storage.get<{ token: string; createdAt: number }>('authToken');
231
+ }
232
+
233
+ async initWithPassphrase(passphrase: string): Promise<void> {
234
+ // Modo passphrase: datos sobreviven recargas de página
235
+ await this.storage.initWithPassphrase(passphrase);
236
+ }
237
+
238
+ async saveWithNamespace(userId: string, data: unknown): Promise<void> {
239
+ // Aislamiento por namespace
240
+ await this.storage.set('profile', data, `user:${userId}`);
241
+ }
242
+
243
+ clearUserData(userId: string): void {
244
+ // Limpiar solo los datos de este usuario
245
+ this.storage.clear(`user:${userId}`);
246
+ }
247
+ }
248
+ ```
249
+
250
+ ### **InputSanitizerService — Prevención XSS**
251
+
252
+ ```typescript
253
+ import { InputSanitizerService } from '@angular-helpers/security';
254
+
255
+ export class CommentComponent {
256
+ private sanitizer = inject(InputSanitizerService);
257
+
258
+ sanitizeUserComment(html: string): string {
259
+ // Limpiar tags peligrosos, mantener seguros (b, i, em, a, etc.)
260
+ return this.sanitizer.sanitizeHtml(html);
261
+ // Ejemplo: '<b>Hola</b><script>alert(1)</script>' → '<b>Hola</b>'
262
+ }
263
+
264
+ validateUserLink(url: string): string | null {
265
+ // Solo permitir URLs http/https
266
+ return this.sanitizer.sanitizeUrl(url);
267
+ // Ejemplo: 'javascript:alert(1)' → null
268
+ // Ejemplo: 'https://example.com' → 'https://example.com/'
269
+ }
270
+
271
+ escapeForDisplay(text: string): string {
272
+ // Seguro para nodos de texto HTML
273
+ return this.sanitizer.escapeHtml(text);
274
+ // Ejemplo: '<b>hola</b>' → '&lt;b&gt;hola&lt;/b&gt;'
275
+ }
276
+
277
+ parseUserJson(json: string): unknown | null {
278
+ // Parseo seguro de JSON sin eval
279
+ return this.sanitizer.sanitizeJson(json);
280
+ }
281
+ }
282
+ ```
283
+
284
+ ### **PasswordStrengthService — Evaluación de Contraseñas**
285
+
286
+ ```typescript
287
+ import { PasswordStrengthService } from '@angular-helpers/security';
288
+
289
+ export class RegistrationComponent {
290
+ private passwordStrength = inject(PasswordStrengthService);
291
+
292
+ checkPasswordStrength(password: string): void {
293
+ const result = this.passwordStrength.assess(password);
294
+
295
+ console.log(`Score: ${result.score}/4`); // 0-4
296
+ console.log(`Label: ${result.label}`); // 'very-weak' a 'very-strong'
297
+ console.log(`Entropía: ${result.entropy} bits`); // entropía calculada
298
+ console.log('Feedback:', result.feedback); // sugerencias de mejora
299
+
300
+ // Ejemplos de resultados:
301
+ // 'password' → score: 0, label: 'very-weak', feedback: ['Contraseña común']
302
+ // 'P@ssw0rd!' → score: 2, label: 'fair', feedback: ['Evita patrones de teclado']
303
+ // 'xK#9mZ$vLq2@rBnT7' → score: 4, label: 'very-strong', feedback: []
304
+ }
145
305
  }
146
306
  ```
147
307
 
package/README.md CHANGED
@@ -19,8 +19,31 @@ Security package for Angular applications that prevents common attacks like ReDo
19
19
 
20
20
  - **Encryption/Decryption**: AES-GCM support for secure data handling
21
21
  - **Hashing**: SHA-256 and other algorithms
22
+ - **HMAC Signing**: HMAC-SHA-256/384/512 for message authentication
22
23
  - **Key Management**: Generate, import, and export cryptographic keys
23
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
24
47
 
25
48
  ### **Builder Pattern**
26
49
 
@@ -44,7 +67,16 @@ import { provideSecurity } from '@angular-helpers/security';
44
67
  bootstrapApplication(AppComponent, {
45
68
  providers: [
46
69
  provideSecurity({
70
+ // Core services (enabled by default)
47
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
48
80
  defaultTimeout: 5000,
49
81
  safeMode: false,
50
82
  }),
@@ -52,6 +84,27 @@ bootstrapApplication(AppComponent, {
52
84
  });
53
85
  ```
54
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
+
55
108
  ### **Service Injection**
56
109
 
57
110
  ```typescript
@@ -212,6 +265,109 @@ export class SecureStorageComponent {
212
265
  generateUUID(): string {
213
266
  return this.cryptoService.randomUUID();
214
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
+ }
215
371
  }
216
372
  ```
217
373
 
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, Injectable, PLATFORM_ID, makeEnvironmentProviders } from '@angular/core';
2
+ import { inject, DestroyRef, Injectable, PLATFORM_ID, InjectionToken, makeEnvironmentProviders } from '@angular/core';
3
3
  import { isPlatformBrowser } from '@angular/common';
4
4
 
5
5
  /**
@@ -470,11 +470,63 @@ class WebCryptoService {
470
470
  }
471
471
  return crypto.randomUUID();
472
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
+ }
473
510
  bufferToHex(buffer) {
474
511
  return Array.from(new Uint8Array(buffer))
475
512
  .map((b) => b.toString(16).padStart(2, '0'))
476
513
  .join('');
477
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
+ }
478
530
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebCryptoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
479
531
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebCryptoService });
480
532
  }
@@ -482,9 +534,492 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
482
534
  type: Injectable
483
535
  }] });
484
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
+
485
1017
  const defaultSecurityConfig = {
486
1018
  enableRegexSecurity: true,
487
1019
  enableWebCrypto: true,
1020
+ enableSecureStorage: false,
1021
+ enableInputSanitizer: false,
1022
+ enablePasswordStrength: false,
488
1023
  defaultTimeout: 5000,
489
1024
  safeMode: false,
490
1025
  };
@@ -497,6 +1032,15 @@ function provideSecurity(config = {}) {
497
1032
  if (mergedConfig.enableWebCrypto) {
498
1033
  providers.push(WebCryptoService);
499
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
+ }
500
1044
  return makeEnvironmentProviders(providers);
501
1045
  }
502
1046
  function provideRegexSecurity() {
@@ -505,9 +1049,24 @@ function provideRegexSecurity() {
505
1049
  function provideWebCrypto() {
506
1050
  return makeEnvironmentProviders([WebCryptoService]);
507
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
+ }
508
1067
 
509
1068
  /**
510
1069
  * Generated bundle index. Do not edit.
511
1070
  */
512
1071
 
513
- export { RegexSecurityBuilder, RegexSecurityService, WebCryptoService, defaultSecurityConfig, provideRegexSecurity, provideSecurity, provideWebCrypto };
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.4",
3
+ "version": "21.2.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;
@@ -152,6 +152,7 @@ declare class RegexSecurityBuilder {
152
152
  }
153
153
 
154
154
  type HashAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
155
+ type HmacAlgorithm = 'HMAC-SHA-256' | 'HMAC-SHA-384' | 'HMAC-SHA-512';
155
156
  type AesKeyLength = 128 | 192 | 256;
156
157
  interface AesEncryptResult {
157
158
  ciphertext: ArrayBuffer;
@@ -170,14 +171,192 @@ declare class WebCryptoService {
170
171
  importAesKey(jwk: JsonWebKey): Promise<CryptoKey>;
171
172
  generateRandomBytes(length: number): Uint8Array;
172
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>;
173
184
  private bufferToHex;
185
+ private hexToBytes;
186
+ private hmacHashName;
174
187
  static ɵfac: i0.ɵɵFactoryDeclaration<WebCryptoService, never>;
175
188
  static ɵprov: i0.ɵɵInjectableDeclaration<WebCryptoService>;
176
189
  }
177
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
+
178
354
  interface SecurityConfig {
179
355
  enableRegexSecurity?: boolean;
180
356
  enableWebCrypto?: boolean;
357
+ enableSecureStorage?: boolean;
358
+ enableInputSanitizer?: boolean;
359
+ enablePasswordStrength?: boolean;
181
360
  defaultTimeout?: number;
182
361
  safeMode?: boolean;
183
362
  }
@@ -185,6 +364,9 @@ declare const defaultSecurityConfig: SecurityConfig;
185
364
  declare function provideSecurity(config?: SecurityConfig): EnvironmentProviders;
186
365
  declare function provideRegexSecurity(): EnvironmentProviders;
187
366
  declare function provideWebCrypto(): EnvironmentProviders;
367
+ declare function provideSecureStorage(config?: SecureStorageConfig): EnvironmentProviders;
368
+ declare function provideInputSanitizer(config?: SanitizerConfig): EnvironmentProviders;
369
+ declare function providePasswordStrength(): EnvironmentProviders;
188
370
 
189
- export { RegexSecurityBuilder, RegexSecurityService, WebCryptoService, defaultSecurityConfig, provideRegexSecurity, provideSecurity, provideWebCrypto };
190
- export type { AesEncryptResult, AesKeyLength, HashAlgorithm, 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 };