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

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.cjs CHANGED
@@ -48,16 +48,22 @@ var SecurityUtils = class {
48
48
  return 0n;
49
49
  return BigInt("0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
50
50
  }
51
- /** Converts bigint to fixed-length big-endian bytes (pads/truncates). */
51
+ /** Converts bigint to fixed-length big-endian bytes (pads only; never truncates). */
52
52
  static bigIntToFixedBytes(bn, length) {
53
+ if (length <= 0)
54
+ throw new Error("Length must be positive.");
53
55
  let hex = bn.toString(16);
54
56
  if (hex.length % 2 !== 0)
55
57
  hex = "0" + hex;
56
- if (hex.length > length * 2)
57
- hex = hex.slice(hex.length - length * 2);
58
- else
59
- hex = hex.padStart(length * 2, "0");
60
- return new Uint8Array(hex.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) || []);
58
+ const byteLength = hex.length / 2;
59
+ if (byteLength > length)
60
+ throw new Error(`Value byte length (${byteLength}) exceeds expected length (${length}). Possible data corruption or context mismatch.`);
61
+ hex = hex.padStart(length * 2, "0");
62
+ const bytes = new Uint8Array(length);
63
+ for (let i = 0; i < length; i++) {
64
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
65
+ }
66
+ return bytes;
61
67
  }
62
68
  /** Constant-time comparison of two Uint8Arrays. */
63
69
  static fixedTimeEquals(a, b) {
@@ -70,16 +76,26 @@ var SecurityUtils = class {
70
76
  }
71
77
  /** Modular exponentiation (base^exp mod mod) using binary exponentiation. */
72
78
  static expMod(base, exp, mod) {
73
- let res = BigInt(1);
79
+ if (mod === 1n) return 0n;
80
+ let res = 1n;
74
81
  base = base % mod;
75
82
  while (exp > 0n) {
76
- if (exp % 2n === 1n)
83
+ if ((exp & 1n) === 1n)
77
84
  res = res * base % mod;
78
- base = base * base % mod;
79
- exp = exp / 2n;
85
+ exp >>= 1n;
86
+ if (exp > 0n)
87
+ base = base * base % mod;
80
88
  }
81
89
  return res;
82
90
  }
91
+ static bigIntToRawBytes(bn) {
92
+ if (bn === 0n)
93
+ return new Uint8Array([0]);
94
+ let hex = bn.toString(16);
95
+ if (hex.length % 2 !== 0)
96
+ hex = "0" + hex;
97
+ return new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
98
+ }
83
99
  };
84
100
 
85
101
  // src/crypto/aes-gcm-options.ts
@@ -132,6 +148,97 @@ var AesGcmOptions = class _AesGcmOptions {
132
148
  }
133
149
  };
134
150
 
151
+ // src/crypto/crypto-profile.ts
152
+ var CryptoProfile = class {
153
+ /**
154
+ * Creates a new CryptoProfile.
155
+ * @param params - Object containing version, kdfOptions, aesGcmOptions.
156
+ */
157
+ constructor(params) {
158
+ this.version = params.version;
159
+ this.kdfOptions = params.kdfOptions;
160
+ this.aesGcmOptions = params.aesGcmOptions;
161
+ }
162
+ };
163
+
164
+ // src/crypto/crypto-version.ts
165
+ var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
166
+ CryptoVersion2[CryptoVersion2["V1"] = 1] = "V1";
167
+ return CryptoVersion2;
168
+ })(CryptoVersion || {});
169
+
170
+ // src/crypto/kdf-options.ts
171
+ var KdfOptions = class _KdfOptions {
172
+ constructor() {
173
+ this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
174
+ this._hashAlgorithm = "SHA-256";
175
+ }
176
+ /** PBKDF2 iteration count (minimum 100_000). */
177
+ get pbkdf2Iterations() {
178
+ return this._pbkdf2Iterations;
179
+ }
180
+ set pbkdf2Iterations(v) {
181
+ if (v < SecurityConstants.Pbkdf2IterationsMinimum)
182
+ throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
183
+ this._pbkdf2Iterations = v;
184
+ }
185
+ /** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
186
+ get hashAlgorithm() {
187
+ return this._hashAlgorithm;
188
+ }
189
+ set hashAlgorithm(v) {
190
+ if (!SupportedHashAlgorithms.includes(v))
191
+ throw new RangeError(`Unsupported hash: ${v}`);
192
+ this._hashAlgorithm = v;
193
+ }
194
+ /** Validates iterations and hash algorithm. */
195
+ validate() {
196
+ if (this.pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum)
197
+ throw new Error(`Pbkdf2Iterations (${this.pbkdf2Iterations}) below minimum`);
198
+ if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
199
+ throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
200
+ }
201
+ /** Default preset: SHA-256, 600_000 iterations. */
202
+ static get default() {
203
+ return new _KdfOptions();
204
+ }
205
+ /** Fluent setter for {@link pbkdf2Iterations}. */
206
+ withPbkdf2Iterations(i) {
207
+ this.pbkdf2Iterations = i;
208
+ return this;
209
+ }
210
+ /** Fluent setter for {@link hashAlgorithm}. */
211
+ withHashAlgorithm(h) {
212
+ this.hashAlgorithm = h;
213
+ return this;
214
+ }
215
+ /** Validates and returns this instance. */
216
+ build() {
217
+ this.validate();
218
+ return this;
219
+ }
220
+ };
221
+
222
+ // src/crypto/crypto-profile-registry.ts
223
+ var CryptoProfileRegistry = class {
224
+ static getProfile(version) {
225
+ switch (version) {
226
+ case 1 /* V1 */:
227
+ return new CryptoProfile({
228
+ version: 1 /* V1 */,
229
+ kdfOptions: KdfOptions.default,
230
+ aesGcmOptions: AesGcmOptions.default
231
+ });
232
+ default:
233
+ throw new Error(`Unsupported crypto version: ${version}`);
234
+ }
235
+ }
236
+ /** Latest supported profile (currently V1). */
237
+ static get latest() {
238
+ return this.getProfile(1 /* V1 */);
239
+ }
240
+ };
241
+
135
242
  // src/crypto/crypto.service.ts
136
243
  var CryptoService = class {
137
244
  constructor() {
@@ -149,12 +256,13 @@ var CryptoService = class {
149
256
  * @param options - AES-GCM configuration; uses default if omitted.
150
257
  * @returns Base64-encoded ciphertext with prepended nonce.
151
258
  */
152
- async encryptData(dataModel, key, options) {
153
- const opts = options ?? AesGcmOptions.default;
259
+ async encryptData(dataModel, key, version = 1 /* V1 */) {
260
+ const profile = CryptoProfileRegistry.getProfile(version);
261
+ const opts = profile.aesGcmOptions;
154
262
  opts.validate();
155
263
  const encoder = new TextEncoder();
156
264
  let jsonString;
157
- if (dataModel instanceof Uint8Array) {
265
+ if (ArrayBuffer.isView(dataModel) && dataModel.constructor === Uint8Array) {
158
266
  jsonString = `"${SecurityUtils.toBase64(dataModel)}"`;
159
267
  } else {
160
268
  jsonString = JSON.stringify(dataModel);
@@ -182,9 +290,10 @@ var CryptoService = class {
182
290
  cryptoKey,
183
291
  plainBytes
184
292
  );
185
- const result = new Uint8Array(opts.nonceSize + encryptedContent.byteLength);
186
- result.set(nonce, 0);
187
- result.set(new Uint8Array(encryptedContent), opts.nonceSize);
293
+ const result = new Uint8Array(1 + opts.nonceSize + encryptedContent.byteLength);
294
+ result[0] = version;
295
+ result.set(nonce, 1);
296
+ result.set(new Uint8Array(encryptedContent), 1 + opts.nonceSize);
188
297
  return SecurityUtils.toBase64(result);
189
298
  }
190
299
  /**
@@ -195,16 +304,19 @@ var CryptoService = class {
195
304
  * @returns Deserialized object, or null if input is empty.
196
305
  * @throws If authentication tag mismatch or corrupted data.
197
306
  */
198
- async decryptData(encryptedBase64, key, options, isBytes = false) {
307
+ async decryptData(encryptedBase64, key, isBytes = false) {
199
308
  if (!encryptedBase64)
200
309
  return null;
201
- const opts = options ?? AesGcmOptions.default;
202
- opts.validate();
203
310
  const encryptedBytes = SecurityUtils.fromBase64(encryptedBase64);
204
- if (encryptedBytes.length < opts.nonceSize + opts.tagSize)
311
+ const version = encryptedBytes[0];
312
+ const payload = encryptedBytes.slice(1);
313
+ const profile = CryptoProfileRegistry.getProfile(version);
314
+ const opts = profile.aesGcmOptions;
315
+ opts.validate();
316
+ if (payload.length < opts.nonceSize + opts.tagSize)
205
317
  throw new Error(`Invalid format: minimum expected ${opts.nonceSize + opts.tagSize} byte.`);
206
- const nonce = encryptedBytes.slice(0, opts.nonceSize);
207
- const ciphertextWithTag = encryptedBytes.slice(opts.nonceSize);
318
+ const nonce = payload.slice(0, opts.nonceSize);
319
+ const ciphertextWithTag = payload.slice(opts.nonceSize);
208
320
  const cryptoKey = await crypto.subtle.importKey(
209
321
  "raw",
210
322
  key,
@@ -214,9 +326,8 @@ var CryptoService = class {
214
326
  );
215
327
  try {
216
328
  let associatedData = new Uint8Array(0);
217
- if (opts.associatedData != null) {
329
+ if (opts.associatedData != null)
218
330
  associatedData = opts.associatedData;
219
- }
220
331
  const decryptedBuffer = await crypto.subtle.decrypt(
221
332
  {
222
333
  name: "AES-GCM",
@@ -233,7 +344,7 @@ var CryptoService = class {
233
344
  if (isBytes && typeof parsed === "string") {
234
345
  return SecurityUtils.fromBase64(parsed);
235
346
  }
236
- return JSON.parse(jsonString);
347
+ return parsed;
237
348
  } catch (e) {
238
349
  console.error("Decryption error:", e);
239
350
  throw new Error("Decryption failed: authentication tag mismatch or corrupted data.");
@@ -241,88 +352,40 @@ var CryptoService = class {
241
352
  }
242
353
  };
243
354
 
244
- // src/crypto/kdf-options.ts
245
- var KdfOptions = class _KdfOptions {
246
- constructor() {
247
- this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
248
- this._hashAlgorithm = "SHA-256";
249
- }
250
- /** PBKDF2 iteration count (minimum 100_000). */
251
- get pbkdf2Iterations() {
252
- return this._pbkdf2Iterations;
253
- }
254
- set pbkdf2Iterations(v) {
255
- if (v < SecurityConstants.Pbkdf2IterationsMinimum)
256
- throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
257
- this._pbkdf2Iterations = v;
258
- }
259
- /** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
260
- get hashAlgorithm() {
261
- return this._hashAlgorithm;
262
- }
263
- set hashAlgorithm(v) {
264
- if (!SupportedHashAlgorithms.includes(v))
265
- throw new RangeError(`Unsupported hash: ${v}`);
266
- this._hashAlgorithm = v;
267
- }
268
- /** Validates iterations and hash algorithm. */
269
- validate() {
270
- if (this.pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum)
271
- throw new Error(`Pbkdf2Iterations (${this.pbkdf2Iterations}) below minimum`);
272
- if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
273
- throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
274
- }
275
- /** Default preset: SHA-256, 600_000 iterations. */
276
- static get default() {
277
- return new _KdfOptions();
278
- }
279
- /** Fluent setter for {@link pbkdf2Iterations}. */
280
- withPbkdf2Iterations(i) {
281
- this.pbkdf2Iterations = i;
282
- return this;
283
- }
284
- /** Fluent setter for {@link hashAlgorithm}. */
285
- withHashAlgorithm(h) {
286
- this.hashAlgorithm = h;
287
- return this;
288
- }
289
- /** Validates and returns this instance. */
290
- build() {
291
- this.validate();
292
- return this;
293
- }
294
- };
295
-
296
355
  // src/crypto/key-derivation.service.ts
297
356
  var KeyDerivationService = class {
298
357
  /**
299
- * Derives KEK and Base64 AuthHash. Identity is normalized (trimmed, lowercase).
358
+ * Derives KEK and Base64 AuthHash. Identity is hashed as-is — caller must
359
+ * normalize (trim, lowercase, etc.) before calling.
360
+ * @param identity - User identity (pre-normalized by caller).
300
361
  * @param identity - User identity (email, username).
301
362
  * @param password - User password.
302
363
  * @param salt - Random salt.
303
364
  * @param options - KDF configuration; uses default if omitted.
304
365
  * @returns Object with `kek` (Uint8Array) and `authHash` (Base64 string).
305
366
  */
306
- async deriveKeysFromPassword(identity, password, salt, options) {
307
- if (!identity || identity.trim().length === 0)
367
+ async deriveKeysFromPassword(identity, password, salt, version) {
368
+ if (!identity || identity.length === 0)
308
369
  throw new Error("Identity cannot be null or empty.");
309
370
  if (!password || password.trim().length === 0)
310
371
  throw new Error("Password cannot be null or empty.");
311
372
  if (!salt || salt.length < 16)
312
373
  throw new Error("Salt must be at least 16 bytes.");
313
- const opts = options ?? KdfOptions.default;
374
+ const profile = CryptoProfileRegistry.getProfile(version);
375
+ const opts = profile.kdfOptions;
314
376
  opts.validate();
315
377
  const safeSalt = new Uint8Array(salt);
316
378
  const encoder = new TextEncoder();
317
379
  const combinedPassword = `${identity}:${password}`;
318
380
  const passwordBytes = encoder.encode(combinedPassword);
319
381
  const baseKey = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits", "deriveKey"]);
382
+ const hashSize = HashSizes[opts.hashAlgorithm] ?? 32;
320
383
  const masterKeyBits = await crypto.subtle.deriveBits({
321
384
  name: "PBKDF2",
322
385
  salt: safeSalt,
323
386
  iterations: opts.pbkdf2Iterations,
324
387
  hash: opts.hashAlgorithm
325
- }, baseKey, SecurityConstants.KeySizeBytes * 8);
388
+ }, baseKey, hashSize * 8);
326
389
  const masterKey = await crypto.subtle.importKey("raw", masterKeyBits, "HKDF", false, ["deriveBits"]);
327
390
  const kek = await crypto.subtle.deriveBits(
328
391
  { name: "HKDF", hash: opts.hashAlgorithm, salt: new Uint8Array(0), info: encoder.encode("AES-GCM-KEK-v1") },
@@ -341,6 +404,8 @@ var KeyDerivationService = class {
341
404
  }
342
405
  /**
343
406
  * Derives an SRP-compatible authentication hash (output size = hash output length).
407
+ * Identity is hashed as-is — caller must normalize (trim, lowercase, etc.) before calling.
408
+ * @param identity - User identity (pre-normalized by caller).
344
409
  * @param identity - User identity.
345
410
  * @param password - User password.
346
411
  * @param salt - Random salt.
@@ -348,14 +413,15 @@ var KeyDerivationService = class {
348
413
  * @param options - KDF configuration; uses default if omitted.
349
414
  * @returns Raw hash bytes for use as SRP verifier input (x).
350
415
  */
351
- async deriveAuthHashForSrp(identity, password, salt, srpHashAlgorithm, options) {
416
+ async deriveAuthHashForSrp(identity, password, salt, srpHashAlgorithm, version) {
352
417
  if (!identity || identity.trim().length === 0)
353
418
  throw new Error("Identity cannot be null or empty.");
354
419
  if (!password || password.trim().length === 0)
355
420
  throw new Error("Password cannot be null or empty.");
356
421
  if (!salt || salt.length < 16)
357
422
  throw new Error("Salt must be at least 16 bytes.");
358
- const opts = options ?? KdfOptions.default;
423
+ const profile = CryptoProfileRegistry.getProfile(version);
424
+ const opts = profile.kdfOptions;
359
425
  opts.validate();
360
426
  const srpHashSize = HashSizes[srpHashAlgorithm];
361
427
  const combinedPassword = `${identity}:${password}`;
@@ -372,7 +438,7 @@ var KeyDerivationService = class {
372
438
  salt,
373
439
  iterations: opts.pbkdf2Iterations,
374
440
  hash: srpHashAlgorithm
375
- }, baseKey, SecurityConstants.KeySizeBytes * 8);
441
+ }, baseKey, srpHashSize * 8);
376
442
  const masterKey = await crypto.subtle.importKey(
377
443
  "raw",
378
444
  masterKeyBits,
@@ -395,45 +461,6 @@ var KeyDerivationService = class {
395
461
  }
396
462
  };
397
463
 
398
- // src/crypto/crypto-version.ts
399
- var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
400
- CryptoVersion2[CryptoVersion2["V1"] = 1] = "V1";
401
- return CryptoVersion2;
402
- })(CryptoVersion || {});
403
-
404
- // src/crypto/crypto-profile.ts
405
- var CryptoProfile = class {
406
- /**
407
- * Creates a new CryptoProfile.
408
- * @param params - Object containing version, kdfOptions, aesGcmOptions.
409
- */
410
- constructor(params) {
411
- this.version = params.version;
412
- this.kdfOptions = params.kdfOptions;
413
- this.aesGcmOptions = params.aesGcmOptions;
414
- }
415
- };
416
-
417
- // src/crypto/crypto-profile-registry.ts
418
- var CryptoProfileRegistry = class {
419
- static getProfile(version) {
420
- switch (version) {
421
- case 1 /* V1 */:
422
- return new CryptoProfile({
423
- version: 1 /* V1 */,
424
- kdfOptions: KdfOptions.default,
425
- aesGcmOptions: AesGcmOptions.default
426
- });
427
- default:
428
- throw new Error(`Unsupported crypto version: ${version}`);
429
- }
430
- }
431
- /** Latest supported profile (currently V1). */
432
- static get latest() {
433
- return this.getProfile(1 /* V1 */);
434
- }
435
- };
436
-
437
464
  // src/utils/srp-encoding.ts
438
465
  var SrpEncoding = class {
439
466
  /** Serializes a bigint to modulus-sized big-endian bytes. */
@@ -462,7 +489,7 @@ var SrpEncoding = class {
462
489
  * @param A - Client ephemeral public key.
463
490
  * @param B - Server ephemeral public key.
464
491
  * @param sessionKeyK - Session key K as raw bytes.
465
- * @param identity - User identity (login). Should already be normalized (trimmed / lowercased) by the caller.
492
+ * @param identity - User identity (login). Must be pre-normalized by the caller.
466
493
  * @param salt - User-specific salt bytes.
467
494
  * @returns The M1 proof as raw hash bytes.
468
495
  */
@@ -497,11 +524,8 @@ var SrpEncoding = class {
497
524
  }
498
525
  /** Computes session key K = H(S). */
499
526
  static async computeSessionKey(ctx, S) {
500
- let hex = S.toString(16);
501
- if (hex.length % 2 !== 0)
502
- hex = "0" + hex;
503
- const sBytes = new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
504
- const hashBuffer = await crypto.subtle.digest(ctx.hashAlgorithmName, sBytes);
527
+ const sBytes = SecurityUtils.bigIntToRawBytes(S);
528
+ const hashBuffer = await crypto.subtle.digest(ctx.hashAlgorithmName, sBytes.buffer);
505
529
  return new Uint8Array(hashBuffer);
506
530
  }
507
531
  /** Hashes bytes and returns raw Uint8Array (для M1/M2). */
@@ -550,23 +574,27 @@ var SrpClientService = class {
550
574
  * Generates client proof (A, M1, session key S) from server challenge.
551
575
  * @param login - User login.
552
576
  * @param password - Plaintext password.
553
- * @param saltBase64 - Server salt (URL-safe Base64).
554
- * @param B_base64 - Server public ephemeral B (URL-safe Base64).
577
+ * @param saltBase64 - Server salt (standard Base64).
578
+ * @param B_base64 - Server public ephemeral B (standard Base64).
555
579
  * @param ctx - SRP context.
556
- * @returns Object with A, M1, S as Base64 strings.
580
+ * @returns Object with A and M1 as standard Base64; SessionKeyK as raw bytes.
557
581
  */
558
- async generateSrpProof(login, password, saltBase64, B_base64, ctx) {
582
+ async generateSrpProof(login, password, saltBase64, B_base64, ctx, version) {
559
583
  const salt = SecurityUtils.fromBase64(saltBase64);
560
- const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName);
584
+ const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName, version);
561
585
  const x = SecurityUtils.bytesToBigInt(authHash);
562
586
  const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
563
- const aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
564
- const a = SecurityUtils.bytesToBigInt(aBytes);
587
+ let aBytes;
588
+ let a;
589
+ do {
590
+ aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
591
+ a = SecurityUtils.bytesToBigInt(aBytes);
592
+ } while (a === 0n);
565
593
  const A = SecurityUtils.expMod(ctx.g, a, ctx.N);
566
- if (A % ctx.N === 0n)
567
- throw new Error("Critical error: A % N === 0");
594
+ if (A <= 0n || A >= ctx.N)
595
+ throw new Error("Invalid client public key A.");
568
596
  const B = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(B_base64));
569
- if (B % ctx.N === 0n)
597
+ if (B <= 0n || B >= ctx.N || B % ctx.N === 0n)
570
598
  throw new Error("Critical error: B % N === 0");
571
599
  const u = await SrpEncoding.hashModuli(ctx, A, B);
572
600
  if (u === 0n)
@@ -614,23 +642,32 @@ var SrpServerService = class {
614
642
  * @returns Session state with private b, verifier, and public B.
615
643
  */
616
644
  async getSrpChallenge(login, verifierBytes, salt, ctx) {
645
+ if (!login || login.trim().length === 0)
646
+ throw new Error("Login cannot be null or empty.");
647
+ if (!verifierBytes || verifierBytes.length === 0)
648
+ throw new Error("Verifier cannot be null or empty.");
617
649
  const v = SecurityUtils.bytesToBigInt(verifierBytes);
650
+ if (v <= 0n || v >= ctx.N)
651
+ throw new Error("The verifier is corrupted");
618
652
  const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
619
- let bBytes;
620
- let B;
621
- do {
622
- bBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
653
+ while (true) {
654
+ const bBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
623
655
  const b = SecurityUtils.bytesToBigInt(bBytes);
656
+ if (b === 0n) {
657
+ continue;
658
+ }
624
659
  const gB = SecurityUtils.expMod(ctx.g, b, ctx.N);
625
- B = (ctx.k * v + gB) % ctx.N;
626
- } while (B === 0n);
627
- return {
628
- login,
629
- privateKeyB: bBytes,
630
- verifier: verifierBytes,
631
- publicKeyB: SrpEncoding.toModulusBytes(ctx, B),
632
- salt
633
- };
660
+ const B = (ctx.k * v + gB) % ctx.N;
661
+ if (B !== 0n) {
662
+ return {
663
+ login,
664
+ privateKeyB: bBytes,
665
+ verifier: verifierBytes,
666
+ publicKeyB: SrpEncoding.toModulusBytes(ctx, B),
667
+ salt
668
+ };
669
+ }
670
+ }
634
671
  }
635
672
  /**
636
673
  * Verifies client M1 proof and returns server M2 proof.
@@ -647,17 +684,21 @@ var SrpServerService = class {
647
684
  const b = SecurityUtils.bytesToBigInt(sessionState.privateKeyB);
648
685
  const v = SecurityUtils.bytesToBigInt(sessionState.verifier);
649
686
  const B = SecurityUtils.bytesToBigInt(sessionState.publicKeyB);
650
- if (v <= 0n)
687
+ if (v <= 0n || v >= ctx.N)
651
688
  throw new Error("The verifier is corrupted");
652
689
  if (A % ctx.N === 0n)
653
690
  throw new Error("Incorrect value of A");
654
691
  if (A <= 0n || A >= ctx.N)
655
692
  throw new Error("Invalid A (out of range)");
693
+ if (B <= 0n || B >= ctx.N)
694
+ throw new Error("Invalid server public key B.");
656
695
  const u = await SrpEncoding.hashModuli(ctx, A, B);
657
696
  if (u === 0n)
658
697
  throw new Error("Error in calculating the parameter u");
659
698
  const vU = SecurityUtils.expMod(v, u, ctx.N);
660
699
  const S = SecurityUtils.expMod(A * vU % ctx.N, b, ctx.N);
700
+ if (S === 0n)
701
+ throw new Error("Critical error: shared secret S is zero (possible malicious A).");
661
702
  const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
662
703
  const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, sessionState.login, sessionState.salt);
663
704
  if (!SecurityUtils.fixedTimeEquals(M1_server, M1_client))