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.
Files changed (3) hide show
  1. package/cli.js +70 -14
  2. package/package.json +2 -2
  3. 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.1.0');
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
- return password.trim();
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.message}`);
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.trim());
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
- const progressBar = new cliProgress.SingleBar({
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
- const progressBar = new cliProgress.SingleBar({
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
- passwordScript: options.passwordScript
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.0",
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.100.0"
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
+