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