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

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