@crossdyne/security 0.4.0-beta.1 → 0.5.0-beta.2

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/dist/index.js CHANGED
@@ -46,16 +46,22 @@ var SecurityUtils = class {
46
46
  return 0n;
47
47
  return BigInt("0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
48
48
  }
49
- /** Converts bigint to fixed-length big-endian bytes (pads/truncates). */
49
+ /** Converts bigint to fixed-length big-endian bytes (pads only; never truncates). */
50
50
  static bigIntToFixedBytes(bn, length) {
51
+ if (length <= 0)
52
+ throw new Error("Length must be positive.");
51
53
  let hex = bn.toString(16);
52
54
  if (hex.length % 2 !== 0)
53
55
  hex = "0" + hex;
54
- if (hex.length > length * 2)
55
- hex = hex.slice(hex.length - length * 2);
56
- else
57
- hex = hex.padStart(length * 2, "0");
58
- return new Uint8Array(hex.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) || []);
56
+ const byteLength = hex.length / 2;
57
+ if (byteLength > length)
58
+ throw new Error(`Value byte length (${byteLength}) exceeds expected length (${length}). Possible data corruption or context mismatch.`);
59
+ hex = hex.padStart(length * 2, "0");
60
+ const bytes = new Uint8Array(length);
61
+ for (let i = 0; i < length; i++) {
62
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
63
+ }
64
+ return bytes;
59
65
  }
60
66
  /** Constant-time comparison of two Uint8Arrays. */
61
67
  static fixedTimeEquals(a, b) {
@@ -66,18 +72,39 @@ var SecurityUtils = class {
66
72
  diff |= a[i] ^ b[i];
67
73
  return diff === 0;
68
74
  }
69
- /** Modular exponentiation (base^exp mod mod) using binary exponentiation. */
70
- static expMod(base, exp, mod) {
71
- let res = BigInt(1);
75
+ /**
76
+ * Async modular exponentiation with event-loop yielding.
77
+ * @param base - The base value.
78
+ * @param exp - The exponent.
79
+ * @param mod - The modulus.
80
+ * @param yieldEvery - Number of iterations before yielding (default 64).
81
+ */
82
+ static async expModAsync(base, exp, mod, yieldEvery = 64) {
83
+ if (mod === 1n) return 0n;
84
+ let res = 1n;
72
85
  base = base % mod;
86
+ let i = 0;
73
87
  while (exp > 0n) {
74
- if (exp % 2n === 1n)
88
+ if ((exp & 1n) === 1n)
75
89
  res = res * base % mod;
76
- base = base * base % mod;
77
- exp = exp / 2n;
90
+ exp >>= 1n;
91
+ if (exp > 0n)
92
+ base = base * base % mod;
93
+ if (++i % yieldEvery === 0) {
94
+ await new Promise((resolve) => setTimeout(resolve, 0));
95
+ }
78
96
  }
79
97
  return res;
80
98
  }
99
+ /** Converts bigint to minimal-length big-endian bytes (no padding). */
100
+ static bigIntToRawBytes(bn) {
101
+ if (bn === 0n)
102
+ return new Uint8Array([0]);
103
+ let hex = bn.toString(16);
104
+ if (hex.length % 2 !== 0)
105
+ hex = "0" + hex;
106
+ return new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
107
+ }
81
108
  };
82
109
 
83
110
  // src/crypto/aes-gcm-options.ts
@@ -130,6 +157,97 @@ var AesGcmOptions = class _AesGcmOptions {
130
157
  }
131
158
  };
132
159
 
160
+ // src/crypto/crypto-profile.ts
161
+ var CryptoProfile = class {
162
+ /**
163
+ * Creates a new CryptoProfile.
164
+ * @param params - Object containing version, kdfOptions, aesGcmOptions.
165
+ */
166
+ constructor(params) {
167
+ this.version = params.version;
168
+ this.kdfOptions = params.kdfOptions;
169
+ this.aesGcmOptions = params.aesGcmOptions;
170
+ }
171
+ };
172
+
173
+ // src/crypto/crypto-version.ts
174
+ var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
175
+ CryptoVersion2[CryptoVersion2["V1"] = 1] = "V1";
176
+ return CryptoVersion2;
177
+ })(CryptoVersion || {});
178
+
179
+ // src/crypto/kdf-options.ts
180
+ var KdfOptions = class _KdfOptions {
181
+ constructor() {
182
+ this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
183
+ this._hashAlgorithm = "SHA-256";
184
+ }
185
+ /** PBKDF2 iteration count (minimum 100_000). */
186
+ get pbkdf2Iterations() {
187
+ return this._pbkdf2Iterations;
188
+ }
189
+ set pbkdf2Iterations(v) {
190
+ if (v < SecurityConstants.Pbkdf2IterationsMinimum)
191
+ throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
192
+ this._pbkdf2Iterations = v;
193
+ }
194
+ /** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
195
+ get hashAlgorithm() {
196
+ return this._hashAlgorithm;
197
+ }
198
+ set hashAlgorithm(v) {
199
+ if (!SupportedHashAlgorithms.includes(v))
200
+ throw new RangeError(`Unsupported hash: ${v}`);
201
+ this._hashAlgorithm = v;
202
+ }
203
+ /** Validates iterations and hash algorithm. */
204
+ validate() {
205
+ if (this.pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum)
206
+ throw new Error(`Pbkdf2Iterations (${this.pbkdf2Iterations}) below minimum`);
207
+ if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
208
+ throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
209
+ }
210
+ /** Default preset: SHA-256, 600_000 iterations. */
211
+ static get default() {
212
+ return new _KdfOptions();
213
+ }
214
+ /** Fluent setter for {@link pbkdf2Iterations}. */
215
+ withPbkdf2Iterations(i) {
216
+ this.pbkdf2Iterations = i;
217
+ return this;
218
+ }
219
+ /** Fluent setter for {@link hashAlgorithm}. */
220
+ withHashAlgorithm(h) {
221
+ this.hashAlgorithm = h;
222
+ return this;
223
+ }
224
+ /** Validates and returns this instance. */
225
+ build() {
226
+ this.validate();
227
+ return this;
228
+ }
229
+ };
230
+
231
+ // src/crypto/crypto-profile-registry.ts
232
+ var CryptoProfileRegistry = class {
233
+ static getProfile(version) {
234
+ switch (version) {
235
+ case 1 /* V1 */:
236
+ return new CryptoProfile({
237
+ version: 1 /* V1 */,
238
+ kdfOptions: KdfOptions.default,
239
+ aesGcmOptions: AesGcmOptions.default
240
+ });
241
+ default:
242
+ throw new Error(`Unsupported crypto version: ${version}`);
243
+ }
244
+ }
245
+ /** Latest supported profile (currently V1). */
246
+ static get latest() {
247
+ return this.getProfile(1 /* V1 */);
248
+ }
249
+ };
250
+
133
251
  // src/crypto/crypto.service.ts
134
252
  var CryptoService = class {
135
253
  constructor() {
@@ -144,15 +262,15 @@ var CryptoService = class {
144
262
  * Encrypts a serializable object to a Base64 string.
145
263
  * @param dataModel - Object or Uint8Array to encrypt.
146
264
  * @param key - AES-256 key (32 bytes).
147
- * @param options - AES-GCM configuration; uses default if omitted.
148
265
  * @returns Base64-encoded ciphertext with prepended nonce.
149
266
  */
150
- async encryptData(dataModel, key, options) {
151
- const opts = options ?? AesGcmOptions.default;
267
+ async encryptData(dataModel, key, version = 1 /* V1 */) {
268
+ const profile = CryptoProfileRegistry.getProfile(version);
269
+ const opts = profile.aesGcmOptions;
152
270
  opts.validate();
153
271
  const encoder = new TextEncoder();
154
272
  let jsonString;
155
- if (dataModel instanceof Uint8Array) {
273
+ if (ArrayBuffer.isView(dataModel) && dataModel.constructor === Uint8Array) {
156
274
  jsonString = `"${SecurityUtils.toBase64(dataModel)}"`;
157
275
  } else {
158
276
  jsonString = JSON.stringify(dataModel);
@@ -180,29 +298,32 @@ var CryptoService = class {
180
298
  cryptoKey,
181
299
  plainBytes
182
300
  );
183
- const result = new Uint8Array(opts.nonceSize + encryptedContent.byteLength);
184
- result.set(nonce, 0);
185
- result.set(new Uint8Array(encryptedContent), opts.nonceSize);
301
+ const result = new Uint8Array(1 + opts.nonceSize + encryptedContent.byteLength);
302
+ result[0] = version;
303
+ result.set(nonce, 1);
304
+ result.set(new Uint8Array(encryptedContent), 1 + opts.nonceSize);
186
305
  return SecurityUtils.toBase64(result);
187
306
  }
188
307
  /**
189
308
  * Decrypts a Base64-encoded ciphertext back to the original object.
190
309
  * @param encryptedBase64 - The encrypted data.
191
310
  * @param key - AES-256 key (32 bytes).
192
- * @param options - AES-GCM configuration; uses default if omitted.
193
311
  * @returns Deserialized object, or null if input is empty.
194
312
  * @throws If authentication tag mismatch or corrupted data.
195
313
  */
196
- async decryptData(encryptedBase64, key, options, isBytes = false) {
314
+ async decryptData(encryptedBase64, key, isBytes = false) {
197
315
  if (!encryptedBase64)
198
316
  return null;
199
- const opts = options ?? AesGcmOptions.default;
200
- opts.validate();
201
317
  const encryptedBytes = SecurityUtils.fromBase64(encryptedBase64);
202
- if (encryptedBytes.length < opts.nonceSize + opts.tagSize)
318
+ const version = encryptedBytes[0];
319
+ const payload = encryptedBytes.slice(1);
320
+ const profile = CryptoProfileRegistry.getProfile(version);
321
+ const opts = profile.aesGcmOptions;
322
+ opts.validate();
323
+ if (payload.length < opts.nonceSize + opts.tagSize)
203
324
  throw new Error(`Invalid format: minimum expected ${opts.nonceSize + opts.tagSize} byte.`);
204
- const nonce = encryptedBytes.slice(0, opts.nonceSize);
205
- const ciphertextWithTag = encryptedBytes.slice(opts.nonceSize);
325
+ const nonce = payload.slice(0, opts.nonceSize);
326
+ const ciphertextWithTag = payload.slice(opts.nonceSize);
206
327
  const cryptoKey = await crypto.subtle.importKey(
207
328
  "raw",
208
329
  key,
@@ -212,9 +333,8 @@ var CryptoService = class {
212
333
  );
213
334
  try {
214
335
  let associatedData = new Uint8Array(0);
215
- if (opts.associatedData != null) {
336
+ if (opts.associatedData != null)
216
337
  associatedData = opts.associatedData;
217
- }
218
338
  const decryptedBuffer = await crypto.subtle.decrypt(
219
339
  {
220
340
  name: "AES-GCM",
@@ -231,7 +351,7 @@ var CryptoService = class {
231
351
  if (isBytes && typeof parsed === "string") {
232
352
  return SecurityUtils.fromBase64(parsed);
233
353
  }
234
- return JSON.parse(jsonString);
354
+ return parsed;
235
355
  } catch (e) {
236
356
  console.error("Decryption error:", e);
237
357
  throw new Error("Decryption failed: authentication tag mismatch or corrupted data.");
@@ -239,88 +359,39 @@ var CryptoService = class {
239
359
  }
240
360
  };
241
361
 
242
- // src/crypto/kdf-options.ts
243
- var KdfOptions = class _KdfOptions {
244
- constructor() {
245
- this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
246
- this._hashAlgorithm = "SHA-256";
247
- }
248
- /** PBKDF2 iteration count (minimum 100_000). */
249
- get pbkdf2Iterations() {
250
- return this._pbkdf2Iterations;
251
- }
252
- set pbkdf2Iterations(v) {
253
- if (v < SecurityConstants.Pbkdf2IterationsMinimum)
254
- throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
255
- this._pbkdf2Iterations = v;
256
- }
257
- /** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
258
- get hashAlgorithm() {
259
- return this._hashAlgorithm;
260
- }
261
- set hashAlgorithm(v) {
262
- if (!SupportedHashAlgorithms.includes(v))
263
- throw new RangeError(`Unsupported hash: ${v}`);
264
- this._hashAlgorithm = v;
265
- }
266
- /** Validates iterations and hash algorithm. */
267
- validate() {
268
- if (this.pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum)
269
- throw new Error(`Pbkdf2Iterations (${this.pbkdf2Iterations}) below minimum`);
270
- if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
271
- throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
272
- }
273
- /** Default preset: SHA-256, 600_000 iterations. */
274
- static get default() {
275
- return new _KdfOptions();
276
- }
277
- /** Fluent setter for {@link pbkdf2Iterations}. */
278
- withPbkdf2Iterations(i) {
279
- this.pbkdf2Iterations = i;
280
- return this;
281
- }
282
- /** Fluent setter for {@link hashAlgorithm}. */
283
- withHashAlgorithm(h) {
284
- this.hashAlgorithm = h;
285
- return this;
286
- }
287
- /** Validates and returns this instance. */
288
- build() {
289
- this.validate();
290
- return this;
291
- }
292
- };
293
-
294
362
  // src/crypto/key-derivation.service.ts
295
363
  var KeyDerivationService = class {
296
364
  /**
297
- * Derives KEK and Base64 AuthHash. Identity is normalized (trimmed, lowercase).
298
- * @param identity - User identity (email, username).
299
- * @param password - User password.
300
- * @param salt - Random salt.
301
- * @param options - KDF configuration; uses default if omitted.
302
- * @returns Object with `kek` (Uint8Array) and `authHash` (Base64 string).
303
- */
304
- async deriveKeysFromPassword(identity, password, salt, options) {
305
- if (!identity || identity.trim().length === 0)
365
+ * Derives KEK and Base64 AuthHash. Identity is used as-is in the
366
+ * combined salt string — caller must normalize (trim, lowercase, etc.) before calling.
367
+ * @param identity - User identity (email, username). Must be pre-normalized by caller.
368
+ * @param password - User password.
369
+ * @param salt - Random salt (minimum 16 bytes).
370
+ * @param version - Crypto version for profile selection.
371
+ * @returns Object with `kek` (Uint8Array) and `authHash` (Base64 string).
372
+ */
373
+ async deriveKeysFromPassword(identity, password, salt, version) {
374
+ if (!identity || identity.length === 0)
306
375
  throw new Error("Identity cannot be null or empty.");
307
376
  if (!password || password.trim().length === 0)
308
377
  throw new Error("Password cannot be null or empty.");
309
378
  if (!salt || salt.length < 16)
310
379
  throw new Error("Salt must be at least 16 bytes.");
311
- const opts = options ?? KdfOptions.default;
380
+ const profile = CryptoProfileRegistry.getProfile(version);
381
+ const opts = profile.kdfOptions;
312
382
  opts.validate();
313
383
  const safeSalt = new Uint8Array(salt);
314
384
  const encoder = new TextEncoder();
315
385
  const combinedPassword = `${identity}:${password}`;
316
386
  const passwordBytes = encoder.encode(combinedPassword);
317
387
  const baseKey = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits", "deriveKey"]);
388
+ const hashSize = HashSizes[opts.hashAlgorithm] ?? 32;
318
389
  const masterKeyBits = await crypto.subtle.deriveBits({
319
390
  name: "PBKDF2",
320
391
  salt: safeSalt,
321
392
  iterations: opts.pbkdf2Iterations,
322
393
  hash: opts.hashAlgorithm
323
- }, baseKey, SecurityConstants.KeySizeBytes * 8);
394
+ }, baseKey, hashSize * 8);
324
395
  const masterKey = await crypto.subtle.importKey("raw", masterKeyBits, "HKDF", false, ["deriveBits"]);
325
396
  const kek = await crypto.subtle.deriveBits(
326
397
  { name: "HKDF", hash: opts.hashAlgorithm, salt: new Uint8Array(0), info: encoder.encode("AES-GCM-KEK-v1") },
@@ -337,99 +408,6 @@ var KeyDerivationService = class {
337
408
  authHash: SecurityUtils.toBase64(new Uint8Array(authBytes))
338
409
  };
339
410
  }
340
- /**
341
- * Derives an SRP-compatible authentication hash (output size = hash output length).
342
- * @param identity - User identity.
343
- * @param password - User password.
344
- * @param salt - Random salt.
345
- * @param srpHashAlgorithm - SRP hash algorithm (SHA-256/384/512).
346
- * @param options - KDF configuration; uses default if omitted.
347
- * @returns Raw hash bytes for use as SRP verifier input (x).
348
- */
349
- async deriveAuthHashForSrp(identity, password, salt, srpHashAlgorithm, options) {
350
- if (!identity || identity.trim().length === 0)
351
- throw new Error("Identity cannot be null or empty.");
352
- if (!password || password.trim().length === 0)
353
- throw new Error("Password cannot be null or empty.");
354
- if (!salt || salt.length < 16)
355
- throw new Error("Salt must be at least 16 bytes.");
356
- const opts = options ?? KdfOptions.default;
357
- opts.validate();
358
- const srpHashSize = HashSizes[srpHashAlgorithm];
359
- const combinedPassword = `${identity}:${password}`;
360
- const passwordBytes = new TextEncoder().encode(combinedPassword);
361
- const baseKey = await crypto.subtle.importKey(
362
- "raw",
363
- passwordBytes,
364
- "PBKDF2",
365
- false,
366
- ["deriveBits"]
367
- );
368
- const masterKeyBits = await crypto.subtle.deriveBits({
369
- name: "PBKDF2",
370
- salt,
371
- iterations: opts.pbkdf2Iterations,
372
- hash: srpHashAlgorithm
373
- }, baseKey, SecurityConstants.KeySizeBytes * 8);
374
- const masterKey = await crypto.subtle.importKey(
375
- "raw",
376
- masterKeyBits,
377
- "HKDF",
378
- false,
379
- ["deriveBits"]
380
- );
381
- const info = new TextEncoder().encode("SRP-AUTH-HASH-v1");
382
- const authBytes = await crypto.subtle.deriveBits(
383
- {
384
- name: "HKDF",
385
- hash: srpHashAlgorithm,
386
- salt: new Uint8Array(0),
387
- info
388
- },
389
- masterKey,
390
- srpHashSize * 8
391
- );
392
- return new Uint8Array(authBytes);
393
- }
394
- };
395
-
396
- // src/crypto/crypto-version.ts
397
- var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
398
- CryptoVersion2[CryptoVersion2["V1"] = 1] = "V1";
399
- return CryptoVersion2;
400
- })(CryptoVersion || {});
401
-
402
- // src/crypto/crypto-profile.ts
403
- var CryptoProfile = class {
404
- /**
405
- * Creates a new CryptoProfile.
406
- * @param params - Object containing version, kdfOptions, aesGcmOptions.
407
- */
408
- constructor(params) {
409
- this.version = params.version;
410
- this.kdfOptions = params.kdfOptions;
411
- this.aesGcmOptions = params.aesGcmOptions;
412
- }
413
- };
414
-
415
- // src/crypto/crypto-profile-registry.ts
416
- var CryptoProfileRegistry = class {
417
- static getProfile(version) {
418
- switch (version) {
419
- case 1 /* V1 */:
420
- return new CryptoProfile({
421
- version: 1 /* V1 */,
422
- kdfOptions: KdfOptions.default,
423
- aesGcmOptions: AesGcmOptions.default
424
- });
425
- default:
426
- throw new Error(`Unsupported crypto version: ${version}`);
427
- }
428
- }
429
- /** Latest supported profile (currently V1). */
430
- static get latest() {
431
- return this.getProfile(1 /* V1 */);
432
- }
433
411
  };
434
412
 
435
413
  // src/utils/srp-encoding.ts
@@ -453,14 +431,14 @@ var SrpEncoding = class {
453
431
  * Follows RFC 5054 / SRP-6a:
454
432
  * - H(N) and H(g) are hashed as modulus-sized values.
455
433
  * - Identity (I) is hashed as raw UTF-8 bytes.
456
- * - A and B are padded to the modulus size before hashing.
434
+ * - A and B are zero-padded to the modulus size before hashing.
457
435
  * - K is the session key (H(S) without padding).
458
436
  *
459
437
  * @param ctx - SRP context containing N, g, hash algorithm, and modulus size.
460
438
  * @param A - Client ephemeral public key.
461
439
  * @param B - Server ephemeral public key.
462
440
  * @param sessionKeyK - Session key K as raw bytes.
463
- * @param identity - User identity (login). Should already be normalized (trimmed / lowercased) by the caller.
441
+ * @param identity - User identity (login). Must be pre-normalized by the caller.
464
442
  * @param salt - User-specific salt bytes.
465
443
  * @returns The M1 proof as raw hash bytes.
466
444
  */
@@ -484,7 +462,10 @@ var SrpEncoding = class {
484
462
  sessionKeyK
485
463
  );
486
464
  }
487
- /** Computes M2 = H(A || M1 || sessionKeyK). */
465
+ /**
466
+ * Computes M2 = H( PAD(A) || M1 || sessionKeyK ).
467
+ * A is zero-padded to the modulus size before hashing.
468
+ */
488
469
  static async computeM2(ctx, A, m1Bytes, sessionKeyK) {
489
470
  return this.computeHash(
490
471
  ctx.hashAlgorithmName,
@@ -495,14 +476,15 @@ var SrpEncoding = class {
495
476
  }
496
477
  /** Computes session key K = H(S). */
497
478
  static async computeSessionKey(ctx, S) {
498
- let hex = S.toString(16);
499
- if (hex.length % 2 !== 0)
500
- hex = "0" + hex;
501
- const sBytes = new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
502
- const hashBuffer = await crypto.subtle.digest(ctx.hashAlgorithmName, sBytes);
479
+ const sBytes = SecurityUtils.bigIntToRawBytes(S);
480
+ const safeBuffer = sBytes.buffer.slice(
481
+ sBytes.byteOffset,
482
+ sBytes.byteOffset + sBytes.byteLength
483
+ );
484
+ const hashBuffer = await crypto.subtle.digest(ctx.hashAlgorithmName, safeBuffer);
503
485
  return new Uint8Array(hashBuffer);
504
486
  }
505
- /** Hashes bytes and returns raw Uint8Array (для M1/M2). */
487
+ /** Hashes concatenated byte arrays and returns raw Uint8Array. */
506
488
  static async computeHash(algo, ...buffers) {
507
489
  const totalLen = buffers.reduce((sum, b) => sum + b.length, 0);
508
490
  const combined = new Uint8Array(totalLen);
@@ -528,143 +510,6 @@ var SrpEncoding = class {
528
510
  }
529
511
  };
530
512
 
531
- // src/srp/srp-client-service.ts
532
- var SrpClientService = class {
533
- constructor() {
534
- this.keyDerivation = new KeyDerivationService();
535
- }
536
- /**
537
- * Computes SRP verifier v = g^x mod N from the authentication hash.
538
- * @param authHash - Auth hash (Base64).
539
- * @param ctx - SRP context (N, g, hash algorithm, etc.).
540
- * @returns Verifier as Base64 string.
541
- */
542
- async generateSrpVerifier(authHash, ctx) {
543
- const x = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(authHash));
544
- const v = SecurityUtils.expMod(ctx.g, x, ctx.N);
545
- return SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, v));
546
- }
547
- /**
548
- * Generates client proof (A, M1, session key S) from server challenge.
549
- * @param login - User login.
550
- * @param password - Plaintext password.
551
- * @param saltBase64 - Server salt (URL-safe Base64).
552
- * @param B_base64 - Server public ephemeral B (URL-safe Base64).
553
- * @param ctx - SRP context.
554
- * @returns Object with A, M1, S as Base64 strings.
555
- */
556
- async generateSrpProof(login, password, saltBase64, B_base64, ctx) {
557
- const salt = SecurityUtils.fromBase64(saltBase64);
558
- const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName);
559
- const x = SecurityUtils.bytesToBigInt(authHash);
560
- const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
561
- const aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
562
- const a = SecurityUtils.bytesToBigInt(aBytes);
563
- const A = SecurityUtils.expMod(ctx.g, a, ctx.N);
564
- if (A % ctx.N === 0n)
565
- throw new Error("Critical error: A % N === 0");
566
- const B = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(B_base64));
567
- if (B % ctx.N === 0n)
568
- throw new Error("Critical error: B % N === 0");
569
- const u = await SrpEncoding.hashModuli(ctx, A, B);
570
- if (u === 0n)
571
- throw new Error("\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 u");
572
- const gX = SecurityUtils.expMod(ctx.g, x, ctx.N);
573
- const term = ctx.k * gX % ctx.N;
574
- const base = (B - term + ctx.N) % ctx.N;
575
- const exponent = a + u * x;
576
- const S = SecurityUtils.expMod(base, exponent, ctx.N);
577
- if (S === 0n)
578
- throw new Error("Critical error: S === 0");
579
- const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
580
- const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, login, salt);
581
- return {
582
- A: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, A)),
583
- M1: SecurityUtils.toBase64(M1),
584
- SessionKeyK: sessionKeyK
585
- };
586
- }
587
- /**
588
- * Validates the server proof M2 to authenticate the server.
589
- * @param A_b64 - Client public A (Base64).
590
- * @param M1_b64 - Client proof M1 (Base64).
591
- * @param S_b64 - Session key S (Base64).
592
- * @param serverM2_b64 - Server proof M2 (Base64).
593
- * @param ctx - SRP context.
594
- * @returns True if the server proof is valid.
595
- */
596
- async verifyServerM2(A_b64, M1_b64, sessionKeyK, serverM2_b64, ctx) {
597
- const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(A_b64));
598
- const M1 = SecurityUtils.fromBase64(M1_b64);
599
- const computedM2 = await SrpEncoding.computeM2(ctx, A, M1, sessionKeyK);
600
- const serverM2Bytes = SecurityUtils.fromBase64(serverM2_b64);
601
- return SecurityUtils.fixedTimeEquals(computedM2, serverM2Bytes);
602
- }
603
- };
604
-
605
- // src/srp/srp-server-service.ts
606
- var SrpServerService = class {
607
- /**
608
- * Generates server challenge B and session state from verifier.
609
- * @param login - User login.
610
- * @param verifierBytes - Stored verifier v as byte array.
611
- * @param ctx - SRP context (hash, N, g, etc.).
612
- * @returns Session state with private b, verifier, and public B.
613
- */
614
- async getSrpChallenge(login, verifierBytes, salt, ctx) {
615
- const v = SecurityUtils.bytesToBigInt(verifierBytes);
616
- const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
617
- let bBytes;
618
- let B;
619
- do {
620
- bBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
621
- const b = SecurityUtils.bytesToBigInt(bBytes);
622
- const gB = SecurityUtils.expMod(ctx.g, b, ctx.N);
623
- B = (ctx.k * v + gB) % ctx.N;
624
- } while (B === 0n);
625
- return {
626
- login,
627
- privateKeyB: bBytes,
628
- verifier: verifierBytes,
629
- publicKeyB: SrpEncoding.toModulusBytes(ctx, B),
630
- salt
631
- };
632
- }
633
- /**
634
- * Verifies client M1 proof and returns server M2 proof.
635
- * @param sessionState - Server session state.
636
- * @param a - Client public A (Base64).
637
- * @param m1 - Client proof M1 (Base64).
638
- * @param ctx - SRP context.
639
- * @returns Server proof M2 as Base64 string.
640
- * @throws If verification fails or input is invalid.
641
- */
642
- async verifySrpProof(sessionState, a, m1, ctx) {
643
- const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(a));
644
- const M1_client = SecurityUtils.fromBase64(m1);
645
- const b = SecurityUtils.bytesToBigInt(sessionState.privateKeyB);
646
- const v = SecurityUtils.bytesToBigInt(sessionState.verifier);
647
- const B = SecurityUtils.bytesToBigInt(sessionState.publicKeyB);
648
- if (v <= 0n)
649
- throw new Error("The verifier is corrupted");
650
- if (A % ctx.N === 0n)
651
- throw new Error("Incorrect value of A");
652
- if (A <= 0n || A >= ctx.N)
653
- throw new Error("Invalid A (out of range)");
654
- const u = await SrpEncoding.hashModuli(ctx, A, B);
655
- if (u === 0n)
656
- throw new Error("Error in calculating the parameter u");
657
- const vU = SecurityUtils.expMod(v, u, ctx.N);
658
- const S = SecurityUtils.expMod(A * vU % ctx.N, b, ctx.N);
659
- const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
660
- const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, sessionState.login, sessionState.salt);
661
- if (!SecurityUtils.fixedTimeEquals(M1_server, M1_client))
662
- throw new Error("Invalid password");
663
- const M2_server = await SrpEncoding.computeM2(ctx, A, M1_client, sessionKeyK);
664
- return SecurityUtils.toBase64(M2_server);
665
- }
666
- };
667
-
668
513
  // src/srp/srp-group.ts
669
514
  var SrpGroup = /* @__PURE__ */ ((SrpGroup2) => {
670
515
  SrpGroup2[SrpGroup2["Rfc5054_1024"] = 1] = "Rfc5054_1024";
@@ -791,6 +636,225 @@ var SrpContextFactory = class {
791
636
  }
792
637
  };
793
638
 
794
- export { AesGcmOptions, CryptoProfile, CryptoProfileRegistry, CryptoService, CryptoVersion, KdfOptions, KeyDerivationService, SecurityConstants, SecurityUtils, SrpClientService, SrpContextFactory, SrpEncoding, SrpGroup, SrpGroupParams, SrpOptions, SrpServerService, SupportedHashAlgorithms };
639
+ // src/srp/srp-client-service.ts
640
+ var SrpClientService = class {
641
+ /**
642
+ * Computes SRP verifier v = g^x mod N from the authentication hash.
643
+ * @param authHash - Auth hash (Base64).
644
+ * @param group - SRP group (determines modulus N, generator g, hash).
645
+ * @returns Verifier as Base64 string.
646
+ */
647
+ async generateSrpVerifier(authHash, group) {
648
+ const ctx = await SrpContextFactory.create(group);
649
+ const x = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(authHash));
650
+ const v = await SecurityUtils.expModAsync(ctx.g, x, ctx.N);
651
+ return SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, v));
652
+ }
653
+ /**
654
+ * Generates client proof (A, M1, session key S) from server challenge.
655
+ * @param login - User login.
656
+ * @param authHashBytes - SRP private exponent x as raw bytes (derived from auth hash).
657
+ * @param saltBase64 - Server salt (standard Base64).
658
+ * @param B_base64 - Server public ephemeral B (standard Base64).
659
+ * @param group - SRP group (determines modulus N, generator g, hash).
660
+ * @returns Object with A and M1 as standard Base64; SessionKeyK as raw bytes.
661
+ */
662
+ async generateSrpProof(login, authHashBytes, saltBase64, B_base64, group) {
663
+ const ctx = await SrpContextFactory.create(group);
664
+ const salt = SecurityUtils.fromBase64(saltBase64);
665
+ const x = SecurityUtils.bytesToBigInt(authHashBytes);
666
+ const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
667
+ let aBytes;
668
+ let a;
669
+ do {
670
+ aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
671
+ a = SecurityUtils.bytesToBigInt(aBytes);
672
+ } while (a === 0n);
673
+ const A = await SecurityUtils.expModAsync(ctx.g, a, ctx.N);
674
+ if (A <= 0n || A >= ctx.N)
675
+ throw new Error("Invalid client public key A.");
676
+ const B = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(B_base64));
677
+ if (B <= 0n || B >= ctx.N || B % ctx.N === 0n)
678
+ throw new Error("Critical error: B % N === 0");
679
+ const u = await SrpEncoding.hashModuli(ctx, A, B);
680
+ if (u === 0n)
681
+ throw new Error("\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 u");
682
+ const gX = await SecurityUtils.expModAsync(ctx.g, x, ctx.N);
683
+ const term = ctx.k * gX % ctx.N;
684
+ const base = (B - term + ctx.N) % ctx.N;
685
+ const exponent = a + u * x;
686
+ const S = await SecurityUtils.expModAsync(base, exponent, ctx.N);
687
+ if (S === 0n)
688
+ throw new Error("Critical error: S === 0");
689
+ const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
690
+ const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, login, salt);
691
+ return {
692
+ A: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, A)),
693
+ M1: SecurityUtils.toBase64(M1),
694
+ SessionKeyK: sessionKeyK
695
+ };
696
+ }
697
+ /**
698
+ * Validates the server proof M2 to authenticate the server.
699
+ * @param A_b64 - Client public A (Base64).
700
+ * @param M1_b64 - Client proof M1 (Base64).
701
+ * @param sessionKeyK - Session key K as raw bytes.
702
+ * @param serverM2_b64 - Server proof M2 (Base64).
703
+ * @param group - SRP group (determines modulus N, generator g, hash).
704
+ * @returns True if the server proof is valid.
705
+ */
706
+ async verifyServerM2(A_b64, M1_b64, sessionKeyK, serverM2_b64, group) {
707
+ const ctx = await SrpContextFactory.create(group);
708
+ const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(A_b64));
709
+ const M1 = SecurityUtils.fromBase64(M1_b64);
710
+ const computedM2 = await SrpEncoding.computeM2(ctx, A, M1, sessionKeyK);
711
+ const serverM2Bytes = SecurityUtils.fromBase64(serverM2_b64);
712
+ return SecurityUtils.fixedTimeEquals(computedM2, serverM2Bytes);
713
+ }
714
+ };
715
+
716
+ // src/srp/srp-server-service.ts
717
+ var SrpServerService = class {
718
+ /**
719
+ * Generates server challenge B and session state from verifier.
720
+ * @param login - User login.
721
+ * @param verifierBytes - Stored verifier v as byte array.
722
+ * @param salt - User salt (raw bytes).
723
+ * @param group - SRP group (determines modulus N, generator g, hash).
724
+ * @returns Session state with private b, verifier, public B, and salt.
725
+ */
726
+ async getSrpChallenge(login, verifierBytes, salt, group) {
727
+ if (!login || login.trim().length === 0)
728
+ throw new Error("Login cannot be null or empty.");
729
+ if (!verifierBytes || verifierBytes.length === 0)
730
+ throw new Error("Verifier cannot be null or empty.");
731
+ const ctx = await SrpContextFactory.create(group);
732
+ const v = SecurityUtils.bytesToBigInt(verifierBytes);
733
+ if (v <= 0n || v >= ctx.N)
734
+ throw new Error("The verifier is corrupted");
735
+ const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
736
+ while (true) {
737
+ const bBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
738
+ const b = SecurityUtils.bytesToBigInt(bBytes);
739
+ if (b === 0n) {
740
+ continue;
741
+ }
742
+ const gB = await SecurityUtils.expModAsync(ctx.g, b, ctx.N);
743
+ const B = (ctx.k * v + gB) % ctx.N;
744
+ if (B !== 0n) {
745
+ return {
746
+ login,
747
+ privateKeyB: bBytes,
748
+ verifier: verifierBytes,
749
+ publicKeyB: SrpEncoding.toModulusBytes(ctx, B),
750
+ salt
751
+ };
752
+ }
753
+ }
754
+ }
755
+ /**
756
+ * Verifies client M1 proof and returns server M2 proof.
757
+ * @param sessionState - Server session state.
758
+ * @param a - Client public A (Base64).
759
+ * @param m1 - Client proof M1 (Base64).
760
+ * @param group - SRP group (determines modulus N, generator g, hash).
761
+ * @returns Server proof M2 as Base64 string.
762
+ * @throws If verification fails or input is invalid.
763
+ */
764
+ async verifySrpProof(sessionState, a, m1, group) {
765
+ const ctx = await SrpContextFactory.create(group);
766
+ const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(a));
767
+ const M1_client = SecurityUtils.fromBase64(m1);
768
+ const b = SecurityUtils.bytesToBigInt(sessionState.privateKeyB);
769
+ const v = SecurityUtils.bytesToBigInt(sessionState.verifier);
770
+ const B = SecurityUtils.bytesToBigInt(sessionState.publicKeyB);
771
+ if (v <= 0n || v >= ctx.N)
772
+ throw new Error("The verifier is corrupted");
773
+ if (A % ctx.N === 0n)
774
+ throw new Error("Incorrect value of A");
775
+ if (A <= 0n || A >= ctx.N)
776
+ throw new Error("Invalid A (out of range)");
777
+ if (B <= 0n || B >= ctx.N)
778
+ throw new Error("Invalid server public key B.");
779
+ const u = await SrpEncoding.hashModuli(ctx, A, B);
780
+ if (u === 0n)
781
+ throw new Error("Error in calculating the parameter u");
782
+ const vU = await SecurityUtils.expModAsync(v, u, ctx.N);
783
+ const S = await SecurityUtils.expModAsync(A * vU % ctx.N, b, ctx.N);
784
+ if (S === 0n)
785
+ throw new Error("Critical error: shared secret S is zero (possible malicious A).");
786
+ const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
787
+ const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, sessionState.login, sessionState.salt);
788
+ if (!SecurityUtils.fixedTimeEquals(M1_server, M1_client))
789
+ throw new Error("Invalid password");
790
+ const M2_server = await SrpEncoding.computeM2(ctx, A, M1_client, sessionKeyK);
791
+ return SecurityUtils.toBase64(M2_server);
792
+ }
793
+ };
794
+
795
+ // src/srp/srp-key-derivation.service.ts
796
+ var SrpKeyDerivationService = class {
797
+ /**
798
+ * Derives an SRP-compatible authentication hash (output size = hash output length).
799
+ * Identity is used as-is in the combined string — caller must normalize
800
+ * (trim, lowercase, etc.) before calling.
801
+ * @param identity - User identity (email, username). Must be pre-normalized by caller.
802
+ * @param password - User password.
803
+ * @param salt - Random salt (minimum 16 bytes).
804
+ * @param srpGroup - SRP group (determines hash algorithm and modulus).
805
+ * @param version - Crypto version for KDF profile selection.
806
+ * @returns Raw hash bytes for use as SRP verifier input (x).
807
+ */
808
+ async deriveAuthHashForSrp(identity, password, salt, srpGroup, version) {
809
+ if (!identity || identity.trim().length === 0)
810
+ throw new Error("Identity cannot be null or empty.");
811
+ if (!password || password.trim().length === 0)
812
+ throw new Error("Password cannot be null or empty.");
813
+ if (!salt || salt.length < 16)
814
+ throw new Error("Salt must be at least 16 bytes.");
815
+ const profile = CryptoProfileRegistry.getProfile(version);
816
+ const opts = profile.kdfOptions;
817
+ opts.validate();
818
+ const ctx = await SrpContextFactory.create(srpGroup);
819
+ const srpHashAlgorithm = ctx.hashAlgorithmName;
820
+ const srpHashSize = HashSizes[srpHashAlgorithm];
821
+ const combinedPassword = `${identity}:${password}`;
822
+ const passwordBytes = new TextEncoder().encode(combinedPassword);
823
+ const baseKey = await crypto.subtle.importKey(
824
+ "raw",
825
+ passwordBytes,
826
+ "PBKDF2",
827
+ false,
828
+ ["deriveBits"]
829
+ );
830
+ const masterKeyBits = await crypto.subtle.deriveBits({
831
+ name: "PBKDF2",
832
+ salt,
833
+ iterations: opts.pbkdf2Iterations,
834
+ hash: srpHashAlgorithm
835
+ }, baseKey, srpHashSize * 8);
836
+ const masterKey = await crypto.subtle.importKey(
837
+ "raw",
838
+ masterKeyBits,
839
+ "HKDF",
840
+ false,
841
+ ["deriveBits"]
842
+ );
843
+ const info = new TextEncoder().encode("SRP-AUTH-HASH-v1");
844
+ const authBytes = await crypto.subtle.deriveBits(
845
+ {
846
+ name: "HKDF",
847
+ hash: srpHashAlgorithm,
848
+ salt: new Uint8Array(0),
849
+ info
850
+ },
851
+ masterKey,
852
+ srpHashSize * 8
853
+ );
854
+ return new Uint8Array(authBytes);
855
+ }
856
+ };
857
+
858
+ export { AesGcmOptions, CryptoProfile, CryptoProfileRegistry, CryptoService, CryptoVersion, KdfOptions, KeyDerivationService, SecurityConstants, SecurityUtils, SrpClientService, SrpContextFactory, SrpEncoding, SrpGroup, SrpGroupParams, SrpKeyDerivationService, SrpOptions, SrpServerService, SupportedHashAlgorithms };
795
859
  //# sourceMappingURL=index.js.map
796
860
  //# sourceMappingURL=index.js.map