@crossdyne/security 0.4.0-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 +195 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -68
- package/dist/index.d.ts +73 -68
- package/dist/index.js +195 -154
- 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) {
|
|
@@ -70,16 +76,26 @@ var SecurityUtils = class {
|
|
|
70
76
|
}
|
|
71
77
|
/** Modular exponentiation (base^exp mod mod) using binary exponentiation. */
|
|
72
78
|
static expMod(base, exp, mod) {
|
|
73
|
-
|
|
79
|
+
if (mod === 1n) return 0n;
|
|
80
|
+
let res = 1n;
|
|
74
81
|
base = base % mod;
|
|
75
82
|
while (exp > 0n) {
|
|
76
|
-
if (exp
|
|
83
|
+
if ((exp & 1n) === 1n)
|
|
77
84
|
res = res * base % mod;
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
exp >>= 1n;
|
|
86
|
+
if (exp > 0n)
|
|
87
|
+
base = base * base % mod;
|
|
80
88
|
}
|
|
81
89
|
return res;
|
|
82
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
|
+
}
|
|
83
99
|
};
|
|
84
100
|
|
|
85
101
|
// src/crypto/aes-gcm-options.ts
|
|
@@ -132,6 +148,97 @@ var AesGcmOptions = class _AesGcmOptions {
|
|
|
132
148
|
}
|
|
133
149
|
};
|
|
134
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
|
+
|
|
135
242
|
// src/crypto/crypto.service.ts
|
|
136
243
|
var CryptoService = class {
|
|
137
244
|
constructor() {
|
|
@@ -149,12 +256,13 @@ var CryptoService = class {
|
|
|
149
256
|
* @param options - AES-GCM configuration; uses default if omitted.
|
|
150
257
|
* @returns Base64-encoded ciphertext with prepended nonce.
|
|
151
258
|
*/
|
|
152
|
-
async encryptData(dataModel, key,
|
|
153
|
-
const
|
|
259
|
+
async encryptData(dataModel, key, version = 1 /* V1 */) {
|
|
260
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
261
|
+
const opts = profile.aesGcmOptions;
|
|
154
262
|
opts.validate();
|
|
155
263
|
const encoder = new TextEncoder();
|
|
156
264
|
let jsonString;
|
|
157
|
-
if (dataModel
|
|
265
|
+
if (ArrayBuffer.isView(dataModel) && dataModel.constructor === Uint8Array) {
|
|
158
266
|
jsonString = `"${SecurityUtils.toBase64(dataModel)}"`;
|
|
159
267
|
} else {
|
|
160
268
|
jsonString = JSON.stringify(dataModel);
|
|
@@ -182,9 +290,10 @@ var CryptoService = class {
|
|
|
182
290
|
cryptoKey,
|
|
183
291
|
plainBytes
|
|
184
292
|
);
|
|
185
|
-
const result = new Uint8Array(opts.nonceSize + encryptedContent.byteLength);
|
|
186
|
-
result
|
|
187
|
-
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);
|
|
188
297
|
return SecurityUtils.toBase64(result);
|
|
189
298
|
}
|
|
190
299
|
/**
|
|
@@ -195,16 +304,19 @@ var CryptoService = class {
|
|
|
195
304
|
* @returns Deserialized object, or null if input is empty.
|
|
196
305
|
* @throws If authentication tag mismatch or corrupted data.
|
|
197
306
|
*/
|
|
198
|
-
async decryptData(encryptedBase64, key,
|
|
307
|
+
async decryptData(encryptedBase64, key, isBytes = false) {
|
|
199
308
|
if (!encryptedBase64)
|
|
200
309
|
return null;
|
|
201
|
-
const opts = options ?? AesGcmOptions.default;
|
|
202
|
-
opts.validate();
|
|
203
310
|
const encryptedBytes = SecurityUtils.fromBase64(encryptedBase64);
|
|
204
|
-
|
|
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)
|
|
205
317
|
throw new Error(`Invalid format: minimum expected ${opts.nonceSize + opts.tagSize} byte.`);
|
|
206
|
-
const nonce =
|
|
207
|
-
const ciphertextWithTag =
|
|
318
|
+
const nonce = payload.slice(0, opts.nonceSize);
|
|
319
|
+
const ciphertextWithTag = payload.slice(opts.nonceSize);
|
|
208
320
|
const cryptoKey = await crypto.subtle.importKey(
|
|
209
321
|
"raw",
|
|
210
322
|
key,
|
|
@@ -214,9 +326,8 @@ var CryptoService = class {
|
|
|
214
326
|
);
|
|
215
327
|
try {
|
|
216
328
|
let associatedData = new Uint8Array(0);
|
|
217
|
-
if (opts.associatedData != null)
|
|
329
|
+
if (opts.associatedData != null)
|
|
218
330
|
associatedData = opts.associatedData;
|
|
219
|
-
}
|
|
220
331
|
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
221
332
|
{
|
|
222
333
|
name: "AES-GCM",
|
|
@@ -233,7 +344,7 @@ var CryptoService = class {
|
|
|
233
344
|
if (isBytes && typeof parsed === "string") {
|
|
234
345
|
return SecurityUtils.fromBase64(parsed);
|
|
235
346
|
}
|
|
236
|
-
return
|
|
347
|
+
return parsed;
|
|
237
348
|
} catch (e) {
|
|
238
349
|
console.error("Decryption error:", e);
|
|
239
350
|
throw new Error("Decryption failed: authentication tag mismatch or corrupted data.");
|
|
@@ -241,88 +352,40 @@ var CryptoService = class {
|
|
|
241
352
|
}
|
|
242
353
|
};
|
|
243
354
|
|
|
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
355
|
// src/crypto/key-derivation.service.ts
|
|
297
356
|
var KeyDerivationService = class {
|
|
298
357
|
/**
|
|
299
|
-
* 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).
|
|
300
361
|
* @param identity - User identity (email, username).
|
|
301
362
|
* @param password - User password.
|
|
302
363
|
* @param salt - Random salt.
|
|
303
364
|
* @param options - KDF configuration; uses default if omitted.
|
|
304
365
|
* @returns Object with `kek` (Uint8Array) and `authHash` (Base64 string).
|
|
305
366
|
*/
|
|
306
|
-
async deriveKeysFromPassword(identity, password, salt,
|
|
307
|
-
if (!identity || identity.
|
|
367
|
+
async deriveKeysFromPassword(identity, password, salt, version) {
|
|
368
|
+
if (!identity || identity.length === 0)
|
|
308
369
|
throw new Error("Identity cannot be null or empty.");
|
|
309
370
|
if (!password || password.trim().length === 0)
|
|
310
371
|
throw new Error("Password cannot be null or empty.");
|
|
311
372
|
if (!salt || salt.length < 16)
|
|
312
373
|
throw new Error("Salt must be at least 16 bytes.");
|
|
313
|
-
const
|
|
374
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
375
|
+
const opts = profile.kdfOptions;
|
|
314
376
|
opts.validate();
|
|
315
377
|
const safeSalt = new Uint8Array(salt);
|
|
316
378
|
const encoder = new TextEncoder();
|
|
317
379
|
const combinedPassword = `${identity}:${password}`;
|
|
318
380
|
const passwordBytes = encoder.encode(combinedPassword);
|
|
319
381
|
const baseKey = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits", "deriveKey"]);
|
|
382
|
+
const hashSize = HashSizes[opts.hashAlgorithm] ?? 32;
|
|
320
383
|
const masterKeyBits = await crypto.subtle.deriveBits({
|
|
321
384
|
name: "PBKDF2",
|
|
322
385
|
salt: safeSalt,
|
|
323
386
|
iterations: opts.pbkdf2Iterations,
|
|
324
387
|
hash: opts.hashAlgorithm
|
|
325
|
-
}, baseKey,
|
|
388
|
+
}, baseKey, hashSize * 8);
|
|
326
389
|
const masterKey = await crypto.subtle.importKey("raw", masterKeyBits, "HKDF", false, ["deriveBits"]);
|
|
327
390
|
const kek = await crypto.subtle.deriveBits(
|
|
328
391
|
{ name: "HKDF", hash: opts.hashAlgorithm, salt: new Uint8Array(0), info: encoder.encode("AES-GCM-KEK-v1") },
|
|
@@ -341,6 +404,8 @@ var KeyDerivationService = class {
|
|
|
341
404
|
}
|
|
342
405
|
/**
|
|
343
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).
|
|
344
409
|
* @param identity - User identity.
|
|
345
410
|
* @param password - User password.
|
|
346
411
|
* @param salt - Random salt.
|
|
@@ -348,14 +413,15 @@ var KeyDerivationService = class {
|
|
|
348
413
|
* @param options - KDF configuration; uses default if omitted.
|
|
349
414
|
* @returns Raw hash bytes for use as SRP verifier input (x).
|
|
350
415
|
*/
|
|
351
|
-
async deriveAuthHashForSrp(identity, password, salt, srpHashAlgorithm,
|
|
416
|
+
async deriveAuthHashForSrp(identity, password, salt, srpHashAlgorithm, version) {
|
|
352
417
|
if (!identity || identity.trim().length === 0)
|
|
353
418
|
throw new Error("Identity cannot be null or empty.");
|
|
354
419
|
if (!password || password.trim().length === 0)
|
|
355
420
|
throw new Error("Password cannot be null or empty.");
|
|
356
421
|
if (!salt || salt.length < 16)
|
|
357
422
|
throw new Error("Salt must be at least 16 bytes.");
|
|
358
|
-
const
|
|
423
|
+
const profile = CryptoProfileRegistry.getProfile(version);
|
|
424
|
+
const opts = profile.kdfOptions;
|
|
359
425
|
opts.validate();
|
|
360
426
|
const srpHashSize = HashSizes[srpHashAlgorithm];
|
|
361
427
|
const combinedPassword = `${identity}:${password}`;
|
|
@@ -372,7 +438,7 @@ var KeyDerivationService = class {
|
|
|
372
438
|
salt,
|
|
373
439
|
iterations: opts.pbkdf2Iterations,
|
|
374
440
|
hash: srpHashAlgorithm
|
|
375
|
-
}, baseKey,
|
|
441
|
+
}, baseKey, srpHashSize * 8);
|
|
376
442
|
const masterKey = await crypto.subtle.importKey(
|
|
377
443
|
"raw",
|
|
378
444
|
masterKeyBits,
|
|
@@ -395,45 +461,6 @@ var KeyDerivationService = class {
|
|
|
395
461
|
}
|
|
396
462
|
};
|
|
397
463
|
|
|
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
|
-
};
|
|
436
|
-
|
|
437
464
|
// src/utils/srp-encoding.ts
|
|
438
465
|
var SrpEncoding = class {
|
|
439
466
|
/** Serializes a bigint to modulus-sized big-endian bytes. */
|
|
@@ -462,7 +489,7 @@ var SrpEncoding = class {
|
|
|
462
489
|
* @param A - Client ephemeral public key.
|
|
463
490
|
* @param B - Server ephemeral public key.
|
|
464
491
|
* @param sessionKeyK - Session key K as raw bytes.
|
|
465
|
-
* @param identity - User identity (login).
|
|
492
|
+
* @param identity - User identity (login). Must be pre-normalized by the caller.
|
|
466
493
|
* @param salt - User-specific salt bytes.
|
|
467
494
|
* @returns The M1 proof as raw hash bytes.
|
|
468
495
|
*/
|
|
@@ -497,11 +524,8 @@ var SrpEncoding = class {
|
|
|
497
524
|
}
|
|
498
525
|
/** Computes session key K = H(S). */
|
|
499
526
|
static async computeSessionKey(ctx, S) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
hex = "0" + hex;
|
|
503
|
-
const sBytes = new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
504
|
-
const hashBuffer = await crypto.subtle.digest(ctx.hashAlgorithmName, sBytes);
|
|
527
|
+
const sBytes = SecurityUtils.bigIntToRawBytes(S);
|
|
528
|
+
const hashBuffer = await crypto.subtle.digest(ctx.hashAlgorithmName, sBytes.buffer);
|
|
505
529
|
return new Uint8Array(hashBuffer);
|
|
506
530
|
}
|
|
507
531
|
/** Hashes bytes and returns raw Uint8Array (для M1/M2). */
|
|
@@ -550,23 +574,27 @@ var SrpClientService = class {
|
|
|
550
574
|
* Generates client proof (A, M1, session key S) from server challenge.
|
|
551
575
|
* @param login - User login.
|
|
552
576
|
* @param password - Plaintext password.
|
|
553
|
-
* @param saltBase64 - Server salt (
|
|
554
|
-
* @param B_base64 - Server public ephemeral B (
|
|
577
|
+
* @param saltBase64 - Server salt (standard Base64).
|
|
578
|
+
* @param B_base64 - Server public ephemeral B (standard Base64).
|
|
555
579
|
* @param ctx - SRP context.
|
|
556
|
-
* @returns Object with A
|
|
580
|
+
* @returns Object with A and M1 as standard Base64; SessionKeyK as raw bytes.
|
|
557
581
|
*/
|
|
558
|
-
async generateSrpProof(login, password, saltBase64, B_base64, ctx) {
|
|
582
|
+
async generateSrpProof(login, password, saltBase64, B_base64, ctx, version) {
|
|
559
583
|
const salt = SecurityUtils.fromBase64(saltBase64);
|
|
560
|
-
const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName);
|
|
584
|
+
const authHash = await this.keyDerivation.deriveAuthHashForSrp(login, password, salt, ctx.hashAlgorithmName, version);
|
|
561
585
|
const x = SecurityUtils.bytesToBigInt(authHash);
|
|
562
586
|
const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
|
|
563
|
-
|
|
564
|
-
|
|
587
|
+
let aBytes;
|
|
588
|
+
let a;
|
|
589
|
+
do {
|
|
590
|
+
aBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
|
|
591
|
+
a = SecurityUtils.bytesToBigInt(aBytes);
|
|
592
|
+
} while (a === 0n);
|
|
565
593
|
const A = SecurityUtils.expMod(ctx.g, a, ctx.N);
|
|
566
|
-
if (A
|
|
567
|
-
throw new Error("
|
|
594
|
+
if (A <= 0n || A >= ctx.N)
|
|
595
|
+
throw new Error("Invalid client public key A.");
|
|
568
596
|
const B = SecurityUtils.bytesToBigInt(SecurityUtils.fromBase64(B_base64));
|
|
569
|
-
if (B % ctx.N === 0n)
|
|
597
|
+
if (B <= 0n || B >= ctx.N || B % ctx.N === 0n)
|
|
570
598
|
throw new Error("Critical error: B % N === 0");
|
|
571
599
|
const u = await SrpEncoding.hashModuli(ctx, A, B);
|
|
572
600
|
if (u === 0n)
|
|
@@ -614,23 +642,32 @@ var SrpServerService = class {
|
|
|
614
642
|
* @returns Session state with private b, verifier, and public B.
|
|
615
643
|
*/
|
|
616
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.");
|
|
617
649
|
const v = SecurityUtils.bytesToBigInt(verifierBytes);
|
|
650
|
+
if (v <= 0n || v >= ctx.N)
|
|
651
|
+
throw new Error("The verifier is corrupted");
|
|
618
652
|
const privateKeySize = Math.max(32, Math.floor(ctx.modulusSize / 2));
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
do {
|
|
622
|
-
bBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
|
|
653
|
+
while (true) {
|
|
654
|
+
const bBytes = crypto.getRandomValues(new Uint8Array(privateKeySize));
|
|
623
655
|
const b = SecurityUtils.bytesToBigInt(bBytes);
|
|
656
|
+
if (b === 0n) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
624
659
|
const gB = SecurityUtils.expMod(ctx.g, b, ctx.N);
|
|
625
|
-
B = (ctx.k * v + gB) % ctx.N;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
+
}
|
|
634
671
|
}
|
|
635
672
|
/**
|
|
636
673
|
* Verifies client M1 proof and returns server M2 proof.
|
|
@@ -647,17 +684,21 @@ var SrpServerService = class {
|
|
|
647
684
|
const b = SecurityUtils.bytesToBigInt(sessionState.privateKeyB);
|
|
648
685
|
const v = SecurityUtils.bytesToBigInt(sessionState.verifier);
|
|
649
686
|
const B = SecurityUtils.bytesToBigInt(sessionState.publicKeyB);
|
|
650
|
-
if (v <= 0n)
|
|
687
|
+
if (v <= 0n || v >= ctx.N)
|
|
651
688
|
throw new Error("The verifier is corrupted");
|
|
652
689
|
if (A % ctx.N === 0n)
|
|
653
690
|
throw new Error("Incorrect value of A");
|
|
654
691
|
if (A <= 0n || A >= ctx.N)
|
|
655
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.");
|
|
656
695
|
const u = await SrpEncoding.hashModuli(ctx, A, B);
|
|
657
696
|
if (u === 0n)
|
|
658
697
|
throw new Error("Error in calculating the parameter u");
|
|
659
698
|
const vU = SecurityUtils.expMod(v, u, ctx.N);
|
|
660
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).");
|
|
661
702
|
const sessionKeyK = await SrpEncoding.computeSessionKey(ctx, S);
|
|
662
703
|
const M1_server = await SrpEncoding.computeM1(ctx, A, B, sessionKeyK, sessionState.login, sessionState.salt);
|
|
663
704
|
if (!SecurityUtils.fixedTimeEquals(M1_server, M1_client))
|