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