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/src/wilcocrypt.js CHANGED
@@ -1,7 +1,22 @@
1
- import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from 'crypto';
2
- import { encode as msgpack_encode, decode as msgpack_decode } from 'notepack.io';
3
- import { gzipSync, gunzipSync } from 'zlib';
4
- import { readFileSync, writeFileSync } from 'fs';
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 = 'WILCOCRYPT_ERROR') {
43
+ constructor(message, code = "WILCOCRYPT_ERROR") {
29
44
  super(message);
30
- this.name = 'WilcoCryptError';
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 = '2.1.1';
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
- 'Invalid encryption key (expected 32-byte Buffer)',
73
- 'INVALID_KEY'
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
- 'Invalid IV (expected 12-byte Buffer for GCM)',
80
- 'INVALID_IV'
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 (typeof password !== 'string' || password.length < wilcocrypt._.MIN_PASSWORD_LENGTH) {
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
- 'WEAK_PASSWORD'
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('aes-256-gcm', key, iv);
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 {string} cipherHex
149
- * @param {string} authTagHex
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 (cipherHex, authTagHex, key, iv) {
175
+ wilcocrypt._.decryptData = function (cipherBuffer, authTagBuffer, key, iv) {
155
176
  wilcocrypt._.assertKeyAndIv(key, iv);
156
177
 
157
178
  try {
158
- const decipher = createDecipheriv('aes-256-gcm', key, iv);
159
- decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
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
- 'Decryption failed (invalid password, corrupted data, or tampered file)',
168
- 'DECRYPTION_FAILED'
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} MessagePack-encoded encrypted payload
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
- wilcocrypt._.assertPassword(password);
208
+ wilcocrypt._.assertPassword(password);
188
209
 
189
- let gzipData;
190
- if (gzip) {
191
- gzipData = gzipSync(plaindata);
192
- } else {
193
- gzipData = plaindata;
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
- const iv = randomBytes(12);
197
- const salt = randomBytes(16);
250
+ const iv = randomBytes(12);
251
+ const salt = randomBytes(16);
198
252
 
199
- const key = scryptSync(password, salt, 32);
253
+ const key = await scryptAsync(password, salt, 32);
200
254
 
201
- const { ciphertext, authTag } = wilcocrypt._.encryptData(gzipData, key, iv);
255
+ const { ciphertext, authTag } = wilcocrypt._.encryptData(gzipData, key, iv);
202
256
 
203
- const envelope = {
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
- return msgpack_encode(envelope);
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
- * @param {Buffer} encryptedData - MessagePack-encoded encrypted payload
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 format, wrong password, version mismatch, or decompression failure
279
+ * @throws {WilcoCryptError} On invalid header, version mismatch, wrong password, or corrupted data
222
280
  */
223
- wilcocrypt.decryptData = function (encryptedData, password, gzip = true) {
224
- wilcocrypt._.assertPassword(password);
225
-
226
- let envelope;
227
- try {
228
- envelope = msgpack_decode(encryptedData);
229
- } catch {
230
- throw new WilcoCryptError(
231
- 'Invalid encrypted data format (not MessagePack)',
232
- 'INVALID_FORMAT'
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
- if (envelope.version !== wilcocrypt._.VERSION) {
237
- throw new WilcoCryptError(
238
- `Version mismatch (expected ${wilcocrypt._.VERSION}, got ${envelope.version})`,
239
- 'VERSION_MISMATCH'
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
- const key = scryptSync(password, Buffer.from(envelope.salt, 'hex'), 32);
303
+ const salt = encryptedBuffer.subarray(offset, (offset += 16));
304
+ const iv = encryptedBuffer.subarray(offset, (offset += 12));
244
305
 
245
- const decrypted = wilcocrypt._.decryptData(
246
- envelope.payload,
247
- envelope.authTag,
248
- key,
249
- Buffer.from(envelope.iv, 'hex')
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
- try {
253
- if (gzip) {
254
- return gunzipSync(decrypted);
255
- } else {
256
- return decrypted;
257
- }
258
- } catch {
259
- throw new WilcoCryptError(
260
- 'Decryption succeeded but decompression failed (data may be corrupted or not compressed)',
261
- 'DECOMPRESSION_FAILED'
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
- const fileData = readFileSync(filePath);
277
- const encryptedData = wilcocrypt.encryptData(fileData, password, gzip);
278
- writeFileSync(`${filePath}.enc`, encryptedData);
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 (filePath, password, gzip = true) {
291
- if (!filePath.endsWith('.enc')) {
292
- throw new WilcoCryptError(
293
- 'Invalid file extension (expected .enc)',
294
- 'INVALID_FILE_EXTENSION'
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
- const encryptedData = readFileSync(filePath);
299
- return wilcocrypt.decryptData(encryptedData, password, gzip);
626
+ await handle.close();
300
627
  };
301
628
 
302
629
  export default wilcocrypt;