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