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