@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 +29 -20
- package/dist/index.cjs +34 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -7
- package/dist/index.d.ts +23 -7
- package/dist/index.js +34 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
71
|
+
cryptoKey,
|
|
70
72
|
CryptoVersion.V1
|
|
71
73
|
);
|
|
72
74
|
|
|
73
|
-
const decrypted = await crypto.decryptData<MyData>(encrypted,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
256
|
+
cryptoKey,
|
|
250
257
|
CryptoVersion.V1
|
|
251
258
|
);
|
|
252
259
|
|
|
253
|
-
const decrypted = await crypto.decryptData<MyData>(encrypted,
|
|
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
|
|
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 -
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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
|