@crossdyne/security 0.3.0-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"];
@@ -190,7 +195,7 @@ var CryptoService = class {
190
195
  * @returns Deserialized object, or null if input is empty.
191
196
  * @throws If authentication tag mismatch or corrupted data.
192
197
  */
193
- async decryptData(encryptedBase64, key, options) {
198
+ async decryptData(encryptedBase64, key, options, isBytes = false) {
194
199
  if (!encryptedBase64)
195
200
  return null;
196
201
  const opts = options ?? AesGcmOptions.default;
@@ -224,6 +229,10 @@ var CryptoService = class {
224
229
  );
225
230
  const decoder = new TextDecoder();
226
231
  const jsonString = decoder.decode(decryptedBuffer);
232
+ const parsed = JSON.parse(jsonString);
233
+ if (isBytes && typeof parsed === "string") {
234
+ return SecurityUtils.fromBase64(parsed);
235
+ }
227
236
  return JSON.parse(jsonString);
228
237
  } catch (e) {
229
238
  console.error("Decryption error:", e);
@@ -295,12 +304,17 @@ var KeyDerivationService = class {
295
304
  * @returns Object with `kek` (Uint8Array) and `authHash` (Base64 string).
296
305
  */
297
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.");
298
313
  const opts = options ?? KdfOptions.default;
299
314
  opts.validate();
300
315
  const safeSalt = new Uint8Array(salt);
301
316
  const encoder = new TextEncoder();
302
- const normalizedLogin = identity.trim().toLowerCase();
303
- const combinedPassword = `${normalizedLogin}:${password}`;
317
+ const combinedPassword = `${identity}:${password}`;
304
318
  const passwordBytes = encoder.encode(combinedPassword);
305
319
  const baseKey = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits", "deriveKey"]);
306
320
  const masterKeyBits = await crypto.subtle.deriveBits({
@@ -335,11 +349,16 @@ var KeyDerivationService = class {
335
349
  * @returns Raw hash bytes for use as SRP verifier input (x).
336
350
  */
337
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.");
338
358
  const opts = options ?? KdfOptions.default;
339
359
  opts.validate();
340
- const srpHashSize = srpHashAlgorithm === "SHA-256" ? 32 : srpHashAlgorithm === "SHA-384" ? 48 : 64;
341
- const normalizedLogin = identity.trim().toLowerCase();
342
- const combinedPassword = `${normalizedLogin}:${password}`;
360
+ const srpHashSize = HashSizes[srpHashAlgorithm];
361
+ const combinedPassword = `${identity}:${password}`;
343
362
  const passwordBytes = new TextEncoder().encode(combinedPassword);
344
363
  const baseKey = await crypto.subtle.importKey(
345
364
  "raw",
@@ -430,21 +449,71 @@ var SrpEncoding = class {
430
449
  const buffers = values.map((v) => this.toModulusBytes(ctx, v));
431
450
  return this.hash(ctx.hashAlgorithmName, ...buffers);
432
451
  }
433
- /** Computes M1 = H(A || B || sessionKeyK). */
434
- static async computeM1(ctx, A, B, sessionKeyK) {
435
- 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
+ );
436
488
  }
437
489
  /** Computes M2 = H(A || M1 || sessionKeyK). */
438
- static async computeM2(ctx, A, M1, sessionKeyK) {
439
- 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
+ );
440
497
  }
441
498
  /** Computes session key K = H(S). */
442
499
  static async computeSessionKey(ctx, S) {
443
- const sBytes = this.toModulusBytes(ctx, S);
444
- const hashBuffer = await crypto.subtle.digest(
445
- ctx.hashAlgorithmName,
446
- sBytes
447
- );
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);
448
517
  return new Uint8Array(hashBuffer);
449
518
  }
450
519
  /** Concatenates byte arrays and returns the hash as bigint. */
@@ -490,7 +559,8 @@ var SrpClientService = class {
490
559
  const salt = SecurityUtils.fromBase64(saltBase64);
491
560
  const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName);
492
561
  const x = SecurityUtils.bytesToBigInt(authHash);
493
- 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));
494
564
  const a = SecurityUtils.bytesToBigInt(aBytes);
495
565
  const A = SecurityUtils.expMod(ctx.g, a, ctx.N);
496
566
  if (A % ctx.N === 0n)
@@ -509,11 +579,11 @@ var SrpClientService = class {
509
579
  if (S === 0n)
510
580
  throw new Error("Critical error: S === 0");
511
581
  const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
512
- const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK);
582
+ const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, login, salt);
513
583
  return {
514
584
  A: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, A)),
515
- M1: SecurityUtils.toBase64(SrpEncoding.toHashBytes(ctx, M1)),
516
- S: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, S))
585
+ M1: SecurityUtils.toBase64(M1),
586
+ SessionKeyK: sessionKeyK
517
587
  };
518
588
  }
519
589
  /**
@@ -525,15 +595,12 @@ var SrpClientService = class {
525
595
  * @param ctx - SRP context.
526
596
  * @returns True if the server proof is valid.
527
597
  */
528
- async verifyServerM2(A_b64, M1_b64, S_b64, serverM2_b64, ctx) {
598
+ async verifyServerM2(A_b64, M1_b64, sessionKeyK, serverM2_b64, ctx) {
529
599
  const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(A_b64));
530
- const M1 = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(M1_b64));
531
- const S = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(S_b64));
532
- const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
600
+ const M1 = SecurityUtils.fromBase64(M1_b64);
533
601
  const computedM2 = await SrpEncoding.computeM2(ctx, A, M1, sessionKeyK);
534
- const computedM2Bytes = SrpEncoding.toHashBytes(ctx, computedM2);
535
602
  const serverM2Bytes = SecurityUtils.fromBase64(serverM2_b64);
536
- return SecurityUtils.fixedTimeEquals(computedM2Bytes, serverM2Bytes);
603
+ return SecurityUtils.fixedTimeEquals(computedM2, serverM2Bytes);
537
604
  }
538
605
  };
539
606
 
@@ -546,17 +613,23 @@ var SrpServerService = class {
546
613
  * @param ctx - SRP context (hash, N, g, etc.).
547
614
  * @returns Session state with private b, verifier, and public B.
548
615
  */
549
- async getSrpChallenge(login, verifierBytes, ctx) {
616
+ async getSrpChallenge(login, verifierBytes, salt, ctx) {
550
617
  const v = SecurityUtils.bytesToBigInt(verifierBytes);
551
- const bBytes = crypto.getRandomValues(new Uint8Array(32));
552
- const b = SecurityUtils.bytesToBigInt(bBytes);
553
- const gB = SecurityUtils.expMod(ctx.g, b, ctx.N);
554
- 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);
555
627
  return {
556
628
  login,
557
- privateKeyB: SecurityUtils.toBase64(bBytes),
558
- verifier: SecurityUtils.toBase64(verifierBytes),
559
- publicKeyB: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, B))
629
+ privateKeyB: bBytes,
630
+ verifier: verifierBytes,
631
+ publicKeyB: SrpEncoding.toModulusBytes(ctx, B),
632
+ salt
560
633
  };
561
634
  }
562
635
  /**
@@ -570,10 +643,10 @@ var SrpServerService = class {
570
643
  */
571
644
  async verifySrpProof(sessionState, a, m1, ctx) {
572
645
  const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(a));
573
- const M1_client = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(m1));
574
- const b = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(sessionState.privateKeyB));
575
- const v = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(sessionState.verifier));
576
- 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);
577
650
  if (v <= 0n)
578
651
  throw new Error("The verifier is corrupted");
579
652
  if (A % ctx.N === 0n)
@@ -586,13 +659,11 @@ var SrpServerService = class {
586
659
  const vU = SecurityUtils.expMod(v, u, ctx.N);
587
660
  const S = SecurityUtils.expMod(A * vU % ctx.N, b, ctx.N);
588
661
  const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
589
- const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK);
590
- const m1ServerBytes = SrpEncoding.toHashBytes(ctx, M1_server);
591
- const m1ClientBytes = SrpEncoding.toHashBytes(ctx, M1_client);
592
- 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))
593
664
  throw new Error("Invalid password");
594
665
  const M2_server = await SrpEncoding.computeM2(ctx, A, M1_client, sessionKeyK);
595
- return SecurityUtils.toBase64(SrpEncoding.toHashBytes(ctx, M2_server));
666
+ return SecurityUtils.toBase64(M2_server);
596
667
  }
597
668
  };
598
669