wilcocrypt 2.2.0 → 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 +58 -64
- package/CHANGELOG.md +38 -0
- package/DOCS.md +286 -94
- package/README.md +15 -14
- package/SECURITY.md +12 -12
- package/package.json +11 -8
- package/src/cli.js +40 -37
- package/src/wilcocrypt.js +260 -53
- package/types/wilcocrypt.d.ts +189 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wilcocrypt",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "A encrypting tool",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"encrypting",
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
"author": "computerwilco",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "src/wilcocrypt.js",
|
|
13
|
-
"bin":
|
|
13
|
+
"bin": {
|
|
14
|
+
"wilcocrypt": "src/cli.js"
|
|
15
|
+
},
|
|
14
16
|
"types": "types/wilcocrypt.d.ts",
|
|
15
17
|
"scripts": {
|
|
16
18
|
"test": "node src/cli.js",
|
|
17
|
-
"
|
|
18
|
-
"lint
|
|
19
|
+
"semistandard": "npx semistandard",
|
|
20
|
+
"lint": "npx semistandard --fix; npx prettier --write .",
|
|
19
21
|
"update": "npx -y Jelmerro/nus",
|
|
20
22
|
"rollup": "node src/scripts/shebang-fix.js pre && rollup -c && node src/scripts/shebang-fix.js post && node src/scripts/fix-cli-min-import.js && node src/scripts/chmod.js",
|
|
21
23
|
"sea:blob": "node src/scripts/sea-blob.js",
|
|
@@ -28,15 +30,16 @@
|
|
|
28
30
|
"clean": "rm -rf dist && rm -rf sea/*.sea && rm -rf release/*"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
|
-
"commander": "
|
|
33
|
+
"commander": "15.0.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
|
-
"@rollup/plugin-commonjs": "29.0.
|
|
36
|
+
"@rollup/plugin-commonjs": "29.0.3",
|
|
35
37
|
"@rollup/plugin-node-resolve": "16.0.3",
|
|
36
38
|
"@rollup/plugin-replace": "6.0.3",
|
|
37
39
|
"@rollup/plugin-terser": "1.0.0",
|
|
38
|
-
"@types/node": "25.
|
|
39
|
-
"
|
|
40
|
+
"@types/node": "25.9.2",
|
|
41
|
+
"prettier": "3.8.3",
|
|
42
|
+
"rollup": "4.61.1",
|
|
40
43
|
"semistandard": "17.0.0"
|
|
41
44
|
}
|
|
42
45
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from
|
|
3
|
-
import wilcocrypt from
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import wilcocrypt from "./wilcocrypt.js";
|
|
4
4
|
|
|
5
5
|
/* =========================
|
|
6
6
|
Helpers
|
|
7
7
|
========================= */
|
|
8
8
|
|
|
9
|
-
function promptPassword
|
|
9
|
+
function promptPassword(promptText = "Password: ") {
|
|
10
10
|
return new Promise((resolve) => {
|
|
11
11
|
const stdin = process.stdin;
|
|
12
12
|
const stdout = process.stdout;
|
|
13
13
|
|
|
14
14
|
if (!stdin.isTTY) {
|
|
15
15
|
throw new wilcocrypt._.WilcoCryptError(
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
"Password prompt requires a TTY",
|
|
17
|
+
"NO_TTY",
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
stdout.write(promptText);
|
|
22
22
|
|
|
23
|
-
let password =
|
|
23
|
+
let password = "";
|
|
24
24
|
|
|
25
25
|
stdin.setRawMode(true);
|
|
26
26
|
stdin.resume();
|
|
27
|
-
stdin.setEncoding(
|
|
27
|
+
stdin.setEncoding("utf8");
|
|
28
28
|
|
|
29
|
-
function onData
|
|
30
|
-
if (char ===
|
|
31
|
-
stdout.write(
|
|
29
|
+
function onData(char) {
|
|
30
|
+
if (char === "\r" || char === "\n") {
|
|
31
|
+
stdout.write("\n");
|
|
32
32
|
stdin.setRawMode(false);
|
|
33
33
|
stdin.pause();
|
|
34
|
-
stdin.removeListener(
|
|
34
|
+
stdin.removeListener("data", onData);
|
|
35
35
|
resolve(password);
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
if (char ===
|
|
40
|
-
stdout.write(
|
|
39
|
+
if (char === "\u0003") {
|
|
40
|
+
stdout.write("\n");
|
|
41
41
|
stdin.setRawMode(false);
|
|
42
42
|
stdin.pause();
|
|
43
|
-
stdin.removeListener(
|
|
43
|
+
stdin.removeListener("data", onData);
|
|
44
44
|
process.exit(1);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
if (char ===
|
|
47
|
+
if (char === "\u007f" || char === "\b") {
|
|
48
48
|
if (password.length > 0) {
|
|
49
49
|
password = password.slice(0, -1);
|
|
50
|
-
stdout.write(
|
|
50
|
+
stdout.write("\b \b");
|
|
51
51
|
}
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
password += char;
|
|
56
|
-
stdout.write(
|
|
56
|
+
stdout.write("*");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
stdin.on(
|
|
59
|
+
stdin.on("data", onData);
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -67,16 +67,22 @@ function promptPassword (promptText = 'Password: ') {
|
|
|
67
67
|
const program = new Command();
|
|
68
68
|
|
|
69
69
|
program
|
|
70
|
-
.name(
|
|
71
|
-
.description(
|
|
72
|
-
.version(wilcocrypt._.VERSION,
|
|
73
|
-
|
|
74
|
-
.option(
|
|
75
|
-
.option(
|
|
76
|
-
.option(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
.name("wilcocrypt")
|
|
71
|
+
.description("File encryption tool")
|
|
72
|
+
.version(wilcocrypt._.VERSION, "--version", "Show version")
|
|
73
|
+
|
|
74
|
+
.option("-e, --encrypt <file>", "Encrypt file")
|
|
75
|
+
.option("-d, --decrypt <file>", "Decrypt file")
|
|
76
|
+
.option(
|
|
77
|
+
"-o, --output <file>",
|
|
78
|
+
"Write output to file instead of stdout (decrypt only)",
|
|
79
|
+
)
|
|
80
|
+
.option(
|
|
81
|
+
"--stdout",
|
|
82
|
+
"Write decrypted output to stdout (default behavior, explicit flag)",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
.helpOption("-h, --help", "Display help");
|
|
80
86
|
|
|
81
87
|
program.parse(process.argv);
|
|
82
88
|
|
|
@@ -86,27 +92,24 @@ const options = program.opts();
|
|
|
86
92
|
Validation
|
|
87
93
|
========================= */
|
|
88
94
|
|
|
89
|
-
const actions = [
|
|
90
|
-
options.encrypt,
|
|
91
|
-
options.decrypt
|
|
92
|
-
].filter(Boolean);
|
|
95
|
+
const actions = [options.encrypt, options.decrypt].filter(Boolean);
|
|
93
96
|
|
|
94
97
|
if (actions.length === 0) {
|
|
95
98
|
program.help();
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
if (actions.length > 1) {
|
|
99
|
-
console.error(
|
|
102
|
+
console.error("error: please specify only one action (-e or -d)");
|
|
100
103
|
process.exit(1);
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
if (options.output && options.stdout) {
|
|
104
|
-
console.error(
|
|
107
|
+
console.error("error: --output and --stdout are mutually exclusive");
|
|
105
108
|
process.exit(1);
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
if (options.output && options.encrypt) {
|
|
109
|
-
console.error(
|
|
112
|
+
console.error("error: --output is only supported for decryption");
|
|
110
113
|
process.exit(1);
|
|
111
114
|
}
|
|
112
115
|
|
|
@@ -117,14 +120,14 @@ if (options.output && options.encrypt) {
|
|
|
117
120
|
(async () => {
|
|
118
121
|
try {
|
|
119
122
|
if (options.encrypt) {
|
|
120
|
-
const password = await promptPassword(
|
|
123
|
+
const password = await promptPassword("Encryption password: ");
|
|
121
124
|
wilcocrypt.encryptFile(options.encrypt, password);
|
|
122
125
|
console.log(`Encrypted: ${options.encrypt}.enc`);
|
|
123
126
|
return;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
if (options.decrypt) {
|
|
127
|
-
const password = await promptPassword(
|
|
130
|
+
const password = await promptPassword("Decryption password: ");
|
|
128
131
|
|
|
129
132
|
if (options.output) {
|
|
130
133
|
wilcocrypt.decryptFile(options.decrypt, password, options.output);
|
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
|
|
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.
|
|
@@ -75,15 +90,15 @@ wilcocrypt._.HEADER = Buffer.from([23, 9, 12, 3, 15, 3, 18, 25, 16, 20]);
|
|
|
75
90
|
wilcocrypt._.assertKeyAndIv = function (key, iv) {
|
|
76
91
|
if (!Buffer.isBuffer(key) || key.length !== 32) {
|
|
77
92
|
throw new WilcoCryptError(
|
|
78
|
-
|
|
79
|
-
|
|
93
|
+
"Invalid encryption key (expected 32-byte Buffer)",
|
|
94
|
+
"INVALID_KEY",
|
|
80
95
|
);
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
if (!Buffer.isBuffer(iv) || iv.length !== 12) {
|
|
84
99
|
throw new WilcoCryptError(
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
"Invalid IV (expected 12-byte Buffer)",
|
|
101
|
+
"INVALID_IV",
|
|
87
102
|
);
|
|
88
103
|
}
|
|
89
104
|
};
|
|
@@ -95,10 +110,13 @@ wilcocrypt._.assertKeyAndIv = function (key, iv) {
|
|
|
95
110
|
* @throws {WilcoCryptError}
|
|
96
111
|
*/
|
|
97
112
|
wilcocrypt._.assertPassword = function (password) {
|
|
98
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
typeof password !== "string" ||
|
|
115
|
+
password.length < wilcocrypt._.MIN_PASSWORD_LENGTH
|
|
116
|
+
) {
|
|
99
117
|
throw new WilcoCryptError(
|
|
100
118
|
`Password must be at least ${wilcocrypt._.MIN_PASSWORD_LENGTH} characters`,
|
|
101
|
-
|
|
119
|
+
"WEAK_PASSWORD",
|
|
102
120
|
);
|
|
103
121
|
}
|
|
104
122
|
};
|
|
@@ -136,15 +154,12 @@ wilcocrypt._.constantTimeEqual = function (a, b) {
|
|
|
136
154
|
wilcocrypt._.encryptData = function (plainData, key, iv) {
|
|
137
155
|
wilcocrypt._.assertKeyAndIv(key, iv);
|
|
138
156
|
|
|
139
|
-
const cipher = createCipheriv(
|
|
140
|
-
const encrypted = Buffer.concat([
|
|
141
|
-
cipher.update(plainData),
|
|
142
|
-
cipher.final()
|
|
143
|
-
]);
|
|
157
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
158
|
+
const encrypted = Buffer.concat([cipher.update(plainData), cipher.final()]);
|
|
144
159
|
|
|
145
160
|
return {
|
|
146
161
|
ciphertext: encrypted,
|
|
147
|
-
authTag: cipher.getAuthTag()
|
|
162
|
+
authTag: cipher.getAuthTag(),
|
|
148
163
|
};
|
|
149
164
|
};
|
|
150
165
|
|
|
@@ -161,17 +176,14 @@ wilcocrypt._.decryptData = function (cipherBuffer, authTagBuffer, key, iv) {
|
|
|
161
176
|
wilcocrypt._.assertKeyAndIv(key, iv);
|
|
162
177
|
|
|
163
178
|
try {
|
|
164
|
-
const decipher = createDecipheriv(
|
|
179
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
165
180
|
decipher.setAuthTag(authTagBuffer);
|
|
166
181
|
|
|
167
|
-
return Buffer.concat([
|
|
168
|
-
decipher.update(cipherBuffer),
|
|
169
|
-
decipher.final()
|
|
170
|
-
]);
|
|
182
|
+
return Buffer.concat([decipher.update(cipherBuffer), decipher.final()]);
|
|
171
183
|
} catch {
|
|
172
184
|
throw new WilcoCryptError(
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
"Decryption failed (invalid password, corrupted data, or tampered file)",
|
|
186
|
+
"DECRYPTION_FAILED",
|
|
175
187
|
);
|
|
176
188
|
}
|
|
177
189
|
};
|
|
@@ -210,7 +222,47 @@ wilcocrypt.encryptData = function (plaindata, password, gzip = true) {
|
|
|
210
222
|
salt, // 16 bytes
|
|
211
223
|
iv, // 12 bytes
|
|
212
224
|
ciphertext, // variable
|
|
213
|
-
authTag // 16 bytes (at the end for streaming compatibility)
|
|
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;
|
|
249
|
+
|
|
250
|
+
const iv = randomBytes(12);
|
|
251
|
+
const salt = randomBytes(16);
|
|
252
|
+
|
|
253
|
+
const key = await scryptAsync(password, salt, 32);
|
|
254
|
+
|
|
255
|
+
const { ciphertext, authTag } = wilcocrypt._.encryptData(gzipData, key, iv);
|
|
256
|
+
|
|
257
|
+
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
258
|
+
|
|
259
|
+
return Buffer.concat([
|
|
260
|
+
wilcocrypt._.HEADER,
|
|
261
|
+
versionBuf,
|
|
262
|
+
salt,
|
|
263
|
+
iv,
|
|
264
|
+
ciphertext,
|
|
265
|
+
authTag,
|
|
214
266
|
]);
|
|
215
267
|
};
|
|
216
268
|
|
|
@@ -232,22 +284,31 @@ wilcocrypt.decryptData = function (encryptedBuffer, password, gzip = true) {
|
|
|
232
284
|
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
233
285
|
let offset = 0;
|
|
234
286
|
|
|
235
|
-
const fileHeader = encryptedBuffer.subarray(
|
|
287
|
+
const fileHeader = encryptedBuffer.subarray(
|
|
288
|
+
offset,
|
|
289
|
+
(offset += wilcocrypt._.HEADER.length),
|
|
290
|
+
);
|
|
236
291
|
if (!fileHeader.equals(wilcocrypt._.HEADER)) {
|
|
237
|
-
throw new WilcoCryptError(
|
|
292
|
+
throw new WilcoCryptError("Invalid WilcoCrypt header", "INVALID_HEADER");
|
|
238
293
|
}
|
|
239
294
|
|
|
240
|
-
const fileVersion = encryptedBuffer.subarray(
|
|
295
|
+
const fileVersion = encryptedBuffer.subarray(
|
|
296
|
+
offset,
|
|
297
|
+
(offset += versionBuf.length),
|
|
298
|
+
);
|
|
241
299
|
if (!fileVersion.equals(versionBuf)) {
|
|
242
|
-
throw new WilcoCryptError(
|
|
300
|
+
throw new WilcoCryptError("Version mismatch", "VERSION_MISMATCH");
|
|
243
301
|
}
|
|
244
302
|
|
|
245
|
-
const salt = encryptedBuffer.subarray(offset, offset += 16);
|
|
246
|
-
const iv = encryptedBuffer.subarray(offset, offset += 12);
|
|
303
|
+
const salt = encryptedBuffer.subarray(offset, (offset += 16));
|
|
304
|
+
const iv = encryptedBuffer.subarray(offset, (offset += 12));
|
|
247
305
|
|
|
248
306
|
// authTag are the last 16 bytes; ciphertext is everything in between
|
|
249
307
|
const authTag = encryptedBuffer.subarray(encryptedBuffer.length - 16);
|
|
250
|
-
const ciphertext = encryptedBuffer.subarray(
|
|
308
|
+
const ciphertext = encryptedBuffer.subarray(
|
|
309
|
+
offset,
|
|
310
|
+
encryptedBuffer.length - 16,
|
|
311
|
+
);
|
|
251
312
|
|
|
252
313
|
const key = scryptSync(password, salt, 32);
|
|
253
314
|
|
|
@@ -256,6 +317,63 @@ wilcocrypt.decryptData = function (encryptedBuffer, password, gzip = true) {
|
|
|
256
317
|
return gzip ? gunzipSync(decrypted) : decrypted;
|
|
257
318
|
};
|
|
258
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;
|
|
375
|
+
};
|
|
376
|
+
|
|
259
377
|
/**
|
|
260
378
|
* Encrypts a file and writes the result to `<filePath>.enc`.
|
|
261
379
|
*
|
|
@@ -271,6 +389,27 @@ wilcocrypt.encryptFile = function (filePath, password, gzip = true) {
|
|
|
271
389
|
writeFileSync(`${filePath}.enc`, encryptedData);
|
|
272
390
|
};
|
|
273
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);
|
|
411
|
+
};
|
|
412
|
+
|
|
274
413
|
/**
|
|
275
414
|
* Decrypts an encrypted `.enc` file.
|
|
276
415
|
*
|
|
@@ -285,17 +424,22 @@ wilcocrypt.encryptFile = function (filePath, password, gzip = true) {
|
|
|
285
424
|
* @returns {Buffer|undefined} Decrypted file contents, or undefined if outputPath was given
|
|
286
425
|
* @throws {WilcoCryptError} If file extension is invalid or decryption fails
|
|
287
426
|
*/
|
|
288
|
-
wilcocrypt.decryptFile = function (
|
|
427
|
+
wilcocrypt.decryptFile = function (
|
|
428
|
+
filePath,
|
|
429
|
+
password,
|
|
430
|
+
outputPath,
|
|
431
|
+
gzip = true,
|
|
432
|
+
) {
|
|
289
433
|
// Support legacy 3-argument form: decryptFile(filePath, password, gzip?)
|
|
290
|
-
if (typeof outputPath ===
|
|
434
|
+
if (typeof outputPath === "boolean") {
|
|
291
435
|
gzip = outputPath;
|
|
292
436
|
outputPath = undefined;
|
|
293
437
|
}
|
|
294
438
|
|
|
295
|
-
if (!filePath.endsWith(
|
|
439
|
+
if (!filePath.endsWith(".enc")) {
|
|
296
440
|
throw new WilcoCryptError(
|
|
297
|
-
|
|
298
|
-
|
|
441
|
+
"Invalid file extension (expected .enc)",
|
|
442
|
+
"INVALID_FILE_EXTENSION",
|
|
299
443
|
);
|
|
300
444
|
}
|
|
301
445
|
|
|
@@ -310,6 +454,53 @@ wilcocrypt.decryptFile = function (filePath, password, outputPath, gzip = true)
|
|
|
310
454
|
return decrypted;
|
|
311
455
|
};
|
|
312
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
|
+
|
|
313
504
|
/**
|
|
314
505
|
* Encrypts a file using streams and writes the result to `outputPath`.
|
|
315
506
|
* Memory-efficient alternative to `encryptFile` for large files.
|
|
@@ -324,7 +515,12 @@ wilcocrypt.decryptFile = function (filePath, password, outputPath, gzip = true)
|
|
|
324
515
|
* @returns {Promise<void>}
|
|
325
516
|
* @throws {WilcoCryptError} If password is invalid
|
|
326
517
|
*/
|
|
327
|
-
wilcocrypt.encryptFileStream = async function (
|
|
518
|
+
wilcocrypt.encryptFileStream = async function (
|
|
519
|
+
inputPath,
|
|
520
|
+
outputPath,
|
|
521
|
+
password,
|
|
522
|
+
gzip = true,
|
|
523
|
+
) {
|
|
328
524
|
wilcocrypt._.assertPassword(password);
|
|
329
525
|
|
|
330
526
|
const salt = randomBytes(16);
|
|
@@ -332,7 +528,7 @@ wilcocrypt.encryptFileStream = async function (inputPath, outputPath, password,
|
|
|
332
528
|
const key = scryptSync(password, salt, 32);
|
|
333
529
|
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
334
530
|
|
|
335
|
-
const cipher = createCipheriv(
|
|
531
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
336
532
|
const writeStream = createWriteStream(outputPath);
|
|
337
533
|
|
|
338
534
|
writeStream.write(wilcocrypt._.HEADER);
|
|
@@ -362,10 +558,15 @@ wilcocrypt.encryptFileStream = async function (inputPath, outputPath, password,
|
|
|
362
558
|
* @returns {Promise<void>}
|
|
363
559
|
* @throws {WilcoCryptError} On invalid header, version mismatch, or decryption/integrity failure
|
|
364
560
|
*/
|
|
365
|
-
wilcocrypt.decryptFileStream = async function (
|
|
561
|
+
wilcocrypt.decryptFileStream = async function (
|
|
562
|
+
inputPath,
|
|
563
|
+
outputPath,
|
|
564
|
+
password,
|
|
565
|
+
gzip = true,
|
|
566
|
+
) {
|
|
366
567
|
wilcocrypt._.assertPassword(password);
|
|
367
568
|
|
|
368
|
-
const handle = await fsPromises.open(inputPath,
|
|
569
|
+
const handle = await fsPromises.open(inputPath, "r");
|
|
369
570
|
const versionBuf = Buffer.from(wilcocrypt._.VERSION);
|
|
370
571
|
|
|
371
572
|
const headLen = wilcocrypt._.HEADER.length;
|
|
@@ -377,19 +578,23 @@ wilcocrypt.decryptFileStream = async function (inputPath, outputPath, password,
|
|
|
377
578
|
const iv = Buffer.alloc(12);
|
|
378
579
|
|
|
379
580
|
let currentPos = 0;
|
|
380
|
-
await handle.read(headerCheck, 0, headLen, currentPos);
|
|
381
|
-
|
|
382
|
-
await handle.read(
|
|
383
|
-
|
|
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;
|
|
384
589
|
|
|
385
590
|
if (!headerCheck.equals(wilcocrypt._.HEADER)) {
|
|
386
591
|
await handle.close();
|
|
387
|
-
throw new WilcoCryptError(
|
|
592
|
+
throw new WilcoCryptError("Invalid WilcoCrypt header", "INVALID_HEADER");
|
|
388
593
|
}
|
|
389
594
|
|
|
390
595
|
if (!versionCheck.equals(versionBuf)) {
|
|
391
596
|
await handle.close();
|
|
392
|
-
throw new WilcoCryptError(
|
|
597
|
+
throw new WilcoCryptError("Version mismatch", "VERSION_MISMATCH");
|
|
393
598
|
}
|
|
394
599
|
|
|
395
600
|
const stats = await handle.stat();
|
|
@@ -397,10 +602,12 @@ wilcocrypt.decryptFileStream = async function (inputPath, outputPath, password,
|
|
|
397
602
|
await handle.read(authTag, 0, 16, stats.size - 16);
|
|
398
603
|
|
|
399
604
|
const key = scryptSync(password, salt, 32);
|
|
400
|
-
const decipher = createDecipheriv(
|
|
605
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
401
606
|
decipher.setAuthTag(authTag);
|
|
402
607
|
|
|
403
|
-
const pipelineSteps = [
|
|
608
|
+
const pipelineSteps = [
|
|
609
|
+
createReadStream(inputPath, { start: currentPos, end: stats.size - 17 }),
|
|
610
|
+
];
|
|
404
611
|
pipelineSteps.push(decipher);
|
|
405
612
|
if (gzip) pipelineSteps.push(createGunzip());
|
|
406
613
|
pipelineSteps.push(createWriteStream(outputPath));
|
|
@@ -411,8 +618,8 @@ wilcocrypt.decryptFileStream = async function (inputPath, outputPath, password,
|
|
|
411
618
|
await handle.close();
|
|
412
619
|
await fsPromises.unlink(outputPath);
|
|
413
620
|
throw new WilcoCryptError(
|
|
414
|
-
|
|
415
|
-
|
|
621
|
+
"Decryption failed (invalid password, corrupted data, or tampered file)",
|
|
622
|
+
"DECRYPTION_FAILED",
|
|
416
623
|
);
|
|
417
624
|
}
|
|
418
625
|
|