wilcocrypt 2.1.0 → 2.2.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.
@@ -0,0 +1,422 @@
1
+ import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from 'crypto';
2
+ import { gzipSync, gunzipSync, createGzip, createGunzip } from 'zlib';
3
+ import { readFileSync, writeFileSync, createReadStream, createWriteStream, promises as fsPromises } from 'fs';
4
+ import { pipeline } from 'stream/promises';
5
+
6
+ /**
7
+ * Main WilcoCrypt namespace.
8
+ */
9
+ const wilcocrypt = {};
10
+
11
+ /**
12
+ * Internal WilcoCrypt utilities and constants.
13
+ */
14
+ wilcocrypt._ = {};
15
+
16
+ /* =========================
17
+ Custom Error
18
+ ========================= */
19
+
20
+ /**
21
+ * Custom error class for all WilcoCrypt-specific errors.
22
+ */
23
+ class WilcoCryptError extends Error {
24
+ /**
25
+ * @param {string} message - Human-readable error message
26
+ * @param {string} [code=WILCOCRYPT_ERROR] - Machine-readable error code
27
+ */
28
+ constructor (message, code = 'WILCOCRYPT_ERROR') {
29
+ super(message);
30
+ this.name = 'WilcoCryptError';
31
+ this.code = code;
32
+
33
+ if (Error.captureStackTrace) {
34
+ Error.captureStackTrace(this, WilcoCryptError);
35
+ }
36
+ }
37
+ }
38
+
39
+ wilcocrypt._.WilcoCryptError = WilcoCryptError;
40
+
41
+ /* =========================
42
+ Internal constants
43
+ ========================= */
44
+
45
+ /**
46
+ * Internal WilcoCrypt version.
47
+ * Must match exactly during decryption.
48
+ * @type {string}
49
+ */
50
+ wilcocrypt._.VERSION = '2.2.0';
51
+
52
+ /**
53
+ * Minimum allowed password length.
54
+ * @type {number}
55
+ */
56
+ wilcocrypt._.MIN_PASSWORD_LENGTH = 6;
57
+
58
+ /**
59
+ * Internal header for encrypted payloads.
60
+ * @type {Buffer}
61
+ */
62
+ wilcocrypt._.HEADER = Buffer.from([23, 9, 12, 3, 15, 3, 18, 25, 16, 20]);
63
+
64
+ /* =========================
65
+ Internal helpers
66
+ ========================= */
67
+
68
+ /**
69
+ * Validates AES-256-GCM key and IV.
70
+ *
71
+ * @param {Buffer} key
72
+ * @param {Buffer} iv
73
+ * @throws {WilcoCryptError}
74
+ */
75
+ wilcocrypt._.assertKeyAndIv = function (key, iv) {
76
+ if (!Buffer.isBuffer(key) || key.length !== 32) {
77
+ throw new WilcoCryptError(
78
+ 'Invalid encryption key (expected 32-byte Buffer)',
79
+ 'INVALID_KEY'
80
+ );
81
+ }
82
+
83
+ if (!Buffer.isBuffer(iv) || iv.length !== 12) {
84
+ throw new WilcoCryptError(
85
+ 'Invalid IV (expected 12-byte Buffer)',
86
+ 'INVALID_IV'
87
+ );
88
+ }
89
+ };
90
+
91
+ /**
92
+ * Validates password strength.
93
+ *
94
+ * @param {string} password
95
+ * @throws {WilcoCryptError}
96
+ */
97
+ wilcocrypt._.assertPassword = function (password) {
98
+ if (typeof password !== 'string' || password.length < wilcocrypt._.MIN_PASSWORD_LENGTH) {
99
+ throw new WilcoCryptError(
100
+ `Password must be at least ${wilcocrypt._.MIN_PASSWORD_LENGTH} characters`,
101
+ 'WEAK_PASSWORD'
102
+ );
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Constant-time buffer comparison.
108
+ * Reserved for future extensions.
109
+ *
110
+ * @param {Buffer} a
111
+ * @param {Buffer} b
112
+ * @returns {boolean}
113
+ */
114
+ wilcocrypt._.constantTimeEqual = function (a, b) {
115
+ if (a.length !== b.length) return false;
116
+
117
+ let result = 0;
118
+ for (let i = 0; i < a.length; i++) {
119
+ result |= a[i] ^ b[i];
120
+ }
121
+ return result === 0;
122
+ };
123
+
124
+ /* =========================
125
+ Crypto layer (internal)
126
+ ========================= */
127
+
128
+ /**
129
+ * Encrypts raw data using AES-256-GCM.
130
+ *
131
+ * @param {Buffer} plainData
132
+ * @param {Buffer} key
133
+ * @param {Buffer} iv
134
+ * @returns {{ciphertext: Buffer, authTag: Buffer}}
135
+ */
136
+ wilcocrypt._.encryptData = function (plainData, key, iv) {
137
+ wilcocrypt._.assertKeyAndIv(key, iv);
138
+
139
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
140
+ const encrypted = Buffer.concat([
141
+ cipher.update(plainData),
142
+ cipher.final()
143
+ ]);
144
+
145
+ return {
146
+ ciphertext: encrypted,
147
+ authTag: cipher.getAuthTag()
148
+ };
149
+ };
150
+
151
+ /**
152
+ * Decrypts AES-256-GCM encrypted data.
153
+ *
154
+ * @param {Buffer} cipherBuffer
155
+ * @param {Buffer} authTagBuffer
156
+ * @param {Buffer} key
157
+ * @param {Buffer} iv
158
+ * @returns {Buffer}
159
+ */
160
+ wilcocrypt._.decryptData = function (cipherBuffer, authTagBuffer, key, iv) {
161
+ wilcocrypt._.assertKeyAndIv(key, iv);
162
+
163
+ try {
164
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
165
+ decipher.setAuthTag(authTagBuffer);
166
+
167
+ return Buffer.concat([
168
+ decipher.update(cipherBuffer),
169
+ decipher.final()
170
+ ]);
171
+ } catch {
172
+ throw new WilcoCryptError(
173
+ 'Decryption failed (invalid password, corrupted data, or tampered file)',
174
+ 'DECRYPTION_FAILED'
175
+ );
176
+ }
177
+ };
178
+
179
+ /* =========================
180
+ Public API
181
+ ========================= */
182
+
183
+ /**
184
+ * Encrypts data using password-based AES-256-GCM.
185
+ *
186
+ * Output format:
187
+ * [HEADER (10 bytes)] + [VERSION (dynamic)] + [salt (16)] + [iv (12)] + [ciphertext] + [authTag (16)]
188
+ *
189
+ * @param {Buffer} plaindata - Raw data to encrypt
190
+ * @param {string} password - Password used for key derivation
191
+ * @param {boolean} [gzip=true] - Whether to compress data before encryption
192
+ * @returns {Buffer} Binary-encoded encrypted payload
193
+ * @throws {WilcoCryptError} If password is invalid
194
+ */
195
+ wilcocrypt.encryptData = function (plaindata, password, gzip = true) {
196
+ wilcocrypt._.assertPassword(password);
197
+
198
+ const gzipData = gzip ? gzipSync(plaindata) : plaindata;
199
+ const iv = randomBytes(12);
200
+ const salt = randomBytes(16);
201
+
202
+ const key = scryptSync(password, salt, 32);
203
+
204
+ const { ciphertext, authTag } = wilcocrypt._.encryptData(gzipData, key, iv);
205
+ const versionBuf = Buffer.from(wilcocrypt._.VERSION);
206
+
207
+ return Buffer.concat([
208
+ wilcocrypt._.HEADER, // 10 bytes
209
+ versionBuf, // dynamic
210
+ salt, // 16 bytes
211
+ iv, // 12 bytes
212
+ ciphertext, // variable
213
+ authTag // 16 bytes (at the end for streaming compatibility)
214
+ ]);
215
+ };
216
+
217
+ /**
218
+ * Decrypts encrypted data using password-based AES-256-GCM.
219
+ *
220
+ * Validates internal header and version, then extracts:
221
+ * salt, iv, authTag and ciphertext from the binary payload.
222
+ *
223
+ * @param {Buffer} encryptedBuffer - Binary-encoded encrypted payload
224
+ * @param {string} password - Password used for decryption
225
+ * @param {boolean} [gzip=true] - Whether to decompress after decryption
226
+ * @returns {Buffer} Decrypted raw data
227
+ * @throws {WilcoCryptError} On invalid header, version mismatch, wrong password, or corrupted data
228
+ */
229
+ wilcocrypt.decryptData = function (encryptedBuffer, password, gzip = true) {
230
+ wilcocrypt._.assertPassword(password);
231
+
232
+ const versionBuf = Buffer.from(wilcocrypt._.VERSION);
233
+ let offset = 0;
234
+
235
+ const fileHeader = encryptedBuffer.subarray(offset, offset += wilcocrypt._.HEADER.length);
236
+ if (!fileHeader.equals(wilcocrypt._.HEADER)) {
237
+ throw new WilcoCryptError('Invalid WilcoCrypt header', 'INVALID_HEADER');
238
+ }
239
+
240
+ const fileVersion = encryptedBuffer.subarray(offset, offset += versionBuf.length);
241
+ if (!fileVersion.equals(versionBuf)) {
242
+ throw new WilcoCryptError('Version mismatch', 'VERSION_MISMATCH');
243
+ }
244
+
245
+ const salt = encryptedBuffer.subarray(offset, offset += 16);
246
+ const iv = encryptedBuffer.subarray(offset, offset += 12);
247
+
248
+ // authTag are the last 16 bytes; ciphertext is everything in between
249
+ const authTag = encryptedBuffer.subarray(encryptedBuffer.length - 16);
250
+ const ciphertext = encryptedBuffer.subarray(offset, encryptedBuffer.length - 16);
251
+
252
+ const key = scryptSync(password, salt, 32);
253
+
254
+ const decrypted = wilcocrypt._.decryptData(ciphertext, authTag, key, iv);
255
+
256
+ return gzip ? gunzipSync(decrypted) : decrypted;
257
+ };
258
+
259
+ /**
260
+ * Encrypts a file and writes the result to `<filePath>.enc`.
261
+ *
262
+ * @param {string} filePath - Path to the file to encrypt
263
+ * @param {string} password - Password used for encryption
264
+ * @param {boolean} [gzip=true] - Whether to compress before encryption
265
+ * @returns {void}
266
+ * @throws {WilcoCryptError} If password is invalid
267
+ */
268
+ wilcocrypt.encryptFile = function (filePath, password, gzip = true) {
269
+ const fileData = readFileSync(filePath);
270
+ const encryptedData = wilcocrypt.encryptData(fileData, password, gzip);
271
+ writeFileSync(`${filePath}.enc`, encryptedData);
272
+ };
273
+
274
+ /**
275
+ * Decrypts an encrypted `.enc` file.
276
+ *
277
+ * If `outputPath` is provided, the decrypted data is written to that file
278
+ * and `undefined` is returned. Otherwise the decrypted Buffer is returned.
279
+ *
280
+ * @param {string} filePath - Path to the `.enc` file
281
+ * @param {string} password - Password used for decryption
282
+ * @param {string|boolean} [outputPath] - Optional path to write decrypted output to.
283
+ * If omitted (or `true`/`false`), the function returns the decrypted Buffer instead.
284
+ * @param {boolean} [gzip=true] - Whether to decompress after decryption
285
+ * @returns {Buffer|undefined} Decrypted file contents, or undefined if outputPath was given
286
+ * @throws {WilcoCryptError} If file extension is invalid or decryption fails
287
+ */
288
+ wilcocrypt.decryptFile = function (filePath, password, outputPath, gzip = true) {
289
+ // Support legacy 3-argument form: decryptFile(filePath, password, gzip?)
290
+ if (typeof outputPath === 'boolean') {
291
+ gzip = outputPath;
292
+ outputPath = undefined;
293
+ }
294
+
295
+ if (!filePath.endsWith('.enc')) {
296
+ throw new WilcoCryptError(
297
+ 'Invalid file extension (expected .enc)',
298
+ 'INVALID_FILE_EXTENSION'
299
+ );
300
+ }
301
+
302
+ const encryptedData = readFileSync(filePath);
303
+ const decrypted = wilcocrypt.decryptData(encryptedData, password, gzip);
304
+
305
+ if (outputPath) {
306
+ writeFileSync(outputPath, decrypted);
307
+ return;
308
+ }
309
+
310
+ return decrypted;
311
+ };
312
+
313
+ /**
314
+ * Encrypts a file using streams and writes the result to `outputPath`.
315
+ * Memory-efficient alternative to `encryptFile` for large files.
316
+ *
317
+ * Output format:
318
+ * [HEADER] + [VERSION] + [salt (16)] + [iv (12)] + [ciphertext] + [authTag (16)]
319
+ *
320
+ * @param {string} inputPath - Path to the file to encrypt
321
+ * @param {string} outputPath - Path to write the encrypted output to
322
+ * @param {string} password - Password used for key derivation
323
+ * @param {boolean} [gzip=true] - Whether to compress data before encryption
324
+ * @returns {Promise<void>}
325
+ * @throws {WilcoCryptError} If password is invalid
326
+ */
327
+ wilcocrypt.encryptFileStream = async function (inputPath, outputPath, password, gzip = true) {
328
+ wilcocrypt._.assertPassword(password);
329
+
330
+ const salt = randomBytes(16);
331
+ const iv = randomBytes(12);
332
+ const key = scryptSync(password, salt, 32);
333
+ const versionBuf = Buffer.from(wilcocrypt._.VERSION);
334
+
335
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
336
+ const writeStream = createWriteStream(outputPath);
337
+
338
+ writeStream.write(wilcocrypt._.HEADER);
339
+ writeStream.write(versionBuf);
340
+ writeStream.write(salt);
341
+ writeStream.write(iv);
342
+
343
+ const pipelineSteps = [createReadStream(inputPath)];
344
+ if (gzip) pipelineSteps.push(createGzip());
345
+ pipelineSteps.push(cipher);
346
+ pipelineSteps.push(writeStream);
347
+
348
+ // end: false so we can still append the authTag after the pipeline finishes
349
+ await pipeline(...pipelineSteps, { end: false });
350
+ writeStream.end(cipher.getAuthTag());
351
+ };
352
+
353
+ /**
354
+ * Decrypts an encrypted `.enc` file using streams.
355
+ * Memory-efficient alternative to `decryptFile` for large files.
356
+ * Cleans up the output file automatically if decryption or integrity check fails.
357
+ *
358
+ * @param {string} inputPath - Path to the encrypted `.enc` file
359
+ * @param {string} outputPath - Path to write the decrypted output to
360
+ * @param {string} password - Password used for decryption
361
+ * @param {boolean} [gzip=true] - Whether to decompress after decryption
362
+ * @returns {Promise<void>}
363
+ * @throws {WilcoCryptError} On invalid header, version mismatch, or decryption/integrity failure
364
+ */
365
+ wilcocrypt.decryptFileStream = async function (inputPath, outputPath, password, gzip = true) {
366
+ wilcocrypt._.assertPassword(password);
367
+
368
+ const handle = await fsPromises.open(inputPath, 'r');
369
+ const versionBuf = Buffer.from(wilcocrypt._.VERSION);
370
+
371
+ const headLen = wilcocrypt._.HEADER.length;
372
+ const verLen = versionBuf.length;
373
+
374
+ const headerCheck = Buffer.alloc(headLen);
375
+ const versionCheck = Buffer.alloc(verLen);
376
+ const salt = Buffer.alloc(16);
377
+ const iv = Buffer.alloc(12);
378
+
379
+ let currentPos = 0;
380
+ await handle.read(headerCheck, 0, headLen, currentPos); currentPos += headLen;
381
+ await handle.read(versionCheck, 0, verLen, currentPos); currentPos += verLen;
382
+ await handle.read(salt, 0, 16, currentPos); currentPos += 16;
383
+ await handle.read(iv, 0, 12, currentPos); currentPos += 12;
384
+
385
+ if (!headerCheck.equals(wilcocrypt._.HEADER)) {
386
+ await handle.close();
387
+ throw new WilcoCryptError('Invalid WilcoCrypt header', 'INVALID_HEADER');
388
+ }
389
+
390
+ if (!versionCheck.equals(versionBuf)) {
391
+ await handle.close();
392
+ throw new WilcoCryptError('Version mismatch', 'VERSION_MISMATCH');
393
+ }
394
+
395
+ const stats = await handle.stat();
396
+ const authTag = Buffer.alloc(16);
397
+ await handle.read(authTag, 0, 16, stats.size - 16);
398
+
399
+ const key = scryptSync(password, salt, 32);
400
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
401
+ decipher.setAuthTag(authTag);
402
+
403
+ const pipelineSteps = [createReadStream(inputPath, { start: currentPos, end: stats.size - 17 })];
404
+ pipelineSteps.push(decipher);
405
+ if (gzip) pipelineSteps.push(createGunzip());
406
+ pipelineSteps.push(createWriteStream(outputPath));
407
+
408
+ try {
409
+ await pipeline(...pipelineSteps);
410
+ } catch {
411
+ await handle.close();
412
+ await fsPromises.unlink(outputPath);
413
+ throw new WilcoCryptError(
414
+ 'Decryption failed (invalid password, corrupted data, or tampered file)',
415
+ 'DECRYPTION_FAILED'
416
+ );
417
+ }
418
+
419
+ await handle.close();
420
+ };
421
+
422
+ export default wilcocrypt;
@@ -0,0 +1,186 @@
1
+ /// <reference types="node" />
2
+
3
+ /**
4
+ * Custom error class for all WilcoCrypt-specific errors.
5
+ */
6
+ export class WilcoCryptError extends Error {
7
+ code: string;
8
+
9
+ /**
10
+ * @param message Human-readable error message
11
+ * @param code Machine-readable error code (default: WILCOCRYPT_ERROR)
12
+ */
13
+ constructor(message: string, code?: string);
14
+ }
15
+
16
+ /**
17
+ * Internal helper namespace used by WilcoCrypt.
18
+ */
19
+ export interface InternalNamespace {
20
+ /**
21
+ * WilcoCrypt version (must match during decryption).
22
+ */
23
+ VERSION: string;
24
+
25
+ /**
26
+ * Minimum allowed password length.
27
+ */
28
+ MIN_PASSWORD_LENGTH: number;
29
+
30
+ /**
31
+ * Internal header used to identify valid WilcoCrypt payloads.
32
+ */
33
+ HEADER: Buffer;
34
+
35
+ /**
36
+ * Internal error class used by WilcoCrypt.
37
+ */
38
+ WilcoCryptError: typeof WilcoCryptError;
39
+
40
+ /**
41
+ * Validates AES-256-GCM key and IV.
42
+ */
43
+ assertKeyAndIv(key: Buffer, iv: Buffer): void;
44
+
45
+ /**
46
+ * Validates password strength.
47
+ */
48
+ assertPassword(password: string): void;
49
+
50
+ /**
51
+ * Constant-time buffer comparison.
52
+ */
53
+ constantTimeEqual(a: Buffer, b: Buffer): boolean;
54
+
55
+ /**
56
+ * Encrypts raw data using AES-256-GCM.
57
+ */
58
+ encryptData(
59
+ plainData: Buffer,
60
+ key: Buffer,
61
+ iv: Buffer
62
+ ): {
63
+ ciphertext: Buffer;
64
+ authTag: Buffer;
65
+ };
66
+
67
+ /**
68
+ * Decrypts AES-256-GCM encrypted data.
69
+ */
70
+ decryptData(
71
+ cipherBuffer: Buffer,
72
+ authTagBuffer: Buffer,
73
+ key: Buffer,
74
+ iv: Buffer
75
+ ): Buffer;
76
+ }
77
+
78
+ /**
79
+ * Main WilcoCrypt API.
80
+ */
81
+ export interface WilcoCrypt {
82
+ _: InternalNamespace;
83
+
84
+ /**
85
+ * Encrypts data using password-based AES-256-GCM.
86
+ *
87
+ * Output format:
88
+ * [HEADER (10 bytes)] + [VERSION (dynamic)] + [salt (16)] + [iv (12)] + [ciphertext] + [authTag (16)]
89
+ *
90
+ * @param plaindata Raw data to encrypt
91
+ * @param password Password used for key derivation
92
+ * @param gzip Whether to compress data before encryption (default: true)
93
+ * @returns Binary-encoded encrypted payload
94
+ */
95
+ encryptData(
96
+ plaindata: Buffer,
97
+ password: string,
98
+ gzip?: boolean
99
+ ): Buffer;
100
+
101
+ /**
102
+ * Decrypts encrypted data using password-based AES-256-GCM.
103
+ *
104
+ * Validates internal header and version, then extracts:
105
+ * salt, iv, authTag and ciphertext from the binary payload.
106
+ *
107
+ * @param encryptedData Binary-encoded encrypted payload
108
+ * @param password Password used for decryption
109
+ * @param gzip Whether to decompress after decryption (default: true)
110
+ * @returns Decrypted raw data
111
+ *
112
+ * @throws WilcoCryptError on:
113
+ * - invalid header
114
+ * - version mismatch
115
+ * - wrong password
116
+ * - corrupted data
117
+ */
118
+ decryptData(
119
+ encryptedData: Buffer,
120
+ password: string,
121
+ gzip?: boolean
122
+ ): Buffer;
123
+
124
+ /**
125
+ * Encrypts a file and writes `<filePath>.enc`.
126
+ */
127
+ encryptFile(
128
+ filePath: string,
129
+ password: string,
130
+ gzip?: boolean
131
+ ): void;
132
+
133
+ /**
134
+ * Decrypts a `.enc` file.
135
+ *
136
+ * If `outputPath` is provided, the decrypted data is written to that file
137
+ * and `undefined` is returned. Otherwise the decrypted Buffer is returned.
138
+ */
139
+ decryptFile(filePath: string, password: string, outputPath: string, gzip?: boolean): undefined;
140
+ decryptFile(filePath: string, password: string, gzip?: boolean): Buffer;
141
+
142
+ /**
143
+ * Encrypts a file using streams and writes the result to `outputPath`.
144
+ * Memory-efficient alternative to `encryptFile` for large files.
145
+ *
146
+ * @param inputPath Path to the file to encrypt
147
+ * @param outputPath Path to write the encrypted output to
148
+ * @param password Password used for key derivation
149
+ * @param gzip Whether to compress data before encryption (default: true)
150
+ */
151
+ encryptFileStream(
152
+ inputPath: string,
153
+ outputPath: string,
154
+ password: string,
155
+ gzip?: boolean
156
+ ): Promise<void>;
157
+
158
+ /**
159
+ * Decrypts an encrypted file using streams.
160
+ * Memory-efficient alternative to `decryptFile` for large files.
161
+ * Cleans up the output file automatically if decryption or integrity check fails.
162
+ *
163
+ * @param inputPath Path to the encrypted file
164
+ * @param outputPath Path to write the decrypted output to
165
+ * @param password Password used for decryption
166
+ * @param gzip Whether to decompress after decryption (default: true)
167
+ *
168
+ * @throws WilcoCryptError on:
169
+ * - invalid header
170
+ * - version mismatch
171
+ * - decryption/integrity failure
172
+ */
173
+ decryptFileStream(
174
+ inputPath: string,
175
+ outputPath: string,
176
+ password: string,
177
+ gzip?: boolean
178
+ ): Promise<void>;
179
+ }
180
+
181
+ /**
182
+ * WilcoCrypt main instance.
183
+ */
184
+ declare const wilcocrypt: WilcoCrypt;
185
+
186
+ export default wilcocrypt;