@crossdyne/security 0.4.0-beta.1 → 0.5.0-beta.2
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 +396 -331
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +150 -132
- package/dist/index.d.ts +150 -132
- package/dist/index.js +396 -332
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -46,16 +46,22 @@ var SecurityUtils = class {
|
|
|
46
46
|
return 0n;
|
|
47
47
|
return BigInt("0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
|
48
48
|
}
|
|
49
|
-
/** Converts bigint to fixed-length big-endian bytes (pads
|
|
49
|
+
/** Converts bigint to fixed-length big-endian bytes (pads only; never truncates). */
|
|
50
50
|
static bigIntToFixedBytes(bn, length) {
|
|
51
|
+
if (length <= 0)
|
|
52
|
+
throw new Error("Length must be positive.");
|
|
51
53
|
let hex = bn.toString(16);
|
|
52
54
|
if (hex.length % 2 !== 0)
|
|
53
55
|
hex = "0" + hex;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const byteLength = hex.length / 2;
|
|
57
|
+
if (byteLength > length)
|
|
58
|
+
throw new Error(`Value byte length (${byteLength}) exceeds expected length (${length}). Possible data corruption or context mismatch.`);
|
|
59
|
+
hex = hex.padStart(length * 2, "0");
|
|
60
|
+
const bytes = new Uint8Array(length);
|
|
61
|
+
for (let i = 0; i < length; i++) {
|
|
62
|
+
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
63
|
+
}
|
|
64
|
+
return bytes;
|
|
59
65
|
}
|
|
60
66
|
/** Constant-time comparison of two Uint8Arrays. */
|
|
61
67
|
static fixedTimeEquals(a, b) {
|
|
@@ -66,18 +72,39 @@ var SecurityUtils = class {
|
|
|
66
72
|
diff |= a[i] ^ b[i];
|
|
67
73
|
return diff === 0;
|
|
68
74
|
}
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Async modular exponentiation with event-loop yielding.
|
|
77
|
+
* @param base - The base value.
|
|
78
|
+
* @param exp - The exponent.
|
|
79
|
+
* @param mod - The modulus.
|
|
80
|
+
* @param yieldEvery - Number of iterations before yielding (default 64).
|
|
81
|
+
*/
|
|
82
|
+
static async expModAsync(base, exp, mod, yieldEvery = 64) {
|
|
83
|
+
if (mod === 1n) return 0n;
|
|
84
|
+
let res = 1n;
|
|
72
85
|
base = base % mod;
|
|
86
|
+
let i = 0;
|
|
73
87
|
while (exp > 0n) {
|
|
74
|
-
if (exp
|
|
88
|
+
if ((exp & 1n) === 1n)
|
|
75
89
|
res = res * base % mod;
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
exp >>= 1n;
|
|
91
|
+
if (exp > 0n)
|
|
92
|
+
base = base * base % mod;
|
|
93
|
+
if (++i % yieldEvery === 0) {
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
95
|
+
}
|
|
78
96
|
}
|
|
79
97
|
return res;
|
|
80
98
|
}
|
|
99
|
+
/** Converts bigint to minimal-length big-endian bytes (no padding). */
|
|
100
|
+
static bigIntToRawBytes(bn) {
|
|
101
|
+
if (bn === 0n)
|
|
102
|
+
return new Uint8Array([0]);
|
|
103
|
+
let hex = bn.toString(16);
|
|
104
|
+
if (hex.length % 2 !== 0)
|
|
105
|
+
hex = "0" + hex;
|
|
106
|
+
return new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
107
|
+
}
|
|
81
108
|
};
|
|
82
109
|
|
|
83
110
|
// src/crypto/aes-gcm-options.ts
|
|
@@ -130,6 +157,97 @@ var AesGcmOptions = class _AesGcmOptions {
|
|
|
130
157
|
}
|
|
131
158
|
};
|
|
132
159
|
|
|
160
|
+
// src/crypto/crypto-profile.ts
|
|
161
|
+
var CryptoProfile = class {
|
|
162
|
+
/**
|
|
163
|
+
* Creates a new CryptoProfile.
|
|
164
|
+
* @param params - Object containing version, kdfOptions, aesGcmOptions.
|
|
165
|
+
*/
|
|
166
|
+
constructor(params) {
|
|
167
|
+
this.version = params.version;
|
|
168
|
+
this.kdfOptions = params.kdfOptions;
|
|
169
|
+
this.aesGcmOptions = params.aesGcmOptions;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/crypto/crypto-version.ts
|
|
174
|
+
var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
|
|
175
|
+
CryptoVersion2[CryptoVersion2["V1"] = 1] = "V1";
|
|
176
|
+
return CryptoVersion2;
|
|
177
|
+
})(CryptoVersion || {});
|
|
178
|
+
|
|
179
|
+
// src/crypto/kdf-options.ts
|
|
180
|
+
var KdfOptions = class _KdfOptions {
|
|
181
|
+
constructor() {
|
|
182
|
+
this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
|
|
183
|
+
this._hashAlgorithm = "SHA-256";
|
|
184
|
+
}
|
|
185
|
+
/** PBKDF2 iteration count (minimum 100_000). */
|
|
186
|
+
get pbkdf2Iterations() {
|
|
187
|
+
return this._pbkdf2Iterations;
|
|
188
|
+
}
|
|
189
|
+
set pbkdf2Iterations(v) {
|
|
190
|
+
if (v < SecurityConstants.Pbkdf2IterationsMinimum)
|
|
191
|
+
throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
|
|
192
|
+
this._pbkdf2Iterations = v;
|
|
193
|
+
}
|
|
194
|
+
/** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
|
|
195
|
+
get hashAlgorithm() {
|
|
196
|
+
return this._hashAlgorithm;
|
|
197
|
+
}
|
|
198
|
+
set hashAlgorithm(v) {
|
|
199
|
+
if (!SupportedHashAlgorithms.includes(v))
|
|
200
|
+
throw new RangeError(`Unsupported hash: ${v}`);
|
|
201
|
+
this._hashAlgorithm = v;
|
|
202
|
+
}
|
|
203
|
+
/** Validates iterations and hash algorithm. */
|
|
204
|
+
validate() {
|
|
205
|
+
if (this.pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum)
|
|
206
|
+
throw new Error(`Pbkdf2Iterations (${this.pbkdf2Iterations}) below minimum`);
|
|
207
|
+
if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
|
|
208
|
+
throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
|
|
209
|
+
}
|
|
210
|
+
/** Default preset: SHA-256, 600_000 iterations. */
|
|
211
|
+
static get default() {
|
|
212
|
+
return new _KdfOptions();
|
|
213
|
+
}
|
|
214
|
+
/** Fluent setter for {@link pbkdf2Iterations}. */
|
|
215
|
+
withPbkdf2Iterations(i) {
|
|
216
|
+
this.pbkdf2Iterations = i;
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
/** Fluent setter for {@link hashAlgorithm}. */
|
|
220
|
+
withHashAlgorithm(h) {
|
|
221
|
+
this.hashAlgorithm = h;
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
/** Validates and returns this instance. */
|
|
225
|
+
build() {
|
|
226
|
+
this.validate();
|
|
227
|
+
return this;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/crypto/crypto-profile-registry.ts
|
|
232
|
+
var CryptoProfileRegistry = class {
|
|
233
|
+
static getProfile(version) {
|
|
234
|
+
switch (version) {
|
|
235
|
+
case 1 /* V1 */:
|
|
236
|
+
return new CryptoProfile({
|
|
237
|
+
version: 1 /* V1 */,
|
|
238
|
+
kdfOptions: KdfOptions.default,
|
|
239
|
+
aesGcmOptions: AesGcmOptions.default
|
|
240
|
+
});
|
|
241
|
+
default:
|
|
242
|
+
throw new Error(`Unsupported crypto version: ${version}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** Latest supported profile (currently V1). */
|
|
246
|
+
static get latest() {
|
|
247
|
+
return this.getProfile(1 /* V1 */);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
133
251
|
// src/crypto/crypto.service.ts
|
|
134
252
|
var CryptoService = class {
|
|
135
253
|
constructor() {
|
|
@@ -144,15 +262,15 @@ var CryptoService = class {
|
|
|
144
262
|
* Encrypts a serializable object to a Base64 string.
|
|
145
263
|
* @param dataModel - Object or Uint8Array to encrypt.
|
|
146
264
|
* @param key - AES-256 key (32 bytes).
|
|
147
|
-
* @param options - AES-GCM configuration; uses default if omitted.
|
|
148
265
|
* @returns Base64-encoded ciphertext with prepended nonce.
|
|
149
266
|
*/
|
|
150
|
-
async encryptData(dataModel, key,
|
|
151
|
-
const
|
|
267
|
+
async encryptData(dataModel, key, version = 1 /* V1 */) {
|
|
268
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
269
|
+
const opts = profile.aesGcmOptions;
|
|
152
270
|
opts.validate();
|
|
153
271
|
const encoder = new TextEncoder();
|
|
154
272
|
let jsonString;
|
|
155
|
-
if (dataModel
|
|
273
|
+
if (ArrayBuffer.isView(dataModel) && dataModel.constructor === Uint8Array) {
|
|
156
274
|
jsonString = `"${SecurityUtils.toBase64(dataModel)}"`;
|
|
157
275
|
} else {
|
|
158
276
|
jsonString = JSON.stringify(dataModel);
|
|
@@ -180,29 +298,32 @@ var CryptoService = class {
|
|
|
180
298
|
cryptoKey,
|
|
181
299
|
plainBytes
|
|
182
300
|
);
|
|
183
|
-
const result = new Uint8Array(opts.nonceSize + encryptedContent.byteLength);
|
|
184
|
-
result
|
|
185
|
-
result.set(
|
|
301
|
+
const result = new Uint8Array(1 + opts.nonceSize + encryptedContent.byteLength);
|
|
302
|
+
result[0] = version;
|
|
303
|
+
result.set(nonce, 1);
|
|
304
|
+
result.set(new Uint8Array(encryptedContent), 1 + opts.nonceSize);
|
|
186
305
|
return SecurityUtils.toBase64(result);
|
|
187
306
|
}
|
|
188
307
|
/**
|
|
189
308
|
* Decrypts a Base64-encoded ciphertext back to the original object.
|
|
190
309
|
* @param encryptedBase64 - The encrypted data.
|
|
191
310
|
* @param key - AES-256 key (32 bytes).
|
|
192
|
-
* @param options - AES-GCM configuration; uses default if omitted.
|
|
193
311
|
* @returns Deserialized object, or null if input is empty.
|
|
194
312
|
* @throws If authentication tag mismatch or corrupted data.
|
|
195
313
|
*/
|
|
196
|
-
async decryptData(encryptedBase64, key,
|
|
314
|
+
async decryptData(encryptedBase64, key, isBytes = false) {
|
|
197
315
|
if (!encryptedBase64)
|
|
198
316
|
return null;
|
|
199
|
-
const opts = options ?? AesGcmOptions.default;
|
|
200
|
-
opts.validate();
|
|
201
317
|
const encryptedBytes = SecurityUtils.fromBase64(encryptedBase64);
|
|
202
|
-
|
|
318
|
+
const version = encryptedBytes[0];
|
|
319
|
+
const payload = encryptedBytes.slice(1);
|
|
320
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
321
|
+
const opts = profile.aesGcmOptions;
|
|
322
|
+
opts.validate();
|
|
323
|
+
if (payload.length < opts.nonceSize + opts.tagSize)
|
|
203
324
|
throw new Error(`Invalid format: minimum expected ${opts.nonceSize + opts.tagSize} byte.`);
|
|
204
|
-
const nonce =
|
|
205
|
-
const ciphertextWithTag =
|
|
325
|
+
const nonce = payload.slice(0, opts.nonceSize);
|
|
326
|
+
const ciphertextWithTag = payload.slice(opts.nonceSize);
|
|
206
327
|
const cryptoKey = await crypto.subtle.importKey(
|
|
207
328
|
"raw",
|
|
208
329
|
key,
|
|
@@ -212,9 +333,8 @@ var CryptoService = class {
|
|
|
212
333
|
);
|
|
213
334
|
try {
|
|
214
335
|
let associatedData = new Uint8Array(0);
|
|
215
|
-
if (opts.associatedData != null)
|
|
336
|
+
if (opts.associatedData != null)
|
|
216
337
|
associatedData = opts.associatedData;
|
|
217
|
-
}
|
|
218
338
|
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
219
339
|
{
|
|
220
340
|
name: "AES-GCM",
|
|
@@ -231,7 +351,7 @@ var CryptoService = class {
|
|
|
231
351
|
if (isBytes && typeof parsed === "string") {
|
|
232
352
|
return SecurityUtils.fromBase64(parsed);
|
|
233
353
|
}
|
|
234
|
-
return
|
|
354
|
+
return parsed;
|
|
235
355
|
} catch (e) {
|
|
236
356
|
console.error("Decryption error:", e);
|
|
237
357
|
throw new Error("Decryption failed: authentication tag mismatch or corrupted data.");
|
|
@@ -239,88 +359,39 @@ var CryptoService = class {
|
|
|
239
359
|
}
|
|
240
360
|
};
|
|
241
361
|
|
|
242
|
-
// src/crypto/kdf-options.ts
|
|
243
|
-
var KdfOptions = class _KdfOptions {
|
|
244
|
-
constructor() {
|
|
245
|
-
this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
|
|
246
|
-
this._hashAlgorithm = "SHA-256";
|
|
247
|
-
}
|
|
248
|
-
/** PBKDF2 iteration count (minimum 100_000). */
|
|
249
|
-
get pbkdf2Iterations() {
|
|
250
|
-
return this._pbkdf2Iterations;
|
|
251
|
-
}
|
|
252
|
-
set pbkdf2Iterations(v) {
|
|
253
|
-
if (v < SecurityConstants.Pbkdf2IterationsMinimum)
|
|
254
|
-
throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
|
|
255
|
-
this._pbkdf2Iterations = v;
|
|
256
|
-
}
|
|
257
|
-
/** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
|
|
258
|
-
get hashAlgorithm() {
|
|
259
|
-
return this._hashAlgorithm;
|
|
260
|
-
}
|
|
261
|
-
set hashAlgorithm(v) {
|
|
262
|
-
if (!SupportedHashAlgorithms.includes(v))
|
|
263
|
-
throw new RangeError(`Unsupported hash: ${v}`);
|
|
264
|
-
this._hashAlgorithm = v;
|
|
265
|
-
}
|
|
266
|
-
/** Validates iterations and hash algorithm. */
|
|
267
|
-
validate() {
|
|
268
|
-
if (this.pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum)
|
|
269
|
-
throw new Error(`Pbkdf2Iterations (${this.pbkdf2Iterations}) below minimum`);
|
|
270
|
-
if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
|
|
271
|
-
throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
|
|
272
|
-
}
|
|
273
|
-
/** Default preset: SHA-256, 600_000 iterations. */
|
|
274
|
-
static get default() {
|
|
275
|
-
return new _KdfOptions();
|
|
276
|
-
}
|
|
277
|
-
/** Fluent setter for {@link pbkdf2Iterations}. */
|
|
278
|
-
withPbkdf2Iterations(i) {
|
|
279
|
-
this.pbkdf2Iterations = i;
|
|
280
|
-
return this;
|
|
281
|
-
}
|
|
282
|
-
/** Fluent setter for {@link hashAlgorithm}. */
|
|
283
|
-
withHashAlgorithm(h) {
|
|
284
|
-
this.hashAlgorithm = h;
|
|
285
|
-
return this;
|
|
286
|
-
}
|
|
287
|
-
/** Validates and returns this instance. */
|
|
288
|
-
build() {
|
|
289
|
-
this.validate();
|
|
290
|
-
return this;
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
|
|
294
362
|
// src/crypto/key-derivation.service.ts
|
|
295
363
|
var KeyDerivationService = class {
|
|
296
364
|
/**
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
365
|
+
* Derives KEK and Base64 AuthHash. Identity is used as-is in the
|
|
366
|
+
* combined salt string — caller must normalize (trim, lowercase, etc.) before calling.
|
|
367
|
+
* @param identity - User identity (email, username). Must be pre-normalized by caller.
|
|
368
|
+
* @param password - User password.
|
|
369
|
+
* @param salt - Random salt (minimum 16 bytes).
|
|
370
|
+
* @param version - Crypto version for profile selection.
|
|
371
|
+
* @returns Object with `kek` (Uint8Array) and `authHash` (Base64 string).
|
|
372
|
+
*/
|
|
373
|
+
async deriveKeysFromPassword(identity, password, salt, version) {
|
|
374
|
+
if (!identity || identity.length === 0)
|
|
306
375
|
throw new Error("Identity cannot be null or empty.");
|
|
307
376
|
if (!password || password.trim().length === 0)
|
|
308
377
|
throw new Error("Password cannot be null or empty.");
|
|
309
378
|
if (!salt || salt.length < 16)
|
|
310
379
|
throw new Error("Salt must be at least 16 bytes.");
|
|
311
|
-
const
|
|
380
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
381
|
+
const opts = profile.kdfOptions;
|
|
312
382
|
opts.validate();
|
|
313
383
|
const safeSalt = new Uint8Array(salt);
|
|
314
384
|
const encoder = new TextEncoder();
|
|
315
385
|
const combinedPassword = `${identity}:${password}`;
|
|
316
386
|
const passwordBytes = encoder.encode(combinedPassword);
|
|
317
387
|
const baseKey = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits", "deriveKey"]);
|
|
388
|
+
const hashSize = HashSizes[opts.hashAlgorithm] ?? 32;
|
|
318
389
|
const masterKeyBits = await crypto.subtle.deriveBits({
|
|
319
390
|
name: "PBKDF2",
|
|
320
391
|
salt: safeSalt,
|
|
321
392
|
iterations: opts.pbkdf2Iterations,
|
|
322
393
|
hash: opts.hashAlgorithm
|
|
323
|
-
}, baseKey,
|
|
394
|
+
}, baseKey, hashSize * 8);
|
|
324
395
|
const masterKey = await crypto.subtle.importKey("raw", masterKeyBits, "HKDF", false, ["deriveBits"]);
|
|
325
396
|
const kek = await crypto.subtle.deriveBits(
|
|
326
397
|
{ name: "HKDF", hash: opts.hashAlgorithm, salt: new Uint8Array(0), info: encoder.encode("AES-GCM-KEK-v1") },
|
|
@@ -337,99 +408,6 @@ var KeyDerivationService = class {
|
|
|
337
408
|
authHash: SecurityUtils.toBase64(new Uint8Array(authBytes))
|
|
338
409
|
};
|
|
339
410
|
}
|
|
340
|
-
/**
|
|
341
|
-
* Derives an SRP-compatible authentication hash (output size = hash output length).
|
|
342
|
-
* @param identity - User identity.
|
|
343
|
-
* @param password - User password.
|
|
344
|
-
* @param salt - Random salt.
|
|
345
|
-
* @param srpHashAlgorithm - SRP hash algorithm (SHA-256/384/512).
|
|
346
|
-
* @param options - KDF configuration; uses default if omitted.
|
|
347
|
-
* @returns Raw hash bytes for use as SRP verifier input (x).
|
|
348
|
-
*/
|
|
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.");
|
|
356
|
-
const opts = options ?? KdfOptions.default;
|
|
357
|
-
opts.validate();
|
|
358
|
-
const srpHashSize = HashSizes[srpHashAlgorithm];
|
|
359
|
-
const combinedPassword = `${identity}:${password}`;
|
|
360
|
-
const passwordBytes = new TextEncoder().encode(combinedPassword);
|
|
361
|
-
const baseKey = await crypto.subtle.importKey(
|
|
362
|
-
"raw",
|
|
363
|
-
passwordBytes,
|
|
364
|
-
"PBKDF2",
|
|
365
|
-
false,
|
|
366
|
-
["deriveBits"]
|
|
367
|
-
);
|
|
368
|
-
const masterKeyBits = await crypto.subtle.deriveBits({
|
|
369
|
-
name: "PBKDF2",
|
|
370
|
-
salt,
|
|
371
|
-
iterations: opts.pbkdf2Iterations,
|
|
372
|
-
hash: srpHashAlgorithm
|
|
373
|
-
}, baseKey, SecurityConstants.KeySizeBytes * 8);
|
|
374
|
-
const masterKey = await crypto.subtle.importKey(
|
|
375
|
-
"raw",
|
|
376
|
-
masterKeyBits,
|
|
377
|
-
"HKDF",
|
|
378
|
-
false,
|
|
379
|
-
["deriveBits"]
|
|
380
|
-
);
|
|
381
|
-
const info = new TextEncoder().encode("SRP-AUTH-HASH-v1");
|
|
382
|
-
const authBytes = await crypto.subtle.deriveBits(
|
|
383
|
-
{
|
|
384
|
-
name: "HKDF",
|
|
385
|
-
hash: srpHashAlgorithm,
|
|
386
|
-
salt: new Uint8Array(0),
|
|
387
|
-
info
|
|
388
|
-
},
|
|
389
|
-
masterKey,
|
|
390
|
-
srpHashSize * 8
|
|
391
|
-
);
|
|
392
|
-
return new Uint8Array(authBytes);
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
// src/crypto/crypto-version.ts
|
|
397
|
-
var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
|
|
398
|
-
CryptoVersion2[CryptoVersion2["V1"] = 1] = "V1";
|
|
399
|
-
return CryptoVersion2;
|
|
400
|
-
})(CryptoVersion || {});
|
|
401
|
-
|
|
402
|
-
// src/crypto/crypto-profile.ts
|
|
403
|
-
var CryptoProfile = class {
|
|
404
|
-
/**
|
|
405
|
-
* Creates a new CryptoProfile.
|
|
406
|
-
* @param params - Object containing version, kdfOptions, aesGcmOptions.
|
|
407
|
-
*/
|
|
408
|
-
constructor(params) {
|
|
409
|
-
this.version = params.version;
|
|
410
|
-
this.kdfOptions = params.kdfOptions;
|
|
411
|
-
this.aesGcmOptions = params.aesGcmOptions;
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
// src/crypto/crypto-profile-registry.ts
|
|
416
|
-
var CryptoProfileRegistry = class {
|
|
417
|
-
static getProfile(version) {
|
|
418
|
-
switch (version) {
|
|
419
|
-
case 1 /* V1 */:
|
|
420
|
-
return new CryptoProfile({
|
|
421
|
-
version: 1 /* V1 */,
|
|
422
|
-
kdfOptions: KdfOptions.default,
|
|
423
|
-
aesGcmOptions: AesGcmOptions.default
|
|
424
|
-
});
|
|
425
|
-
default:
|
|
426
|
-
throw new Error(`Unsupported crypto version: ${version}`);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
/** Latest supported profile (currently V1). */
|
|
430
|
-
static get latest() {
|
|
431
|
-
return this.getProfile(1 /* V1 */);
|
|
432
|
-
}
|
|
433
411
|
};
|
|
434
412
|
|
|
435
413
|
// src/utils/srp-encoding.ts
|
|
@@ -453,14 +431,14 @@ var SrpEncoding = class {
|
|
|
453
431
|
* Follows RFC 5054 / SRP-6a:
|
|
454
432
|
* - H(N) and H(g) are hashed as modulus-sized values.
|
|
455
433
|
* - Identity (I) is hashed as raw UTF-8 bytes.
|
|
456
|
-
* - A and B are padded to the modulus size before hashing.
|
|
434
|
+
* - A and B are zero-padded to the modulus size before hashing.
|
|
457
435
|
* - K is the session key (H(S) without padding).
|
|
458
436
|
*
|
|
459
437
|
* @param ctx - SRP context containing N, g, hash algorithm, and modulus size.
|
|
460
438
|
* @param A - Client ephemeral public key.
|
|
461
439
|
* @param B - Server ephemeral public key.
|
|
462
440
|
* @param sessionKeyK - Session key K as raw bytes.
|
|
463
|
-
* @param identity - User identity (login).
|
|
441
|
+
* @param identity - User identity (login). Must be pre-normalized by the caller.
|
|
464
442
|
* @param salt - User-specific salt bytes.
|
|
465
443
|
* @returns The M1 proof as raw hash bytes.
|
|
466
444
|
*/
|
|
@@ -484,7 +462,10 @@ var SrpEncoding = class {
|
|
|
484
462
|
sessionKeyK
|
|
485
463
|
);
|
|
486
464
|
}
|
|
487
|
-
/**
|
|
465
|
+
/**
|
|
466
|
+
* Computes M2 = H( PAD(A) || M1 || sessionKeyK ).
|
|
467
|
+
* A is zero-padded to the modulus size before hashing.
|
|
468
|
+
*/
|
|
488
469
|
static async computeM2(ctx, A, m1Bytes, sessionKeyK) {
|
|
489
470
|
return this.computeHash(
|
|
490
471
|
ctx.hashAlgorithmName,
|
|
@@ -495,14 +476,15 @@ var SrpEncoding = class {
|
|
|
495
476
|
}
|
|
496
477
|
/** Computes session key K = H(S). */
|
|
497
478
|
static async computeSessionKey(ctx, S) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
479
|
+
const sBytes = SecurityUtils.bigIntToRawBytes(S);
|
|
480
|
+
const safeBuffer = sBytes.buffer.slice(
|
|
481
|
+
sBytes.byteOffset,
|
|
482
|
+
sBytes.byteOffset + sBytes.byteLength
|
|
483
|
+
);
|
|
484
|
+
const hashBuffer = await crypto.subtle.digest(ctx.hashAlgorithmName, safeBuffer);
|
|
503
485
|
return new Uint8Array(hashBuffer);
|
|
504
486
|
}
|
|
505
|
-
/** Hashes
|
|
487
|
+
/** Hashes concatenated byte arrays and returns raw Uint8Array. */
|
|
506
488
|
static async computeHash(algo, ...buffers) {
|
|
507
489
|
const totalLen = buffers.reduce((sum, b) => sum + b.length, 0);
|
|
508
490
|
const combined = new Uint8Array(totalLen);
|
|
@@ -528,143 +510,6 @@ var SrpEncoding = class {
|
|
|
528
510
|
}
|
|
529
511
|
};
|
|
530
512
|
|
|
531
|
-
// src/srp/srp-client-service.ts
|
|
532
|
-
var SrpClientService = class {
|
|
533
|
-
constructor() {
|
|
534
|
-
this.keyDerivation = new KeyDerivationService();
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Computes SRP verifier v = g^x mod N from the authentication hash.
|
|
538
|
-
* @param authHash - Auth hash (Base64).
|
|
539
|
-
* @param ctx - SRP context (N, g, hash algorithm, etc.).
|
|
540
|
-
* @returns Verifier as Base64 string.
|
|
541
|
-
*/
|
|
542
|
-
async generateSrpVerifier(authHash, ctx) {
|
|
543
|
-
const x = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(authHash));
|
|
544
|
-
const v = SecurityUtils.expMod(ctx.g, x, ctx.N);
|
|
545
|
-
return SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, v));
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* Generates client proof (A, M1, session key S) from server challenge.
|
|
549
|
-
* @param login - User login.
|
|
550
|
-
* @param password - Plaintext password.
|
|
551
|
-
* @param saltBase64 - Server salt (URL-safe Base64).
|
|
552
|
-
* @param B_base64 - Server public ephemeral B (URL-safe Base64).
|
|
553
|
-
* @param ctx - SRP context.
|
|
554
|
-
* @returns Object with A, M1, S as Base64 strings.
|
|
555
|
-
*/
|
|
556
|
-
async generateSrpProof(login, password, saltBase64, B_base64, ctx) {
|
|
557
|
-
const salt = SecurityUtils.fromBase64(saltBase64);
|
|
558
|
-
const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName);
|
|
559
|
-
const x = SecurityUtils.bytesToBigInt(authHash);
|
|
560
|
-
const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
|
|
561
|
-
const aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
|
|
562
|
-
const a = SecurityUtils.bytesToBigInt(aBytes);
|
|
563
|
-
const A = SecurityUtils.expMod(ctx.g, a, ctx.N);
|
|
564
|
-
if (A % ctx.N === 0n)
|
|
565
|
-
throw new Error("Critical error: A % N === 0");
|
|
566
|
-
const B = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(B_base64));
|
|
567
|
-
if (B % ctx.N === 0n)
|
|
568
|
-
throw new Error("Critical error: B % N === 0");
|
|
569
|
-
const u = await SrpEncoding.hashModuli(ctx, A, B);
|
|
570
|
-
if (u === 0n)
|
|
571
|
-
throw new Error("\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 u");
|
|
572
|
-
const gX = SecurityUtils.expMod(ctx.g, x, ctx.N);
|
|
573
|
-
const term = ctx.k * gX % ctx.N;
|
|
574
|
-
const base = (B - term + ctx.N) % ctx.N;
|
|
575
|
-
const exponent = a + u * x;
|
|
576
|
-
const S = SecurityUtils.expMod(base, exponent, ctx.N);
|
|
577
|
-
if (S === 0n)
|
|
578
|
-
throw new Error("Critical error: S === 0");
|
|
579
|
-
const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
|
|
580
|
-
const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, login, salt);
|
|
581
|
-
return {
|
|
582
|
-
A: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, A)),
|
|
583
|
-
M1: SecurityUtils.toBase64(M1),
|
|
584
|
-
SessionKeyK: sessionKeyK
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Validates the server proof M2 to authenticate the server.
|
|
589
|
-
* @param A_b64 - Client public A (Base64).
|
|
590
|
-
* @param M1_b64 - Client proof M1 (Base64).
|
|
591
|
-
* @param S_b64 - Session key S (Base64).
|
|
592
|
-
* @param serverM2_b64 - Server proof M2 (Base64).
|
|
593
|
-
* @param ctx - SRP context.
|
|
594
|
-
* @returns True if the server proof is valid.
|
|
595
|
-
*/
|
|
596
|
-
async verifyServerM2(A_b64, M1_b64, sessionKeyK, serverM2_b64, ctx) {
|
|
597
|
-
const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(A_b64));
|
|
598
|
-
const M1 = SecurityUtils.fromBase64(M1_b64);
|
|
599
|
-
const computedM2 = await SrpEncoding.computeM2(ctx, A, M1, sessionKeyK);
|
|
600
|
-
const serverM2Bytes = SecurityUtils.fromBase64(serverM2_b64);
|
|
601
|
-
return SecurityUtils.fixedTimeEquals(computedM2, serverM2Bytes);
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
// src/srp/srp-server-service.ts
|
|
606
|
-
var SrpServerService = class {
|
|
607
|
-
/**
|
|
608
|
-
* Generates server challenge B and session state from verifier.
|
|
609
|
-
* @param login - User login.
|
|
610
|
-
* @param verifierBytes - Stored verifier v as byte array.
|
|
611
|
-
* @param ctx - SRP context (hash, N, g, etc.).
|
|
612
|
-
* @returns Session state with private b, verifier, and public B.
|
|
613
|
-
*/
|
|
614
|
-
async getSrpChallenge(login, verifierBytes, salt, ctx) {
|
|
615
|
-
const v = SecurityUtils.bytesToBigInt(verifierBytes);
|
|
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);
|
|
625
|
-
return {
|
|
626
|
-
login,
|
|
627
|
-
privateKeyB: bBytes,
|
|
628
|
-
verifier: verifierBytes,
|
|
629
|
-
publicKeyB: SrpEncoding.toModulusBytes(ctx, B),
|
|
630
|
-
salt
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Verifies client M1 proof and returns server M2 proof.
|
|
635
|
-
* @param sessionState - Server session state.
|
|
636
|
-
* @param a - Client public A (Base64).
|
|
637
|
-
* @param m1 - Client proof M1 (Base64).
|
|
638
|
-
* @param ctx - SRP context.
|
|
639
|
-
* @returns Server proof M2 as Base64 string.
|
|
640
|
-
* @throws If verification fails or input is invalid.
|
|
641
|
-
*/
|
|
642
|
-
async verifySrpProof(sessionState, a, m1, ctx) {
|
|
643
|
-
const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(a));
|
|
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);
|
|
648
|
-
if (v <= 0n)
|
|
649
|
-
throw new Error("The verifier is corrupted");
|
|
650
|
-
if (A % ctx.N === 0n)
|
|
651
|
-
throw new Error("Incorrect value of A");
|
|
652
|
-
if (A <= 0n || A >= ctx.N)
|
|
653
|
-
throw new Error("Invalid A (out of range)");
|
|
654
|
-
const u = await SrpEncoding.hashModuli(ctx, A, B);
|
|
655
|
-
if (u === 0n)
|
|
656
|
-
throw new Error("Error in calculating the parameter u");
|
|
657
|
-
const vU = SecurityUtils.expMod(v, u, ctx.N);
|
|
658
|
-
const S = SecurityUtils.expMod(A * vU % ctx.N, b, ctx.N);
|
|
659
|
-
const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
|
|
660
|
-
const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, sessionState.login, sessionState.salt);
|
|
661
|
-
if (!SecurityUtils.fixedTimeEquals(M1_server, M1_client))
|
|
662
|
-
throw new Error("Invalid password");
|
|
663
|
-
const M2_server = await SrpEncoding.computeM2(ctx, A, M1_client, sessionKeyK);
|
|
664
|
-
return SecurityUtils.toBase64(M2_server);
|
|
665
|
-
}
|
|
666
|
-
};
|
|
667
|
-
|
|
668
513
|
// src/srp/srp-group.ts
|
|
669
514
|
var SrpGroup = /* @__PURE__ */ ((SrpGroup2) => {
|
|
670
515
|
SrpGroup2[SrpGroup2["Rfc5054_1024"] = 1] = "Rfc5054_1024";
|
|
@@ -791,6 +636,225 @@ var SrpContextFactory = class {
|
|
|
791
636
|
}
|
|
792
637
|
};
|
|
793
638
|
|
|
794
|
-
|
|
639
|
+
// src/srp/srp-client-service.ts
|
|
640
|
+
var SrpClientService = class {
|
|
641
|
+
/**
|
|
642
|
+
* Computes SRP verifier v = g^x mod N from the authentication hash.
|
|
643
|
+
* @param authHash - Auth hash (Base64).
|
|
644
|
+
* @param group - SRP group (determines modulus N, generator g, hash).
|
|
645
|
+
* @returns Verifier as Base64 string.
|
|
646
|
+
*/
|
|
647
|
+
async generateSrpVerifier(authHash, group) {
|
|
648
|
+
const ctx = await SrpContextFactory.create(group);
|
|
649
|
+
const x = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(authHash));
|
|
650
|
+
const v = await SecurityUtils.expModAsync(ctx.g, x, ctx.N);
|
|
651
|
+
return SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, v));
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Generates client proof (A, M1, session key S) from server challenge.
|
|
655
|
+
* @param login - User login.
|
|
656
|
+
* @param authHashBytes - SRP private exponent x as raw bytes (derived from auth hash).
|
|
657
|
+
* @param saltBase64 - Server salt (standard Base64).
|
|
658
|
+
* @param B_base64 - Server public ephemeral B (standard Base64).
|
|
659
|
+
* @param group - SRP group (determines modulus N, generator g, hash).
|
|
660
|
+
* @returns Object with A and M1 as standard Base64; SessionKeyK as raw bytes.
|
|
661
|
+
*/
|
|
662
|
+
async generateSrpProof(login, authHashBytes, saltBase64, B_base64, group) {
|
|
663
|
+
const ctx = await SrpContextFactory.create(group);
|
|
664
|
+
const salt = SecurityUtils.fromBase64(saltBase64);
|
|
665
|
+
const x = SecurityUtils.bytesToBigInt(authHashBytes);
|
|
666
|
+
const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
|
|
667
|
+
let aBytes;
|
|
668
|
+
let a;
|
|
669
|
+
do {
|
|
670
|
+
aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
|
|
671
|
+
a = SecurityUtils.bytesToBigInt(aBytes);
|
|
672
|
+
} while (a === 0n);
|
|
673
|
+
const A = await SecurityUtils.expModAsync(ctx.g, a, ctx.N);
|
|
674
|
+
if (A <= 0n || A >= ctx.N)
|
|
675
|
+
throw new Error("Invalid client public key A.");
|
|
676
|
+
const B = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(B_base64));
|
|
677
|
+
if (B <= 0n || B >= ctx.N || B % ctx.N === 0n)
|
|
678
|
+
throw new Error("Critical error: B % N === 0");
|
|
679
|
+
const u = await SrpEncoding.hashModuli(ctx, A, B);
|
|
680
|
+
if (u === 0n)
|
|
681
|
+
throw new Error("\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 u");
|
|
682
|
+
const gX = await SecurityUtils.expModAsync(ctx.g, x, ctx.N);
|
|
683
|
+
const term = ctx.k * gX % ctx.N;
|
|
684
|
+
const base = (B - term + ctx.N) % ctx.N;
|
|
685
|
+
const exponent = a + u * x;
|
|
686
|
+
const S = await SecurityUtils.expModAsync(base, exponent, ctx.N);
|
|
687
|
+
if (S === 0n)
|
|
688
|
+
throw new Error("Critical error: S === 0");
|
|
689
|
+
const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
|
|
690
|
+
const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, login, salt);
|
|
691
|
+
return {
|
|
692
|
+
A: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, A)),
|
|
693
|
+
M1: SecurityUtils.toBase64(M1),
|
|
694
|
+
SessionKeyK: sessionKeyK
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Validates the server proof M2 to authenticate the server.
|
|
699
|
+
* @param A_b64 - Client public A (Base64).
|
|
700
|
+
* @param M1_b64 - Client proof M1 (Base64).
|
|
701
|
+
* @param sessionKeyK - Session key K as raw bytes.
|
|
702
|
+
* @param serverM2_b64 - Server proof M2 (Base64).
|
|
703
|
+
* @param group - SRP group (determines modulus N, generator g, hash).
|
|
704
|
+
* @returns True if the server proof is valid.
|
|
705
|
+
*/
|
|
706
|
+
async verifyServerM2(A_b64, M1_b64, sessionKeyK, serverM2_b64, group) {
|
|
707
|
+
const ctx = await SrpContextFactory.create(group);
|
|
708
|
+
const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(A_b64));
|
|
709
|
+
const M1 = SecurityUtils.fromBase64(M1_b64);
|
|
710
|
+
const computedM2 = await SrpEncoding.computeM2(ctx, A, M1, sessionKeyK);
|
|
711
|
+
const serverM2Bytes = SecurityUtils.fromBase64(serverM2_b64);
|
|
712
|
+
return SecurityUtils.fixedTimeEquals(computedM2, serverM2Bytes);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/srp/srp-server-service.ts
|
|
717
|
+
var SrpServerService = class {
|
|
718
|
+
/**
|
|
719
|
+
* Generates server challenge B and session state from verifier.
|
|
720
|
+
* @param login - User login.
|
|
721
|
+
* @param verifierBytes - Stored verifier v as byte array.
|
|
722
|
+
* @param salt - User salt (raw bytes).
|
|
723
|
+
* @param group - SRP group (determines modulus N, generator g, hash).
|
|
724
|
+
* @returns Session state with private b, verifier, public B, and salt.
|
|
725
|
+
*/
|
|
726
|
+
async getSrpChallenge(login, verifierBytes, salt, group) {
|
|
727
|
+
if (!login || login.trim().length === 0)
|
|
728
|
+
throw new Error("Login cannot be null or empty.");
|
|
729
|
+
if (!verifierBytes || verifierBytes.length === 0)
|
|
730
|
+
throw new Error("Verifier cannot be null or empty.");
|
|
731
|
+
const ctx = await SrpContextFactory.create(group);
|
|
732
|
+
const v = SecurityUtils.bytesToBigInt(verifierBytes);
|
|
733
|
+
if (v <= 0n || v >= ctx.N)
|
|
734
|
+
throw new Error("The verifier is corrupted");
|
|
735
|
+
const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
|
|
736
|
+
while (true) {
|
|
737
|
+
const bBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
|
|
738
|
+
const b = SecurityUtils.bytesToBigInt(bBytes);
|
|
739
|
+
if (b === 0n) {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const gB = await SecurityUtils.expModAsync(ctx.g, b, ctx.N);
|
|
743
|
+
const B = (ctx.k * v + gB) % ctx.N;
|
|
744
|
+
if (B !== 0n) {
|
|
745
|
+
return {
|
|
746
|
+
login,
|
|
747
|
+
privateKeyB: bBytes,
|
|
748
|
+
verifier: verifierBytes,
|
|
749
|
+
publicKeyB: SrpEncoding.toModulusBytes(ctx, B),
|
|
750
|
+
salt
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Verifies client M1 proof and returns server M2 proof.
|
|
757
|
+
* @param sessionState - Server session state.
|
|
758
|
+
* @param a - Client public A (Base64).
|
|
759
|
+
* @param m1 - Client proof M1 (Base64).
|
|
760
|
+
* @param group - SRP group (determines modulus N, generator g, hash).
|
|
761
|
+
* @returns Server proof M2 as Base64 string.
|
|
762
|
+
* @throws If verification fails or input is invalid.
|
|
763
|
+
*/
|
|
764
|
+
async verifySrpProof(sessionState, a, m1, group) {
|
|
765
|
+
const ctx = await SrpContextFactory.create(group);
|
|
766
|
+
const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(a));
|
|
767
|
+
const M1_client = SecurityUtils.fromBase64(m1);
|
|
768
|
+
const b = SecurityUtils.bytesToBigInt(sessionState.privateKeyB);
|
|
769
|
+
const v = SecurityUtils.bytesToBigInt(sessionState.verifier);
|
|
770
|
+
const B = SecurityUtils.bytesToBigInt(sessionState.publicKeyB);
|
|
771
|
+
if (v <= 0n || v >= ctx.N)
|
|
772
|
+
throw new Error("The verifier is corrupted");
|
|
773
|
+
if (A % ctx.N === 0n)
|
|
774
|
+
throw new Error("Incorrect value of A");
|
|
775
|
+
if (A <= 0n || A >= ctx.N)
|
|
776
|
+
throw new Error("Invalid A (out of range)");
|
|
777
|
+
if (B <= 0n || B >= ctx.N)
|
|
778
|
+
throw new Error("Invalid server public key B.");
|
|
779
|
+
const u = await SrpEncoding.hashModuli(ctx, A, B);
|
|
780
|
+
if (u === 0n)
|
|
781
|
+
throw new Error("Error in calculating the parameter u");
|
|
782
|
+
const vU = await SecurityUtils.expModAsync(v, u, ctx.N);
|
|
783
|
+
const S = await SecurityUtils.expModAsync(A * vU % ctx.N, b, ctx.N);
|
|
784
|
+
if (S === 0n)
|
|
785
|
+
throw new Error("Critical error: shared secret S is zero (possible malicious A).");
|
|
786
|
+
const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
|
|
787
|
+
const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, sessionState.login, sessionState.salt);
|
|
788
|
+
if (!SecurityUtils.fixedTimeEquals(M1_server, M1_client))
|
|
789
|
+
throw new Error("Invalid password");
|
|
790
|
+
const M2_server = await SrpEncoding.computeM2(ctx, A, M1_client, sessionKeyK);
|
|
791
|
+
return SecurityUtils.toBase64(M2_server);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
// src/srp/srp-key-derivation.service.ts
|
|
796
|
+
var SrpKeyDerivationService = class {
|
|
797
|
+
/**
|
|
798
|
+
* Derives an SRP-compatible authentication hash (output size = hash output length).
|
|
799
|
+
* Identity is used as-is in the combined string — caller must normalize
|
|
800
|
+
* (trim, lowercase, etc.) before calling.
|
|
801
|
+
* @param identity - User identity (email, username). Must be pre-normalized by caller.
|
|
802
|
+
* @param password - User password.
|
|
803
|
+
* @param salt - Random salt (minimum 16 bytes).
|
|
804
|
+
* @param srpGroup - SRP group (determines hash algorithm and modulus).
|
|
805
|
+
* @param version - Crypto version for KDF profile selection.
|
|
806
|
+
* @returns Raw hash bytes for use as SRP verifier input (x).
|
|
807
|
+
*/
|
|
808
|
+
async deriveAuthHashForSrp(identity, password, salt, srpGroup, version) {
|
|
809
|
+
if (!identity || identity.trim().length === 0)
|
|
810
|
+
throw new Error("Identity cannot be null or empty.");
|
|
811
|
+
if (!password || password.trim().length === 0)
|
|
812
|
+
throw new Error("Password cannot be null or empty.");
|
|
813
|
+
if (!salt || salt.length < 16)
|
|
814
|
+
throw new Error("Salt must be at least 16 bytes.");
|
|
815
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
816
|
+
const opts = profile.kdfOptions;
|
|
817
|
+
opts.validate();
|
|
818
|
+
const ctx = await SrpContextFactory.create(srpGroup);
|
|
819
|
+
const srpHashAlgorithm = ctx.hashAlgorithmName;
|
|
820
|
+
const srpHashSize = HashSizes[srpHashAlgorithm];
|
|
821
|
+
const combinedPassword = `${identity}:${password}`;
|
|
822
|
+
const passwordBytes = new TextEncoder().encode(combinedPassword);
|
|
823
|
+
const baseKey = await crypto.subtle.importKey(
|
|
824
|
+
"raw",
|
|
825
|
+
passwordBytes,
|
|
826
|
+
"PBKDF2",
|
|
827
|
+
false,
|
|
828
|
+
["deriveBits"]
|
|
829
|
+
);
|
|
830
|
+
const masterKeyBits = await crypto.subtle.deriveBits({
|
|
831
|
+
name: "PBKDF2",
|
|
832
|
+
salt,
|
|
833
|
+
iterations: opts.pbkdf2Iterations,
|
|
834
|
+
hash: srpHashAlgorithm
|
|
835
|
+
}, baseKey, srpHashSize * 8);
|
|
836
|
+
const masterKey = await crypto.subtle.importKey(
|
|
837
|
+
"raw",
|
|
838
|
+
masterKeyBits,
|
|
839
|
+
"HKDF",
|
|
840
|
+
false,
|
|
841
|
+
["deriveBits"]
|
|
842
|
+
);
|
|
843
|
+
const info = new TextEncoder().encode("SRP-AUTH-HASH-v1");
|
|
844
|
+
const authBytes = await crypto.subtle.deriveBits(
|
|
845
|
+
{
|
|
846
|
+
name: "HKDF",
|
|
847
|
+
hash: srpHashAlgorithm,
|
|
848
|
+
salt: new Uint8Array(0),
|
|
849
|
+
info
|
|
850
|
+
},
|
|
851
|
+
masterKey,
|
|
852
|
+
srpHashSize * 8
|
|
853
|
+
);
|
|
854
|
+
return new Uint8Array(authBytes);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
export { AesGcmOptions, CryptoProfile, CryptoProfileRegistry, CryptoService, CryptoVersion, KdfOptions, KeyDerivationService, SecurityConstants, SecurityUtils, SrpClientService, SrpContextFactory, SrpEncoding, SrpGroup, SrpGroupParams, SrpKeyDerivationService, SrpOptions, SrpServerService, SupportedHashAlgorithms };
|
|
795
859
|
//# sourceMappingURL=index.js.map
|
|
796
860
|
//# sourceMappingURL=index.js.map
|