@crossdyne/security 0.5.0-beta.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +367 -8
- package/dist/index.cjs +89 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -48
- package/dist/index.d.ts +60 -48
- package/dist/index.js +89 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,374 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Crossdyne.Security
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Cross-platform cryptographic library — [.NET](https://github.com/crossdyne/dotnet-security) | [TypeScript](https://github.com/crossdyne/typescript-security)
|
|
4
|
+
>
|
|
5
|
+
> [English](#english) | [Русский](#русский)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
---
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
<a name="english"></a>
|
|
10
|
+
## English
|
|
11
|
+
|
|
12
|
+
TypeScript / JavaScript implementation of the Crossdyne.Security cryptographic library via the native **Web Crypto API**.
|
|
10
13
|
|
|
11
14
|
## Installation
|
|
12
15
|
|
|
13
16
|
```bash
|
|
14
|
-
npm install @
|
|
15
|
-
```
|
|
17
|
+
npm install @crossdyne/security
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- **AES-256-GCM** — authenticated symmetric encryption via `crypto.subtle`
|
|
23
|
+
- **PBKDF2 + HKDF** — secure key derivation from passwords
|
|
24
|
+
- **SRP-6a** — password-authenticated key exchange without sending the password to the server
|
|
25
|
+
- **Zero-memory** — sensitive buffers are explicitly overwritten after use
|
|
26
|
+
- **Cross-platform** — compatible with the [.NET implementation](https://github.com/crossdyne/dotnet-security). Encrypted payloads and SRP messages are interchangeable between TS and .NET.
|
|
27
|
+
|
|
28
|
+
### Requirements
|
|
29
|
+
|
|
30
|
+
- TypeScript 5.3+
|
|
31
|
+
- Web Crypto API (`crypto.subtle`) — modern browsers and Node.js 18+
|
|
32
|
+
|
|
33
|
+
### Project Structure
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
crossdyne-security/
|
|
37
|
+
├── crypto/
|
|
38
|
+
│ ├── crypto-service.ts # AES-GCM encrypt / decrypt
|
|
39
|
+
│ ├── key-derivation-service.ts # Derive KEK and AuthHash (PBKDF2 → HKDF)
|
|
40
|
+
│ ├── crypto-profile-registry.ts # Versioned crypto profiles
|
|
41
|
+
│ ├── crypto-version.ts # Enum: V1, V2, ...
|
|
42
|
+
│ └── kdf-options.ts # PBKDF2 / HKDF parameters
|
|
43
|
+
├── srp/
|
|
44
|
+
│ ├── srp-server-service.ts # SRP server (challenge, verify)
|
|
45
|
+
│ ├── srp-client-service.ts # SRP client (proof, verifier)
|
|
46
|
+
│ ├── srp-key-derivation-service.ts # Derive auth-hash for SRP
|
|
47
|
+
│ ├── srp-context-factory.ts # Creates SRP context (N, g, k, hash)
|
|
48
|
+
│ ├── srp-group.ts # Enum: Group2048, Group4096, ...
|
|
49
|
+
│ └── srp-encoding.ts # BigInt ↔ bytes helpers
|
|
50
|
+
└── utils/
|
|
51
|
+
├── security-utils.ts # Base64, BigInt, fixed-time compare
|
|
52
|
+
└── srp-encoding.ts # Hash moduli, session key, M1 / M2
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Quick Start
|
|
56
|
+
|
|
57
|
+
#### Encrypt / Decrypt
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { CryptoService } from './crypto/crypto-service.js';
|
|
61
|
+
import { CryptoVersion } from './crypto/crypto-version.js';
|
|
62
|
+
|
|
63
|
+
const crypto = new CryptoService();
|
|
64
|
+
const rawKey = crypto.generateRandomBytes(32); // AES-256 raw key
|
|
65
|
+
|
|
66
|
+
// Import the raw key into a non-extractable CryptoKey
|
|
67
|
+
const cryptoKey = await crypto.importKey(rawKey, CryptoVersion.V1, ['encrypt', 'decrypt']);
|
|
68
|
+
|
|
69
|
+
const encrypted = await crypto.encryptData(
|
|
70
|
+
{ message: "Hello, World!" },
|
|
71
|
+
cryptoKey,
|
|
72
|
+
CryptoVersion.V1
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const decrypted = await crypto.decryptData<MyData>(encrypted, cryptoKey);
|
|
76
|
+
|
|
77
|
+
// Best practice: securely wipe the raw key from memory after import
|
|
78
|
+
rawKey.fill(0);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Encrypted payload format (Base64):
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
[Version (1 byte)][Nonce (N bytes)][Ciphertext + Tag]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Key Derivation from Password
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { KeyDerivationService } from './crypto/key-derivation-service.js';
|
|
91
|
+
import { CryptoService } from './crypto/crypto-service.js';
|
|
92
|
+
import { CryptoVersion } from './crypto/crypto-version.js';
|
|
93
|
+
|
|
94
|
+
const kdf = new KeyDerivationService();
|
|
95
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
96
|
+
|
|
97
|
+
const { kek, authHash } = await kdf.deriveKeysFromPassword(
|
|
98
|
+
"user@example.com",
|
|
99
|
+
"SuperSecret123!",
|
|
100
|
+
salt,
|
|
101
|
+
CryptoVersion.V1
|
|
102
|
+
);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `kek` — `Uint8Array` key for AES-GCM
|
|
106
|
+
- `authHash` — Base64 string for SRP authentication
|
|
107
|
+
|
|
108
|
+
#### SRP Authentication Flow
|
|
109
|
+
|
|
110
|
+
**Server — generate challenge**
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { SrpServerService } from './srp/srp-server-service.js';
|
|
114
|
+
import { SrpGroup } from './srp/srp-group.js';
|
|
115
|
+
|
|
116
|
+
const server = new SrpServerService();
|
|
117
|
+
|
|
118
|
+
const session = await server.getSrpChallenge(
|
|
119
|
+
"user@example.com",
|
|
120
|
+
verifierBytes,
|
|
121
|
+
salt,
|
|
122
|
+
SrpGroup.Group2048
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Send to client: salt + session.publicKeyB (B)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Client — generate proof**
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { SrpClientService } from './srp/srp-client-service.js';
|
|
132
|
+
import { SrpKeyDerivationService } from './srp/srp-key-derivation-service.js';
|
|
133
|
+
import { SrpGroup } from './srp/srp-group.js';
|
|
134
|
+
import { CryptoVersion } from './crypto/crypto-version.js';
|
|
135
|
+
|
|
136
|
+
const client = new SrpClientService();
|
|
137
|
+
const kdf = new SrpKeyDerivationService();
|
|
138
|
+
|
|
139
|
+
// 1. Derive auth-hash from password
|
|
140
|
+
const authHash = await kdf.deriveAuthHashForSrp(
|
|
141
|
+
identity, password, salt, SrpGroup.Group2048, CryptoVersion.V1
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// 2. Generate proof
|
|
145
|
+
const { A, M1, SessionKeyK } = await client.generateSrpProof(
|
|
146
|
+
identity,
|
|
147
|
+
authHash,
|
|
148
|
+
btoa(String.fromCharCode(...salt)),
|
|
149
|
+
btoa(String.fromCharCode(...session.publicKeyB)),
|
|
150
|
+
SrpGroup.Group2048
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Send to server: A + M1
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Server — verify proof**
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const serverM2 = await server.verifySrpProof(
|
|
160
|
+
session,
|
|
161
|
+
A,
|
|
162
|
+
M1,
|
|
163
|
+
SrpGroup.Group2048
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Send to client: serverM2
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Client — verify server proof**
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const isValid = await client.verifyServerM2(
|
|
173
|
+
A,
|
|
174
|
+
M1,
|
|
175
|
+
SessionKeyK,
|
|
176
|
+
serverM2,
|
|
177
|
+
SrpGroup.Group2048
|
|
178
|
+
);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Security Notes
|
|
182
|
+
|
|
183
|
+
- All sensitive buffers (like raw keys) should be explicitly overwritten (`fill(0)`) after being imported into a `CryptoKey`.
|
|
184
|
+
- Salt must be **at least 16 bytes**.
|
|
185
|
+
- AES-256 key must be **exactly 32 bytes**.
|
|
186
|
+
- SRP groups are protected against small-subgroup attacks (checks `A % N != 0`, `B != 0`).
|
|
187
|
+
|
|
188
|
+
### License
|
|
189
|
+
|
|
190
|
+
MIT
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
<a name="русский"></a>
|
|
195
|
+
## Русский
|
|
196
|
+
|
|
197
|
+
Реализация криптографической библиотеки Crossdyne.Security для TypeScript / JavaScript через нативный **Web Crypto API**.
|
|
198
|
+
|
|
199
|
+
## Установка
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npm install @crossdyne/security
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Возможности
|
|
206
|
+
|
|
207
|
+
- **AES-256-GCM** — симметричное шифрование с аутентификацией через `crypto.subtle`
|
|
208
|
+
- **PBKDF2 + HKDF** — надёжный вывод ключей из пароля
|
|
209
|
+
- **SRP-6a** — протокол аутентификации без передачи пароля на сервер
|
|
210
|
+
- **Zero-memory** — чувствительные буферы явно перезаписываются после использования
|
|
211
|
+
- **Кроссплатформенность** — совместима с [.NET-реализацией](https://github.com/crossdyne/dotnet-security). Зашифрованные данные и SRP-сообщения взаимозаменяемы между TS и .NET.
|
|
212
|
+
|
|
213
|
+
### Требования
|
|
214
|
+
|
|
215
|
+
- TypeScript 5.3+
|
|
216
|
+
- Web Crypto API (`crypto.subtle`) — современные браузеры и Node.js 18+
|
|
217
|
+
|
|
218
|
+
### Структура проекта
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
crossdyne-security/
|
|
222
|
+
├── crypto/
|
|
223
|
+
│ ├── crypto-service.ts # AES-GCM шифрование / дешифрование
|
|
224
|
+
│ ├── key-derivation-service.ts # Вывод KEK и AuthHash (PBKDF2 → HKDF)
|
|
225
|
+
│ ├── crypto-profile-registry.ts # Версионированные криптопрофили
|
|
226
|
+
│ ├── crypto-version.ts # Enum: V1, V2, ...
|
|
227
|
+
│ └── kdf-options.ts # Параметры PBKDF2 / HKDF
|
|
228
|
+
├── srp/
|
|
229
|
+
│ ├── srp-server-service.ts # SRP сервер (challenge, проверка)
|
|
230
|
+
│ ├── srp-client-service.ts # SRP клиент (proof, верификатор)
|
|
231
|
+
│ ├── srp-key-derivation-service.ts # Вывод auth-hash для SRP
|
|
232
|
+
│ ├── srp-context-factory.ts # Создание SRP-контекста (N, g, k, hash)
|
|
233
|
+
│ ├── srp-group.ts # Enum: Group2048, Group4096, ...
|
|
234
|
+
│ └── srp-encoding.ts # BigInt ↔ bytes хелперы
|
|
235
|
+
└── utils/
|
|
236
|
+
├── security-utils.ts # Base64, BigInt, сравнение в постоянное время
|
|
237
|
+
└── srp-encoding.ts # Хеш модулей, сессионный ключ, M1 / M2
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Быстрый старт
|
|
241
|
+
|
|
242
|
+
#### Шифрование / дешифрование
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { CryptoService } from './crypto/crypto-service.js';
|
|
246
|
+
import { CryptoVersion } from './crypto/crypto-version.js';
|
|
247
|
+
|
|
248
|
+
const crypto = new CryptoService();
|
|
249
|
+
const rawKey = crypto.generateRandomBytes(32); // Сырой ключ AES-256
|
|
250
|
+
|
|
251
|
+
// Импортируем сырой ключ в неизвлекаемый CryptoKey
|
|
252
|
+
const cryptoKey = await crypto.importKey(rawKey, CryptoVersion.V1, ['encrypt', 'decrypt']);
|
|
253
|
+
|
|
254
|
+
const encrypted = await crypto.encryptData(
|
|
255
|
+
{ message: "Hello, World!" },
|
|
256
|
+
cryptoKey,
|
|
257
|
+
CryptoVersion.V1
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const decrypted = await crypto.decryptData<MyData>(encrypted, cryptoKey);
|
|
261
|
+
|
|
262
|
+
// Рекомендуемая практика: безопасно очистить сырой ключ из памяти сразу после импорта
|
|
263
|
+
rawKey.fill(0);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Формат зашифрованных данных (Base64):
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
[Version (1 byte)][Nonce (N bytes)][Ciphertext + Tag]
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### Вывод ключей из пароля
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { KeyDerivationService } from './crypto/key-derivation-service.js';
|
|
276
|
+
import { CryptoVersion } from './crypto/crypto-version.js';
|
|
277
|
+
|
|
278
|
+
const kdf = new KeyDerivationService();
|
|
279
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
280
|
+
|
|
281
|
+
const { kek, authHash } = await kdf.deriveKeysFromPassword(
|
|
282
|
+
"user@example.com",
|
|
283
|
+
"SuperSecret123!",
|
|
284
|
+
salt,
|
|
285
|
+
CryptoVersion.V1
|
|
286
|
+
);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
- `kek` — `Uint8Array` ключ для AES-GCM
|
|
290
|
+
- `authHash` — Base64-строка для SRP-аутентификации
|
|
291
|
+
|
|
292
|
+
#### SRP-аутентификация
|
|
293
|
+
|
|
294
|
+
**Сервер — генерация challenge**
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { SrpServerService } from './srp/srp-server-service.js';
|
|
298
|
+
import { SrpGroup } from './srp/srp-group.js';
|
|
299
|
+
|
|
300
|
+
const server = new SrpServerService();
|
|
301
|
+
|
|
302
|
+
const session = await server.getSrpChallenge(
|
|
303
|
+
"user@example.com",
|
|
304
|
+
verifierBytes,
|
|
305
|
+
salt,
|
|
306
|
+
SrpGroup.Group2048
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Отправить клиенту: salt + session.publicKeyB (B)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Клиент — генерация proof**
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { SrpClientService } from './srp/srp-client-service.js';
|
|
316
|
+
import { SrpKeyDerivationService } from './srp/srp-key-derivation-service.js';
|
|
317
|
+
import { SrpGroup } from './srp/srp-group.js';
|
|
318
|
+
import { CryptoVersion } from './crypto/crypto-version.js';
|
|
319
|
+
|
|
320
|
+
const client = new SrpClientService();
|
|
321
|
+
const kdf = new SrpKeyDerivationService();
|
|
322
|
+
|
|
323
|
+
// 1. Вывести auth-hash из пароля
|
|
324
|
+
const authHash = await kdf.deriveAuthHashForSrp(
|
|
325
|
+
identity, password, salt, SrpGroup.Group2048, CryptoVersion.V1
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// 2. Сгенерировать proof
|
|
329
|
+
const { A, M1, SessionKeyK } = await client.generateSrpProof(
|
|
330
|
+
identity,
|
|
331
|
+
authHash,
|
|
332
|
+
btoa(String.fromCharCode(...salt)),
|
|
333
|
+
btoa(String.fromCharCode(...session.publicKeyB)),
|
|
334
|
+
SrpGroup.Group2048
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// Отправить серверу: A + M1
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Сервер — проверка proof**
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
const serverM2 = await server.verifySrpProof(
|
|
344
|
+
session,
|
|
345
|
+
A,
|
|
346
|
+
M1,
|
|
347
|
+
SrpGroup.Group2048
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Отправить клиенту: serverM2
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Клиент — проверка server proof**
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
const isValid = await client.verifyServerM2(
|
|
357
|
+
A,
|
|
358
|
+
M1,
|
|
359
|
+
SessionKeyK,
|
|
360
|
+
serverM2,
|
|
361
|
+
SrpGroup.Group2048
|
|
362
|
+
);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Безопасность
|
|
366
|
+
|
|
367
|
+
- Все чувствительные буферы (сырые ключи и тд) должны быть явно перезаписаны (`fill(0)`) после их импорта в `CryptoKey`.
|
|
368
|
+
- Соль должна быть **минимум 16 байт**.
|
|
369
|
+
- Ключ AES-256 — **строго 32 байта**.
|
|
370
|
+
- SRP-группы защищены от атак на малые подгруппы (проверки `A % N != 0`, `B != 0`).
|
|
371
|
+
|
|
372
|
+
### Лицензия
|
|
373
|
+
|
|
374
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
// src/configurations/security-constants.ts
|
|
4
4
|
var SecurityConstants = {
|
|
5
|
+
/** Standard nonce size for AES-GCM (96 bits). Fixed by NIST SP 800-38D. */
|
|
5
6
|
AesGcmNonceSize: 12,
|
|
6
|
-
|
|
7
|
+
/** Minimum allowed authentication tag size (96 bits). */
|
|
7
8
|
AesGcmTagSizeMin: 12,
|
|
9
|
+
/** Maximum allowed authentication tag size (128 bits). */
|
|
8
10
|
AesGcmTagSizeMax: 16,
|
|
11
|
+
/** Key size for AES-256 (256 bits). */
|
|
9
12
|
KeySizeBytes: 32,
|
|
10
|
-
|
|
11
|
-
Pbkdf2IterationsDefault: 6e5,
|
|
13
|
+
/** Absolute minimum PBKDF2 iterations for any profile version. */
|
|
12
14
|
Pbkdf2IterationsMinimum: 1e5
|
|
13
15
|
};
|
|
14
16
|
var HashSizes = {
|
|
@@ -110,54 +112,36 @@ var SecurityUtils = class {
|
|
|
110
112
|
};
|
|
111
113
|
|
|
112
114
|
// src/crypto/aes-gcm-options.ts
|
|
113
|
-
var
|
|
114
|
-
constructor() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return this._tagSize;
|
|
129
|
-
}
|
|
130
|
-
set tagSize(v) {
|
|
131
|
-
if (v < SecurityConstants.AesGcmTagSizeMin || v > SecurityConstants.AesGcmTagSizeMax) throw new RangeError(`Tag size must be between ${SecurityConstants.AesGcmTagSizeMin} and ${SecurityConstants.AesGcmTagSizeMax}`);
|
|
132
|
-
this._tagSize = v;
|
|
115
|
+
var _AesGcmOptions = class _AesGcmOptions {
|
|
116
|
+
constructor(nonceSize, tagSize) {
|
|
117
|
+
if (nonceSize !== SecurityConstants.AesGcmNonceSize) {
|
|
118
|
+
throw new RangeError(
|
|
119
|
+
`AES-GCM requires exactly ${SecurityConstants.AesGcmNonceSize}-byte nonce per NIST SP 800-38D.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (tagSize < SecurityConstants.AesGcmTagSizeMin || tagSize > SecurityConstants.AesGcmTagSizeMax) {
|
|
123
|
+
throw new RangeError(
|
|
124
|
+
`Tag size must be between ${SecurityConstants.AesGcmTagSizeMin} and ${SecurityConstants.AesGcmTagSizeMax} bytes.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
this.nonceSize = nonceSize;
|
|
128
|
+
this.tagSize = tagSize;
|
|
129
|
+
Object.freeze(this);
|
|
133
130
|
}
|
|
134
131
|
/** Validates that {@link tagSize} is in the allowed range. */
|
|
135
132
|
validate() {
|
|
136
|
-
if (this.tagSize < SecurityConstants.AesGcmTagSizeMin || this.tagSize > SecurityConstants.AesGcmTagSizeMax)
|
|
137
|
-
|
|
138
|
-
/** Default preset: nonce=12, tag=16, no AAD. */
|
|
139
|
-
static get default() {
|
|
140
|
-
return new _AesGcmOptions();
|
|
141
|
-
}
|
|
142
|
-
/** Fluent setter for {@link tagSize}. */
|
|
143
|
-
withTagSize(s) {
|
|
144
|
-
this.tagSize = s;
|
|
145
|
-
return this;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Fluent setter for {@link associatedData}.
|
|
149
|
-
* Accepts a byte array or a UTF-8 string (encoded internally).
|
|
150
|
-
*/
|
|
151
|
-
withAssociatedData(aad) {
|
|
152
|
-
this.associatedData = typeof aad === "string" ? new TextEncoder().encode(aad) : aad;
|
|
153
|
-
return this;
|
|
154
|
-
}
|
|
155
|
-
/** Validates and returns this instance. */
|
|
156
|
-
build() {
|
|
157
|
-
this.validate();
|
|
158
|
-
return this;
|
|
133
|
+
if (this.tagSize < SecurityConstants.AesGcmTagSizeMin || this.tagSize > SecurityConstants.AesGcmTagSizeMax)
|
|
134
|
+
throw new Error(`Invalid Tag Size: ${this.tagSize}`);
|
|
159
135
|
}
|
|
160
136
|
};
|
|
137
|
+
/**
|
|
138
|
+
* V1 preset: nonce=12, tag=16, no AAD.
|
|
139
|
+
* These exact values are frozen for all V1-encrypted payloads.
|
|
140
|
+
*/
|
|
141
|
+
_AesGcmOptions.V1 = Object.freeze(
|
|
142
|
+
new _AesGcmOptions(12, 16)
|
|
143
|
+
);
|
|
144
|
+
var AesGcmOptions = _AesGcmOptions;
|
|
161
145
|
|
|
162
146
|
// src/crypto/crypto-profile.ts
|
|
163
147
|
var CryptoProfile = class {
|
|
@@ -167,6 +151,7 @@ var CryptoProfile = class {
|
|
|
167
151
|
*/
|
|
168
152
|
constructor(params) {
|
|
169
153
|
this.version = params.version;
|
|
154
|
+
this.algorithmName = params.algorithmName;
|
|
170
155
|
this.kdfOptions = params.kdfOptions;
|
|
171
156
|
this.aesGcmOptions = params.aesGcmOptions;
|
|
172
157
|
}
|
|
@@ -179,28 +164,21 @@ var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
|
|
|
179
164
|
})(CryptoVersion || {});
|
|
180
165
|
|
|
181
166
|
// src/crypto/kdf-options.ts
|
|
182
|
-
var
|
|
183
|
-
constructor() {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
get hashAlgorithm() {
|
|
198
|
-
return this._hashAlgorithm;
|
|
199
|
-
}
|
|
200
|
-
set hashAlgorithm(v) {
|
|
201
|
-
if (!SupportedHashAlgorithms.includes(v))
|
|
202
|
-
throw new RangeError(`Unsupported hash: ${v}`);
|
|
203
|
-
this._hashAlgorithm = v;
|
|
167
|
+
var _KdfOptions = class _KdfOptions {
|
|
168
|
+
constructor(pbkdf2Iterations, hashAlgorithm) {
|
|
169
|
+
if (pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum) {
|
|
170
|
+
throw new RangeError(
|
|
171
|
+
`PBKDF2 iterations must be at least ${SecurityConstants.Pbkdf2IterationsMinimum}.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (!SupportedHashAlgorithms.includes(hashAlgorithm)) {
|
|
175
|
+
throw new RangeError(
|
|
176
|
+
`Unsupported hash algorithm: ${hashAlgorithm}. Supported: ${SupportedHashAlgorithms.join(", ")}.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
this.pbkdf2Iterations = pbkdf2Iterations;
|
|
180
|
+
this.hashAlgorithm = hashAlgorithm;
|
|
181
|
+
Object.freeze(this);
|
|
204
182
|
}
|
|
205
183
|
/** Validates iterations and hash algorithm. */
|
|
206
184
|
validate() {
|
|
@@ -209,46 +187,33 @@ var KdfOptions = class _KdfOptions {
|
|
|
209
187
|
if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
|
|
210
188
|
throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
|
|
211
189
|
}
|
|
212
|
-
/** Default preset: SHA-256, 600_000 iterations. */
|
|
213
|
-
static get default() {
|
|
214
|
-
return new _KdfOptions();
|
|
215
|
-
}
|
|
216
|
-
/** Fluent setter for {@link pbkdf2Iterations}. */
|
|
217
|
-
withPbkdf2Iterations(i) {
|
|
218
|
-
this.pbkdf2Iterations = i;
|
|
219
|
-
return this;
|
|
220
|
-
}
|
|
221
|
-
/** Fluent setter for {@link hashAlgorithm}. */
|
|
222
|
-
withHashAlgorithm(h) {
|
|
223
|
-
this.hashAlgorithm = h;
|
|
224
|
-
return this;
|
|
225
|
-
}
|
|
226
|
-
/** Validates and returns this instance. */
|
|
227
|
-
build() {
|
|
228
|
-
this.validate();
|
|
229
|
-
return this;
|
|
230
|
-
}
|
|
231
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* V1 preset: SHA-256, 600_000 iterations.
|
|
193
|
+
* These exact values are frozen for all V1-derived keys.
|
|
194
|
+
*/
|
|
195
|
+
_KdfOptions.V1 = Object.freeze(
|
|
196
|
+
new _KdfOptions(6e5, "SHA-256")
|
|
197
|
+
);
|
|
198
|
+
var KdfOptions = _KdfOptions;
|
|
232
199
|
|
|
233
200
|
// src/crypto/crypto-profile-registry.ts
|
|
234
201
|
var CryptoProfileRegistry = class {
|
|
235
202
|
static getProfile(version) {
|
|
236
203
|
switch (version) {
|
|
237
204
|
case 1 /* V1 */:
|
|
238
|
-
return
|
|
239
|
-
version: 1 /* V1 */,
|
|
240
|
-
kdfOptions: KdfOptions.default,
|
|
241
|
-
aesGcmOptions: AesGcmOptions.default
|
|
242
|
-
});
|
|
205
|
+
return this.V1_PROFILE;
|
|
243
206
|
default:
|
|
244
207
|
throw new Error(`Unsupported crypto version: ${version}`);
|
|
245
208
|
}
|
|
246
209
|
}
|
|
247
|
-
/** Latest supported profile (currently V1). */
|
|
248
|
-
static get latest() {
|
|
249
|
-
return this.getProfile(1 /* V1 */);
|
|
250
|
-
}
|
|
251
210
|
};
|
|
211
|
+
CryptoProfileRegistry.V1_PROFILE = new CryptoProfile({
|
|
212
|
+
version: 1 /* V1 */,
|
|
213
|
+
algorithmName: "AES-GCM",
|
|
214
|
+
kdfOptions: KdfOptions.V1,
|
|
215
|
+
aesGcmOptions: AesGcmOptions.V1
|
|
216
|
+
});
|
|
252
217
|
|
|
253
218
|
// src/crypto/crypto.service.ts
|
|
254
219
|
var CryptoService = class {
|
|
@@ -263,7 +228,7 @@ var CryptoService = class {
|
|
|
263
228
|
/**
|
|
264
229
|
* Encrypts a serializable object to a Base64 string.
|
|
265
230
|
* @param dataModel - Object or Uint8Array to encrypt.
|
|
266
|
-
* @param key -
|
|
231
|
+
* @param key - A non-extractable CryptoKey obtained via {@link CryptoService.importKey}.
|
|
267
232
|
* @returns Base64-encoded ciphertext with prepended nonce.
|
|
268
233
|
*/
|
|
269
234
|
async encryptData(dataModel, key, version = 1 /* V1 */) {
|
|
@@ -279,25 +244,13 @@ var CryptoService = class {
|
|
|
279
244
|
}
|
|
280
245
|
const plainBytes = encoder.encode(jsonString);
|
|
281
246
|
const nonce = crypto.getRandomValues(new Uint8Array(opts.nonceSize));
|
|
282
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
283
|
-
"raw",
|
|
284
|
-
key,
|
|
285
|
-
"AES-GCM",
|
|
286
|
-
false,
|
|
287
|
-
["encrypt"]
|
|
288
|
-
);
|
|
289
|
-
let associatedData = new Uint8Array(0);
|
|
290
|
-
if (opts.associatedData != null) {
|
|
291
|
-
associatedData = opts.associatedData;
|
|
292
|
-
}
|
|
293
247
|
const encryptedContent = await crypto.subtle.encrypt(
|
|
294
248
|
{
|
|
295
249
|
name: "AES-GCM",
|
|
296
250
|
iv: nonce,
|
|
297
|
-
tagLength: opts.tagSize * 8
|
|
298
|
-
additionalData: associatedData
|
|
251
|
+
tagLength: opts.tagSize * 8
|
|
299
252
|
},
|
|
300
|
-
|
|
253
|
+
key,
|
|
301
254
|
plainBytes
|
|
302
255
|
);
|
|
303
256
|
const result = new Uint8Array(1 + opts.nonceSize + encryptedContent.byteLength);
|
|
@@ -309,7 +262,7 @@ var CryptoService = class {
|
|
|
309
262
|
/**
|
|
310
263
|
* Decrypts a Base64-encoded ciphertext back to the original object.
|
|
311
264
|
* @param encryptedBase64 - The encrypted data.
|
|
312
|
-
* @param key -
|
|
265
|
+
* @param key - A non-extractable CryptoKey obtained via {@link CryptoService.importKey}.
|
|
313
266
|
* @returns Deserialized object, or null if input is empty.
|
|
314
267
|
* @throws If authentication tag mismatch or corrupted data.
|
|
315
268
|
*/
|
|
@@ -326,25 +279,14 @@ var CryptoService = class {
|
|
|
326
279
|
throw new Error(`Invalid format: minimum expected ${opts.nonceSize + opts.tagSize} byte.`);
|
|
327
280
|
const nonce = payload.slice(0, opts.nonceSize);
|
|
328
281
|
const ciphertextWithTag = payload.slice(opts.nonceSize);
|
|
329
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
330
|
-
"raw",
|
|
331
|
-
key,
|
|
332
|
-
"AES-GCM",
|
|
333
|
-
false,
|
|
334
|
-
["decrypt"]
|
|
335
|
-
);
|
|
336
282
|
try {
|
|
337
|
-
let associatedData = new Uint8Array(0);
|
|
338
|
-
if (opts.associatedData != null)
|
|
339
|
-
associatedData = opts.associatedData;
|
|
340
283
|
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
341
284
|
{
|
|
342
285
|
name: "AES-GCM",
|
|
343
286
|
iv: nonce,
|
|
344
|
-
tagLength: opts.tagSize * 8
|
|
345
|
-
additionalData: associatedData
|
|
287
|
+
tagLength: opts.tagSize * 8
|
|
346
288
|
},
|
|
347
|
-
|
|
289
|
+
key,
|
|
348
290
|
ciphertextWithTag
|
|
349
291
|
);
|
|
350
292
|
const decoder = new TextDecoder();
|
|
@@ -359,6 +301,28 @@ var CryptoService = class {
|
|
|
359
301
|
throw new Error("Decryption failed: authentication tag mismatch or corrupted data.");
|
|
360
302
|
}
|
|
361
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Imports raw key bytes into a non-extractable CryptoKey based on the crypto profile version.
|
|
306
|
+
*
|
|
307
|
+
* The resulting key cannot be exported back to raw bytes (extractable: false),
|
|
308
|
+
* ensuring that sensitive key material does not persist in JavaScript-accessible memory.
|
|
309
|
+
* Callers should securely wipe the original raw key buffer immediately after import.
|
|
310
|
+
*
|
|
311
|
+
* @param rawKey - Raw key bytes (e.g. a Data Encryption Key).
|
|
312
|
+
* @param version - Crypto profile version that determines the algorithm and parameters.
|
|
313
|
+
* @param usages - Allowed key operations (e.g. ['encrypt', 'decrypt']).
|
|
314
|
+
* @returns A non-extractable CryptoKey bound to the algorithm defined by the profile.
|
|
315
|
+
*/
|
|
316
|
+
async importKey(rawKey, version, usages) {
|
|
317
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
318
|
+
return await crypto.subtle.importKey(
|
|
319
|
+
"raw",
|
|
320
|
+
rawKey,
|
|
321
|
+
profile.algorithmName,
|
|
322
|
+
false,
|
|
323
|
+
usages
|
|
324
|
+
);
|
|
325
|
+
}
|
|
362
326
|
};
|
|
363
327
|
|
|
364
328
|
// src/crypto/key-derivation.service.ts
|