secure-hash-vault 1.0.0

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 ADDED
@@ -0,0 +1,792 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CipherForge: () => CipherForge,
24
+ CipherForgeError: () => CipherForgeError,
25
+ CipherVaultError: () => CipherVaultError,
26
+ DecryptionError: () => DecryptionError,
27
+ EncryptionError: () => EncryptionError,
28
+ InvalidConfigError: () => InvalidConfigError,
29
+ InvalidPasswordHashError: () => InvalidPasswordHashError,
30
+ InvalidPayloadError: () => InvalidPayloadError,
31
+ PasswordVerificationError: () => PasswordVerificationError,
32
+ SecureHashVaultClient: () => SecureHashVaultClient,
33
+ SecureHashVaultError: () => SecureHashVaultError,
34
+ cipherForge: () => cipherForge,
35
+ cipherVault: () => cipherVault,
36
+ createCipherForge: () => createCipherForge,
37
+ createCipherVault: () => createCipherVault,
38
+ createSecureHashVault: () => createSecureHashVault,
39
+ decryptBuffer: () => decryptBuffer,
40
+ decryptFile: () => decryptFile,
41
+ decryptJson: () => decryptJson,
42
+ decryptText: () => decryptText,
43
+ default: () => index_default,
44
+ detectPayload: () => detectPayload,
45
+ encryptBuffer: () => encryptBuffer,
46
+ encryptFile: () => encryptFile,
47
+ encryptJson: () => encryptJson,
48
+ encryptText: () => encryptText,
49
+ fromBase64Url: () => fromBase64Url,
50
+ fromHex: () => fromHex,
51
+ generatePepper: () => generatePepper,
52
+ generateSalt: () => generateSalt,
53
+ hashPassword: () => hashPassword,
54
+ needsRehash: () => needsRehash,
55
+ randomBuffer: () => randomBuffer,
56
+ secureVault: () => secureVault,
57
+ toBase64Url: () => toBase64Url,
58
+ toHex: () => toHex,
59
+ verifyPassword: () => verifyPassword
60
+ });
61
+ module.exports = __toCommonJS(index_exports);
62
+
63
+ // src/crypto/decryptBuffer.ts
64
+ var import_node_crypto2 = require("crypto");
65
+
66
+ // src/core/constants.ts
67
+ var PASSWORD_FORMAT_PREFIX = "$cv$";
68
+ var PASSWORD_VERSION = "v1";
69
+ var PASSWORD_ALGORITHM = "scrypt";
70
+ var ENCRYPTION_FORMAT = "cv.enc.v1";
71
+ var ENCRYPTION_COMPACT_PREFIX = "$cvenc$";
72
+ var ENCRYPTION_VERSION = "v1";
73
+ var ENCRYPTION_ALGORITHM = "aes-256-gcm";
74
+ var ENCRYPTION_KDF = "scrypt";
75
+ var DEFAULT_PASSWORD_PARAMS = {
76
+ N: 16384,
77
+ r: 8,
78
+ p: 1,
79
+ keyLen: 64
80
+ };
81
+ var DEFAULT_ENCRYPTION_PARAMS = {
82
+ N: 16384,
83
+ r: 8,
84
+ p: 1,
85
+ keyLen: 32
86
+ };
87
+ var DEFAULT_SALT_LENGTH = 16;
88
+ var DEFAULT_IV_LENGTH = 12;
89
+
90
+ // src/core/errors.ts
91
+ var SecureHashVaultError = class extends Error {
92
+ constructor(message, options) {
93
+ super(message, options);
94
+ this.name = new.target.name;
95
+ }
96
+ };
97
+ var CipherForgeError = class extends SecureHashVaultError {
98
+ };
99
+ var CipherVaultError = class extends CipherForgeError {
100
+ };
101
+ var InvalidPasswordHashError = class extends SecureHashVaultError {
102
+ };
103
+ var PasswordVerificationError = class extends SecureHashVaultError {
104
+ };
105
+ var EncryptionError = class extends SecureHashVaultError {
106
+ };
107
+ var DecryptionError = class extends SecureHashVaultError {
108
+ };
109
+ var InvalidPayloadError = class extends SecureHashVaultError {
110
+ };
111
+ var InvalidConfigError = class extends SecureHashVaultError {
112
+ };
113
+
114
+ // src/core/encoding.ts
115
+ function toBase64Url(input) {
116
+ const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input, "utf8");
117
+ return buffer.toString("base64url");
118
+ }
119
+ function fromBase64Url(value, fieldName = "value") {
120
+ if (typeof value !== "string" || value.length === 0) {
121
+ throw new InvalidPayloadError(`${fieldName} must be a non-empty base64url string.`);
122
+ }
123
+ try {
124
+ return Buffer.from(value, "base64url");
125
+ } catch (error) {
126
+ throw new InvalidPayloadError(`${fieldName} is not valid base64url data.`, { cause: error });
127
+ }
128
+ }
129
+ function toHex(input) {
130
+ const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input, "utf8");
131
+ return buffer.toString("hex");
132
+ }
133
+ function fromHex(value, fieldName = "value") {
134
+ if (typeof value !== "string" || value.length === 0 || !/^[\da-f]+$/i.test(value)) {
135
+ throw new InvalidPayloadError(`${fieldName} must be a non-empty hex string.`);
136
+ }
137
+ return Buffer.from(value, "hex");
138
+ }
139
+
140
+ // src/core/kdf.ts
141
+ var import_node_crypto = require("crypto");
142
+ async function deriveScryptKey(secret, salt, params) {
143
+ const maxmem = Math.max(32 * 1024 * 1024, 128 * params.N * params.r + 1024 * 1024);
144
+ return new Promise((resolve, reject) => {
145
+ (0, import_node_crypto.scrypt)(
146
+ secret,
147
+ salt,
148
+ params.keyLen,
149
+ {
150
+ N: params.N,
151
+ r: params.r,
152
+ p: params.p,
153
+ maxmem
154
+ },
155
+ (error, derivedKey) => {
156
+ if (error) {
157
+ reject(error);
158
+ return;
159
+ }
160
+ resolve(derivedKey);
161
+ }
162
+ );
163
+ });
164
+ }
165
+ function applyPepper(password, pepper) {
166
+ if (!pepper) {
167
+ return Buffer.from(password, "utf8");
168
+ }
169
+ return Buffer.concat([Buffer.from(password, "utf8"), Buffer.from("\0", "utf8"), Buffer.from(pepper, "utf8")]);
170
+ }
171
+
172
+ // src/core/validators.ts
173
+ function assertPassword(password) {
174
+ if (typeof password !== "string") {
175
+ throw new InvalidConfigError("Password must be a string.");
176
+ }
177
+ if (password.length === 0) {
178
+ throw new InvalidConfigError("Password cannot be empty.");
179
+ }
180
+ }
181
+ function assertSecret(secret) {
182
+ if (typeof secret !== "string") {
183
+ throw new InvalidConfigError("Secret must be a string.");
184
+ }
185
+ if (secret.length === 0) {
186
+ throw new InvalidConfigError("Secret cannot be empty.");
187
+ }
188
+ }
189
+ function assertPasswordAlgorithm(algorithm) {
190
+ if (algorithm !== void 0 && algorithm !== PASSWORD_ALGORITHM) {
191
+ throw new InvalidConfigError(`Unsupported password algorithm: ${String(algorithm)}.`);
192
+ }
193
+ }
194
+ function assertEncryptionAlgorithm(algorithm) {
195
+ if (algorithm !== void 0 && algorithm !== ENCRYPTION_ALGORITHM) {
196
+ throw new InvalidConfigError(`Unsupported encryption algorithm: ${String(algorithm)}.`);
197
+ }
198
+ }
199
+ function normalizeScryptParams(defaults, params) {
200
+ const normalized = { ...defaults, ...params };
201
+ validateScryptParams(normalized);
202
+ return normalized;
203
+ }
204
+ function validateScryptParams(params) {
205
+ const { N, r, p, keyLen } = params;
206
+ if (!Number.isInteger(N) || N < 4096 || N > 1048576 || (N & N - 1) !== 0) {
207
+ throw new InvalidConfigError("scrypt N must be a power-of-two integer between 4096 and 1048576.");
208
+ }
209
+ if (!Number.isInteger(r) || r < 1 || r > 32) {
210
+ throw new InvalidConfigError("scrypt r must be an integer between 1 and 32.");
211
+ }
212
+ if (!Number.isInteger(p) || p < 1 || p > 16) {
213
+ throw new InvalidConfigError("scrypt p must be an integer between 1 and 16.");
214
+ }
215
+ if (!Number.isInteger(keyLen) || keyLen < 32 || keyLen > 128) {
216
+ throw new InvalidConfigError("scrypt keyLen must be an integer between 32 and 128.");
217
+ }
218
+ }
219
+
220
+ // src/utils/safeJson.ts
221
+ function parseJson(value, message = "Invalid JSON payload.") {
222
+ try {
223
+ return JSON.parse(value);
224
+ } catch (error) {
225
+ throw new InvalidPayloadError(message, { cause: error });
226
+ }
227
+ }
228
+ function stringifyJson(value) {
229
+ try {
230
+ return JSON.stringify(value);
231
+ } catch (error) {
232
+ throw new InvalidPayloadError("Value cannot be serialized to JSON.", { cause: error });
233
+ }
234
+ }
235
+
236
+ // src/crypto/cryptoFormat.ts
237
+ function serializeCryptoParams(params) {
238
+ return `N=${params.N},r=${params.r},p=${params.p},keyLen=${params.keyLen}`;
239
+ }
240
+ function parseCryptoParams(value) {
241
+ const record = {};
242
+ for (const pair of value.split(",")) {
243
+ const [key, rawValue] = pair.split("=");
244
+ if (!key || rawValue === void 0 || !["N", "r", "p", "keyLen"].includes(key)) {
245
+ throw new InvalidPayloadError("Encrypted payload contains malformed KDF parameters.");
246
+ }
247
+ record[key] = Number(rawValue);
248
+ }
249
+ const { N, r, p, keyLen } = record;
250
+ if (!N || !r || !p || !keyLen) {
251
+ throw new InvalidPayloadError("Encrypted payload is missing required KDF parameters.");
252
+ }
253
+ const params = { N, r, p, keyLen };
254
+ validateScryptParams(params);
255
+ return params;
256
+ }
257
+ function payloadToCompact(payload) {
258
+ return `${ENCRYPTION_COMPACT_PREFIX}${ENCRYPTION_VERSION}$${payload.algorithm}$${payload.kdf}$${serializeCryptoParams(payload.params)}$${payload.salt}$${payload.iv}$${payload.tag}$${payload.cipherText}`;
259
+ }
260
+ function parseEncryptedPayload(payload) {
261
+ if (typeof payload === "string") {
262
+ if (payload.startsWith(ENCRYPTION_COMPACT_PREFIX)) {
263
+ return parseCompactPayload(payload);
264
+ }
265
+ return parseJson(payload, "Encrypted payload string must be compact format or JSON.");
266
+ }
267
+ if (!payload || typeof payload !== "object") {
268
+ throw new InvalidPayloadError("Encrypted payload must be an object or string.");
269
+ }
270
+ validatePayloadShape(payload);
271
+ return payload;
272
+ }
273
+ function validatePayloadShape(payload) {
274
+ if (payload.format !== ENCRYPTION_FORMAT) {
275
+ throw new InvalidPayloadError("Unsupported encrypted payload format.");
276
+ }
277
+ if (payload.algorithm !== ENCRYPTION_ALGORITHM) {
278
+ throw new InvalidPayloadError("Unsupported encrypted payload algorithm.");
279
+ }
280
+ if (payload.kdf !== ENCRYPTION_KDF) {
281
+ throw new InvalidPayloadError("Unsupported encrypted payload KDF.");
282
+ }
283
+ validateScryptParams(payload.params);
284
+ for (const field of ["salt", "iv", "tag", "cipherText"]) {
285
+ if (typeof payload[field] !== "string" || payload[field].length === 0) {
286
+ throw new InvalidPayloadError(`Encrypted payload field ${field} must be a non-empty string.`);
287
+ }
288
+ }
289
+ }
290
+ function parseCompactPayload(compact) {
291
+ const parts = compact.split("$");
292
+ const [, prefix, version, algorithm, kdf, rawParams, salt, iv, tag, cipherText] = parts;
293
+ if (parts.length !== 10 || prefix !== "cvenc" || version !== ENCRYPTION_VERSION || algorithm !== ENCRYPTION_ALGORITHM || kdf !== ENCRYPTION_KDF || !rawParams || !salt || !iv || !tag || !cipherText) {
294
+ throw new InvalidPayloadError("Malformed compact encrypted payload.");
295
+ }
296
+ return {
297
+ format: ENCRYPTION_FORMAT,
298
+ algorithm,
299
+ kdf,
300
+ params: parseCryptoParams(rawParams),
301
+ salt,
302
+ iv,
303
+ tag,
304
+ cipherText
305
+ };
306
+ }
307
+
308
+ // src/crypto/decryptBuffer.ts
309
+ async function decryptBuffer(payload, secret, options = {}) {
310
+ assertSecret(secret);
311
+ try {
312
+ const parsed = parseEncryptedPayload(payload);
313
+ const key = await deriveScryptKey(secret, fromBase64Url(parsed.salt, "salt"), parsed.params);
314
+ const decipher = (0, import_node_crypto2.createDecipheriv)(ENCRYPTION_ALGORITHM, key, fromBase64Url(parsed.iv, "iv"));
315
+ if (options.aad) {
316
+ decipher.setAAD(Buffer.from(options.aad, "utf8"));
317
+ }
318
+ decipher.setAuthTag(fromBase64Url(parsed.tag, "auth tag"));
319
+ return Buffer.concat([
320
+ decipher.update(fromBase64Url(parsed.cipherText, "cipherText")),
321
+ decipher.final()
322
+ ]);
323
+ } catch (error) {
324
+ throw new DecryptionError("Decryption failed. Check the secret, payload, and authenticated data.", {
325
+ cause: error
326
+ });
327
+ }
328
+ }
329
+
330
+ // src/crypto/decryptFile.ts
331
+ var import_promises = require("fs/promises");
332
+ var import_node_crypto3 = require("crypto");
333
+
334
+ // src/utils/metadata.ts
335
+ function createdAt() {
336
+ return (/* @__PURE__ */ new Date()).toISOString();
337
+ }
338
+ function withCreatedAt(metadata) {
339
+ return {
340
+ ...metadata ?? {},
341
+ createdAt: typeof metadata?.createdAt === "string" ? metadata.createdAt : createdAt()
342
+ };
343
+ }
344
+
345
+ // src/crypto/decryptFile.ts
346
+ async function decryptFile(inputPath, outputPath, secret, options = {}) {
347
+ assertSecret(secret);
348
+ try {
349
+ const file = await (0, import_promises.readFile)(inputPath);
350
+ const headerEnd = file.indexOf(10);
351
+ if (headerEnd <= 0) {
352
+ throw new InvalidPayloadError("Encrypted file is missing CipherForge header.");
353
+ }
354
+ const header = parseJson(file.subarray(0, headerEnd).toString("utf8"), "Encrypted file header is invalid.");
355
+ if (header.format !== "cv.file.v1" || header.algorithm !== ENCRYPTION_ALGORITHM || header.kdf !== "scrypt") {
356
+ throw new InvalidPayloadError("Unsupported encrypted file format.");
357
+ }
358
+ const body = file.subarray(headerEnd + 1);
359
+ if (body.length < 16) {
360
+ throw new InvalidPayloadError("Encrypted file is missing authentication tag.");
361
+ }
362
+ const cipherText = body.subarray(0, body.length - 16);
363
+ const tag = body.subarray(body.length - 16);
364
+ const params = parseCryptoParams(header.params);
365
+ const key = await deriveScryptKey(secret, fromBase64Url(header.salt, "salt"), params);
366
+ const decipher = (0, import_node_crypto3.createDecipheriv)(ENCRYPTION_ALGORITHM, key, fromBase64Url(header.iv, "iv"));
367
+ if (options.aad) {
368
+ decipher.setAAD(Buffer.from(options.aad, "utf8"));
369
+ } else if (header.aad) {
370
+ throw new InvalidPayloadError("Encrypted file requires authenticated data.");
371
+ }
372
+ decipher.setAuthTag(tag);
373
+ await (0, import_promises.writeFile)(outputPath, Buffer.concat([decipher.update(cipherText), decipher.final()]));
374
+ const inputStats = await (0, import_promises.stat)(inputPath);
375
+ const outputStats = await (0, import_promises.stat)(outputPath);
376
+ return {
377
+ inputPath,
378
+ outputPath,
379
+ bytesRead: inputStats.size,
380
+ bytesWritten: outputStats.size,
381
+ algorithm: ENCRYPTION_ALGORITHM,
382
+ createdAt: createdAt()
383
+ };
384
+ } catch (error) {
385
+ throw new DecryptionError("File decryption failed. Check the secret, payload, and authenticated data.", {
386
+ cause: error
387
+ });
388
+ }
389
+ }
390
+
391
+ // src/crypto/decryptText.ts
392
+ async function decryptText(payload, secret, options = {}) {
393
+ const buffer = await decryptBuffer(payload, secret, options);
394
+ return buffer.toString("utf8");
395
+ }
396
+
397
+ // src/crypto/decryptJson.ts
398
+ async function decryptJson(payload, secret, options = {}) {
399
+ return parseJson(await decryptText(payload, secret, options), "Decrypted payload is not valid JSON.");
400
+ }
401
+
402
+ // src/crypto/encryptBuffer.ts
403
+ var import_node_crypto5 = require("crypto");
404
+
405
+ // src/core/random.ts
406
+ var import_node_crypto4 = require("crypto");
407
+ function randomBuffer(size = DEFAULT_SALT_LENGTH) {
408
+ if (!Number.isInteger(size) || size < 8 || size > 1024) {
409
+ throw new InvalidConfigError("Random byte size must be an integer between 8 and 1024.");
410
+ }
411
+ return (0, import_node_crypto4.randomBytes)(size);
412
+ }
413
+ function generateSalt(size = DEFAULT_SALT_LENGTH) {
414
+ return toBase64Url(randomBuffer(size));
415
+ }
416
+ function generatePepper(size = 32) {
417
+ return toBase64Url(randomBuffer(size));
418
+ }
419
+
420
+ // src/crypto/encryptBuffer.ts
421
+ async function encryptBuffer(buffer, secret, options = {}) {
422
+ if (!Buffer.isBuffer(buffer)) {
423
+ throw new EncryptionError("encryptBuffer input must be a Buffer.");
424
+ }
425
+ assertSecret(secret);
426
+ assertEncryptionAlgorithm(options.algorithm);
427
+ try {
428
+ const params = normalizeScryptParams(DEFAULT_ENCRYPTION_PARAMS, options.params);
429
+ const salt = randomBuffer(DEFAULT_SALT_LENGTH);
430
+ const iv = randomBuffer(DEFAULT_IV_LENGTH);
431
+ const key = await deriveScryptKey(secret, salt, params);
432
+ const cipher = (0, import_node_crypto5.createCipheriv)(ENCRYPTION_ALGORITHM, key, iv);
433
+ if (options.aad) {
434
+ cipher.setAAD(Buffer.from(options.aad, "utf8"));
435
+ }
436
+ const cipherText = Buffer.concat([cipher.update(buffer), cipher.final()]);
437
+ const tag = cipher.getAuthTag();
438
+ const payload = {
439
+ format: ENCRYPTION_FORMAT,
440
+ algorithm: ENCRYPTION_ALGORITHM,
441
+ kdf: ENCRYPTION_KDF,
442
+ params,
443
+ iv: toBase64Url(iv),
444
+ salt: toBase64Url(salt),
445
+ tag: toBase64Url(tag),
446
+ cipherText: toBase64Url(cipherText),
447
+ metadata: withCreatedAt(options.metadata)
448
+ };
449
+ return options.output === "compact" ? payloadToCompact(payload) : payload;
450
+ } catch (error) {
451
+ throw new EncryptionError("Encryption failed.", { cause: error });
452
+ }
453
+ }
454
+
455
+ // src/crypto/encryptFile.ts
456
+ var import_node_fs = require("fs");
457
+ var import_promises2 = require("fs/promises");
458
+ var import_node_crypto6 = require("crypto");
459
+ var import_promises3 = require("stream/promises");
460
+ async function encryptFile(inputPath, outputPath, secret, options = {}) {
461
+ assertSecret(secret);
462
+ try {
463
+ const params = normalizeScryptParams(DEFAULT_ENCRYPTION_PARAMS, options.params);
464
+ const salt = randomBuffer(DEFAULT_SALT_LENGTH);
465
+ const iv = randomBuffer(DEFAULT_IV_LENGTH);
466
+ const key = await deriveScryptKey(secret, salt, params);
467
+ const cipher = (0, import_node_crypto6.createCipheriv)(ENCRYPTION_ALGORITHM, key, iv);
468
+ if (options.aad) {
469
+ cipher.setAAD(Buffer.from(options.aad, "utf8"));
470
+ }
471
+ const header = Buffer.from(
472
+ JSON.stringify({
473
+ format: "cv.file.v1",
474
+ algorithm: ENCRYPTION_ALGORITHM,
475
+ kdf: "scrypt",
476
+ params: serializeCryptoParams(params),
477
+ salt: toBase64Url(salt),
478
+ iv: toBase64Url(iv),
479
+ aad: options.aad ? true : void 0
480
+ }) + "\n",
481
+ "utf8"
482
+ );
483
+ const output = (0, import_node_fs.createWriteStream)(outputPath);
484
+ output.write(header);
485
+ await (0, import_promises3.pipeline)((0, import_node_fs.createReadStream)(inputPath), cipher, output, { end: false });
486
+ output.write(cipher.getAuthTag());
487
+ output.end();
488
+ const inputStats = await (0, import_promises2.stat)(inputPath);
489
+ const outputStats = await (0, import_promises2.stat)(outputPath);
490
+ return {
491
+ inputPath,
492
+ outputPath,
493
+ bytesRead: inputStats.size,
494
+ bytesWritten: outputStats.size,
495
+ algorithm: ENCRYPTION_ALGORITHM,
496
+ createdAt: createdAt()
497
+ };
498
+ } catch (error) {
499
+ throw new EncryptionError("File encryption failed.", { cause: error });
500
+ }
501
+ }
502
+
503
+ // src/crypto/encryptText.ts
504
+ async function encryptText(plainText, secret, options = {}) {
505
+ if (typeof plainText !== "string") {
506
+ throw new TypeError("plainText must be a string.");
507
+ }
508
+ return encryptBuffer(Buffer.from(plainText, "utf8"), secret, options);
509
+ }
510
+
511
+ // src/crypto/encryptJson.ts
512
+ async function encryptJson(data, secret, options = {}) {
513
+ return encryptText(stringifyJson(data), secret, options);
514
+ }
515
+
516
+ // src/password/passwordFormat.ts
517
+ function serializePasswordParams(params) {
518
+ return `N=${params.N},r=${params.r},p=${params.p},keyLen=${params.keyLen}`;
519
+ }
520
+ function parsePasswordParams(value) {
521
+ const pairs = value.split(",");
522
+ const record = {};
523
+ for (const pair of pairs) {
524
+ const [key, rawValue] = pair.split("=");
525
+ if (!key || rawValue === void 0 || !["N", "r", "p", "keyLen"].includes(key)) {
526
+ throw new InvalidPasswordHashError("Password hash contains malformed scrypt parameters.");
527
+ }
528
+ const numberValue = Number(rawValue);
529
+ if (!Number.isInteger(numberValue)) {
530
+ throw new InvalidPasswordHashError("Password hash contains non-integer scrypt parameters.");
531
+ }
532
+ record[key] = numberValue;
533
+ }
534
+ const { N, r, p, keyLen } = record;
535
+ if (!N || !r || !p || !keyLen) {
536
+ throw new InvalidPasswordHashError("Password hash is missing required scrypt parameters.");
537
+ }
538
+ const params = { N, r, p, keyLen };
539
+ validateScryptParams(params);
540
+ return params;
541
+ }
542
+ function formatPasswordHash(params, salt, hash) {
543
+ return `${PASSWORD_FORMAT_PREFIX}${PASSWORD_VERSION}$${PASSWORD_ALGORITHM}$${serializePasswordParams(params)}$${salt}$${hash}`;
544
+ }
545
+ function parsePasswordHash(storedHash) {
546
+ if (typeof storedHash !== "string" || storedHash.length === 0) {
547
+ throw new InvalidPasswordHashError("Stored password hash must be a non-empty string.");
548
+ }
549
+ if (!storedHash.startsWith(PASSWORD_FORMAT_PREFIX)) {
550
+ throw new InvalidPasswordHashError("Unsupported password hash format. Password hashes cannot be decrypted.");
551
+ }
552
+ const parts = storedHash.split("$");
553
+ const [, prefix, version, algorithm, rawParams, salt, hash] = parts;
554
+ if (parts.length !== 7 || prefix !== "cv" || version !== PASSWORD_VERSION || algorithm !== PASSWORD_ALGORITHM) {
555
+ throw new InvalidPasswordHashError("Malformed CipherForge password hash.");
556
+ }
557
+ if (!rawParams || !salt || !hash) {
558
+ throw new InvalidPasswordHashError("Password hash is missing metadata, salt, or hash value.");
559
+ }
560
+ return {
561
+ prefix,
562
+ version,
563
+ algorithm,
564
+ params: parsePasswordParams(rawParams),
565
+ salt,
566
+ hash
567
+ };
568
+ }
569
+
570
+ // src/password/hashPassword.ts
571
+ async function hashPassword(password, options = {}) {
572
+ assertPassword(password);
573
+ assertPasswordAlgorithm(options.algorithm);
574
+ const params = normalizeScryptParams(DEFAULT_PASSWORD_PARAMS, options.params);
575
+ const salt = options.salt ?? generateSalt(options.saltLength ?? DEFAULT_SALT_LENGTH);
576
+ const saltBuffer = fromBase64Url(salt, "salt");
577
+ const passwordBuffer = applyPepper(password, options.pepper);
578
+ const derivedKey = await deriveScryptKey(passwordBuffer, saltBuffer, params);
579
+ const hash = toBase64Url(derivedKey);
580
+ return {
581
+ algorithm: PASSWORD_ALGORITHM,
582
+ version: PASSWORD_VERSION,
583
+ hash: formatPasswordHash(params, salt, hash),
584
+ salt,
585
+ params,
586
+ createdAt: createdAt()
587
+ };
588
+ }
589
+
590
+ // src/password/needsRehash.ts
591
+ function needsRehash(storedHash, currentOptions = {}) {
592
+ assertPasswordAlgorithm(currentOptions.algorithm);
593
+ let parsed;
594
+ try {
595
+ parsed = parsePasswordHash(storedHash);
596
+ } catch (error) {
597
+ if (error instanceof InvalidPasswordHashError) {
598
+ return true;
599
+ }
600
+ throw error;
601
+ }
602
+ const currentParams = normalizeScryptParams(DEFAULT_PASSWORD_PARAMS, currentOptions.params);
603
+ return parsed.version !== PASSWORD_VERSION || parsed.algorithm !== PASSWORD_ALGORITHM || parsed.params.N !== currentParams.N || parsed.params.r !== currentParams.r || parsed.params.p !== currentParams.p || parsed.params.keyLen !== currentParams.keyLen;
604
+ }
605
+
606
+ // src/core/timingSafe.ts
607
+ var import_node_crypto7 = require("crypto");
608
+ function timingSafeCompare(left, right) {
609
+ if (left.length !== right.length) {
610
+ const paddedLeft = Buffer.alloc(Math.max(left.length, right.length));
611
+ const paddedRight = Buffer.alloc(Math.max(left.length, right.length));
612
+ left.copy(paddedLeft);
613
+ right.copy(paddedRight);
614
+ (0, import_node_crypto7.timingSafeEqual)(paddedLeft, paddedRight);
615
+ return false;
616
+ }
617
+ return (0, import_node_crypto7.timingSafeEqual)(left, right);
618
+ }
619
+
620
+ // src/password/verifyPassword.ts
621
+ async function verifyPassword(password, storedHash, options = {}) {
622
+ assertPassword(password);
623
+ try {
624
+ const parsed = parsePasswordHash(storedHash);
625
+ const saltBuffer = fromBase64Url(parsed.salt, "salt");
626
+ const expected = fromBase64Url(parsed.hash, "password hash");
627
+ const candidate = await deriveScryptKey(applyPepper(password, options.pepper), saltBuffer, parsed.params);
628
+ const valid = timingSafeCompare(Buffer.from(toBase64Url(candidate)), Buffer.from(toBase64Url(expected)));
629
+ return {
630
+ valid,
631
+ needsRehash: valid ? needsRehash(storedHash, options.currentOptions ?? {}) : false,
632
+ algorithm: parsed.algorithm,
633
+ version: parsed.version
634
+ };
635
+ } catch (error) {
636
+ throw new PasswordVerificationError("Password verification failed. Password hashes cannot be decrypted.", {
637
+ cause: error
638
+ });
639
+ }
640
+ }
641
+
642
+ // src/utils/detectPayload.ts
643
+ function detectPayload(value) {
644
+ if (Buffer.isBuffer(value)) {
645
+ return "buffer";
646
+ }
647
+ if (typeof value === "string") {
648
+ if (value.startsWith(PASSWORD_FORMAT_PREFIX)) {
649
+ return "password-hash";
650
+ }
651
+ if (value.startsWith(ENCRYPTION_COMPACT_PREFIX)) {
652
+ return "encrypted-payload";
653
+ }
654
+ try {
655
+ const parsed = JSON.parse(value);
656
+ return parsed.format === ENCRYPTION_FORMAT ? "encrypted-payload" : "json";
657
+ } catch {
658
+ return "plain-text";
659
+ }
660
+ }
661
+ if (value && typeof value === "object" && value.format === ENCRYPTION_FORMAT) {
662
+ return "encrypted-payload";
663
+ }
664
+ if (value && typeof value === "object") {
665
+ return "json";
666
+ }
667
+ return "unknown";
668
+ }
669
+
670
+ // src/client/SecureHashVaultClient.ts
671
+ var SecureHashVaultClient = class {
672
+ constructor(options = {}) {
673
+ this.options = options;
674
+ this.password = {
675
+ hash: (password, options2) => hashPassword(password, { ...this.options.password, ...options2 }),
676
+ verify: (password, storedHash, options2) => {
677
+ const currentOptions = options2?.currentOptions ?? this.options.password;
678
+ return verifyPassword(password, storedHash, {
679
+ ...options2,
680
+ ...currentOptions ? { currentOptions } : {}
681
+ });
682
+ },
683
+ needsRehash: (storedHash, options2) => needsRehash(storedHash, { ...this.options.password, ...options2 })
684
+ };
685
+ this.crypto = {
686
+ encryptText: (plainText, secret, options2) => encryptText(plainText, secret, { ...this.options.encryption, ...options2 }),
687
+ decryptText,
688
+ encryptJson: (data, secret, options2) => encryptJson(data, secret, { ...this.options.encryption, ...options2 }),
689
+ decryptJson,
690
+ encryptBuffer: (buffer, secret, options2) => encryptBuffer(buffer, secret, { ...this.options.encryption, ...options2 }),
691
+ decryptBuffer,
692
+ encryptFile: (inputPath, outputPath, secret, options2) => encryptFile(inputPath, outputPath, secret, { ...this.options.encryption, ...options2 }),
693
+ decryptFile
694
+ };
695
+ }
696
+ options;
697
+ password;
698
+ crypto;
699
+ hash(password, options) {
700
+ return this.password.hash(password, options);
701
+ }
702
+ verify(password, storedHash, options) {
703
+ return this.password.verify(password, storedHash, options);
704
+ }
705
+ async encrypt(data, secret, options) {
706
+ if (Buffer.isBuffer(data)) {
707
+ return this.crypto.encryptBuffer(data, secret, options);
708
+ }
709
+ if (typeof data === "string") {
710
+ return this.crypto.encryptText(data, secret, options);
711
+ }
712
+ if (data && typeof data === "object") {
713
+ return this.crypto.encryptJson(data, secret, options);
714
+ }
715
+ throw new InvalidPayloadError("encrypt() supports strings, Buffers, objects, and arrays.");
716
+ }
717
+ async decrypt(payload, secret, options) {
718
+ if (options?.as === "buffer") {
719
+ return this.crypto.decryptBuffer(payload, secret, options);
720
+ }
721
+ const decryptedText = await this.crypto.decryptText(payload, secret, options);
722
+ if (options?.as === "json") {
723
+ return JSON.parse(decryptedText);
724
+ }
725
+ const detected = detectPayload(decryptedText);
726
+ if (detected === "json") {
727
+ try {
728
+ return JSON.parse(decryptedText);
729
+ } catch {
730
+ return decryptedText;
731
+ }
732
+ }
733
+ return decryptedText;
734
+ }
735
+ decryptPasswordHash() {
736
+ throw new InvalidPayloadError("Password hashes cannot be decrypted. Use verifyPassword() or vault.password.verify().");
737
+ }
738
+ };
739
+
740
+ // src/client/createSecureHashVault.ts
741
+ function createCipherForge(options = {}) {
742
+ return new SecureHashVaultClient(options);
743
+ }
744
+ var createCipherVault = createCipherForge;
745
+ var createSecureHashVault = createCipherVault;
746
+
747
+ // src/index.ts
748
+ var cipherForge = createCipherForge();
749
+ var CipherForge = cipherForge;
750
+ var cipherVault = cipherForge;
751
+ var secureVault = cipherForge;
752
+ var index_default = cipherForge;
753
+ // Annotate the CommonJS export names for ESM import in node:
754
+ 0 && (module.exports = {
755
+ CipherForge,
756
+ CipherForgeError,
757
+ CipherVaultError,
758
+ DecryptionError,
759
+ EncryptionError,
760
+ InvalidConfigError,
761
+ InvalidPasswordHashError,
762
+ InvalidPayloadError,
763
+ PasswordVerificationError,
764
+ SecureHashVaultClient,
765
+ SecureHashVaultError,
766
+ cipherForge,
767
+ cipherVault,
768
+ createCipherForge,
769
+ createCipherVault,
770
+ createSecureHashVault,
771
+ decryptBuffer,
772
+ decryptFile,
773
+ decryptJson,
774
+ decryptText,
775
+ detectPayload,
776
+ encryptBuffer,
777
+ encryptFile,
778
+ encryptJson,
779
+ encryptText,
780
+ fromBase64Url,
781
+ fromHex,
782
+ generatePepper,
783
+ generateSalt,
784
+ hashPassword,
785
+ needsRehash,
786
+ randomBuffer,
787
+ secureVault,
788
+ toBase64Url,
789
+ toHex,
790
+ verifyPassword
791
+ });
792
+ //# sourceMappingURL=index.cjs.map