@crossdyne/security 1.0.0 → 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
@@ -23,7 +23,6 @@ npm install @crossdyne/security
23
23
  - **PBKDF2 + HKDF** — secure key derivation from passwords
24
24
  - **SRP-6a** — password-authenticated key exchange without sending the password to the server
25
25
  - **Zero-memory** — sensitive buffers are explicitly overwritten after use
26
- - **No custom crypto primitives** — relies entirely on the browser / Node.js Web Crypto API
27
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.
28
27
 
29
28
  ### Requirements
@@ -50,7 +49,7 @@ crossdyne-security/
50
49
  │ └── srp-encoding.ts # BigInt ↔ bytes helpers
51
50
  └── utils/
52
51
  ├── security-utils.ts # Base64, BigInt, fixed-time compare
53
- └── srp-encoding.ts # Hash moduli, session key, M1 / M2
52
+ └── srp-encoding.ts # Hash moduli, session key, M1 / M2
54
53
  ```
55
54
 
56
55
  ### Quick Start
@@ -62,15 +61,21 @@ import { CryptoService } from './crypto/crypto-service.js';
62
61
  import { CryptoVersion } from './crypto/crypto-version.js';
63
62
 
64
63
  const crypto = new CryptoService();
65
- const key = crypto.generateRandomBytes(32); // AES-256
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']);
66
68
 
67
69
  const encrypted = await crypto.encryptData(
68
70
  { message: "Hello, World!" },
69
- key,
71
+ cryptoKey,
70
72
  CryptoVersion.V1
71
73
  );
72
74
 
73
- const decrypted = await crypto.decryptData<MyData>(encrypted, key);
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);
74
79
  ```
75
80
 
76
81
  Encrypted payload format (Base64):
@@ -83,6 +88,7 @@ Encrypted payload format (Base64):
83
88
 
84
89
  ```typescript
85
90
  import { KeyDerivationService } from './crypto/key-derivation-service.js';
91
+ import { CryptoService } from './crypto/crypto-service.js';
86
92
  import { CryptoVersion } from './crypto/crypto-version.js';
87
93
 
88
94
  const kdf = new KeyDerivationService();
@@ -174,11 +180,10 @@ const isValid = await client.verifyServerM2(
174
180
 
175
181
  ### Security Notes
176
182
 
177
- - All sensitive buffers are explicitly overwritten after use
178
- - Salt must be **at least 16 bytes**
179
- - AES-256 key must be **exactly 32 bytes**
180
- - SRP groups are protected against small-subgroup attacks (checks `A % N != 0`, `B != 0`)
181
- - Relies entirely on the native Web Crypto API — no custom crypto primitives
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`).
182
187
 
183
188
  ### License
184
189
 
@@ -203,7 +208,6 @@ npm install @crossdyne/security
203
208
  - **PBKDF2 + HKDF** — надёжный вывод ключей из пароля
204
209
  - **SRP-6a** — протокол аутентификации без передачи пароля на сервер
205
210
  - **Zero-memory** — чувствительные буферы явно перезаписываются после использования
206
- - **Никаких самописных криптопримитивов** — только нативный Web Crypto API браузера / Node.js
207
211
  - **Кроссплатформенность** — совместима с [.NET-реализацией](https://github.com/crossdyne/dotnet-security). Зашифрованные данные и SRP-сообщения взаимозаменяемы между TS и .NET.
208
212
 
209
213
  ### Требования
@@ -230,7 +234,7 @@ crossdyne-security/
230
234
  │ └── srp-encoding.ts # BigInt ↔ bytes хелперы
231
235
  └── utils/
232
236
  ├── security-utils.ts # Base64, BigInt, сравнение в постоянное время
233
- └── srp-encoding.ts # Хеш модулей, сессионный ключ, M1 / M2
237
+ └── srp-encoding.ts # Хеш модулей, сессионный ключ, M1 / M2
234
238
  ```
235
239
 
236
240
  ### Быстрый старт
@@ -242,15 +246,21 @@ import { CryptoService } from './crypto/crypto-service.js';
242
246
  import { CryptoVersion } from './crypto/crypto-version.js';
243
247
 
244
248
  const crypto = new CryptoService();
245
- const key = crypto.generateRandomBytes(32); // AES-256
249
+ const rawKey = crypto.generateRandomBytes(32); // Сырой ключ AES-256
250
+
251
+ // Импортируем сырой ключ в неизвлекаемый CryptoKey
252
+ const cryptoKey = await crypto.importKey(rawKey, CryptoVersion.V1, ['encrypt', 'decrypt']);
246
253
 
247
254
  const encrypted = await crypto.encryptData(
248
255
  { message: "Hello, World!" },
249
- key,
256
+ cryptoKey,
250
257
  CryptoVersion.V1
251
258
  );
252
259
 
253
- const decrypted = await crypto.decryptData<MyData>(encrypted, key);
260
+ const decrypted = await crypto.decryptData<MyData>(encrypted, cryptoKey);
261
+
262
+ // Рекомендуемая практика: безопасно очистить сырой ключ из памяти сразу после импорта
263
+ rawKey.fill(0);
254
264
  ```
255
265
 
256
266
  Формат зашифрованных данных (Base64):
@@ -354,11 +364,10 @@ const isValid = await client.verifyServerM2(
354
364
 
355
365
  ### Безопасность
356
366
 
357
- - Все чувствительные буферы явно перезаписываются после использования
358
- - Соль должна быть **минимум 16 байт**
359
- - Ключ AES-256 — **строго 32 байта**
360
- - SRP-группы защищены от атак на малые подгруппы (проверки `A % N != 0`, `B != 0`)
361
- - Полностью опирается на нативный Web Crypto API — никаких самописных криптопримитивов
367
+ - Все чувствительные буферы (сырые ключи и тд) должны быть явно перезаписаны (`fill(0)`) после их импорта в `CryptoKey`.
368
+ - Соль должна быть **минимум 16 байт**.
369
+ - Ключ AES-256 — **строго 32 байта**.
370
+ - SRP-группы защищены от атак на малые подгруппы (проверки `A % N != 0`, `B != 0`).
362
371
 
363
372
  ### Лицензия
364
373
 
package/dist/index.cjs CHANGED
@@ -151,6 +151,7 @@ var CryptoProfile = class {
151
151
  */
152
152
  constructor(params) {
153
153
  this.version = params.version;
154
+ this.algorithmName = params.algorithmName;
154
155
  this.kdfOptions = params.kdfOptions;
155
156
  this.aesGcmOptions = params.aesGcmOptions;
156
157
  }
@@ -201,20 +202,18 @@ var CryptoProfileRegistry = class {
201
202
  static getProfile(version) {
202
203
  switch (version) {
203
204
  case 1 /* V1 */:
204
- return new CryptoProfile({
205
- version: 1 /* V1 */,
206
- kdfOptions: KdfOptions.V1,
207
- aesGcmOptions: AesGcmOptions.V1
208
- });
205
+ return this.V1_PROFILE;
209
206
  default:
210
207
  throw new Error(`Unsupported crypto version: ${version}`);
211
208
  }
212
209
  }
213
- /** Latest supported profile (currently V1). */
214
- static get latest() {
215
- return this.getProfile(1 /* V1 */);
216
- }
217
210
  };
211
+ CryptoProfileRegistry.V1_PROFILE = new CryptoProfile({
212
+ version: 1 /* V1 */,
213
+ algorithmName: "AES-GCM",
214
+ kdfOptions: KdfOptions.V1,
215
+ aesGcmOptions: AesGcmOptions.V1
216
+ });
218
217
 
219
218
  // src/crypto/crypto.service.ts
220
219
  var CryptoService = class {
@@ -229,7 +228,7 @@ var CryptoService = class {
229
228
  /**
230
229
  * Encrypts a serializable object to a Base64 string.
231
230
  * @param dataModel - Object or Uint8Array to encrypt.
232
- * @param key - AES-256 key (32 bytes).
231
+ * @param key - A non-extractable CryptoKey obtained via {@link CryptoService.importKey}.
233
232
  * @returns Base64-encoded ciphertext with prepended nonce.
234
233
  */
235
234
  async encryptData(dataModel, key, version = 1 /* V1 */) {
@@ -245,20 +244,13 @@ var CryptoService = class {
245
244
  }
246
245
  const plainBytes = encoder.encode(jsonString);
247
246
  const nonce = crypto.getRandomValues(new Uint8Array(opts.nonceSize));
248
- const cryptoKey = await crypto.subtle.importKey(
249
- "raw",
250
- key,
251
- "AES-GCM",
252
- false,
253
- ["encrypt"]
254
- );
255
247
  const encryptedContent = await crypto.subtle.encrypt(
256
248
  {
257
249
  name: "AES-GCM",
258
250
  iv: nonce,
259
251
  tagLength: opts.tagSize * 8
260
252
  },
261
- cryptoKey,
253
+ key,
262
254
  plainBytes
263
255
  );
264
256
  const result = new Uint8Array(1 + opts.nonceSize + encryptedContent.byteLength);
@@ -270,7 +262,7 @@ var CryptoService = class {
270
262
  /**
271
263
  * Decrypts a Base64-encoded ciphertext back to the original object.
272
264
  * @param encryptedBase64 - The encrypted data.
273
- * @param key - AES-256 key (32 bytes).
265
+ * @param key - A non-extractable CryptoKey obtained via {@link CryptoService.importKey}.
274
266
  * @returns Deserialized object, or null if input is empty.
275
267
  * @throws If authentication tag mismatch or corrupted data.
276
268
  */
@@ -287,13 +279,6 @@ var CryptoService = class {
287
279
  throw new Error(`Invalid format: minimum expected ${opts.nonceSize + opts.tagSize} byte.`);
288
280
  const nonce = payload.slice(0, opts.nonceSize);
289
281
  const ciphertextWithTag = payload.slice(opts.nonceSize);
290
- const cryptoKey = await crypto.subtle.importKey(
291
- "raw",
292
- key,
293
- "AES-GCM",
294
- false,
295
- ["decrypt"]
296
- );
297
282
  try {
298
283
  const decryptedBuffer = await crypto.subtle.decrypt(
299
284
  {
@@ -301,7 +286,7 @@ var CryptoService = class {
301
286
  iv: nonce,
302
287
  tagLength: opts.tagSize * 8
303
288
  },
304
- cryptoKey,
289
+ key,
305
290
  ciphertextWithTag
306
291
  );
307
292
  const decoder = new TextDecoder();
@@ -316,6 +301,28 @@ var CryptoService = class {
316
301
  throw new Error("Decryption failed: authentication tag mismatch or corrupted data.");
317
302
  }
318
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
+ }
319
326
  };
320
327
 
321
328
  // src/crypto/key-derivation.service.ts