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