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