@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 CHANGED
@@ -1,15 +1,374 @@
1
- # @quantropic/security
1
+ # Crossdyne.Security
2
2
 
3
- Security library with SRP authentication and cryptography utilities.
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
- ## Features
7
+ ---
6
8
 
7
- - SRP (Secure Remote Password) protocol implementation
8
- - Cryptographic utilities
9
- - Key derivation functions
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 @quantropic/security
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
- AesGcmTagSize: 16,
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
- // 256 bits
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 AesGcmOptions = class _AesGcmOptions {
114
- constructor() {
115
- this._nonceSize = SecurityConstants.AesGcmNonceSize;
116
- this._tagSize = SecurityConstants.AesGcmTagSize;
117
- }
118
- /** Nonce size in bytes (must be 12). */
119
- get nonceSize() {
120
- return this._nonceSize;
121
- }
122
- set nonceSize(v) {
123
- if (v !== SecurityConstants.AesGcmNonceSize) throw new RangeError(`AES-GCM requires ${SecurityConstants.AesGcmNonceSize}-byte nonce`);
124
- this._nonceSize = v;
125
- }
126
- /** Tag size in bytes (12–16, default 16). */
127
- get tagSize() {
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) throw new Error(`Invalid Tag Size: ${this.tagSize}`);
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 KdfOptions = class _KdfOptions {
183
- constructor() {
184
- this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
185
- this._hashAlgorithm = "SHA-256";
186
- }
187
- /** PBKDF2 iteration count (minimum 100_000). */
188
- get pbkdf2Iterations() {
189
- return this._pbkdf2Iterations;
190
- }
191
- set pbkdf2Iterations(v) {
192
- if (v < SecurityConstants.Pbkdf2IterationsMinimum)
193
- throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
194
- this._pbkdf2Iterations = v;
195
- }
196
- /** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
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 new CryptoProfile({
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 - AES-256 key (32 bytes).
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
- cryptoKey,
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 - AES-256 key (32 bytes).
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
- cryptoKey,
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