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