@crossdyne/security 0.3.1-beta.1 → 0.5.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 +286 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -78
- package/dist/index.d.ts +103 -78
- package/dist/index.js +286 -178
- 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"];
|
|
@@ -43,16 +48,22 @@ var SecurityUtils = class {
|
|
|
43
48
|
return 0n;
|
|
44
49
|
return BigInt("0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
|
45
50
|
}
|
|
46
|
-
/** Converts bigint to fixed-length big-endian bytes (pads
|
|
51
|
+
/** Converts bigint to fixed-length big-endian bytes (pads only; never truncates). */
|
|
47
52
|
static bigIntToFixedBytes(bn, length) {
|
|
53
|
+
if (length <= 0)
|
|
54
|
+
throw new Error("Length must be positive.");
|
|
48
55
|
let hex = bn.toString(16);
|
|
49
56
|
if (hex.length % 2 !== 0)
|
|
50
57
|
hex = "0" + hex;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
56
67
|
}
|
|
57
68
|
/** Constant-time comparison of two Uint8Arrays. */
|
|
58
69
|
static fixedTimeEquals(a, b) {
|
|
@@ -65,16 +76,26 @@ var SecurityUtils = class {
|
|
|
65
76
|
}
|
|
66
77
|
/** Modular exponentiation (base^exp mod mod) using binary exponentiation. */
|
|
67
78
|
static expMod(base, exp, mod) {
|
|
68
|
-
|
|
79
|
+
if (mod === 1n) return 0n;
|
|
80
|
+
let res = 1n;
|
|
69
81
|
base = base % mod;
|
|
70
82
|
while (exp > 0n) {
|
|
71
|
-
if (exp
|
|
83
|
+
if ((exp & 1n) === 1n)
|
|
72
84
|
res = res * base % mod;
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
exp >>= 1n;
|
|
86
|
+
if (exp > 0n)
|
|
87
|
+
base = base * base % mod;
|
|
75
88
|
}
|
|
76
89
|
return res;
|
|
77
90
|
}
|
|
91
|
+
static bigIntToRawBytes(bn) {
|
|
92
|
+
if (bn === 0n)
|
|
93
|
+
return new Uint8Array([0]);
|
|
94
|
+
let hex = bn.toString(16);
|
|
95
|
+
if (hex.length % 2 !== 0)
|
|
96
|
+
hex = "0" + hex;
|
|
97
|
+
return new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
98
|
+
}
|
|
78
99
|
};
|
|
79
100
|
|
|
80
101
|
// src/crypto/aes-gcm-options.ts
|
|
@@ -127,6 +148,97 @@ var AesGcmOptions = class _AesGcmOptions {
|
|
|
127
148
|
}
|
|
128
149
|
};
|
|
129
150
|
|
|
151
|
+
// src/crypto/crypto-profile.ts
|
|
152
|
+
var CryptoProfile = class {
|
|
153
|
+
/**
|
|
154
|
+
* Creates a new CryptoProfile.
|
|
155
|
+
* @param params - Object containing version, kdfOptions, aesGcmOptions.
|
|
156
|
+
*/
|
|
157
|
+
constructor(params) {
|
|
158
|
+
this.version = params.version;
|
|
159
|
+
this.kdfOptions = params.kdfOptions;
|
|
160
|
+
this.aesGcmOptions = params.aesGcmOptions;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/crypto/crypto-version.ts
|
|
165
|
+
var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
|
|
166
|
+
CryptoVersion2[CryptoVersion2["V1"] = 1] = "V1";
|
|
167
|
+
return CryptoVersion2;
|
|
168
|
+
})(CryptoVersion || {});
|
|
169
|
+
|
|
170
|
+
// src/crypto/kdf-options.ts
|
|
171
|
+
var KdfOptions = class _KdfOptions {
|
|
172
|
+
constructor() {
|
|
173
|
+
this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
|
|
174
|
+
this._hashAlgorithm = "SHA-256";
|
|
175
|
+
}
|
|
176
|
+
/** PBKDF2 iteration count (minimum 100_000). */
|
|
177
|
+
get pbkdf2Iterations() {
|
|
178
|
+
return this._pbkdf2Iterations;
|
|
179
|
+
}
|
|
180
|
+
set pbkdf2Iterations(v) {
|
|
181
|
+
if (v < SecurityConstants.Pbkdf2IterationsMinimum)
|
|
182
|
+
throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
|
|
183
|
+
this._pbkdf2Iterations = v;
|
|
184
|
+
}
|
|
185
|
+
/** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
|
|
186
|
+
get hashAlgorithm() {
|
|
187
|
+
return this._hashAlgorithm;
|
|
188
|
+
}
|
|
189
|
+
set hashAlgorithm(v) {
|
|
190
|
+
if (!SupportedHashAlgorithms.includes(v))
|
|
191
|
+
throw new RangeError(`Unsupported hash: ${v}`);
|
|
192
|
+
this._hashAlgorithm = v;
|
|
193
|
+
}
|
|
194
|
+
/** Validates iterations and hash algorithm. */
|
|
195
|
+
validate() {
|
|
196
|
+
if (this.pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum)
|
|
197
|
+
throw new Error(`Pbkdf2Iterations (${this.pbkdf2Iterations}) below minimum`);
|
|
198
|
+
if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
|
|
199
|
+
throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
|
|
200
|
+
}
|
|
201
|
+
/** Default preset: SHA-256, 600_000 iterations. */
|
|
202
|
+
static get default() {
|
|
203
|
+
return new _KdfOptions();
|
|
204
|
+
}
|
|
205
|
+
/** Fluent setter for {@link pbkdf2Iterations}. */
|
|
206
|
+
withPbkdf2Iterations(i) {
|
|
207
|
+
this.pbkdf2Iterations = i;
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
/** Fluent setter for {@link hashAlgorithm}. */
|
|
211
|
+
withHashAlgorithm(h) {
|
|
212
|
+
this.hashAlgorithm = h;
|
|
213
|
+
return this;
|
|
214
|
+
}
|
|
215
|
+
/** Validates and returns this instance. */
|
|
216
|
+
build() {
|
|
217
|
+
this.validate();
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/crypto/crypto-profile-registry.ts
|
|
223
|
+
var CryptoProfileRegistry = class {
|
|
224
|
+
static getProfile(version) {
|
|
225
|
+
switch (version) {
|
|
226
|
+
case 1 /* V1 */:
|
|
227
|
+
return new CryptoProfile({
|
|
228
|
+
version: 1 /* V1 */,
|
|
229
|
+
kdfOptions: KdfOptions.default,
|
|
230
|
+
aesGcmOptions: AesGcmOptions.default
|
|
231
|
+
});
|
|
232
|
+
default:
|
|
233
|
+
throw new Error(`Unsupported crypto version: ${version}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Latest supported profile (currently V1). */
|
|
237
|
+
static get latest() {
|
|
238
|
+
return this.getProfile(1 /* V1 */);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
130
242
|
// src/crypto/crypto.service.ts
|
|
131
243
|
var CryptoService = class {
|
|
132
244
|
constructor() {
|
|
@@ -144,12 +256,13 @@ var CryptoService = class {
|
|
|
144
256
|
* @param options - AES-GCM configuration; uses default if omitted.
|
|
145
257
|
* @returns Base64-encoded ciphertext with prepended nonce.
|
|
146
258
|
*/
|
|
147
|
-
async encryptData(dataModel, key,
|
|
148
|
-
const
|
|
259
|
+
async encryptData(dataModel, key, version = 1 /* V1 */) {
|
|
260
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
261
|
+
const opts = profile.aesGcmOptions;
|
|
149
262
|
opts.validate();
|
|
150
263
|
const encoder = new TextEncoder();
|
|
151
264
|
let jsonString;
|
|
152
|
-
if (dataModel
|
|
265
|
+
if (ArrayBuffer.isView(dataModel) && dataModel.constructor === Uint8Array) {
|
|
153
266
|
jsonString = `"${SecurityUtils.toBase64(dataModel)}"`;
|
|
154
267
|
} else {
|
|
155
268
|
jsonString = JSON.stringify(dataModel);
|
|
@@ -177,9 +290,10 @@ var CryptoService = class {
|
|
|
177
290
|
cryptoKey,
|
|
178
291
|
plainBytes
|
|
179
292
|
);
|
|
180
|
-
const result = new Uint8Array(opts.nonceSize + encryptedContent.byteLength);
|
|
181
|
-
result
|
|
182
|
-
result.set(
|
|
293
|
+
const result = new Uint8Array(1 + opts.nonceSize + encryptedContent.byteLength);
|
|
294
|
+
result[0] = version;
|
|
295
|
+
result.set(nonce, 1);
|
|
296
|
+
result.set(new Uint8Array(encryptedContent), 1 + opts.nonceSize);
|
|
183
297
|
return SecurityUtils.toBase64(result);
|
|
184
298
|
}
|
|
185
299
|
/**
|
|
@@ -190,16 +304,19 @@ var CryptoService = class {
|
|
|
190
304
|
* @returns Deserialized object, or null if input is empty.
|
|
191
305
|
* @throws If authentication tag mismatch or corrupted data.
|
|
192
306
|
*/
|
|
193
|
-
async decryptData(encryptedBase64, key,
|
|
307
|
+
async decryptData(encryptedBase64, key, isBytes = false) {
|
|
194
308
|
if (!encryptedBase64)
|
|
195
309
|
return null;
|
|
196
|
-
const opts = options ?? AesGcmOptions.default;
|
|
197
|
-
opts.validate();
|
|
198
310
|
const encryptedBytes = SecurityUtils.fromBase64(encryptedBase64);
|
|
199
|
-
|
|
311
|
+
const version = encryptedBytes[0];
|
|
312
|
+
const payload = encryptedBytes.slice(1);
|
|
313
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
314
|
+
const opts = profile.aesGcmOptions;
|
|
315
|
+
opts.validate();
|
|
316
|
+
if (payload.length < opts.nonceSize + opts.tagSize)
|
|
200
317
|
throw new Error(`Invalid format: minimum expected ${opts.nonceSize + opts.tagSize} byte.`);
|
|
201
|
-
const nonce =
|
|
202
|
-
const ciphertextWithTag =
|
|
318
|
+
const nonce = payload.slice(0, opts.nonceSize);
|
|
319
|
+
const ciphertextWithTag = payload.slice(opts.nonceSize);
|
|
203
320
|
const cryptoKey = await crypto.subtle.importKey(
|
|
204
321
|
"raw",
|
|
205
322
|
key,
|
|
@@ -209,9 +326,8 @@ var CryptoService = class {
|
|
|
209
326
|
);
|
|
210
327
|
try {
|
|
211
328
|
let associatedData = new Uint8Array(0);
|
|
212
|
-
if (opts.associatedData != null)
|
|
329
|
+
if (opts.associatedData != null)
|
|
213
330
|
associatedData = opts.associatedData;
|
|
214
|
-
}
|
|
215
331
|
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
216
332
|
{
|
|
217
333
|
name: "AES-GCM",
|
|
@@ -228,7 +344,7 @@ var CryptoService = class {
|
|
|
228
344
|
if (isBytes && typeof parsed === "string") {
|
|
229
345
|
return SecurityUtils.fromBase64(parsed);
|
|
230
346
|
}
|
|
231
|
-
return
|
|
347
|
+
return parsed;
|
|
232
348
|
} catch (e) {
|
|
233
349
|
console.error("Decryption error:", e);
|
|
234
350
|
throw new Error("Decryption failed: authentication tag mismatch or corrupted data.");
|
|
@@ -236,83 +352,40 @@ var CryptoService = class {
|
|
|
236
352
|
}
|
|
237
353
|
};
|
|
238
354
|
|
|
239
|
-
// src/crypto/kdf-options.ts
|
|
240
|
-
var KdfOptions = class _KdfOptions {
|
|
241
|
-
constructor() {
|
|
242
|
-
this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
|
|
243
|
-
this._hashAlgorithm = "SHA-256";
|
|
244
|
-
}
|
|
245
|
-
/** PBKDF2 iteration count (minimum 100_000). */
|
|
246
|
-
get pbkdf2Iterations() {
|
|
247
|
-
return this._pbkdf2Iterations;
|
|
248
|
-
}
|
|
249
|
-
set pbkdf2Iterations(v) {
|
|
250
|
-
if (v < SecurityConstants.Pbkdf2IterationsMinimum)
|
|
251
|
-
throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
|
|
252
|
-
this._pbkdf2Iterations = v;
|
|
253
|
-
}
|
|
254
|
-
/** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
|
|
255
|
-
get hashAlgorithm() {
|
|
256
|
-
return this._hashAlgorithm;
|
|
257
|
-
}
|
|
258
|
-
set hashAlgorithm(v) {
|
|
259
|
-
if (!SupportedHashAlgorithms.includes(v))
|
|
260
|
-
throw new RangeError(`Unsupported hash: ${v}`);
|
|
261
|
-
this._hashAlgorithm = v;
|
|
262
|
-
}
|
|
263
|
-
/** Validates iterations and hash algorithm. */
|
|
264
|
-
validate() {
|
|
265
|
-
if (this.pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum)
|
|
266
|
-
throw new Error(`Pbkdf2Iterations (${this.pbkdf2Iterations}) below minimum`);
|
|
267
|
-
if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
|
|
268
|
-
throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
|
|
269
|
-
}
|
|
270
|
-
/** Default preset: SHA-256, 600_000 iterations. */
|
|
271
|
-
static get default() {
|
|
272
|
-
return new _KdfOptions();
|
|
273
|
-
}
|
|
274
|
-
/** Fluent setter for {@link pbkdf2Iterations}. */
|
|
275
|
-
withPbkdf2Iterations(i) {
|
|
276
|
-
this.pbkdf2Iterations = i;
|
|
277
|
-
return this;
|
|
278
|
-
}
|
|
279
|
-
/** Fluent setter for {@link hashAlgorithm}. */
|
|
280
|
-
withHashAlgorithm(h) {
|
|
281
|
-
this.hashAlgorithm = h;
|
|
282
|
-
return this;
|
|
283
|
-
}
|
|
284
|
-
/** Validates and returns this instance. */
|
|
285
|
-
build() {
|
|
286
|
-
this.validate();
|
|
287
|
-
return this;
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
|
|
291
355
|
// src/crypto/key-derivation.service.ts
|
|
292
356
|
var KeyDerivationService = class {
|
|
293
357
|
/**
|
|
294
|
-
* Derives KEK and Base64 AuthHash. Identity is
|
|
358
|
+
* Derives KEK and Base64 AuthHash. Identity is hashed as-is — caller must
|
|
359
|
+
* normalize (trim, lowercase, etc.) before calling.
|
|
360
|
+
* @param identity - User identity (pre-normalized by caller).
|
|
295
361
|
* @param identity - User identity (email, username).
|
|
296
362
|
* @param password - User password.
|
|
297
363
|
* @param salt - Random salt.
|
|
298
364
|
* @param options - KDF configuration; uses default if omitted.
|
|
299
365
|
* @returns Object with `kek` (Uint8Array) and `authHash` (Base64 string).
|
|
300
366
|
*/
|
|
301
|
-
async deriveKeysFromPassword(identity, password, salt,
|
|
302
|
-
|
|
367
|
+
async deriveKeysFromPassword(identity, password, salt, version) {
|
|
368
|
+
if (!identity || identity.length === 0)
|
|
369
|
+
throw new Error("Identity cannot be null or empty.");
|
|
370
|
+
if (!password || password.trim().length === 0)
|
|
371
|
+
throw new Error("Password cannot be null or empty.");
|
|
372
|
+
if (!salt || salt.length < 16)
|
|
373
|
+
throw new Error("Salt must be at least 16 bytes.");
|
|
374
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
375
|
+
const opts = profile.kdfOptions;
|
|
303
376
|
opts.validate();
|
|
304
377
|
const safeSalt = new Uint8Array(salt);
|
|
305
378
|
const encoder = new TextEncoder();
|
|
306
|
-
const
|
|
307
|
-
const combinedPassword = `${normalizedLogin}:${password}`;
|
|
379
|
+
const combinedPassword = `${identity}:${password}`;
|
|
308
380
|
const passwordBytes = encoder.encode(combinedPassword);
|
|
309
381
|
const baseKey = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits", "deriveKey"]);
|
|
382
|
+
const hashSize = HashSizes[opts.hashAlgorithm] ?? 32;
|
|
310
383
|
const masterKeyBits = await crypto.subtle.deriveBits({
|
|
311
384
|
name: "PBKDF2",
|
|
312
385
|
salt: safeSalt,
|
|
313
386
|
iterations: opts.pbkdf2Iterations,
|
|
314
387
|
hash: opts.hashAlgorithm
|
|
315
|
-
}, baseKey,
|
|
388
|
+
}, baseKey, hashSize * 8);
|
|
316
389
|
const masterKey = await crypto.subtle.importKey("raw", masterKeyBits, "HKDF", false, ["deriveBits"]);
|
|
317
390
|
const kek = await crypto.subtle.deriveBits(
|
|
318
391
|
{ name: "HKDF", hash: opts.hashAlgorithm, salt: new Uint8Array(0), info: encoder.encode("AES-GCM-KEK-v1") },
|
|
@@ -331,6 +404,8 @@ var KeyDerivationService = class {
|
|
|
331
404
|
}
|
|
332
405
|
/**
|
|
333
406
|
* Derives an SRP-compatible authentication hash (output size = hash output length).
|
|
407
|
+
* Identity is hashed as-is — caller must normalize (trim, lowercase, etc.) before calling.
|
|
408
|
+
* @param identity - User identity (pre-normalized by caller).
|
|
334
409
|
* @param identity - User identity.
|
|
335
410
|
* @param password - User password.
|
|
336
411
|
* @param salt - Random salt.
|
|
@@ -338,12 +413,18 @@ var KeyDerivationService = class {
|
|
|
338
413
|
* @param options - KDF configuration; uses default if omitted.
|
|
339
414
|
* @returns Raw hash bytes for use as SRP verifier input (x).
|
|
340
415
|
*/
|
|
341
|
-
async deriveAuthHashForSrp(identity, password, salt, srpHashAlgorithm,
|
|
342
|
-
|
|
416
|
+
async deriveAuthHashForSrp(identity, password, salt, srpHashAlgorithm, version) {
|
|
417
|
+
if (!identity || identity.trim().length === 0)
|
|
418
|
+
throw new Error("Identity cannot be null or empty.");
|
|
419
|
+
if (!password || password.trim().length === 0)
|
|
420
|
+
throw new Error("Password cannot be null or empty.");
|
|
421
|
+
if (!salt || salt.length < 16)
|
|
422
|
+
throw new Error("Salt must be at least 16 bytes.");
|
|
423
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
424
|
+
const opts = profile.kdfOptions;
|
|
343
425
|
opts.validate();
|
|
344
|
-
const srpHashSize = srpHashAlgorithm
|
|
345
|
-
const
|
|
346
|
-
const combinedPassword = `${normalizedLogin}:${password}`;
|
|
426
|
+
const srpHashSize = HashSizes[srpHashAlgorithm];
|
|
427
|
+
const combinedPassword = `${identity}:${password}`;
|
|
347
428
|
const passwordBytes = new TextEncoder().encode(combinedPassword);
|
|
348
429
|
const baseKey = await crypto.subtle.importKey(
|
|
349
430
|
"raw",
|
|
@@ -357,7 +438,7 @@ var KeyDerivationService = class {
|
|
|
357
438
|
salt,
|
|
358
439
|
iterations: opts.pbkdf2Iterations,
|
|
359
440
|
hash: srpHashAlgorithm
|
|
360
|
-
}, baseKey,
|
|
441
|
+
}, baseKey, srpHashSize * 8);
|
|
361
442
|
const masterKey = await crypto.subtle.importKey(
|
|
362
443
|
"raw",
|
|
363
444
|
masterKeyBits,
|
|
@@ -380,45 +461,6 @@ var KeyDerivationService = class {
|
|
|
380
461
|
}
|
|
381
462
|
};
|
|
382
463
|
|
|
383
|
-
// src/crypto/crypto-version.ts
|
|
384
|
-
var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
|
|
385
|
-
CryptoVersion2[CryptoVersion2["V1"] = 1] = "V1";
|
|
386
|
-
return CryptoVersion2;
|
|
387
|
-
})(CryptoVersion || {});
|
|
388
|
-
|
|
389
|
-
// src/crypto/crypto-profile.ts
|
|
390
|
-
var CryptoProfile = class {
|
|
391
|
-
/**
|
|
392
|
-
* Creates a new CryptoProfile.
|
|
393
|
-
* @param params - Object containing version, kdfOptions, aesGcmOptions.
|
|
394
|
-
*/
|
|
395
|
-
constructor(params) {
|
|
396
|
-
this.version = params.version;
|
|
397
|
-
this.kdfOptions = params.kdfOptions;
|
|
398
|
-
this.aesGcmOptions = params.aesGcmOptions;
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
// src/crypto/crypto-profile-registry.ts
|
|
403
|
-
var CryptoProfileRegistry = class {
|
|
404
|
-
static getProfile(version) {
|
|
405
|
-
switch (version) {
|
|
406
|
-
case 1 /* V1 */:
|
|
407
|
-
return new CryptoProfile({
|
|
408
|
-
version: 1 /* V1 */,
|
|
409
|
-
kdfOptions: KdfOptions.default,
|
|
410
|
-
aesGcmOptions: AesGcmOptions.default
|
|
411
|
-
});
|
|
412
|
-
default:
|
|
413
|
-
throw new Error(`Unsupported crypto version: ${version}`);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
/** Latest supported profile (currently V1). */
|
|
417
|
-
static get latest() {
|
|
418
|
-
return this.getProfile(1 /* V1 */);
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
|
|
422
464
|
// src/utils/srp-encoding.ts
|
|
423
465
|
var SrpEncoding = class {
|
|
424
466
|
/** Serializes a bigint to modulus-sized big-endian bytes. */
|
|
@@ -434,21 +476,68 @@ var SrpEncoding = class {
|
|
|
434
476
|
const buffers = values.map((v) => this.toModulusBytes(ctx, v));
|
|
435
477
|
return this.hash(ctx.hashAlgorithmName, ...buffers);
|
|
436
478
|
}
|
|
437
|
-
/**
|
|
438
|
-
|
|
439
|
-
|
|
479
|
+
/**
|
|
480
|
+
* Computes the client proof M1 = H( H(N) ⊕ H(g) | H(I) | s | PAD(A) | PAD(B) | K ).
|
|
481
|
+
*
|
|
482
|
+
* Follows RFC 5054 / SRP-6a:
|
|
483
|
+
* - H(N) and H(g) are hashed as modulus-sized values.
|
|
484
|
+
* - Identity (I) is hashed as raw UTF-8 bytes.
|
|
485
|
+
* - A and B are padded to the modulus size before hashing.
|
|
486
|
+
* - K is the session key (H(S) without padding).
|
|
487
|
+
*
|
|
488
|
+
* @param ctx - SRP context containing N, g, hash algorithm, and modulus size.
|
|
489
|
+
* @param A - Client ephemeral public key.
|
|
490
|
+
* @param B - Server ephemeral public key.
|
|
491
|
+
* @param sessionKeyK - Session key K as raw bytes.
|
|
492
|
+
* @param identity - User identity (login). Must be pre-normalized by the caller.
|
|
493
|
+
* @param salt - User-specific salt bytes.
|
|
494
|
+
* @returns The M1 proof as raw hash bytes.
|
|
495
|
+
*/
|
|
496
|
+
static async computeM1(ctx, A, B, sessionKeyK, identity, salt) {
|
|
497
|
+
const nBytes = this.toModulusBytes(ctx, ctx.N);
|
|
498
|
+
const gBytes = this.toModulusBytes(ctx, ctx.g);
|
|
499
|
+
const hashN = await this.computeHash(ctx.hashAlgorithmName, nBytes);
|
|
500
|
+
const hashG = await this.computeHash(ctx.hashAlgorithmName, gBytes);
|
|
501
|
+
const xorNg = new Uint8Array(hashN.length);
|
|
502
|
+
for (let i = 0; i < hashN.length; i++)
|
|
503
|
+
xorNg[i] = hashN[i] ^ hashG[i];
|
|
504
|
+
const encoder = new TextEncoder();
|
|
505
|
+
const hashI = await this.computeHash(ctx.hashAlgorithmName, encoder.encode(identity));
|
|
506
|
+
return this.computeHash(
|
|
507
|
+
ctx.hashAlgorithmName,
|
|
508
|
+
xorNg,
|
|
509
|
+
hashI,
|
|
510
|
+
salt,
|
|
511
|
+
this.toModulusBytes(ctx, A),
|
|
512
|
+
this.toModulusBytes(ctx, B),
|
|
513
|
+
sessionKeyK
|
|
514
|
+
);
|
|
440
515
|
}
|
|
441
516
|
/** Computes M2 = H(A || M1 || sessionKeyK). */
|
|
442
|
-
static async computeM2(ctx, A,
|
|
443
|
-
return this.
|
|
517
|
+
static async computeM2(ctx, A, m1Bytes, sessionKeyK) {
|
|
518
|
+
return this.computeHash(
|
|
519
|
+
ctx.hashAlgorithmName,
|
|
520
|
+
this.toModulusBytes(ctx, A),
|
|
521
|
+
m1Bytes,
|
|
522
|
+
sessionKeyK
|
|
523
|
+
);
|
|
444
524
|
}
|
|
445
525
|
/** Computes session key K = H(S). */
|
|
446
526
|
static async computeSessionKey(ctx, S) {
|
|
447
|
-
const sBytes =
|
|
448
|
-
const hashBuffer = await crypto.subtle.digest(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
527
|
+
const sBytes = SecurityUtils.bigIntToRawBytes(S);
|
|
528
|
+
const hashBuffer = await crypto.subtle.digest(ctx.hashAlgorithmName, sBytes.buffer);
|
|
529
|
+
return new Uint8Array(hashBuffer);
|
|
530
|
+
}
|
|
531
|
+
/** Hashes bytes and returns raw Uint8Array (для M1/M2). */
|
|
532
|
+
static async computeHash(algo, ...buffers) {
|
|
533
|
+
const totalLen = buffers.reduce((sum, b) => sum + b.length, 0);
|
|
534
|
+
const combined = new Uint8Array(totalLen);
|
|
535
|
+
let offset = 0;
|
|
536
|
+
for (const buf of buffers) {
|
|
537
|
+
combined.set(buf, offset);
|
|
538
|
+
offset += buf.length;
|
|
539
|
+
}
|
|
540
|
+
const hashBuffer = await crypto.subtle.digest(algo, combined);
|
|
452
541
|
return new Uint8Array(hashBuffer);
|
|
453
542
|
}
|
|
454
543
|
/** Concatenates byte arrays and returns the hash as bigint. */
|
|
@@ -485,22 +574,27 @@ var SrpClientService = class {
|
|
|
485
574
|
* Generates client proof (A, M1, session key S) from server challenge.
|
|
486
575
|
* @param login - User login.
|
|
487
576
|
* @param password - Plaintext password.
|
|
488
|
-
* @param saltBase64 - Server salt (
|
|
489
|
-
* @param B_base64 - Server public ephemeral B (
|
|
577
|
+
* @param saltBase64 - Server salt (standard Base64).
|
|
578
|
+
* @param B_base64 - Server public ephemeral B (standard Base64).
|
|
490
579
|
* @param ctx - SRP context.
|
|
491
|
-
* @returns Object with A
|
|
580
|
+
* @returns Object with A and M1 as standard Base64; SessionKeyK as raw bytes.
|
|
492
581
|
*/
|
|
493
|
-
async generateSrpProof(login, password, saltBase64, B_base64, ctx) {
|
|
582
|
+
async generateSrpProof(login, password, saltBase64, B_base64, ctx, version) {
|
|
494
583
|
const salt = SecurityUtils.fromBase64(saltBase64);
|
|
495
|
-
const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName);
|
|
584
|
+
const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName, version);
|
|
496
585
|
const x = SecurityUtils.bytesToBigInt(authHash);
|
|
497
|
-
const
|
|
498
|
-
|
|
586
|
+
const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
|
|
587
|
+
let aBytes;
|
|
588
|
+
let a;
|
|
589
|
+
do {
|
|
590
|
+
aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
|
|
591
|
+
a = SecurityUtils.bytesToBigInt(aBytes);
|
|
592
|
+
} while (a === 0n);
|
|
499
593
|
const A = SecurityUtils.expMod(ctx.g, a, ctx.N);
|
|
500
|
-
if (A
|
|
501
|
-
throw new Error("
|
|
594
|
+
if (A <= 0n || A >= ctx.N)
|
|
595
|
+
throw new Error("Invalid client public key A.");
|
|
502
596
|
const B = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(B_base64));
|
|
503
|
-
if (B % ctx.N === 0n)
|
|
597
|
+
if (B <= 0n || B >= ctx.N || B % ctx.N === 0n)
|
|
504
598
|
throw new Error("Critical error: B % N === 0");
|
|
505
599
|
const u = await SrpEncoding.hashModuli(ctx, A, B);
|
|
506
600
|
if (u === 0n)
|
|
@@ -513,11 +607,11 @@ var SrpClientService = class {
|
|
|
513
607
|
if (S === 0n)
|
|
514
608
|
throw new Error("Critical error: S === 0");
|
|
515
609
|
const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
|
|
516
|
-
const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK);
|
|
610
|
+
const M1 = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, login, salt);
|
|
517
611
|
return {
|
|
518
612
|
A: SecurityUtils.toBase64(SrpEncoding.toModulusBytes(ctx, A)),
|
|
519
|
-
M1: SecurityUtils.toBase64(
|
|
520
|
-
|
|
613
|
+
M1: SecurityUtils.toBase64(M1),
|
|
614
|
+
SessionKeyK: sessionKeyK
|
|
521
615
|
};
|
|
522
616
|
}
|
|
523
617
|
/**
|
|
@@ -529,15 +623,12 @@ var SrpClientService = class {
|
|
|
529
623
|
* @param ctx - SRP context.
|
|
530
624
|
* @returns True if the server proof is valid.
|
|
531
625
|
*/
|
|
532
|
-
async verifyServerM2(A_b64, M1_b64,
|
|
626
|
+
async verifyServerM2(A_b64, M1_b64, sessionKeyK, serverM2_b64, ctx) {
|
|
533
627
|
const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(A_b64));
|
|
534
|
-
const M1 = SecurityUtils.
|
|
535
|
-
const S = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(S_b64));
|
|
536
|
-
const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
|
|
628
|
+
const M1 = SecurityUtils.fromBase64(M1_b64);
|
|
537
629
|
const computedM2 = await SrpEncoding.computeM2(ctx, A, M1, sessionKeyK);
|
|
538
|
-
const computedM2Bytes = SrpEncoding.toHashBytes(ctx, computedM2);
|
|
539
630
|
const serverM2Bytes = SecurityUtils.fromBase64(serverM2_b64);
|
|
540
|
-
return SecurityUtils.fixedTimeEquals(
|
|
631
|
+
return SecurityUtils.fixedTimeEquals(computedM2, serverM2Bytes);
|
|
541
632
|
}
|
|
542
633
|
};
|
|
543
634
|
|
|
@@ -550,18 +641,33 @@ var SrpServerService = class {
|
|
|
550
641
|
* @param ctx - SRP context (hash, N, g, etc.).
|
|
551
642
|
* @returns Session state with private b, verifier, and public B.
|
|
552
643
|
*/
|
|
553
|
-
async getSrpChallenge(login, verifierBytes, ctx) {
|
|
644
|
+
async getSrpChallenge(login, verifierBytes, salt, ctx) {
|
|
645
|
+
if (!login || login.trim().length === 0)
|
|
646
|
+
throw new Error("Login cannot be null or empty.");
|
|
647
|
+
if (!verifierBytes || verifierBytes.length === 0)
|
|
648
|
+
throw new Error("Verifier cannot be null or empty.");
|
|
554
649
|
const v = SecurityUtils.bytesToBigInt(verifierBytes);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
650
|
+
if (v <= 0n || v >= ctx.N)
|
|
651
|
+
throw new Error("The verifier is corrupted");
|
|
652
|
+
const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
|
|
653
|
+
while (true) {
|
|
654
|
+
const bBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
|
|
655
|
+
const b = SecurityUtils.bytesToBigInt(bBytes);
|
|
656
|
+
if (b === 0n) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const gB = SecurityUtils.expMod(ctx.g, b, ctx.N);
|
|
660
|
+
const B = (ctx.k * v + gB) % ctx.N;
|
|
661
|
+
if (B !== 0n) {
|
|
662
|
+
return {
|
|
663
|
+
login,
|
|
664
|
+
privateKeyB: bBytes,
|
|
665
|
+
verifier: verifierBytes,
|
|
666
|
+
publicKeyB: SrpEncoding.toModulusBytes(ctx, B),
|
|
667
|
+
salt
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
565
671
|
}
|
|
566
672
|
/**
|
|
567
673
|
* Verifies client M1 proof and returns server M2 proof.
|
|
@@ -574,29 +680,31 @@ var SrpServerService = class {
|
|
|
574
680
|
*/
|
|
575
681
|
async verifySrpProof(sessionState, a, m1, ctx) {
|
|
576
682
|
const A = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(a));
|
|
577
|
-
const M1_client = SecurityUtils.
|
|
578
|
-
const b = SecurityUtils.bytesToBigInt(
|
|
579
|
-
const v = SecurityUtils.bytesToBigInt(
|
|
580
|
-
const B = SecurityUtils.bytesToBigInt(
|
|
581
|
-
if (v <= 0n)
|
|
683
|
+
const M1_client = SecurityUtils.fromBase64(m1);
|
|
684
|
+
const b = SecurityUtils.bytesToBigInt(sessionState.privateKeyB);
|
|
685
|
+
const v = SecurityUtils.bytesToBigInt(sessionState.verifier);
|
|
686
|
+
const B = SecurityUtils.bytesToBigInt(sessionState.publicKeyB);
|
|
687
|
+
if (v <= 0n || v >= ctx.N)
|
|
582
688
|
throw new Error("The verifier is corrupted");
|
|
583
689
|
if (A % ctx.N === 0n)
|
|
584
690
|
throw new Error("Incorrect value of A");
|
|
585
691
|
if (A <= 0n || A >= ctx.N)
|
|
586
692
|
throw new Error("Invalid A (out of range)");
|
|
693
|
+
if (B <= 0n || B >= ctx.N)
|
|
694
|
+
throw new Error("Invalid server public key B.");
|
|
587
695
|
const u = await SrpEncoding.hashModuli(ctx, A, B);
|
|
588
696
|
if (u === 0n)
|
|
589
697
|
throw new Error("Error in calculating the parameter u");
|
|
590
698
|
const vU = SecurityUtils.expMod(v, u, ctx.N);
|
|
591
699
|
const S = SecurityUtils.expMod(A * vU % ctx.N, b, ctx.N);
|
|
700
|
+
if (S === 0n)
|
|
701
|
+
throw new Error("Critical error: shared secret S is zero (possible malicious A).");
|
|
592
702
|
const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
|
|
593
|
-
const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK);
|
|
594
|
-
|
|
595
|
-
const m1ClientBytes = SrpEncoding.toHashBytes(ctx, M1_client);
|
|
596
|
-
if (!SecurityUtils.fixedTimeEquals(m1ServerBytes, m1ClientBytes))
|
|
703
|
+
const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, sessionState.login, sessionState.salt);
|
|
704
|
+
if (!SecurityUtils.fixedTimeEquals(M1_server, M1_client))
|
|
597
705
|
throw new Error("Invalid password");
|
|
598
706
|
const M2_server = await SrpEncoding.computeM2(ctx, A, M1_client, sessionKeyK);
|
|
599
|
-
return SecurityUtils.toBase64(
|
|
707
|
+
return SecurityUtils.toBase64(M2_server);
|
|
600
708
|
}
|
|
601
709
|
};
|
|
602
710
|
|