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