@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 +161 -1
- package/README.md +156 -0
- package/fesm2022/angular-helpers-security.mjs +561 -2
- package/package.json +12 -5
- package/types/angular-helpers-security.d.ts +185 -3
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>' → '<b>hola</b>'
|
|
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>' → '<b>hello</b>'
|
|
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, '&')
|
|
814
|
+
.replace(/</g, '<')
|
|
815
|
+
.replace(/>/g, '>')
|
|
816
|
+
.replace(/"/g, '"')
|
|
817
|
+
.replace(/'/g, ''');
|
|
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
|
|
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
|
|
25
|
+
"url": "https://github.com/Gaspar1992/angular-helpers"
|
|
19
26
|
},
|
|
20
27
|
"bugs": {
|
|
21
|
-
"url": "https://github.com/angular-helpers/
|
|
28
|
+
"url": "https://github.com/Gaspar1992/angular-helpers/issues"
|
|
22
29
|
},
|
|
23
|
-
"homepage": "https://github.
|
|
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 };
|