@crossdyne/security 0.3.1-beta.1 → 0.4.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"];
@@ -299,12 +304,17 @@ var KeyDerivationService = class {
299
304
  * @returns Object with `kek` (Uint8Array) and `authHash` (Base64 string).
300
305
  */
301
306
  async deriveKeysFromPassword(identity, password, salt, options) {
307
+ if (!identity || identity.trim().length === 0)
308
+ throw new Error("Identity cannot be null or empty.");
309
+ if (!password || password.trim().length === 0)
310
+ throw new Error("Password cannot be null or empty.");
311
+ if (!salt || salt.length < 16)
312
+ throw new Error("Salt must be at least 16 bytes.");
302
313
  const opts = options ?? KdfOptions.default;
303
314
  opts.validate();
304
315
  const safeSalt = new Uint8Array(salt);
305
316
  const encoder = new TextEncoder();
306
- const normalizedLogin = identity.trim().toLowerCase();
307
- const combinedPassword = `${normalizedLogin}:${password}`;
317
+ const combinedPassword = `${identity}:${password}`;
308
318
  const passwordBytes = encoder.encode(combinedPassword);
309
319
  const baseKey = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits", "deriveKey"]);
310
320
  const masterKeyBits = await crypto.subtle.deriveBits({
@@ -339,11 +349,16 @@ var KeyDerivationService = class {
339
349
  * @returns Raw hash bytes for use as SRP verifier input (x).
340
350
  */
341
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.");
342
358
  const opts = options ?? KdfOptions.default;
343
359
  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}`;
360
+ const srpHashSize = HashSizes[srpHashAlgorithm];
361
+ const combinedPassword = `${identity}:${password}`;
347
362
  const passwordBytes = new TextEncoder().encode(combinedPassword);
348
363
  const baseKey = await crypto.subtle.importKey(
349
364
  "raw",
@@ -434,21 +449,71 @@ var SrpEncoding = class {
434
449
  const buffers = values.map((v) => this.toModulusBytes(ctx, v));
435
450
  return this.hash(ctx.hashAlgorithmName, ...buffers);
436
451
  }
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);
452
+ /**
453
+ * Computes the client proof M1 = H( H(N) ⊕ H(g) | H(I) | s | PAD(A) | PAD(B) | K ).
454
+ *
455
+ * Follows RFC 5054 / SRP-6a:
456
+ * - H(N) and H(g) are hashed as modulus-sized values.
457
+ * - Identity (I) is hashed as raw UTF-8 bytes.
458
+ * - A and B are padded to the modulus size before hashing.
459
+ * - K is the session key (H(S) without padding).
460
+ *
461
+ * @param ctx - SRP context containing N, g, hash algorithm, and modulus size.
462
+ * @param A - Client ephemeral public key.
463
+ * @param B - Server ephemeral public key.
464
+ * @param sessionKeyK - Session key K as raw bytes.
465
+ * @param identity - User identity (login). Should already be normalized (trimmed / lowercased) by the caller.
466
+ * @param salt - User-specific salt bytes.
467
+ * @returns The M1 proof as raw hash bytes.
468
+ */
469
+ static async computeM1(ctx, A, B, sessionKeyK, identity, salt) {
470
+ const nBytes = this.toModulusBytes(ctx, ctx.N);
471
+ const gBytes = this.toModulusBytes(ctx, ctx.g);
472
+ const hashN = await this.computeHash(ctx.hashAlgorithmName, nBytes);
473
+ const hashG = await this.computeHash(ctx.hashAlgorithmName, gBytes);
474
+ const xorNg = new Uint8Array(hashN.length);
475
+ for (let i = 0; i < hashN.length; i++)
476
+ xorNg[i] = hashN[i] ^ hashG[i];
477
+ const encoder = new TextEncoder();
478
+ const hashI = await this.computeHash(ctx.hashAlgorithmName, encoder.encode(identity));
479
+ return this.computeHash(
480
+ ctx.hashAlgorithmName,
481
+ xorNg,
482
+ hashI,
483
+ salt,
484
+ this.toModulusBytes(ctx, A),
485
+ this.toModulusBytes(ctx, B),
486
+ sessionKeyK
487
+ );
440
488
  }
441
489
  /** 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);
490
+ static async computeM2(ctx, A, m1Bytes, sessionKeyK) {
491
+ return this.computeHash(
492
+ ctx.hashAlgorithmName,
493
+ this.toModulusBytes(ctx, A),
494
+ m1Bytes,
495
+ sessionKeyK
496
+ );
444
497
  }
445
498
  /** Computes session key K = H(S). */
446
499
  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
- );
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);
505
+ return new Uint8Array(hashBuffer);
506
+ }
507
+ /** Hashes bytes and returns raw Uint8Array (для M1/M2). */
508
+ static async computeHash(algo, ...buffers) {
509
+ const totalLen = buffers.reduce((sum, b) => sum + b.length, 0);
510
+ const combined = new Uint8Array(totalLen);
511
+ let offset = 0;
512
+ for (const buf of buffers) {
513
+ combined.set(buf, offset);
514
+ offset += buf.length;
515
+ }
516
+ const hashBuffer = await crypto.subtle.digest(algo, combined);
452
517
  return new Uint8Array(hashBuffer);
453
518
  }
454
519
  /** Concatenates byte arrays and returns the hash as bigint. */
@@ -494,7 +559,8 @@ var SrpClientService = class {
494
559
  const salt = SecurityUtils.fromBase64(saltBase64);
495
560
  const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName);
496
561
  const x = SecurityUtils.bytesToBigInt(authHash);
497
- const aBytes = crypto.getRandomValues(new Uint8Array(32));
562
+ const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
563
+ const aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
498
564
  const a = SecurityUtils.bytesToBigInt(aBytes);
499
565
  const A = SecurityUtils.expMod(ctx.g, a, ctx.N);
500
566
  if (A % ctx.N === 0n)
@@ -513,11 +579,11 @@ var SrpClientService = class {
513
579
  if (S === 0n)
514
580
  throw new Error("Critical error: S === 0");
515
581
  const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
516
- const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK);
582
+ const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, login, salt);
517
583
  return {
518
584
  A: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, A)),
519
- M1: SecurityUtils.toBase64(SrpEncoding.toHashBytes(ctx, M1)),
520
- S: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, S))
585
+ M1: SecurityUtils.toBase64(M1),
586
+ SessionKeyK: sessionKeyK
521
587
  };
522
588
  }
523
589
  /**
@@ -529,15 +595,12 @@ var SrpClientService = class {
529
595
  * @param ctx - SRP context.
530
596
  * @returns True if the server proof is valid.
531
597
  */
532
- async verifyServerM2(A_b64, M1_b64, S_b64, serverM2_b64, ctx) {
598
+ async verifyServerM2(A_b64, M1_b64, sessionKeyK, serverM2_b64, ctx) {
533
599
  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);
600
+ const M1 = SecurityUtils.fromBase64(M1_b64);
537
601
  const computedM2 = await SrpEncoding.computeM2(ctx, A, M1, sessionKeyK);
538
- const computedM2Bytes = SrpEncoding.toHashBytes(ctx, computedM2);
539
602
  const serverM2Bytes = SecurityUtils.fromBase64(serverM2_b64);
540
- return SecurityUtils.fixedTimeEquals(computedM2Bytes, serverM2Bytes);
603
+ return SecurityUtils.fixedTimeEquals(computedM2, serverM2Bytes);
541
604
  }
542
605
  };
543
606
 
@@ -550,17 +613,23 @@ var SrpServerService = class {
550
613
  * @param ctx - SRP context (hash, N, g, etc.).
551
614
  * @returns Session state with private b, verifier, and public B.
552
615
  */
553
- async getSrpChallenge(login, verifierBytes, ctx) {
616
+ async getSrpChallenge(login, verifierBytes, salt, ctx) {
554
617
  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;
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);
559
627
  return {
560
628
  login,
561
- privateKeyB: SecurityUtils.toBase64(bBytes),
562
- verifier: SecurityUtils.toBase64(verifierBytes),
563
- publicKeyB: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, B))
629
+ privateKeyB: bBytes,
630
+ verifier: verifierBytes,
631
+ publicKeyB: SrpEncoding.toModulusBytes(ctx, B),
632
+ salt
564
633
  };
565
634
  }
566
635
  /**
@@ -574,10 +643,10 @@ var SrpServerService = class {
574
643
  */
575
644
  async verifySrpProof(sessionState, a, m1, ctx) {
576
645
  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));
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);
581
650
  if (v <= 0n)
582
651
  throw new Error("The verifier is corrupted");
583
652
  if (A % ctx.N === 0n)
@@ -590,13 +659,11 @@ var SrpServerService = class {
590
659
  const vU = SecurityUtils.expMod(v, u, ctx.N);
591
660
  const S = SecurityUtils.expMod(A * vU % ctx.N, b, ctx.N);
592
661
  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))
662
+ const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, sessionState.login, sessionState.salt);
663
+ if (!SecurityUtils.fixedTimeEquals(M1_server, M1_client))
597
664
  throw new Error("Invalid password");
598
665
  const M2_server = await SrpEncoding.computeM2(ctx, A, M1_client, sessionKeyK);
599
- return SecurityUtils.toBase64(SrpEncoding.toHashBytes(ctx, M2_server));
666
+ return SecurityUtils.toBase64(M2_server);
600
667
  }
601
668
  };
602
669