@crossdyne/security 0.3.1-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
@@ -11,6 +11,11 @@ var SecurityConstants = {
11
11
  Pbkdf2IterationsDefault: 6e5,
12
12
  Pbkdf2IterationsMinimum: 1e5
13
13
  };
14
+ var HashSizes = {
15
+ "SHA-256": 32,
16
+ "SHA-384": 48,
17
+ "SHA-512": 64
18
+ };
14
19
 
15
20
  // src/configurations/supported-hash-algorithms.ts
16
21
  var SupportedHashAlgorithms = ["SHA-256", "SHA-384", "SHA-512"];
@@ -43,16 +48,22 @@ var SecurityUtils = class {
43
48
  return 0n;
44
49
  return BigInt("0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
45
50
  }
46
- /** Converts bigint to fixed-length big-endian bytes (pads/truncates). */
51
+ /** Converts bigint to fixed-length big-endian bytes (pads only; never truncates). */
47
52
  static bigIntToFixedBytes(bn, length) {
53
+ if (length <= 0)
54
+ throw new Error("Length must be positive.");
48
55
  let hex = bn.toString(16);
49
56
  if (hex.length % 2 !== 0)
50
57
  hex = "0" + hex;
51
- if (hex.length > length * 2)
52
- hex = hex.slice(hex.length - length * 2);
53
- else
54
- hex = hex.padStart(length * 2, "0");
55
- 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;
56
67
  }
57
68
  /** Constant-time comparison of two Uint8Arrays. */
58
69
  static fixedTimeEquals(a, b) {
@@ -65,16 +76,26 @@ var SecurityUtils = class {
65
76
  }
66
77
  /** Modular exponentiation (base^exp mod mod) using binary exponentiation. */
67
78
  static expMod(base, exp, mod) {
68
- let res = BigInt(1);
79
+ if (mod === 1n) return 0n;
80
+ let res = 1n;
69
81
  base = base % mod;
70
82
  while (exp > 0n) {
71
- if (exp % 2n === 1n)
83
+ if ((exp & 1n) === 1n)
72
84
  res = res * base % mod;
73
- base = base * base % mod;
74
- exp = exp / 2n;
85
+ exp >>= 1n;
86
+ if (exp > 0n)
87
+ base = base * base % mod;
75
88
  }
76
89
  return res;
77
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
+ }
78
99
  };
79
100
 
80
101
  // src/crypto/aes-gcm-options.ts
@@ -127,6 +148,97 @@ var AesGcmOptions = class _AesGcmOptions {
127
148
  }
128
149
  };
129
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
+
130
242
  // src/crypto/crypto.service.ts
131
243
  var CryptoService = class {
132
244
  constructor() {
@@ -144,12 +256,13 @@ var CryptoService = class {
144
256
  * @param options - AES-GCM configuration; uses default if omitted.
145
257
  * @returns Base64-encoded ciphertext with prepended nonce.
146
258
  */
147
- async encryptData(dataModel, key, options) {
148
- const opts = options ?? AesGcmOptions.default;
259
+ async encryptData(dataModel, key, version = 1 /* V1 */) {
260
+ const profile = CryptoProfileRegistry.getProfile(version);
261
+ const opts = profile.aesGcmOptions;
149
262
  opts.validate();
150
263
  const encoder = new TextEncoder();
151
264
  let jsonString;
152
- if (dataModel instanceof Uint8Array) {
265
+ if (ArrayBuffer.isView(dataModel) && dataModel.constructor === Uint8Array) {
153
266
  jsonString = `"${SecurityUtils.toBase64(dataModel)}"`;
154
267
  } else {
155
268
  jsonString = JSON.stringify(dataModel);
@@ -177,9 +290,10 @@ var CryptoService = class {
177
290
  cryptoKey,
178
291
  plainBytes
179
292
  );
180
- const result = new Uint8Array(opts.nonceSize + encryptedContent.byteLength);
181
- result.set(nonce, 0);
182
- 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);
183
297
  return SecurityUtils.toBase64(result);
184
298
  }
185
299
  /**
@@ -190,16 +304,19 @@ var CryptoService = class {
190
304
  * @returns Deserialized object, or null if input is empty.
191
305
  * @throws If authentication tag mismatch or corrupted data.
192
306
  */
193
- async decryptData(encryptedBase64, key, options, isBytes = false) {
307
+ async decryptData(encryptedBase64, key, isBytes = false) {
194
308
  if (!encryptedBase64)
195
309
  return null;
196
- const opts = options ?? AesGcmOptions.default;
197
- opts.validate();
198
310
  const encryptedBytes = SecurityUtils.fromBase64(encryptedBase64);
199
- 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)
200
317
  throw new Error(`Invalid format: minimum expected ${opts.nonceSize + opts.tagSize} byte.`);
201
- const nonce = encryptedBytes.slice(0, opts.nonceSize);
202
- const ciphertextWithTag = encryptedBytes.slice(opts.nonceSize);
318
+ const nonce = payload.slice(0, opts.nonceSize);
319
+ const ciphertextWithTag = payload.slice(opts.nonceSize);
203
320
  const cryptoKey = await crypto.subtle.importKey(
204
321
  "raw",
205
322
  key,
@@ -209,9 +326,8 @@ var CryptoService = class {
209
326
  );
210
327
  try {
211
328
  let associatedData = new Uint8Array(0);
212
- if (opts.associatedData != null) {
329
+ if (opts.associatedData != null)
213
330
  associatedData = opts.associatedData;
214
- }
215
331
  const decryptedBuffer = await crypto.subtle.decrypt(
216
332
  {
217
333
  name: "AES-GCM",
@@ -228,7 +344,7 @@ var CryptoService = class {
228
344
  if (isBytes && typeof parsed === "string") {
229
345
  return SecurityUtils.fromBase64(parsed);
230
346
  }
231
- return JSON.parse(jsonString);
347
+ return parsed;
232
348
  } catch (e) {
233
349
  console.error("Decryption error:", e);
234
350
  throw new Error("Decryption failed: authentication tag mismatch or corrupted data.");
@@ -236,83 +352,40 @@ var CryptoService = class {
236
352
  }
237
353
  };
238
354
 
239
- // src/crypto/kdf-options.ts
240
- var KdfOptions = class _KdfOptions {
241
- constructor() {
242
- this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
243
- this._hashAlgorithm = "SHA-256";
244
- }
245
- /** PBKDF2 iteration count (minimum 100_000). */
246
- get pbkdf2Iterations() {
247
- return this._pbkdf2Iterations;
248
- }
249
- set pbkdf2Iterations(v) {
250
- if (v < SecurityConstants.Pbkdf2IterationsMinimum)
251
- throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
252
- this._pbkdf2Iterations = v;
253
- }
254
- /** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
255
- get hashAlgorithm() {
256
- return this._hashAlgorithm;
257
- }
258
- set hashAlgorithm(v) {
259
- if (!SupportedHashAlgorithms.includes(v))
260
- throw new RangeError(`Unsupported hash: ${v}`);
261
- this._hashAlgorithm = v;
262
- }
263
- /** Validates iterations and hash algorithm. */
264
- validate() {
265
- if (this.pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum)
266
- throw new Error(`Pbkdf2Iterations (${this.pbkdf2Iterations}) below minimum`);
267
- if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
268
- throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
269
- }
270
- /** Default preset: SHA-256, 600_000 iterations. */
271
- static get default() {
272
- return new _KdfOptions();
273
- }
274
- /** Fluent setter for {@link pbkdf2Iterations}. */
275
- withPbkdf2Iterations(i) {
276
- this.pbkdf2Iterations = i;
277
- return this;
278
- }
279
- /** Fluent setter for {@link hashAlgorithm}. */
280
- withHashAlgorithm(h) {
281
- this.hashAlgorithm = h;
282
- return this;
283
- }
284
- /** Validates and returns this instance. */
285
- build() {
286
- this.validate();
287
- return this;
288
- }
289
- };
290
-
291
355
  // src/crypto/key-derivation.service.ts
292
356
  var KeyDerivationService = class {
293
357
  /**
294
- * 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).
295
361
  * @param identity - User identity (email, username).
296
362
  * @param password - User password.
297
363
  * @param salt - Random salt.
298
364
  * @param options - KDF configuration; uses default if omitted.
299
365
  * @returns Object with `kek` (Uint8Array) and `authHash` (Base64 string).
300
366
  */
301
- async deriveKeysFromPassword(identity, password, salt, options) {
302
- const opts = options ?? KdfOptions.default;
367
+ async deriveKeysFromPassword(identity, password, salt, version) {
368
+ if (!identity || identity.length === 0)
369
+ throw new Error("Identity cannot be null or empty.");
370
+ if (!password || password.trim().length === 0)
371
+ throw new Error("Password cannot be null or empty.");
372
+ if (!salt || salt.length < 16)
373
+ throw new Error("Salt must be at least 16 bytes.");
374
+ const profile = CryptoProfileRegistry.getProfile(version);
375
+ const opts = profile.kdfOptions;
303
376
  opts.validate();
304
377
  const safeSalt = new Uint8Array(salt);
305
378
  const encoder = new TextEncoder();
306
- const normalizedLogin = identity.trim().toLowerCase();
307
- const combinedPassword = `${normalizedLogin}:${password}`;
379
+ const combinedPassword = `${identity}:${password}`;
308
380
  const passwordBytes = encoder.encode(combinedPassword);
309
381
  const baseKey = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits", "deriveKey"]);
382
+ const hashSize = HashSizes[opts.hashAlgorithm] ?? 32;
310
383
  const masterKeyBits = await crypto.subtle.deriveBits({
311
384
  name: "PBKDF2",
312
385
  salt: safeSalt,
313
386
  iterations: opts.pbkdf2Iterations,
314
387
  hash: opts.hashAlgorithm
315
- }, baseKey, SecurityConstants.KeySizeBytes * 8);
388
+ }, baseKey, hashSize * 8);
316
389
  const masterKey = await crypto.subtle.importKey("raw", masterKeyBits, "HKDF", false, ["deriveBits"]);
317
390
  const kek = await crypto.subtle.deriveBits(
318
391
  { name: "HKDF", hash: opts.hashAlgorithm, salt: new Uint8Array(0), info: encoder.encode("AES-GCM-KEK-v1") },
@@ -331,6 +404,8 @@ var KeyDerivationService = class {
331
404
  }
332
405
  /**
333
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).
334
409
  * @param identity - User identity.
335
410
  * @param password - User password.
336
411
  * @param salt - Random salt.
@@ -338,12 +413,18 @@ var KeyDerivationService = class {
338
413
  * @param options - KDF configuration; uses default if omitted.
339
414
  * @returns Raw hash bytes for use as SRP verifier input (x).
340
415
  */
341
- async deriveAuthHashForSrp(identity, password, salt, srpHashAlgorithm, options) {
342
- const opts = options ?? KdfOptions.default;
416
+ async deriveAuthHashForSrp(identity, password, salt, srpHashAlgorithm, version) {
417
+ if (!identity || identity.trim().length === 0)
418
+ throw new Error("Identity cannot be null or empty.");
419
+ if (!password || password.trim().length === 0)
420
+ throw new Error("Password cannot be null or empty.");
421
+ if (!salt || salt.length < 16)
422
+ throw new Error("Salt must be at least 16 bytes.");
423
+ const profile = CryptoProfileRegistry.getProfile(version);
424
+ const opts = profile.kdfOptions;
343
425
  opts.validate();
344
- const srpHashSize = srpHashAlgorithm === "SHA-256" ? 32 : srpHashAlgorithm === "SHA-384" ? 48 : 64;
345
- const normalizedLogin = identity.trim().toLowerCase();
346
- const combinedPassword = `${normalizedLogin}:${password}`;
426
+ const srpHashSize = HashSizes[srpHashAlgorithm];
427
+ const combinedPassword = `${identity}:${password}`;
347
428
  const passwordBytes = new TextEncoder().encode(combinedPassword);
348
429
  const baseKey = await crypto.subtle.importKey(
349
430
  "raw",
@@ -357,7 +438,7 @@ var KeyDerivationService = class {
357
438
  salt,
358
439
  iterations: opts.pbkdf2Iterations,
359
440
  hash: srpHashAlgorithm
360
- }, baseKey, SecurityConstants.KeySizeBytes * 8);
441
+ }, baseKey, srpHashSize * 8);
361
442
  const masterKey = await crypto.subtle.importKey(
362
443
  "raw",
363
444
  masterKeyBits,
@@ -380,45 +461,6 @@ var KeyDerivationService = class {
380
461
  }
381
462
  };
382
463
 
383
- // src/crypto/crypto-version.ts
384
- var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
385
- CryptoVersion2[CryptoVersion2["V1"] = 1] = "V1";
386
- return CryptoVersion2;
387
- })(CryptoVersion || {});
388
-
389
- // src/crypto/crypto-profile.ts
390
- var CryptoProfile = class {
391
- /**
392
- * Creates a new CryptoProfile.
393
- * @param params - Object containing version, kdfOptions, aesGcmOptions.
394
- */
395
- constructor(params) {
396
- this.version = params.version;
397
- this.kdfOptions = params.kdfOptions;
398
- this.aesGcmOptions = params.aesGcmOptions;
399
- }
400
- };
401
-
402
- // src/crypto/crypto-profile-registry.ts
403
- var CryptoProfileRegistry = class {
404
- static getProfile(version) {
405
- switch (version) {
406
- case 1 /* V1 */:
407
- return new CryptoProfile({
408
- version: 1 /* V1 */,
409
- kdfOptions: KdfOptions.default,
410
- aesGcmOptions: AesGcmOptions.default
411
- });
412
- default:
413
- throw new Error(`Unsupported crypto version: ${version}`);
414
- }
415
- }
416
- /** Latest supported profile (currently V1). */
417
- static get latest() {
418
- return this.getProfile(1 /* V1 */);
419
- }
420
- };
421
-
422
464
  // src/utils/srp-encoding.ts
423
465
  var SrpEncoding = class {
424
466
  /** Serializes a bigint to modulus-sized big-endian bytes. */
@@ -434,21 +476,68 @@ var SrpEncoding = class {
434
476
  const buffers = values.map((v) => this.toModulusBytes(ctx, v));
435
477
  return this.hash(ctx.hashAlgorithmName, ...buffers);
436
478
  }
437
- /** Computes M1 = H(A || B || sessionKeyK). */
438
- static async computeM1(ctx, A, B, sessionKeyK) {
439
- return this.hash(ctx.hashAlgorithmName, this.toModulusBytes(ctx, A), this.toModulusBytes(ctx, B), sessionKeyK);
479
+ /**
480
+ * Computes the client proof M1 = H( H(N) ⊕ H(g) | H(I) | s | PAD(A) | PAD(B) | K ).
481
+ *
482
+ * Follows RFC 5054 / SRP-6a:
483
+ * - H(N) and H(g) are hashed as modulus-sized values.
484
+ * - Identity (I) is hashed as raw UTF-8 bytes.
485
+ * - A and B are padded to the modulus size before hashing.
486
+ * - K is the session key (H(S) without padding).
487
+ *
488
+ * @param ctx - SRP context containing N, g, hash algorithm, and modulus size.
489
+ * @param A - Client ephemeral public key.
490
+ * @param B - Server ephemeral public key.
491
+ * @param sessionKeyK - Session key K as raw bytes.
492
+ * @param identity - User identity (login). Must be pre-normalized by the caller.
493
+ * @param salt - User-specific salt bytes.
494
+ * @returns The M1 proof as raw hash bytes.
495
+ */
496
+ static async computeM1(ctx, A, B, sessionKeyK, identity, salt) {
497
+ const nBytes = this.toModulusBytes(ctx, ctx.N);
498
+ const gBytes = this.toModulusBytes(ctx, ctx.g);
499
+ const hashN = await this.computeHash(ctx.hashAlgorithmName, nBytes);
500
+ const hashG = await this.computeHash(ctx.hashAlgorithmName, gBytes);
501
+ const xorNg = new Uint8Array(hashN.length);
502
+ for (let i = 0; i < hashN.length; i++)
503
+ xorNg[i] = hashN[i] ^ hashG[i];
504
+ const encoder = new TextEncoder();
505
+ const hashI = await this.computeHash(ctx.hashAlgorithmName, encoder.encode(identity));
506
+ return this.computeHash(
507
+ ctx.hashAlgorithmName,
508
+ xorNg,
509
+ hashI,
510
+ salt,
511
+ this.toModulusBytes(ctx, A),
512
+ this.toModulusBytes(ctx, B),
513
+ sessionKeyK
514
+ );
440
515
  }
441
516
  /** Computes M2 = H(A || M1 || sessionKeyK). */
442
- static async computeM2(ctx, A, M1, sessionKeyK) {
443
- return this.hash(ctx.hashAlgorithmName, this.toModulusBytes(ctx, A), this.toHashBytes(ctx, M1), sessionKeyK);
517
+ static async computeM2(ctx, A, m1Bytes, sessionKeyK) {
518
+ return this.computeHash(
519
+ ctx.hashAlgorithmName,
520
+ this.toModulusBytes(ctx, A),
521
+ m1Bytes,
522
+ sessionKeyK
523
+ );
444
524
  }
445
525
  /** Computes session key K = H(S). */
446
526
  static async computeSessionKey(ctx, S) {
447
- const sBytes = this.toModulusBytes(ctx, S);
448
- const hashBuffer = await crypto.subtle.digest(
449
- ctx.hashAlgorithmName,
450
- sBytes
451
- );
527
+ const sBytes = SecurityUtils.bigIntToRawBytes(S);
528
+ const hashBuffer = await crypto.subtle.digest(ctx.hashAlgorithmName, sBytes.buffer);
529
+ return new Uint8Array(hashBuffer);
530
+ }
531
+ /** Hashes bytes and returns raw Uint8Array (для M1/M2). */
532
+ static async computeHash(algo, ...buffers) {
533
+ const totalLen = buffers.reduce((sum, b) => sum + b.length, 0);
534
+ const combined = new Uint8Array(totalLen);
535
+ let offset = 0;
536
+ for (const buf of buffers) {
537
+ combined.set(buf, offset);
538
+ offset += buf.length;
539
+ }
540
+ const hashBuffer = await crypto.subtle.digest(algo, combined);
452
541
  return new Uint8Array(hashBuffer);
453
542
  }
454
543
  /** Concatenates byte arrays and returns the hash as bigint. */
@@ -485,22 +574,27 @@ var SrpClientService = class {
485
574
  * Generates client proof (A, M1, session key S) from server challenge.
486
575
  * @param login - User login.
487
576
  * @param password - Plaintext password.
488
- * @param saltBase64 - Server salt (URL-safe Base64).
489
- * @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).
490
579
  * @param ctx - SRP context.
491
- * @returns Object with A, M1, S as Base64 strings.
580
+ * @returns Object with A and M1 as standard Base64; SessionKeyK as raw bytes.
492
581
  */
493
- async generateSrpProof(login, password, saltBase64, B_base64, ctx) {
582
+ async generateSrpProof(login, password, saltBase64, B_base64, ctx, version) {
494
583
  const salt = SecurityUtils.fromBase64(saltBase64);
495
- const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName);
584
+ const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName, version);
496
585
  const x = SecurityUtils.bytesToBigInt(authHash);
497
- const aBytes = crypto.getRandomValues(new Uint8Array(32));
498
- const a = SecurityUtils.bytesToBigInt(aBytes);
586
+ const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
587
+ let aBytes;
588
+ let a;
589
+ do {
590
+ aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
591
+ a = SecurityUtils.bytesToBigInt(aBytes);
592
+ } while (a === 0n);
499
593
  const A = SecurityUtils.expMod(ctx.g, a, ctx.N);
500
- if (A % ctx.N === 0n)
501
- throw new Error("Critical error: A % N === 0");
594
+ if (A <= 0n || A >= ctx.N)
595
+ throw new Error("Invalid client public key A.");
502
596
  const B = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(B_base64));
503
- if (B % ctx.N === 0n)
597
+ if (B <= 0n || B >= ctx.N || B % ctx.N === 0n)
504
598
  throw new Error("Critical error: B % N === 0");
505
599
  const u = await SrpEncoding.hashModuli(ctx, A, B);
506
600
  if (u === 0n)
@@ -513,11 +607,11 @@ var SrpClientService = class {
513
607
  if (S === 0n)
514
608
  throw new Error("Critical error: S === 0");
515
609
  const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
516
- const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK);
610
+ const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, login, salt);
517
611
  return {
518
612
  A: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, A)),
519
- M1: SecurityUtils.toBase64(SrpEncoding.toHashBytes(ctx, M1)),
520
- S: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, S))
613
+ M1: SecurityUtils.toBase64(M1),
614
+ SessionKeyK: sessionKeyK
521
615
  };
522
616
  }
523
617
  /**
@@ -529,15 +623,12 @@ var SrpClientService = class {
529
623
  * @param ctx - SRP context.
530
624
  * @returns True if the server proof is valid.
531
625
  */
532
- async verifyServerM2(A_b64, M1_b64, S_b64, serverM2_b64, ctx) {
626
+ async verifyServerM2(A_b64, M1_b64, sessionKeyK, serverM2_b64, ctx) {
533
627
  const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(A_b64));
534
- const M1 = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(M1_b64));
535
- const S = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(S_b64));
536
- const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
628
+ const M1 = SecurityUtils.fromBase64(M1_b64);
537
629
  const computedM2 = await SrpEncoding.computeM2(ctx, A, M1, sessionKeyK);
538
- const computedM2Bytes = SrpEncoding.toHashBytes(ctx, computedM2);
539
630
  const serverM2Bytes = SecurityUtils.fromBase64(serverM2_b64);
540
- return SecurityUtils.fixedTimeEquals(computedM2Bytes, serverM2Bytes);
631
+ return SecurityUtils.fixedTimeEquals(computedM2, serverM2Bytes);
541
632
  }
542
633
  };
543
634
 
@@ -550,18 +641,33 @@ var SrpServerService = class {
550
641
  * @param ctx - SRP context (hash, N, g, etc.).
551
642
  * @returns Session state with private b, verifier, and public B.
552
643
  */
553
- async getSrpChallenge(login, verifierBytes, ctx) {
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.");
554
649
  const v = SecurityUtils.bytesToBigInt(verifierBytes);
555
- const bBytes = crypto.getRandomValues(new Uint8Array(32));
556
- const b = SecurityUtils.bytesToBigInt(bBytes);
557
- const gB = SecurityUtils.expMod(ctx.g, b, ctx.N);
558
- const B = (ctx.k * v + gB) % ctx.N;
559
- return {
560
- login,
561
- privateKeyB: SecurityUtils.toBase64(bBytes),
562
- verifier: SecurityUtils.toBase64(verifierBytes),
563
- publicKeyB: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, B))
564
- };
650
+ if (v <= 0n || v >= ctx.N)
651
+ throw new Error("The verifier is corrupted");
652
+ const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
653
+ while (true) {
654
+ const bBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
655
+ const b = SecurityUtils.bytesToBigInt(bBytes);
656
+ if (b === 0n) {
657
+ continue;
658
+ }
659
+ const gB = SecurityUtils.expMod(ctx.g, b, ctx.N);
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
+ }
565
671
  }
566
672
  /**
567
673
  * Verifies client M1 proof and returns server M2 proof.
@@ -574,29 +680,31 @@ var SrpServerService = class {
574
680
  */
575
681
  async verifySrpProof(sessionState, a, m1, ctx) {
576
682
  const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(a));
577
- const M1_client = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(m1));
578
- const b = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(sessionState.privateKeyB));
579
- const v = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(sessionState.verifier));
580
- const B = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(sessionState.publicKeyB));
581
- if (v <= 0n)
683
+ const M1_client = SecurityUtils.fromBase64(m1);
684
+ const b = SecurityUtils.bytesToBigInt(sessionState.privateKeyB);
685
+ const v = SecurityUtils.bytesToBigInt(sessionState.verifier);
686
+ const B = SecurityUtils.bytesToBigInt(sessionState.publicKeyB);
687
+ if (v <= 0n || v >= ctx.N)
582
688
  throw new Error("The verifier is corrupted");
583
689
  if (A % ctx.N === 0n)
584
690
  throw new Error("Incorrect value of A");
585
691
  if (A <= 0n || A >= ctx.N)
586
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.");
587
695
  const u = await SrpEncoding.hashModuli(ctx, A, B);
588
696
  if (u === 0n)
589
697
  throw new Error("Error in calculating the parameter u");
590
698
  const vU = SecurityUtils.expMod(v, u, ctx.N);
591
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).");
592
702
  const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
593
- const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK);
594
- const m1ServerBytes = SrpEncoding.toHashBytes(ctx, M1_server);
595
- const m1ClientBytes = SrpEncoding.toHashBytes(ctx, M1_client);
596
- if (!SecurityUtils.fixedTimeEquals(m1ServerBytes, m1ClientBytes))
703
+ const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, sessionState.login, sessionState.salt);
704
+ if (!SecurityUtils.fixedTimeEquals(M1_server, M1_client))
597
705
  throw new Error("Invalid password");
598
706
  const M2_server = await SrpEncoding.computeM2(ctx, A, M1_client, sessionKeyK);
599
- return SecurityUtils.toBase64(SrpEncoding.toHashBytes(ctx, M2_server));
707
+ return SecurityUtils.toBase64(M2_server);
600
708
  }
601
709
  };
602
710