wilcocrypt 2.1.1 → 2.2.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/.github/ISSUE_TEMPLATE/bug.md +72 -0
- package/.github/ISSUE_TEMPLATE/config.yml +6 -0
- package/.github/ISSUE_TEMPLATE/feature.yml +38 -0
- package/.github/dependabot.yml +11 -0
- package/CHANGELOG.md +100 -0
- package/DOCS.md +527 -0
- package/README.md +61 -65
- package/SECURITY.md +48 -0
- package/package.json +14 -8
- package/src/cli.js +57 -37
- package/src/wilcocrypt.js +429 -102
- package/types/wilcocrypt.d.ts +309 -24
package/src/wilcocrypt.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
randomBytes,
|
|
3
|
+
scryptSync,
|
|
4
|
+
scrypt,
|
|
5
|
+
createCipheriv,
|
|
6
|
+
createDecipheriv,
|
|
7
|
+
} from "crypto";
|
|
8
|
+
import { gzipSync, gunzipSync, createGzip, createGunzip } from "zlib";
|
|
9
|
+
import {
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
createReadStream,
|
|
13
|
+
createWriteStream,
|
|
14
|
+
promises as fsPromises,
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { pipeline } from "stream/promises";
|
|
17
|
+
import { promisify } from "util";
|
|
18
|
+
|
|
19
|
+
const scryptAsync = promisify(scrypt);
|
|
5
20
|
|
|
6
21
|
/**
|
|
7
22
|
* Main WilcoCrypt namespace.
|
|
@@ -25,9 +40,9 @@ class WilcoCryptError extends Error {
|
|
|
25
40
|
* @param {string} message - Human-readable error message
|
|
26
41
|
* @param {string} [code=WILCOCRYPT_ERROR] - Machine-readable error code
|
|
27
42
|
*/
|
|
28
|
-
constructor(message, code =
|
|
43
|
+
constructor(message, code = "WILCOCRYPT_ERROR") {
|
|
29
44
|
super(message);
|
|
30
|
-
this.name =
|
|
45
|
+
this.name = "WilcoCryptError";
|
|
31
46
|
this.code = code;
|
|
32
47
|
|
|
33
48
|
if (Error.captureStackTrace) {
|
|
@@ -47,7 +62,7 @@ wilcocrypt._.WilcoCryptError = WilcoCryptError;
|
|
|
47
62
|
* Must match exactly during decryption.
|
|
48
63
|
* @type {string}
|
|
49
64
|
*/
|
|
50
|
-
wilcocrypt._.VERSION =
|
|
65
|
+
wilcocrypt._.VERSION = "2.2.0";
|
|
51
66
|
|
|
52
67
|
/**
|
|
53
68
|
* Minimum allowed password length.
|
|
@@ -55,6 +70,12 @@ wilcocrypt._.VERSION = '2.1.1';
|
|
|
55
70
|
*/
|
|
56
71
|
wilcocrypt._.MIN_PASSWORD_LENGTH = 6;
|
|
57
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Internal header for encrypted payloads.
|
|
75
|
+
* @type {Buffer}
|
|
76
|
+
*/
|
|
77
|
+
wilcocrypt._.HEADER = Buffer.from([23, 9, 12, 3, 15, 3, 18, 25, 16, 20]);
|
|
78
|
+
|
|
58
79
|
/* =========================
|
|
59
80
|
Internal helpers
|
|
60
81
|
========================= */
|
|
@@ -69,15 +90,15 @@ wilcocrypt._.MIN_PASSWORD_LENGTH = 6;
|
|
|
69
90
|
wilcocrypt._.assertKeyAndIv = function (key, iv) {
|
|
70
91
|
if (!Buffer.isBuffer(key) || key.length !== 32) {
|
|
71
92
|
throw new WilcoCryptError(
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
"Invalid encryption key (expected 32-byte Buffer)",
|
|
94
|
+
"INVALID_KEY",
|
|
74
95
|
);
|
|
75
96
|
}
|
|
76
97
|
|
|
77
98
|
if (!Buffer.isBuffer(iv) || iv.length !== 12) {
|
|
78
99
|
throw new WilcoCryptError(
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
"Invalid IV (expected 12-byte Buffer)",
|
|
101
|
+
"INVALID_IV",
|
|
81
102
|
);
|
|
82
103
|
}
|
|
83
104
|
};
|
|
@@ -89,10 +110,13 @@ wilcocrypt._.assertKeyAndIv = function (key, iv) {
|
|
|
89
110
|
* @throws {WilcoCryptError}
|
|
90
111
|
*/
|
|
91
112
|
wilcocrypt._.assertPassword = function (password) {
|
|
92
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
typeof password !== "string" ||
|
|
115
|
+
password.length < wilcocrypt._.MIN_PASSWORD_LENGTH
|
|
116
|
+
) {
|
|
93
117
|
throw new WilcoCryptError(
|
|
94
118
|
`Password must be at least ${wilcocrypt._.MIN_PASSWORD_LENGTH} characters`,
|
|
95
|
-
|
|
119
|
+
"WEAK_PASSWORD",
|
|
96
120
|
);
|
|
97
121
|
}
|
|
98
122
|
};
|
|
@@ -130,42 +154,36 @@ wilcocrypt._.constantTimeEqual = function (a, b) {
|
|
|
130
154
|
wilcocrypt._.encryptData = function (plainData, key, iv) {
|
|
131
155
|
wilcocrypt._.assertKeyAndIv(key, iv);
|
|
132
156
|
|
|
133
|
-
const cipher = createCipheriv(
|
|
134
|
-
const encrypted = Buffer.concat([
|
|
135
|
-
cipher.update(plainData),
|
|
136
|
-
cipher.final()
|
|
137
|
-
]);
|
|
157
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
158
|
+
const encrypted = Buffer.concat([cipher.update(plainData), cipher.final()]);
|
|
138
159
|
|
|
139
160
|
return {
|
|
140
161
|
ciphertext: encrypted,
|
|
141
|
-
authTag: cipher.getAuthTag()
|
|
162
|
+
authTag: cipher.getAuthTag(),
|
|
142
163
|
};
|
|
143
164
|
};
|
|
144
165
|
|
|
145
166
|
/**
|
|
146
167
|
* Decrypts AES-256-GCM encrypted data.
|
|
147
168
|
*
|
|
148
|
-
* @param {
|
|
149
|
-
* @param {
|
|
169
|
+
* @param {Buffer} cipherBuffer
|
|
170
|
+
* @param {Buffer} authTagBuffer
|
|
150
171
|
* @param {Buffer} key
|
|
151
172
|
* @param {Buffer} iv
|
|
152
173
|
* @returns {Buffer}
|
|
153
174
|
*/
|
|
154
|
-
wilcocrypt._.decryptData = function (
|
|
175
|
+
wilcocrypt._.decryptData = function (cipherBuffer, authTagBuffer, key, iv) {
|
|
155
176
|
wilcocrypt._.assertKeyAndIv(key, iv);
|
|
156
177
|
|
|
157
178
|
try {
|
|
158
|
-
const decipher = createDecipheriv(
|
|
159
|
-
decipher.setAuthTag(
|
|
179
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
180
|
+
decipher.setAuthTag(authTagBuffer);
|
|
160
181
|
|
|
161
|
-
return Buffer.concat([
|
|
162
|
-
decipher.update(Buffer.from(cipherHex, 'hex')),
|
|
163
|
-
decipher.final()
|
|
164
|
-
]);
|
|
182
|
+
return Buffer.concat([decipher.update(cipherBuffer), decipher.final()]);
|
|
165
183
|
} catch {
|
|
166
184
|
throw new WilcoCryptError(
|
|
167
|
-
|
|
168
|
-
|
|
185
|
+
"Decryption failed (invalid password, corrupted data, or tampered file)",
|
|
186
|
+
"DECRYPTION_FAILED",
|
|
169
187
|
);
|
|
170
188
|
}
|
|
171
189
|
};
|
|
@@ -177,90 +195,183 @@ wilcocrypt._.decryptData = function (cipherHex, authTagHex, key, iv) {
|
|
|
177
195
|
/**
|
|
178
196
|
* Encrypts data using password-based AES-256-GCM.
|
|
179
197
|
*
|
|
198
|
+
* Output format:
|
|
199
|
+
* [HEADER (10 bytes)] + [VERSION (dynamic)] + [salt (16)] + [iv (12)] + [ciphertext] + [authTag (16)]
|
|
200
|
+
*
|
|
180
201
|
* @param {Buffer} plaindata - Raw data to encrypt
|
|
181
202
|
* @param {string} password - Password used for key derivation
|
|
182
203
|
* @param {boolean} [gzip=true] - Whether to compress data before encryption
|
|
183
|
-
* @returns {Buffer}
|
|
204
|
+
* @returns {Buffer} Binary-encoded encrypted payload
|
|
184
205
|
* @throws {WilcoCryptError} If password is invalid
|
|
185
206
|
*/
|
|
186
207
|
wilcocrypt.encryptData = function (plaindata, password, gzip = true) {
|
|
187
|
-
|
|
208
|
+
wilcocrypt._.assertPassword(password);
|
|
188
209
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
210
|
+
const gzipData = gzip ? gzipSync(plaindata) : plaindata;
|
|
211
|
+
const iv = randomBytes(12);
|
|
212
|
+
const salt = randomBytes(16);
|
|
213
|
+
|
|
214
|
+
const key = scryptSync(password, salt, 32);
|
|
215
|
+
|
|
216
|
+
const { ciphertext, authTag } = wilcocrypt._.encryptData(gzipData, key, iv);
|
|
217
|
+
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
218
|
+
|
|
219
|
+
return Buffer.concat([
|
|
220
|
+
wilcocrypt._.HEADER, // 10 bytes
|
|
221
|
+
versionBuf, // dynamic
|
|
222
|
+
salt, // 16 bytes
|
|
223
|
+
iv, // 12 bytes
|
|
224
|
+
ciphertext, // variable
|
|
225
|
+
authTag, // 16 bytes (at the end for streaming compatibility)
|
|
226
|
+
]);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Encrypts data asynchronously using password-based AES-256-GCM.
|
|
231
|
+
*
|
|
232
|
+
* Output format:
|
|
233
|
+
* [HEADER (10 bytes)] + [VERSION (dynamic)] + [salt (16)] + [iv (12)] + [ciphertext] + [authTag (16)]
|
|
234
|
+
*
|
|
235
|
+
* @param {Buffer} plaindata - Raw data to encrypt
|
|
236
|
+
* @param {string} password - Password used for key derivation
|
|
237
|
+
* @param {boolean} [gzip=true] - Whether to compress data before encryption
|
|
238
|
+
* @returns {Promise<Buffer>} Binary-encoded encrypted payload
|
|
239
|
+
* @throws {WilcoCryptError} If password is invalid
|
|
240
|
+
*/
|
|
241
|
+
wilcocrypt.encryptDataAsync = async function (
|
|
242
|
+
plaindata,
|
|
243
|
+
password,
|
|
244
|
+
gzip = true,
|
|
245
|
+
) {
|
|
246
|
+
wilcocrypt._.assertPassword(password);
|
|
247
|
+
|
|
248
|
+
const gzipData = gzip ? gzipSync(plaindata) : plaindata;
|
|
195
249
|
|
|
196
|
-
|
|
197
|
-
|
|
250
|
+
const iv = randomBytes(12);
|
|
251
|
+
const salt = randomBytes(16);
|
|
198
252
|
|
|
199
|
-
|
|
253
|
+
const key = await scryptAsync(password, salt, 32);
|
|
200
254
|
|
|
201
|
-
|
|
255
|
+
const { ciphertext, authTag } = wilcocrypt._.encryptData(gzipData, key, iv);
|
|
202
256
|
|
|
203
|
-
|
|
204
|
-
payload: ciphertext.toString('hex'),
|
|
205
|
-
authTag: authTag.toString('hex'),
|
|
206
|
-
salt: salt.toString('hex'),
|
|
207
|
-
iv: iv.toString('hex'),
|
|
208
|
-
version: wilcocrypt._.VERSION
|
|
209
|
-
};
|
|
257
|
+
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
210
258
|
|
|
211
|
-
|
|
259
|
+
return Buffer.concat([
|
|
260
|
+
wilcocrypt._.HEADER,
|
|
261
|
+
versionBuf,
|
|
262
|
+
salt,
|
|
263
|
+
iv,
|
|
264
|
+
ciphertext,
|
|
265
|
+
authTag,
|
|
266
|
+
]);
|
|
212
267
|
};
|
|
213
268
|
|
|
214
269
|
/**
|
|
215
270
|
* Decrypts encrypted data using password-based AES-256-GCM.
|
|
216
271
|
*
|
|
217
|
-
*
|
|
272
|
+
* Validates internal header and version, then extracts:
|
|
273
|
+
* salt, iv, authTag and ciphertext from the binary payload.
|
|
274
|
+
*
|
|
275
|
+
* @param {Buffer} encryptedBuffer - Binary-encoded encrypted payload
|
|
218
276
|
* @param {string} password - Password used for decryption
|
|
219
277
|
* @param {boolean} [gzip=true] - Whether to decompress after decryption
|
|
220
278
|
* @returns {Buffer} Decrypted raw data
|
|
221
|
-
* @throws {WilcoCryptError} On invalid
|
|
279
|
+
* @throws {WilcoCryptError} On invalid header, version mismatch, wrong password, or corrupted data
|
|
222
280
|
*/
|
|
223
|
-
wilcocrypt.decryptData = function (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
281
|
+
wilcocrypt.decryptData = function (encryptedBuffer, password, gzip = true) {
|
|
282
|
+
wilcocrypt._.assertPassword(password);
|
|
283
|
+
|
|
284
|
+
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
285
|
+
let offset = 0;
|
|
286
|
+
|
|
287
|
+
const fileHeader = encryptedBuffer.subarray(
|
|
288
|
+
offset,
|
|
289
|
+
(offset += wilcocrypt._.HEADER.length),
|
|
290
|
+
);
|
|
291
|
+
if (!fileHeader.equals(wilcocrypt._.HEADER)) {
|
|
292
|
+
throw new WilcoCryptError("Invalid WilcoCrypt header", "INVALID_HEADER");
|
|
293
|
+
}
|
|
235
294
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
295
|
+
const fileVersion = encryptedBuffer.subarray(
|
|
296
|
+
offset,
|
|
297
|
+
(offset += versionBuf.length),
|
|
298
|
+
);
|
|
299
|
+
if (!fileVersion.equals(versionBuf)) {
|
|
300
|
+
throw new WilcoCryptError("Version mismatch", "VERSION_MISMATCH");
|
|
301
|
+
}
|
|
242
302
|
|
|
243
|
-
|
|
303
|
+
const salt = encryptedBuffer.subarray(offset, (offset += 16));
|
|
304
|
+
const iv = encryptedBuffer.subarray(offset, (offset += 12));
|
|
244
305
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
306
|
+
// authTag are the last 16 bytes; ciphertext is everything in between
|
|
307
|
+
const authTag = encryptedBuffer.subarray(encryptedBuffer.length - 16);
|
|
308
|
+
const ciphertext = encryptedBuffer.subarray(
|
|
309
|
+
offset,
|
|
310
|
+
encryptedBuffer.length - 16,
|
|
311
|
+
);
|
|
251
312
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
313
|
+
const key = scryptSync(password, salt, 32);
|
|
314
|
+
|
|
315
|
+
const decrypted = wilcocrypt._.decryptData(ciphertext, authTag, key, iv);
|
|
316
|
+
|
|
317
|
+
return gzip ? gunzipSync(decrypted) : decrypted;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Decrypts encrypted data asynchronously using password-based AES-256-GCM.
|
|
322
|
+
*
|
|
323
|
+
* Validates internal header and version, then extracts:
|
|
324
|
+
* salt, iv, authTag and ciphertext from the binary payload.
|
|
325
|
+
*
|
|
326
|
+
* @param {Buffer} encryptedBuffer - Binary-encoded encrypted payload
|
|
327
|
+
* @param {string} password - Password used for decryption
|
|
328
|
+
* @param {boolean} [gzip=true] - Whether to decompress after decryption
|
|
329
|
+
* @returns {Promise<Buffer>} Decrypted raw data
|
|
330
|
+
* @throws {WilcoCryptError} On invalid header, version mismatch, wrong password, or corrupted data
|
|
331
|
+
*/
|
|
332
|
+
wilcocrypt.decryptDataAsync = async function (
|
|
333
|
+
encryptedBuffer,
|
|
334
|
+
password,
|
|
335
|
+
gzip = true,
|
|
336
|
+
) {
|
|
337
|
+
wilcocrypt._.assertPassword(password);
|
|
338
|
+
|
|
339
|
+
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
340
|
+
let offset = 0;
|
|
341
|
+
|
|
342
|
+
const fileHeader = encryptedBuffer.subarray(
|
|
343
|
+
offset,
|
|
344
|
+
(offset += wilcocrypt._.HEADER.length),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (!fileHeader.equals(wilcocrypt._.HEADER)) {
|
|
348
|
+
throw new WilcoCryptError("Invalid WilcoCrypt header", "INVALID_HEADER");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const fileVersion = encryptedBuffer.subarray(
|
|
352
|
+
offset,
|
|
353
|
+
(offset += versionBuf.length),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (!fileVersion.equals(versionBuf)) {
|
|
357
|
+
throw new WilcoCryptError("Version mismatch", "VERSION_MISMATCH");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const salt = encryptedBuffer.subarray(offset, (offset += 16));
|
|
361
|
+
const iv = encryptedBuffer.subarray(offset, (offset += 12));
|
|
362
|
+
|
|
363
|
+
const authTag = encryptedBuffer.subarray(encryptedBuffer.length - 16);
|
|
364
|
+
|
|
365
|
+
const ciphertext = encryptedBuffer.subarray(
|
|
366
|
+
offset,
|
|
367
|
+
encryptedBuffer.length - 16,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const key = await scryptAsync(password, salt, 32);
|
|
371
|
+
|
|
372
|
+
const decrypted = wilcocrypt._.decryptData(ciphertext, authTag, key, iv);
|
|
373
|
+
|
|
374
|
+
return gzip ? gunzipSync(decrypted) : decrypted;
|
|
264
375
|
};
|
|
265
376
|
|
|
266
377
|
/**
|
|
@@ -273,30 +384,246 @@ wilcocrypt.decryptData = function (encryptedData, password, gzip = true) {
|
|
|
273
384
|
* @throws {WilcoCryptError} If password is invalid
|
|
274
385
|
*/
|
|
275
386
|
wilcocrypt.encryptFile = function (filePath, password, gzip = true) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
387
|
+
const fileData = readFileSync(filePath);
|
|
388
|
+
const encryptedData = wilcocrypt.encryptData(fileData, password, gzip);
|
|
389
|
+
writeFileSync(`${filePath}.enc`, encryptedData);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Encrypts a file asynchronously and writes the result to `<filePath>.enc`.
|
|
394
|
+
*
|
|
395
|
+
* @param {string} filePath - Path to the file to encrypt
|
|
396
|
+
* @param {string} password - Password used for encryption
|
|
397
|
+
* @param {boolean} [gzip=true] - Whether to compress before encryption
|
|
398
|
+
* @returns {Promise<void>}
|
|
399
|
+
* @throws {WilcoCryptError} If password is invalid
|
|
400
|
+
*/
|
|
401
|
+
wilcocrypt.encryptFileAsync = async function (filePath, password, gzip = true) {
|
|
402
|
+
const fileData = await fsPromises.readFile(filePath);
|
|
403
|
+
|
|
404
|
+
const encryptedData = await wilcocrypt.encryptDataAsync(
|
|
405
|
+
fileData,
|
|
406
|
+
password,
|
|
407
|
+
gzip,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
await fsPromises.writeFile(`${filePath}.enc`, encryptedData);
|
|
279
411
|
};
|
|
280
412
|
|
|
281
413
|
/**
|
|
282
414
|
* Decrypts an encrypted `.enc` file.
|
|
283
415
|
*
|
|
416
|
+
* If `outputPath` is provided, the decrypted data is written to that file
|
|
417
|
+
* and `undefined` is returned. Otherwise the decrypted Buffer is returned.
|
|
418
|
+
*
|
|
284
419
|
* @param {string} filePath - Path to the `.enc` file
|
|
285
420
|
* @param {string} password - Password used for decryption
|
|
421
|
+
* @param {string|boolean} [outputPath] - Optional path to write decrypted output to.
|
|
422
|
+
* If omitted (or `true`/`false`), the function returns the decrypted Buffer instead.
|
|
286
423
|
* @param {boolean} [gzip=true] - Whether to decompress after decryption
|
|
287
|
-
* @returns {Buffer} Decrypted file contents
|
|
424
|
+
* @returns {Buffer|undefined} Decrypted file contents, or undefined if outputPath was given
|
|
288
425
|
* @throws {WilcoCryptError} If file extension is invalid or decryption fails
|
|
289
426
|
*/
|
|
290
|
-
wilcocrypt.decryptFile = function (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
427
|
+
wilcocrypt.decryptFile = function (
|
|
428
|
+
filePath,
|
|
429
|
+
password,
|
|
430
|
+
outputPath,
|
|
431
|
+
gzip = true,
|
|
432
|
+
) {
|
|
433
|
+
// Support legacy 3-argument form: decryptFile(filePath, password, gzip?)
|
|
434
|
+
if (typeof outputPath === "boolean") {
|
|
435
|
+
gzip = outputPath;
|
|
436
|
+
outputPath = undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!filePath.endsWith(".enc")) {
|
|
440
|
+
throw new WilcoCryptError(
|
|
441
|
+
"Invalid file extension (expected .enc)",
|
|
442
|
+
"INVALID_FILE_EXTENSION",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const encryptedData = readFileSync(filePath);
|
|
447
|
+
const decrypted = wilcocrypt.decryptData(encryptedData, password, gzip);
|
|
448
|
+
|
|
449
|
+
if (outputPath) {
|
|
450
|
+
writeFileSync(outputPath, decrypted);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return decrypted;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Decrypts an encrypted `.enc` file asynchronously.
|
|
459
|
+
*
|
|
460
|
+
* If `outputPath` is provided, the decrypted data is written to that file
|
|
461
|
+
* and `undefined` is returned. Otherwise the decrypted Buffer is returned.
|
|
462
|
+
*
|
|
463
|
+
* @param {string} filePath - Path to the `.enc` file
|
|
464
|
+
* @param {string} password - Password used for decryption
|
|
465
|
+
* @param {string|boolean} [outputPath] - Optional output path
|
|
466
|
+
* @param {boolean} [gzip=true] - Whether to decompress after decryption
|
|
467
|
+
* @returns {Promise<Buffer|undefined>}
|
|
468
|
+
* @throws {WilcoCryptError}
|
|
469
|
+
*/
|
|
470
|
+
wilcocrypt.decryptFileAsync = async function (
|
|
471
|
+
filePath,
|
|
472
|
+
password,
|
|
473
|
+
outputPath,
|
|
474
|
+
gzip = true,
|
|
475
|
+
) {
|
|
476
|
+
if (typeof outputPath === "boolean") {
|
|
477
|
+
gzip = outputPath;
|
|
478
|
+
outputPath = undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!filePath.endsWith(".enc")) {
|
|
482
|
+
throw new WilcoCryptError(
|
|
483
|
+
"Invalid file extension (expected .enc)",
|
|
484
|
+
"INVALID_FILE_EXTENSION",
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const encryptedData = await fsPromises.readFile(filePath);
|
|
489
|
+
|
|
490
|
+
const decrypted = await wilcocrypt.decryptDataAsync(
|
|
491
|
+
encryptedData,
|
|
492
|
+
password,
|
|
493
|
+
gzip,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
if (outputPath) {
|
|
497
|
+
await fsPromises.writeFile(outputPath, decrypted);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return decrypted;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Encrypts a file using streams and writes the result to `outputPath`.
|
|
506
|
+
* Memory-efficient alternative to `encryptFile` for large files.
|
|
507
|
+
*
|
|
508
|
+
* Output format:
|
|
509
|
+
* [HEADER] + [VERSION] + [salt (16)] + [iv (12)] + [ciphertext] + [authTag (16)]
|
|
510
|
+
*
|
|
511
|
+
* @param {string} inputPath - Path to the file to encrypt
|
|
512
|
+
* @param {string} outputPath - Path to write the encrypted output to
|
|
513
|
+
* @param {string} password - Password used for key derivation
|
|
514
|
+
* @param {boolean} [gzip=true] - Whether to compress data before encryption
|
|
515
|
+
* @returns {Promise<void>}
|
|
516
|
+
* @throws {WilcoCryptError} If password is invalid
|
|
517
|
+
*/
|
|
518
|
+
wilcocrypt.encryptFileStream = async function (
|
|
519
|
+
inputPath,
|
|
520
|
+
outputPath,
|
|
521
|
+
password,
|
|
522
|
+
gzip = true,
|
|
523
|
+
) {
|
|
524
|
+
wilcocrypt._.assertPassword(password);
|
|
525
|
+
|
|
526
|
+
const salt = randomBytes(16);
|
|
527
|
+
const iv = randomBytes(12);
|
|
528
|
+
const key = scryptSync(password, salt, 32);
|
|
529
|
+
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
530
|
+
|
|
531
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
532
|
+
const writeStream = createWriteStream(outputPath);
|
|
533
|
+
|
|
534
|
+
writeStream.write(wilcocrypt._.HEADER);
|
|
535
|
+
writeStream.write(versionBuf);
|
|
536
|
+
writeStream.write(salt);
|
|
537
|
+
writeStream.write(iv);
|
|
538
|
+
|
|
539
|
+
const pipelineSteps = [createReadStream(inputPath)];
|
|
540
|
+
if (gzip) pipelineSteps.push(createGzip());
|
|
541
|
+
pipelineSteps.push(cipher);
|
|
542
|
+
pipelineSteps.push(writeStream);
|
|
543
|
+
|
|
544
|
+
// end: false so we can still append the authTag after the pipeline finishes
|
|
545
|
+
await pipeline(...pipelineSteps, { end: false });
|
|
546
|
+
writeStream.end(cipher.getAuthTag());
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Decrypts an encrypted `.enc` file using streams.
|
|
551
|
+
* Memory-efficient alternative to `decryptFile` for large files.
|
|
552
|
+
* Cleans up the output file automatically if decryption or integrity check fails.
|
|
553
|
+
*
|
|
554
|
+
* @param {string} inputPath - Path to the encrypted `.enc` file
|
|
555
|
+
* @param {string} outputPath - Path to write the decrypted output to
|
|
556
|
+
* @param {string} password - Password used for decryption
|
|
557
|
+
* @param {boolean} [gzip=true] - Whether to decompress after decryption
|
|
558
|
+
* @returns {Promise<void>}
|
|
559
|
+
* @throws {WilcoCryptError} On invalid header, version mismatch, or decryption/integrity failure
|
|
560
|
+
*/
|
|
561
|
+
wilcocrypt.decryptFileStream = async function (
|
|
562
|
+
inputPath,
|
|
563
|
+
outputPath,
|
|
564
|
+
password,
|
|
565
|
+
gzip = true,
|
|
566
|
+
) {
|
|
567
|
+
wilcocrypt._.assertPassword(password);
|
|
568
|
+
|
|
569
|
+
const handle = await fsPromises.open(inputPath, "r");
|
|
570
|
+
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
571
|
+
|
|
572
|
+
const headLen = wilcocrypt._.HEADER.length;
|
|
573
|
+
const verLen = versionBuf.length;
|
|
574
|
+
|
|
575
|
+
const headerCheck = Buffer.alloc(headLen);
|
|
576
|
+
const versionCheck = Buffer.alloc(verLen);
|
|
577
|
+
const salt = Buffer.alloc(16);
|
|
578
|
+
const iv = Buffer.alloc(12);
|
|
579
|
+
|
|
580
|
+
let currentPos = 0;
|
|
581
|
+
await handle.read(headerCheck, 0, headLen, currentPos);
|
|
582
|
+
currentPos += headLen;
|
|
583
|
+
await handle.read(versionCheck, 0, verLen, currentPos);
|
|
584
|
+
currentPos += verLen;
|
|
585
|
+
await handle.read(salt, 0, 16, currentPos);
|
|
586
|
+
currentPos += 16;
|
|
587
|
+
await handle.read(iv, 0, 12, currentPos);
|
|
588
|
+
currentPos += 12;
|
|
589
|
+
|
|
590
|
+
if (!headerCheck.equals(wilcocrypt._.HEADER)) {
|
|
591
|
+
await handle.close();
|
|
592
|
+
throw new WilcoCryptError("Invalid WilcoCrypt header", "INVALID_HEADER");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!versionCheck.equals(versionBuf)) {
|
|
596
|
+
await handle.close();
|
|
597
|
+
throw new WilcoCryptError("Version mismatch", "VERSION_MISMATCH");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const stats = await handle.stat();
|
|
601
|
+
const authTag = Buffer.alloc(16);
|
|
602
|
+
await handle.read(authTag, 0, 16, stats.size - 16);
|
|
603
|
+
|
|
604
|
+
const key = scryptSync(password, salt, 32);
|
|
605
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
606
|
+
decipher.setAuthTag(authTag);
|
|
607
|
+
|
|
608
|
+
const pipelineSteps = [
|
|
609
|
+
createReadStream(inputPath, { start: currentPos, end: stats.size - 17 }),
|
|
610
|
+
];
|
|
611
|
+
pipelineSteps.push(decipher);
|
|
612
|
+
if (gzip) pipelineSteps.push(createGunzip());
|
|
613
|
+
pipelineSteps.push(createWriteStream(outputPath));
|
|
614
|
+
|
|
615
|
+
try {
|
|
616
|
+
await pipeline(...pipelineSteps);
|
|
617
|
+
} catch {
|
|
618
|
+
await handle.close();
|
|
619
|
+
await fsPromises.unlink(outputPath);
|
|
620
|
+
throw new WilcoCryptError(
|
|
621
|
+
"Decryption failed (invalid password, corrupted data, or tampered file)",
|
|
622
|
+
"DECRYPTION_FAILED",
|
|
623
|
+
);
|
|
624
|
+
}
|
|
297
625
|
|
|
298
|
-
|
|
299
|
-
return wilcocrypt.decryptData(encryptedData, password, gzip);
|
|
626
|
+
await handle.close();
|
|
300
627
|
};
|
|
301
628
|
|
|
302
629
|
export default wilcocrypt;
|