@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 +114 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -12
- package/dist/index.d.ts +32 -12
- package/dist/index.js +114 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
341
|
-
const
|
|
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
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
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,
|
|
439
|
-
return this.
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
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(
|
|
516
|
-
|
|
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,
|
|
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.
|
|
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(
|
|
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
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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:
|
|
558
|
-
verifier:
|
|
559
|
-
publicKeyB:
|
|
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.
|
|
574
|
-
const b = SecurityUtils.bytesToBigInt(
|
|
575
|
-
const v = SecurityUtils.bytesToBigInt(
|
|
576
|
-
const B = SecurityUtils.bytesToBigInt(
|
|
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
|
-
|
|
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(
|
|
666
|
+
return SecurityUtils.toBase64(M2_server);
|
|
596
667
|
}
|
|
597
668
|
};
|
|
598
669
|
|