obscr 0.1.2 → 0.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/bin/index.js CHANGED
@@ -2,8 +2,9 @@
2
2
  const yargs = require("yargs");
3
3
  const chalk = require("chalk");
4
4
  const fs = require("fs");
5
-
6
5
  const { prompt } = require("inquirer");
6
+ const ora = require("ora");
7
+ const boxen = require("boxen");
7
8
 
8
9
  const { encrypt, decrypt } = require("./utils/crypto");
9
10
  const {
@@ -11,147 +12,478 @@ const {
11
12
  extractMessageFromImage,
12
13
  } = require("./utils/steg");
13
14
 
14
- //just to increase obfuscation for steg, not actually used for the AES encryption
15
+ // NOTE: This is just to increase obfuscation for steganography, NOT used for AES encryption
16
+ // Kept for backward compatibility with images encoded using older versions
15
17
  const SECRET_KEY = "S3cReTK3Y";
16
18
 
17
- const usage = chalk.keyword("violet")("\nUsage: obscr <cmd> [options]");
19
+ // Global flags
20
+ let VERBOSE = false;
21
+ let QUIET = false;
18
22
 
19
- const options = yargs
20
- .usage(usage)
23
+ /**
24
+ * Logging utilities
25
+ */
26
+ const log = {
27
+ info: (msg) => !QUIET && console.log(chalk.blue("ℹ"), msg),
28
+ success: (msg) => !QUIET && console.log(chalk.green("✔"), msg),
29
+ error: (msg) => console.error(chalk.red("✖"), msg),
30
+ warn: (msg) => !QUIET && console.log(chalk.yellow("⚠"), msg),
31
+ verbose: (msg) => VERBOSE && console.log(chalk.gray("→"), msg),
32
+ box: (msg, options = {}) => !QUIET && console.log(boxen(msg, {
33
+ padding: 1,
34
+ margin: 1,
35
+ borderStyle: "round",
36
+ borderColor: "cyan",
37
+ ...options
38
+ })),
39
+ };
40
+
41
+ /**
42
+ * Display welcome banner
43
+ */
44
+ function showBanner() {
45
+ if (QUIET) return;
46
+
47
+ const banner = chalk.keyword("violet").bold("obscr") +
48
+ chalk.gray(" - Encrypt and hide your secure data\n") +
49
+ chalk.dim("v0.2.1");
50
+
51
+ log.box(banner, { borderColor: "magenta" });
52
+ }
53
+
54
+ /**
55
+ * Show usage examples
56
+ */
57
+ function showExamples() {
58
+ const examples = `
59
+ ${chalk.bold("Examples:")}
60
+
61
+ ${chalk.cyan("Encrypt with compression:")}
62
+ $ obscr encrypt -f photo.png -o secret.png --compress
63
+
64
+ ${chalk.cyan("Decrypt to file:")}
65
+ $ obscr decrypt -f secret.png -o message.txt
66
+
67
+ ${chalk.cyan("Interactive mode:")}
68
+ $ obscr interactive
69
+
70
+ ${chalk.cyan("Verbose output:")}
71
+ $ obscr encrypt -f image.png --verbose
72
+
73
+ ${chalk.cyan("Quiet mode (minimal output):")}
74
+ $ obscr decrypt -f image.png --quiet
75
+ `;
76
+
77
+ console.log(examples);
78
+ }
79
+
80
+ /**
81
+ * Interactive mode - guided workflow
82
+ */
83
+ async function interactiveMode() {
84
+ showBanner();
85
+
86
+ const { action } = await prompt({
87
+ type: "list",
88
+ name: "action",
89
+ message: "What would you like to do?",
90
+ choices: [
91
+ { name: "🔒 Encrypt and hide a message in an image", value: "encrypt" },
92
+ { name: "🔓 Decrypt and extract a message from an image", value: "decrypt" },
93
+ { name: "ℹ️ Show usage examples", value: "examples" },
94
+ { name: "❌ Exit", value: "exit" },
95
+ ],
96
+ });
97
+
98
+ if (action === "exit") {
99
+ log.info("Goodbye!");
100
+ return;
101
+ }
102
+
103
+ if (action === "examples") {
104
+ showExamples();
105
+ return;
106
+ }
107
+
108
+ if (action === "encrypt") {
109
+ const answers = await prompt([
110
+ {
111
+ type: "input",
112
+ name: "filename",
113
+ message: "Path to the PNG image:",
114
+ validate: (input) => {
115
+ if (!input) return "Filename is required";
116
+ if (!fs.existsSync(input)) return "File does not exist";
117
+ if (!input.toLowerCase().endsWith(".png")) return "Must be a PNG file";
118
+ return true;
119
+ },
120
+ },
121
+ {
122
+ type: "input",
123
+ name: "output",
124
+ message: "Output filename:",
125
+ default: "encoded.png",
126
+ },
127
+ {
128
+ type: "confirm",
129
+ name: "compress",
130
+ message: "Enable compression?",
131
+ default: false,
132
+ },
133
+ {
134
+ type: "editor",
135
+ name: "message",
136
+ message: "Enter your secret message (editor will open):",
137
+ },
138
+ {
139
+ type: "password",
140
+ name: "password",
141
+ message: "Enter password:",
142
+ mask: "*",
143
+ },
144
+ {
145
+ type: "password",
146
+ name: "confirmPassword",
147
+ message: "Confirm password:",
148
+ mask: "*",
149
+ },
150
+ ]);
151
+
152
+ if (answers.password !== answers.confirmPassword) {
153
+ log.error("Passwords don't match!");
154
+ return;
155
+ }
156
+
157
+ await encryptCommand(
158
+ answers.filename,
159
+ answers.output,
160
+ answers.compress,
161
+ answers.message,
162
+ answers.password
163
+ );
164
+ } else if (action === "decrypt") {
165
+ const answers = await prompt([
166
+ {
167
+ type: "input",
168
+ name: "filename",
169
+ message: "Path to the encoded PNG image:",
170
+ validate: (input) => {
171
+ if (!input) return "Filename is required";
172
+ if (!fs.existsSync(input)) return "File does not exist";
173
+ return true;
174
+ },
175
+ },
176
+ {
177
+ type: "input",
178
+ name: "output",
179
+ message: "Save to file (leave empty to display):",
180
+ default: "",
181
+ },
182
+ {
183
+ type: "password",
184
+ name: "password",
185
+ message: "Enter password:",
186
+ mask: "*",
187
+ },
188
+ ]);
189
+
190
+ await decryptCommand(answers.filename, answers.output || null, answers.password);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Encrypt command with enhanced UX
196
+ */
197
+ async function encryptCommand(filename, output, compress, message, password) {
198
+ const spinner = ora();
199
+
200
+ try {
201
+ // Validate input file
202
+ log.verbose(`Validating input file: ${filename}`);
203
+ if (!fs.existsSync(filename)) {
204
+ log.error(`Image file not found: ${filename}`);
205
+ return process.exit(1);
206
+ }
207
+
208
+ // Check for output file overwrite
209
+ if (fs.existsSync(output)) {
210
+ const { overwrite } = await prompt({
211
+ type: "confirm",
212
+ name: "overwrite",
213
+ message: `File ${output} already exists. Overwrite?`,
214
+ default: false,
215
+ });
216
+
217
+ if (!overwrite) {
218
+ log.warn("Operation cancelled");
219
+ return process.exit(0);
220
+ }
221
+ }
222
+
223
+ if (!message || message.trim().length === 0) {
224
+ log.error("Message cannot be empty");
225
+ return process.exit(1);
226
+ }
227
+
228
+ // Step 1: Encrypt message
229
+ spinner.start("Encrypting message with AES-256-GCM...");
230
+ log.verbose(`Using ${compress ? "compressed" : "uncompressed"} format`);
231
+
232
+ const encrypted = await encrypt(message, password, compress);
233
+
234
+ spinner.succeed("Message encrypted");
235
+ log.verbose(`Encrypted data length: ${encrypted.length} characters`);
236
+
237
+ if (compress) {
238
+ log.info("Compression enabled");
239
+ }
240
+
241
+ // Step 2: Embed in image
242
+ spinner.start("Embedding encrypted message into image...");
243
+ log.verbose(`Encoding to: ${output}`);
244
+
245
+ const result = await encodeMessageToImage(
246
+ filename,
247
+ encrypted,
248
+ password + SECRET_KEY,
249
+ output
250
+ );
251
+
252
+ if (!result.success) {
253
+ spinner.fail("Encoding failed");
254
+ log.error(result.error);
255
+ return process.exit(1);
256
+ }
257
+
258
+ spinner.succeed("Message successfully hidden in image");
259
+
260
+ // Show summary
261
+ if (!QUIET) {
262
+ const summary = `
263
+ ${chalk.green.bold("✔ Encryption Successful")}
21
264
 
22
- // Create encrypt command
265
+ ${chalk.bold("Output:")} ${output}
266
+ ${chalk.bold("Capacity Used:")} ${result.capacity.utilization}
267
+ ${chalk.bold(" Total:")} ${result.capacity.totalBits} bits
268
+ ${chalk.bold(" Used:")} ${result.capacity.usedBits} bits
269
+ ${compress ? chalk.bold("Compression:") + " Enabled" : ""}
270
+ `;
271
+ log.box(summary.trim(), { borderColor: "green" });
272
+ }
273
+
274
+ log.verbose("Encryption complete");
275
+ } catch (err) {
276
+ spinner.fail("Encryption failed");
277
+ log.error(err.message);
278
+ log.verbose(err.stack);
279
+ process.exit(1);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Decrypt command with enhanced UX
285
+ */
286
+ async function decryptCommand(filename, output, password) {
287
+ const spinner = ora();
288
+
289
+ try {
290
+ // Validate input file
291
+ log.verbose(`Validating input file: ${filename}`);
292
+ if (!fs.existsSync(filename)) {
293
+ log.error(`Image file not found: ${filename}`);
294
+ return process.exit(1);
295
+ }
296
+
297
+ // Step 1: Extract from image
298
+ spinner.start("Extracting encrypted message from image...");
299
+ log.verbose(`Reading from: ${filename}`);
300
+
301
+ const extractResult = await extractMessageFromImage(
302
+ filename,
303
+ password + SECRET_KEY
304
+ );
305
+
306
+ if (!extractResult.success) {
307
+ spinner.fail("Extraction failed");
308
+ log.error(extractResult.error);
309
+ return process.exit(1);
310
+ }
311
+
312
+ spinner.succeed("Encrypted message extracted");
313
+ log.verbose(`Extracted ${extractResult.data.length} characters`);
314
+
315
+ // Step 2: Decrypt message
316
+ spinner.start("Decrypting message with AES-256-GCM...");
317
+
318
+ const decrypted = await decrypt(extractResult.data, password);
319
+
320
+ spinner.succeed("Message decrypted successfully");
321
+
322
+ // Display or save result
323
+ if (output) {
324
+ fs.writeFileSync(output, decrypted);
325
+ log.success(`Message saved to: ${output}`);
326
+ } else {
327
+ if (!QUIET) {
328
+ const messageBox = `
329
+ ${chalk.green.bold("✔ Decryption Successful")}
330
+
331
+ ${chalk.bold("Message:")}
332
+ ${chalk.white(decrypted)}
333
+ `;
334
+ log.box(messageBox.trim(), { borderColor: "green" });
335
+ } else {
336
+ console.log(decrypted);
337
+ }
338
+ }
339
+
340
+ log.verbose("Decryption complete");
341
+ } catch (err) {
342
+ spinner.fail("Decryption failed");
343
+ log.error("Could not decrypt. Wrong password or corrupted image.");
344
+ log.verbose(err.message);
345
+ log.verbose(err.stack);
346
+ process.exit(1);
347
+ }
348
+ }
349
+
350
+ // Parse arguments
351
+ const usage = chalk.keyword("violet")("\nUsage: obscr <command> [options]");
352
+
353
+ const argv = yargs
354
+ .usage(usage)
355
+ .option("verbose", {
356
+ alias: "v",
357
+ describe: "Show detailed output",
358
+ type: "boolean",
359
+ global: true,
360
+ })
361
+ .option("quiet", {
362
+ alias: "q",
363
+ describe: "Minimal output (only essential messages)",
364
+ type: "boolean",
365
+ global: true,
366
+ })
23
367
  .command(
24
368
  "encrypt",
25
- "Encrypts and hides the message into an image.",
369
+ "Encrypt and hide a message in an image",
26
370
  {
27
371
  f: {
28
372
  alias: "filename",
29
- describe: "Name of the png image to hide the message in",
30
- demandOption: true, // Required
373
+ describe: "PNG image to hide the message in",
374
+ demandOption: true,
31
375
  type: "string",
32
376
  },
377
+ o: {
378
+ alias: "output",
379
+ describe: "Output filename for encoded image",
380
+ demandOption: false,
381
+ type: "string",
382
+ default: "encoded.png",
383
+ },
33
384
  c: {
34
385
  alias: "compress",
35
- describe: "Compress the secret message",
386
+ describe: "Compress message before encryption (50-90% size reduction)",
36
387
  demandOption: false,
37
- type: "string",
388
+ type: "boolean",
389
+ default: false,
38
390
  },
39
391
  },
40
-
41
392
  async (argv) => {
42
- const { f: filename, c: compress } = argv;
393
+ VERBOSE = argv.verbose;
394
+ QUIET = argv.quiet;
395
+
396
+ if (!QUIET) showBanner();
397
+
398
+ const { f: filename, o: output, c: compress } = argv;
43
399
 
44
400
  const { password, confirmPassword, message } = await prompt([
45
401
  {
46
402
  type: "editor",
47
403
  name: "message",
48
- message: "Type the secret message",
404
+ message: "Type the secret message:",
49
405
  },
50
406
  {
51
407
  type: "password",
52
408
  name: "password",
53
- message: "Enter Password:",
409
+ message: "Enter password:",
410
+ mask: "*",
54
411
  },
55
412
  {
56
413
  type: "password",
57
414
  name: "confirmPassword",
58
- message: "Re-type Password:",
415
+ message: "Confirm password:",
416
+ mask: "*",
59
417
  },
60
418
  ]);
61
419
 
62
- // config.set({ token });
63
420
  if (password !== confirmPassword) {
64
- return console.log(chalk.keyword("red")("Passwords don't match"));
65
- }
66
-
67
- /*
68
- #1. encrypt message with key using AES-256-GCM
69
- */
70
-
71
- try {
72
- const encrypted = encrypt(message, password);
73
- console.log(encrypted);
74
- await encodeMessageToImage(filename, encrypted, password + SECRET_KEY);
75
- } catch (e) {
76
- return console.log(chalk.keyword("red")(e.message));
421
+ log.error("Passwords don't match");
422
+ return process.exit(1);
77
423
  }
78
424
 
79
- /*
80
- #2. embed encrypted message scattered into image
81
- */
82
-
83
- console.log(chalk.keyword("green")("Successful"));
425
+ await encryptCommand(filename, output, compress, message, password);
84
426
  }
85
427
  )
86
- //create decrypt command
87
428
  .command(
88
429
  "decrypt",
89
- "Decrypts message from image.",
430
+ "Decrypt and extract a message from an image",
90
431
  {
91
432
  f: {
92
433
  alias: "filename",
93
- describe: "Name of the png image to hide the message in",
94
- demandOption: true, // Required
434
+ describe: "PNG image containing hidden message",
435
+ demandOption: true,
95
436
  type: "string",
96
437
  },
97
438
  o: {
98
439
  alias: "output",
99
- describe: "Output filename",
100
- demandOption: false, // Required
440
+ describe: "Save decrypted message to file",
441
+ demandOption: false,
101
442
  type: "string",
102
443
  },
103
444
  },
104
-
105
445
  async (argv) => {
446
+ VERBOSE = argv.verbose;
447
+ QUIET = argv.quiet;
448
+
449
+ if (!QUIET) showBanner();
450
+
106
451
  const { f: filename, o: output } = argv;
107
452
 
108
453
  const { password } = await prompt({
109
454
  type: "password",
110
455
  name: "password",
111
- message: "Enter Password:",
456
+ message: "Enter password:",
457
+ mask: "*",
112
458
  });
113
459
 
114
- /*
115
- #2. extract encrypted message from image
116
- */
117
-
118
- /*
119
- #1. decrypt message with key using AES-256-GCM
120
- */
121
-
122
- try {
123
- const [succeeded, extracted] = await extractMessageFromImage(
124
- filename,
125
- password + SECRET_KEY
126
- );
127
-
128
- if (!succeeded) throw extracted;
129
- const decrypted = decrypt(extracted, password);
130
-
131
- console.log(decrypted);
132
-
133
- if (output) fs.writeFileSync(output, decrypted);
134
- } catch (e) {
135
- console.log(e);
136
- return console.log(chalk.keyword("red")("Could not decrypt"));
137
- }
138
-
139
- console.log(chalk.keyword("green")("Successful"));
460
+ await decryptCommand(filename, output, password);
140
461
  }
141
462
  )
142
-
143
- .epilog("copyright 2022")
144
- .help(true).argv;
145
-
146
- if (yargs.argv._[0] == null) {
147
- yargs.showHelp();
148
- process.exit(0);
149
- }
150
-
151
- /*
152
- TODO: error out if image is too small
153
- TODO: implement compression
154
- TODO: implement streaming
155
- TODO: file encryption
156
-
157
- */
463
+ .command(
464
+ "interactive",
465
+ "Start interactive mode with guided workflow",
466
+ {},
467
+ async (argv) => {
468
+ VERBOSE = argv.verbose;
469
+ QUIET = argv.quiet;
470
+ await interactiveMode();
471
+ }
472
+ )
473
+ .command(
474
+ "examples",
475
+ "Show usage examples",
476
+ {},
477
+ () => {
478
+ showBanner();
479
+ showExamples();
480
+ }
481
+ )
482
+ .epilog(`Run ${chalk.cyan("obscr examples")} to see usage examples`)
483
+ .help()
484
+ .alias("help", "h")
485
+ .version("0.2.1")
486
+ .alias("version", "V")
487
+ .demandCommand(1, "You must specify a command")
488
+ .strict()
489
+ .argv;