sdc-cli 1.1.0 → 1.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/cli.js +70 -14
- package/package.json +2 -2
- package/sha256file.js +41 -0
package/cli.js
CHANGED
|
@@ -7,10 +7,16 @@ import path from 'path';
|
|
|
7
7
|
import { spawn } from 'child_process';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import cliProgress from 'cli-progress';
|
|
10
|
+
import { sha256File } from './sha256file.js';
|
|
10
11
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = path.dirname(__filename);
|
|
13
14
|
|
|
15
|
+
const exists = async name => {
|
|
16
|
+
try { await fs.access(name); return true }
|
|
17
|
+
catch { return false }
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
class CryptCLI {
|
|
15
21
|
constructor() {
|
|
16
22
|
this.setupProgram();
|
|
@@ -20,16 +26,19 @@ class CryptCLI {
|
|
|
20
26
|
program
|
|
21
27
|
.name('sdc-cli')
|
|
22
28
|
.description('A useful CLI in Node.js to use simple-data-crypto library')
|
|
23
|
-
.version('1.
|
|
29
|
+
.version('1.2.0');
|
|
24
30
|
|
|
25
31
|
// Encrypt command
|
|
26
32
|
program
|
|
27
33
|
.command('encrypt')
|
|
28
34
|
.description('Encrypt file or data')
|
|
29
35
|
.option('--password <password>', 'Specify password directly (NOT RECOMMENDED for security reasons because this exposes the password in the command line)')
|
|
30
|
-
.option('--password-file <file>', 'Read password from specified file')
|
|
36
|
+
.option('--password-file <file>', 'Read password text from specified file (binary file not supported)')
|
|
37
|
+
.option('--password-data <file>', 'Use a binary file\'s SHA256 hash as the password')
|
|
31
38
|
.option('--password-script <script>', 'Execute script and read password from its stdout (similar to GIT_ASKPASS)')
|
|
39
|
+
.option('-r', 'Do not trim the password')
|
|
32
40
|
.option('-k, --skip-confirm', 'Skip password confirmation for encryption')
|
|
41
|
+
.option('-f, --force', 'Force overrides the output file')
|
|
33
42
|
.argument('[inputFile]', 'Input file path (empty for string encryption, "-" for stdin)')
|
|
34
43
|
.argument('[outputFile]', 'Output file path (stdout if not specified)')
|
|
35
44
|
.action(this.handleEncrypt.bind(this));
|
|
@@ -39,8 +48,11 @@ class CryptCLI {
|
|
|
39
48
|
.command('decrypt')
|
|
40
49
|
.description('Decrypt file or data')
|
|
41
50
|
.option('--password <password>', 'Specify password directly (NOT RECOMMENDED for security reasons)')
|
|
42
|
-
.option('--password-file <file>', 'Read password from specified file')
|
|
51
|
+
.option('--password-file <file>', 'Read password text from specified file (binary file not supported)')
|
|
52
|
+
.option('--password-data <file>', 'Use a binary file\'s SHA256 hash as the password')
|
|
43
53
|
.option('--password-script <script>', 'Execute script and read password from its stdout (similar to GIT_ASKPASS)')
|
|
54
|
+
.option('-r', 'Do not trim the password')
|
|
55
|
+
.option('-f, --force', 'Force overrides the output file')
|
|
44
56
|
.argument('[inputFile]', 'Input file path (empty for string decryption, "-" for stdin)')
|
|
45
57
|
.argument('[outputFile]', 'Output file path (stdout if not specified)')
|
|
46
58
|
.action(this.handleDecrypt.bind(this));
|
|
@@ -50,11 +62,15 @@ class CryptCLI {
|
|
|
50
62
|
.command('change-password')
|
|
51
63
|
.description('Change password of encrypted file')
|
|
52
64
|
.option('--password <password>', 'Specify current password directly (NOT RECOMMENDED)')
|
|
53
|
-
.option('--password-file <file>', 'Read current password from specified file')
|
|
65
|
+
.option('--password-file <file>', 'Read current password text from specified file')
|
|
66
|
+
.option('--password-data <file>', 'Use a binary file\'s SHA256 hash as the current password')
|
|
54
67
|
.option('--password-script <script>', 'Execute script and read current password from its stdout')
|
|
55
68
|
.option('--new-password <password>', 'Specify new password directly (NOT RECOMMENDED)')
|
|
56
|
-
.option('--new-password-file <file>', 'Read new password from specified file')
|
|
69
|
+
.option('--new-password-file <file>', 'Read new password text from specified file')
|
|
70
|
+
.option('--new-password-data <file>', 'Use a binary file\'s SHA256 hash as the new password')
|
|
57
71
|
.option('--new-password-script <script>', 'Execute script and read new password from its stdout')
|
|
72
|
+
.option('-r', 'Do not trim the password')
|
|
73
|
+
.option('-f, --force', 'Force overrides the output file (if specified outputFile)')
|
|
58
74
|
.argument('<inputFile>', 'Input file path (cannot be empty, "-" for stdin)')
|
|
59
75
|
.argument('[outputFile]', 'Output file path (in-place modification if not specified)')
|
|
60
76
|
.action(this.handleChangePassword.bind(this));
|
|
@@ -75,13 +91,40 @@ class CryptCLI {
|
|
|
75
91
|
return options.password;
|
|
76
92
|
}
|
|
77
93
|
|
|
94
|
+
// If password data is specified
|
|
95
|
+
if (options.passwordData) {
|
|
96
|
+
const progressBar = new cliProgress.SingleBar({
|
|
97
|
+
format: 'Computing password |{bar}| {percentage}% | {value}/{total} bytes',
|
|
98
|
+
barCompleteChar: '\u2588',
|
|
99
|
+
barIncompleteChar: '\u2591',
|
|
100
|
+
hideCursor: true
|
|
101
|
+
});
|
|
102
|
+
progressBar.start(0, 0);
|
|
103
|
+
try {
|
|
104
|
+
return await sha256File(options.passwordData, (_, bytesRead, totalSize) => {
|
|
105
|
+
if (progressBar.getTotal() !== totalSize) {
|
|
106
|
+
progressBar.setTotal(totalSize);
|
|
107
|
+
}
|
|
108
|
+
progressBar.update(bytesRead);
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`Failed to read password from file: ${error}`);
|
|
112
|
+
} finally {
|
|
113
|
+
progressBar.stop();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
78
117
|
// If password file is specified
|
|
79
118
|
if (options.passwordFile) {
|
|
80
119
|
try {
|
|
81
120
|
const password = await fs.readFile(options.passwordFile, 'utf8');
|
|
82
|
-
|
|
121
|
+
if (password.includes('\ufffd')) process.stderr.write('Caution: Password file doesn\'t seem like UTF-8 text, this may cause dangerous encryption\n');
|
|
122
|
+
if (options.r) return password;
|
|
123
|
+
const trimmed = password.trim();
|
|
124
|
+
if (trimmed !== password) process.stdout.write('Warning: Password was trimmed');
|
|
125
|
+
return trimmed;
|
|
83
126
|
} catch (error) {
|
|
84
|
-
throw new Error(`Failed to read password from file: ${error
|
|
127
|
+
throw new Error(`Failed to read password from file: ${error}`);
|
|
85
128
|
}
|
|
86
129
|
}
|
|
87
130
|
|
|
@@ -97,7 +140,10 @@ class CryptCLI {
|
|
|
97
140
|
|
|
98
141
|
child.on('close', (code) => {
|
|
99
142
|
if (code === 0) {
|
|
100
|
-
resolve(stdout
|
|
143
|
+
if (options.r) resolve(stdout);
|
|
144
|
+
const trimmed = stdout.trim();
|
|
145
|
+
if (trimmed !== stdout) process.stdout.write('Warning: Password was trimmed');
|
|
146
|
+
resolve(trimmed);
|
|
101
147
|
} else {
|
|
102
148
|
reject(new Error(`Password script failed with code ${code}`));
|
|
103
149
|
}
|
|
@@ -167,13 +213,14 @@ class CryptCLI {
|
|
|
167
213
|
}
|
|
168
214
|
}
|
|
169
215
|
|
|
170
|
-
async createFileWriter(filePath) {
|
|
216
|
+
async createFileWriter(filePath, force = false) {
|
|
171
217
|
if (filePath === '-') {
|
|
172
218
|
return (data) => {
|
|
173
219
|
process.stdout.write(Buffer.from(data));
|
|
174
220
|
return Promise.resolve();
|
|
175
221
|
};
|
|
176
222
|
} else {
|
|
223
|
+
if (!force && (await exists(filePath))) throw new Error('Output file already exists')
|
|
177
224
|
const fileHandle = await fs.open(filePath, 'w');
|
|
178
225
|
return async (data) => {
|
|
179
226
|
await fileHandle.write(Buffer.from(data));
|
|
@@ -196,6 +243,7 @@ class CryptCLI {
|
|
|
196
243
|
}
|
|
197
244
|
|
|
198
245
|
async handleEncrypt(inputFile, outputFile, options) {
|
|
246
|
+
let progressBar;
|
|
199
247
|
try {
|
|
200
248
|
// Auto set skipConfirm if password is provided via non-interactive methods
|
|
201
249
|
if (options.password || options.passwordFile || options.passwordScript) {
|
|
@@ -230,7 +278,7 @@ class CryptCLI {
|
|
|
230
278
|
} else {
|
|
231
279
|
// File encryption
|
|
232
280
|
const fileReader = await this.createFileReader(inputFile);
|
|
233
|
-
const fileWriter = await this.createFileWriter(outputFile || '-');
|
|
281
|
+
const fileWriter = await this.createFileWriter(outputFile || '-', options.force);
|
|
234
282
|
if (inputFile === '-' && (!options.passwordScript)) options.passwordScript = 'sdc-askpass';
|
|
235
283
|
const password = await this.getPassword(options, 'password') || '';
|
|
236
284
|
if ('' === password) {
|
|
@@ -250,7 +298,7 @@ class CryptCLI {
|
|
|
250
298
|
}
|
|
251
299
|
|
|
252
300
|
// Create progress bar for file encryption
|
|
253
|
-
|
|
301
|
+
progressBar = new cliProgress.SingleBar({
|
|
254
302
|
format: 'Encrypting |{bar}| {percentage}% | {value}/{total} bytes',
|
|
255
303
|
barCompleteChar: '\u2588',
|
|
256
304
|
barIncompleteChar: '\u2591',
|
|
@@ -272,6 +320,7 @@ class CryptCLI {
|
|
|
272
320
|
progressBar.update(totalBytes);
|
|
273
321
|
}
|
|
274
322
|
progressBar.stop();
|
|
323
|
+
progressBar = null;
|
|
275
324
|
|
|
276
325
|
if (success) {
|
|
277
326
|
if (outputFile) {
|
|
@@ -282,12 +331,14 @@ class CryptCLI {
|
|
|
282
331
|
}
|
|
283
332
|
}
|
|
284
333
|
} catch (error) {
|
|
334
|
+
if (progressBar) (progressBar.stop(), process.stderr.write('\n'));
|
|
285
335
|
process.stderr.write(`${error}\n`);
|
|
286
336
|
process.exit(1);
|
|
287
337
|
}
|
|
288
338
|
}
|
|
289
339
|
|
|
290
340
|
async handleDecrypt(inputFile, outputFile, options) {
|
|
341
|
+
let progressBar;
|
|
291
342
|
try {
|
|
292
343
|
options.skipConfirm = true;
|
|
293
344
|
if (!inputFile) {
|
|
@@ -314,7 +365,7 @@ class CryptCLI {
|
|
|
314
365
|
} else {
|
|
315
366
|
// File decryption
|
|
316
367
|
const fileReader = await this.createFileReader(inputFile);
|
|
317
|
-
const fileWriter = await this.createFileWriter(outputFile || '-');
|
|
368
|
+
const fileWriter = await this.createFileWriter(outputFile || '-', options.force);
|
|
318
369
|
if (inputFile === '-' && (!options.passwordScript)) options.passwordScript = 'sdc-askpass';
|
|
319
370
|
const password = await this.getPassword(options, 'password') || '';
|
|
320
371
|
|
|
@@ -331,7 +382,7 @@ class CryptCLI {
|
|
|
331
382
|
}
|
|
332
383
|
|
|
333
384
|
// Create progress bar for file decryption
|
|
334
|
-
|
|
385
|
+
progressBar = new cliProgress.SingleBar({
|
|
335
386
|
format: 'Decrypting |{bar}| {percentage}% | {value}/{total} bytes',
|
|
336
387
|
barCompleteChar: '\u2588',
|
|
337
388
|
barIncompleteChar: '\u2591',
|
|
@@ -354,6 +405,7 @@ class CryptCLI {
|
|
|
354
405
|
progressBar.update(totalBytes);
|
|
355
406
|
}
|
|
356
407
|
progressBar.stop();
|
|
408
|
+
progressBar = null;
|
|
357
409
|
|
|
358
410
|
if (success) {
|
|
359
411
|
if (outputFile) {
|
|
@@ -364,6 +416,7 @@ class CryptCLI {
|
|
|
364
416
|
}
|
|
365
417
|
}
|
|
366
418
|
} catch (error) {
|
|
419
|
+
if (progressBar) (progressBar.stop(), process.stderr.write('\n'));
|
|
367
420
|
process.stderr.write(`${error}\n`);
|
|
368
421
|
process.exit(1);
|
|
369
422
|
}
|
|
@@ -379,13 +432,15 @@ class CryptCLI {
|
|
|
379
432
|
const currentPassword = await this.getPassword({
|
|
380
433
|
password: options.password,
|
|
381
434
|
passwordFile: options.passwordFile,
|
|
382
|
-
|
|
435
|
+
passwordData: options.passwordData,
|
|
436
|
+
passwordScript: options.passwordScript,
|
|
383
437
|
}, 'current password') || '';
|
|
384
438
|
|
|
385
439
|
// Get new password
|
|
386
440
|
const newPassword = await this.getPassword({
|
|
387
441
|
password: options.newPassword,
|
|
388
442
|
passwordFile: options.newPasswordFile,
|
|
443
|
+
passwordData: options.newPasswordData,
|
|
389
444
|
passwordScript: options.newPasswordScript,
|
|
390
445
|
skipConfirm: true
|
|
391
446
|
}, 'new password') || '';
|
|
@@ -419,6 +474,7 @@ class CryptCLI {
|
|
|
419
474
|
newHeaderBuffer,
|
|
420
475
|
fileBuffer.slice(newHeader.size)
|
|
421
476
|
]);
|
|
477
|
+
if (!options.force && await fs.exists(outputFile)) throw new Error('Output file already exists');
|
|
422
478
|
await fs.writeFile(outputFile, outputBuffer);
|
|
423
479
|
process.stderr.write(`New password has been applied to ${outputFile}\n`);
|
|
424
480
|
} else if (inputFile !== '-') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdc-cli",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A useful CLI in Node.js to use simple-data-crypto library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"encryption",
|
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
"cli-progress": "^3.12.0",
|
|
26
26
|
"commander": "^14.0.2",
|
|
27
27
|
"inquirer": "^13.0.1",
|
|
28
|
-
"simple-data-crypto": "^1.
|
|
28
|
+
"simple-data-crypto": "^1.101.1"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/sha256file.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { createReadStream, statSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
export function sha256File(filename, onProgress = null) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
let fileSize;
|
|
7
|
+
try {
|
|
8
|
+
const stats = statSync(filename);
|
|
9
|
+
fileSize = stats.size;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
reject(error);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let totalBytesRead = 0;
|
|
16
|
+
const hash = createHash('sha256');
|
|
17
|
+
const stream = createReadStream(filename);
|
|
18
|
+
|
|
19
|
+
stream.on('data', (chunk) => {
|
|
20
|
+
hash.update(chunk);
|
|
21
|
+
totalBytesRead += chunk.length;
|
|
22
|
+
|
|
23
|
+
if (onProgress && typeof onProgress === 'function') {
|
|
24
|
+
const progress = fileSize > 0 ? totalBytesRead / fileSize : 0;
|
|
25
|
+
onProgress(progress, totalBytesRead, fileSize);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
stream.on('end', () => {
|
|
30
|
+
if (onProgress && typeof onProgress === 'function') {
|
|
31
|
+
onProgress(1, fileSize, fileSize);
|
|
32
|
+
}
|
|
33
|
+
resolve(hash.digest('hex'));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
stream.on('error', (error) => {
|
|
37
|
+
reject(error);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|