obscr 0.2.1 → 1.0.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.
package/bin/index.js CHANGED
@@ -6,11 +6,12 @@ const { prompt } = require("inquirer");
6
6
  const ora = require("ora");
7
7
  const boxen = require("boxen");
8
8
 
9
- const { encrypt, decrypt } = require("./utils/crypto");
9
+ // Shared core logic (reused by CLI and desktop UI)
10
+ const { encrypt, decrypt } = require("../lib/crypto");
10
11
  const {
11
12
  encodeMessageToImage,
12
13
  extractMessageFromImage,
13
- } = require("./utils/steg");
14
+ } = require("../lib/steg");
14
15
 
15
16
  // NOTE: This is just to increase obfuscation for steganography, NOT used for AES encryption
16
17
  // Kept for backward compatibility with images encoded using older versions
@@ -20,35 +21,67 @@ const SECRET_KEY = "S3cReTK3Y";
20
21
  let VERBOSE = false;
21
22
  let QUIET = false;
22
23
 
24
+ // Color palette - using custom green #B4FA72 for consistent branding
25
+ const colors = {
26
+ primary: chalk.hex("#B4FA72"), // Custom lime green
27
+ secondary: chalk.hex("#B4FA72"), // Same green for consistency
28
+ accent: chalk.hex("#B4FA72"), // Same green
29
+ muted: chalk.gray, // Gray for secondary text
30
+ error: chalk.red,
31
+ warning: chalk.yellow,
32
+ dim: chalk.hex("#B4FA72").dim, // Dimmed custom green for subtle text
33
+ };
34
+
35
+ /**
36
+ * ASCII Art Logo (matching the UI)
37
+ */
38
+ const asciiLogo = `
39
+ ██████╗ ██████╗ ███████╗ ██████╗██████╗
40
+ ██╔═══██╗██╔══██╗██╔════╝██╔════╝██╔══██╗
41
+ ██║ ██║██████╔╝███████╗██║ ██████╔╝
42
+ ██║ ██║██╔══██╗╚════██║██║ ██╔══██╗
43
+ ╚██████╔╝██████╔╝███████║╚██████╗██║ ██║
44
+ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝
45
+ `;
46
+
47
+ /**
48
+ * Terminal-style controls (for visual effect)
49
+ */
50
+ const terminalControls = chalk.red("●") + " " + chalk.yellow("●") + " " + chalk.green("●");
51
+
23
52
  /**
24
53
  * Logging utilities
25
54
  */
26
55
  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),
56
+ info: (msg) => !QUIET && console.log(colors.accent("ℹ"), msg),
57
+ success: (msg) => !QUIET && console.log(colors.primary("✔"), msg),
58
+ error: (msg) => console.error(colors.error("✖"), msg),
59
+ warn: (msg) => !QUIET && console.log(colors.warning("⚠"), msg),
60
+ verbose: (msg) => VERBOSE && console.log(colors.dim("→"), msg),
32
61
  box: (msg, options = {}) => !QUIET && console.log(boxen(msg, {
33
62
  padding: 1,
34
63
  margin: 1,
35
64
  borderStyle: "round",
36
- borderColor: "cyan",
65
+ borderColor: "#B4FA72", // Custom lime green
37
66
  ...options
38
67
  })),
39
68
  };
40
69
 
41
70
  /**
42
- * Display welcome banner
71
+ * Display welcome banner with ASCII art
43
72
  */
44
73
  function showBanner() {
45
74
  if (QUIET) return;
46
75
 
47
- const banner = chalk.keyword("violet").bold("obscr") +
48
- chalk.gray(" - Encrypt and hide your secure data\n") +
49
- chalk.dim("v0.2.1");
76
+ const header = terminalControls;
77
+ const logo = colors.primary(asciiLogo);
78
+ const subtitle = colors.primary("$") + " " + chalk.white("Steganography & Encryption Tool");
79
+ const info = colors.muted("AES-256-GCM Encryption | LSB Steganography | Secure Data Hiding");
80
+ const version = colors.dim("v1.0.0");
50
81
 
51
- log.box(banner, { borderColor: "magenta" });
82
+ const banner = `${header}\n${logo}\n${subtitle}\n${info}\n\n${version}`;
83
+
84
+ log.box(banner, { borderColor: "green", padding: 0, margin: { top: 1, bottom: 1, left: 1, right: 1 } });
52
85
  }
53
86
 
54
87
  /**
@@ -56,22 +89,22 @@ function showBanner() {
56
89
  */
57
90
  function showExamples() {
58
91
  const examples = `
59
- ${chalk.bold("Examples:")}
92
+ ${colors.primary.bold("Examples:")}
60
93
 
61
- ${chalk.cyan("Encrypt with compression:")}
62
- $ obscr encrypt -f photo.png -o secret.png --compress
94
+ ${colors.primary(">")} ${colors.secondary("Encrypt with compression:")}
95
+ ${colors.dim("$")} obscr encrypt -f photo.png -o secret.png --compress
63
96
 
64
- ${chalk.cyan("Decrypt to file:")}
65
- $ obscr decrypt -f secret.png -o message.txt
97
+ ${colors.primary(">")} ${colors.secondary("Decrypt to file:")}
98
+ ${colors.dim("$")} obscr decrypt -f secret.png -o message.txt
66
99
 
67
- ${chalk.cyan("Interactive mode:")}
68
- $ obscr interactive
100
+ ${colors.primary(">")} ${colors.secondary("Interactive mode:")}
101
+ ${colors.dim("$")} obscr interactive
69
102
 
70
- ${chalk.cyan("Verbose output:")}
71
- $ obscr encrypt -f image.png --verbose
103
+ ${colors.primary(">")} ${colors.secondary("Verbose output:")}
104
+ ${colors.dim("$")} obscr encrypt -f image.png --verbose
72
105
 
73
- ${chalk.cyan("Quiet mode (minimal output):")}
74
- $ obscr decrypt -f image.png --quiet
106
+ ${colors.primary(">")} ${colors.secondary("Quiet mode (minimal output):")}
107
+ ${colors.dim("$")} obscr decrypt -f image.png --quiet
75
108
  `;
76
109
 
77
110
  console.log(examples);
@@ -88,10 +121,10 @@ async function interactiveMode() {
88
121
  name: "action",
89
122
  message: "What would you like to do?",
90
123
  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" },
124
+ { name: colors.primary("[+]") + " Encrypt and hide a message in an image", value: "encrypt" },
125
+ { name: colors.secondary("[-]") + " Decrypt and extract a message from an image", value: "decrypt" },
126
+ { name: colors.accent("[?]") + " Show usage examples", value: "examples" },
127
+ { name: colors.dim("[x]") + " Exit", value: "exit" },
95
128
  ],
96
129
  });
97
130
 
@@ -260,13 +293,13 @@ async function encryptCommand(filename, output, compress, message, password) {
260
293
  // Show summary
261
294
  if (!QUIET) {
262
295
  const summary = `
263
- ${chalk.green.bold("✔ Encryption Successful")}
296
+ ${colors.primary.bold("✔ Encryption Successful")}
264
297
 
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" : ""}
298
+ ${colors.primary(">")} ${colors.muted("Output:")} ${chalk.white(output)}
299
+ ${colors.primary(">")} ${colors.muted("Capacity Used:")} ${colors.secondary(result.capacity.utilization)}
300
+ ${colors.dim("Total:")} ${result.capacity.totalBits} bits
301
+ ${colors.dim("Used:")} ${result.capacity.usedBits} bits
302
+ ${compress ? colors.primary(">") + " " + colors.muted("Compression:") + " " + colors.secondary("Enabled") : ""}
270
303
  `;
271
304
  log.box(summary.trim(), { borderColor: "green" });
272
305
  }
@@ -326,9 +359,10 @@ async function decryptCommand(filename, output, password) {
326
359
  } else {
327
360
  if (!QUIET) {
328
361
  const messageBox = `
329
- ${chalk.green.bold("✔ Decryption Successful")}
362
+ ${colors.primary.bold("✔ Decryption Successful")}
363
+
364
+ ${colors.primary(">")} ${colors.muted("Message:")}
330
365
 
331
- ${chalk.bold("Message:")}
332
366
  ${chalk.white(decrypted)}
333
367
  `;
334
368
  log.box(messageBox.trim(), { borderColor: "green" });
@@ -348,7 +382,10 @@ ${chalk.white(decrypted)}
348
382
  }
349
383
 
350
384
  // Parse arguments
351
- const usage = chalk.keyword("violet")("\nUsage: obscr <command> [options]");
385
+ const usage = colors.primary("\nUsage: obscr <command> [options]");
386
+
387
+ // Track if a command was executed
388
+ let commandExecuted = false;
352
389
 
353
390
  const argv = yargs
354
391
  .usage(usage)
@@ -390,6 +427,7 @@ const argv = yargs
390
427
  },
391
428
  },
392
429
  async (argv) => {
430
+ commandExecuted = true;
393
431
  VERBOSE = argv.verbose;
394
432
  QUIET = argv.quiet;
395
433
 
@@ -443,6 +481,7 @@ const argv = yargs
443
481
  },
444
482
  },
445
483
  async (argv) => {
484
+ commandExecuted = true;
446
485
  VERBOSE = argv.verbose;
447
486
  QUIET = argv.quiet;
448
487
 
@@ -465,6 +504,7 @@ const argv = yargs
465
504
  "Start interactive mode with guided workflow",
466
505
  {},
467
506
  async (argv) => {
507
+ commandExecuted = true;
468
508
  VERBOSE = argv.verbose;
469
509
  QUIET = argv.quiet;
470
510
  await interactiveMode();
@@ -475,15 +515,27 @@ const argv = yargs
475
515
  "Show usage examples",
476
516
  {},
477
517
  () => {
518
+ commandExecuted = true;
478
519
  showBanner();
479
520
  showExamples();
480
521
  }
481
522
  )
482
- .epilog(`Run ${chalk.cyan("obscr examples")} to see usage examples`)
523
+ .epilog(`Run ${colors.primary("obscr examples")} to see usage examples`)
483
524
  .help()
484
525
  .alias("help", "h")
485
- .version("0.2.1")
526
+ .version("1.0.0")
486
527
  .alias("version", "V")
487
- .demandCommand(1, "You must specify a command")
488
- .strict()
489
- .argv;
528
+ .strict();
529
+
530
+ // Parse the arguments
531
+ const parsedArgv = argv.parse();
532
+
533
+ // If no command was executed and not showing help/version, enter interactive mode
534
+ if (!commandExecuted && !parsedArgv.help && !parsedArgv.version) {
535
+ VERBOSE = parsedArgv.verbose;
536
+ QUIET = parsedArgv.quiet;
537
+ interactiveMode().catch((err) => {
538
+ log.error(err.message);
539
+ process.exit(1);
540
+ });
541
+ }
@@ -70,32 +70,56 @@ const encrypt = async (message, password, compress = false) => {
70
70
  * @throws {Error} If decryption fails
71
71
  */
72
72
  const decrypt = async (encrypted, password) => {
73
- const dataSplit = encrypted.split(":");
74
-
75
- // Check if compression flag is present (backward compatible)
76
- const isCompressed = dataSplit.length === 5 && dataSplit[4] === "1";
73
+ // Validate input
74
+ if (!encrypted || typeof encrypted !== "string") {
75
+ throw new Error(
76
+ "Invalid encrypted data. The image may not contain a hidden message, or the extraction failed."
77
+ );
78
+ }
77
79
 
78
- const salt = base64Decoding(dataSplit[0]);
79
- const nonce = base64Decoding(dataSplit[1]);
80
- const ciphertext = dataSplit[2];
81
- const gcmTag = base64Decoding(dataSplit[3]);
80
+ const dataSplit = encrypted.split(":");
82
81
 
83
- const key = await pbkdf2Async(
84
- password,
85
- salt,
86
- cryptoConfig.iterations,
87
- cryptoConfig.keyLength,
88
- cryptoConfig.digest
89
- );
82
+ // Validate format - should have 4 or 5 parts (salt:nonce:ciphertext:tag[:compressed])
83
+ if (dataSplit.length < 4 || dataSplit.length > 5) {
84
+ throw new Error(
85
+ "Invalid encrypted message format. This may indicate:\n" +
86
+ " • Wrong password used for decryption\n" +
87
+ " • Image does not contain an encrypted message\n" +
88
+ " • Corrupted or modified image file"
89
+ );
90
+ }
90
91
 
91
- const decipher = crypto.createDecipheriv(
92
- cryptoConfig.cipherAlgorithm,
93
- key,
94
- nonce
95
- );
96
- decipher.setAuthTag(gcmTag);
92
+ // Validate that all required parts exist and are not empty
93
+ if (!dataSplit[0] || !dataSplit[1] || !dataSplit[2] || !dataSplit[3]) {
94
+ throw new Error(
95
+ "Incomplete encrypted data. Wrong password or corrupted message."
96
+ );
97
+ }
97
98
 
98
99
  try {
100
+ // Check if compression flag is present (backward compatible)
101
+ const isCompressed = dataSplit.length === 5 && dataSplit[4] === "1";
102
+
103
+ const salt = base64Decoding(dataSplit[0]);
104
+ const nonce = base64Decoding(dataSplit[1]);
105
+ const ciphertext = dataSplit[2];
106
+ const gcmTag = base64Decoding(dataSplit[3]);
107
+
108
+ const key = await pbkdf2Async(
109
+ password,
110
+ salt,
111
+ cryptoConfig.iterations,
112
+ cryptoConfig.keyLength,
113
+ cryptoConfig.digest
114
+ );
115
+
116
+ const decipher = crypto.createDecipheriv(
117
+ cryptoConfig.cipherAlgorithm,
118
+ key,
119
+ nonce
120
+ );
121
+ decipher.setAuthTag(gcmTag);
122
+
99
123
  const decryptedChunks = [];
100
124
  decryptedChunks.push(decipher.update(Buffer.from(ciphertext, "base64")));
101
125
  decryptedChunks.push(decipher.final());
@@ -108,7 +132,27 @@ const decrypt = async (encrypted, password) => {
108
132
 
109
133
  return decrypted.toString("utf8");
110
134
  } catch (err) {
111
- throw new Error(`Decryption failed: ${err.message}`);
135
+ // Check for common error patterns
136
+ if (err.message && err.message.includes("Unsupported state or unable to authenticate data")) {
137
+ throw new Error(
138
+ "Authentication failed: Incorrect password or tampered message.\n" +
139
+ "Make sure you're using the same password that was used for encryption."
140
+ );
141
+ }
142
+
143
+ if (err.message && err.message.includes("Invalid base64")) {
144
+ throw new Error(
145
+ "Invalid data encoding. This indicates:\n" +
146
+ " • Wrong password (most likely)\n" +
147
+ " • Corrupted or modified image"
148
+ );
149
+ }
150
+
151
+ // Generic decryption error
152
+ throw new Error(
153
+ `Decryption failed: ${err.message}\n` +
154
+ "This is most likely due to an incorrect password."
155
+ );
112
156
  }
113
157
  };
114
158
 
@@ -127,3 +171,4 @@ const base64Encoding = (input) => input.toString("base64");
127
171
  const base64Decoding = (input) => Buffer.from(input, "base64");
128
172
 
129
173
  module.exports = { encrypt, decrypt };
174
+
@@ -45,7 +45,7 @@
45
45
  permission.
46
46
 
47
47
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
48
- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
48
+ \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
49
49
  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
50
50
  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
51
51
  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
@@ -185,3 +185,4 @@ MersenneTwister.prototype.genrand_int32 = function () {
185
185
  /* These real versions are due to Isaku Wada, 2002/01/09 added */
186
186
 
187
187
  module.exports = { MersenneTwister };
188
+
@@ -7,31 +7,33 @@ const { PNG } = require("pngjs");
7
7
  * @param {number[]} dataBits - Array of data bits to write
8
8
  * @param {string} encryptionKey - Key for scrambling the data
9
9
  * @param {number} totalCapacity - Total capacity of the image in bits
10
- * @returns {number[]} Scrambled bit array with obfuscation
10
+ * @param {boolean} obfuscate - Whether to fill unused bits with random data (default: true)
11
+ * @returns {Object} Object with sparse bit mapping and metadata
11
12
  * @throws {Error} If data is too large for the image
12
13
  */
13
- const prepare_write_data = (dataBits, encryptionKey, totalCapacity) => {
14
+ const prepare_write_data = (dataBits, encryptionKey, totalCapacity, obfuscate = true) => {
14
15
  const dataBitsLength = dataBits.length;
15
16
  if (dataBitsLength > totalCapacity) {
16
17
  throw new Error(
17
18
  `Message too large! Message requires ${dataBitsLength} bits, but image can only hold ${totalCapacity} bits. ` +
18
- `Try using a larger image or enabling compression with --compress flag.`
19
+ `Try using a larger image or enabling compression with --compress flag.`
19
20
  );
20
21
  }
21
22
 
22
- // Initialize with random bits for obfuscation
23
- const result = new Array(totalCapacity);
24
- for (let i = 0; i < totalCapacity; i++) {
25
- result[i] = Math.floor(Math.random() * 2);
26
- }
27
-
28
- // Scramble actual data into random positions
23
+ // Always scramble message bits across entire image using password-derived positions
29
24
  const scrambledOrder = get_hashed_order(encryptionKey, totalCapacity);
25
+ const sparseData = {};
26
+
30
27
  for (let i = 0; i < dataBitsLength; i++) {
31
- result[scrambledOrder[i]] = dataBits[i];
28
+ const position = scrambledOrder[i];
29
+ sparseData[position] = dataBits[i];
32
30
  }
33
31
 
34
- return result;
32
+ return {
33
+ sparse: sparseData,
34
+ obfuscate: obfuscate,
35
+ totalCapacity: totalCapacity,
36
+ };
35
37
  };
36
38
 
37
39
  /**
@@ -61,9 +63,9 @@ const get_bits_lsb = (imageData) => {
61
63
  const result = [];
62
64
  // Process RGB channels (skip alpha at i+3)
63
65
  for (let i = 0; i < imageData.data.length; i += 4) {
64
- result.push(imageData.data[i] % 2 === 1 ? 1 : 0); // Red LSB
65
- result.push(imageData.data[i + 1] % 2 === 1 ? 1 : 0); // Green LSB
66
- result.push(imageData.data[i + 2] % 2 === 1 ? 1 : 0); // Blue LSB
66
+ result.push(imageData.data[i] % 2 === 1 ? 1 : 0); // Red LSB
67
+ result.push(imageData.data[i + 1] % 2 === 1 ? 1 : 0); // Green LSB
68
+ result.push(imageData.data[i + 2] % 2 === 1 ? 1 : 0); // Blue LSB
67
69
  }
68
70
  return result;
69
71
  };
@@ -71,10 +73,10 @@ const get_bits_lsb = (imageData) => {
71
73
  /**
72
74
  * Writes bits to the least significant bits of image RGB channels
73
75
  * @param {Object} imageData - PNG image data object
74
- * @param {number[]} bitsToWrite - Array of bits to write
76
+ * @param {Object} writeData - Object with sparse bit mapping and metadata
75
77
  * @returns {Object} Modified image data
76
78
  */
77
- const write_lsb = (imageData, bitsToWrite) => {
79
+ const write_lsb = (imageData, writeData) => {
78
80
  /**
79
81
  * Clears the LSB of a byte value
80
82
  * @param {number} value - Byte value
@@ -93,21 +95,29 @@ const write_lsb = (imageData, bitsToWrite) => {
93
95
  return value % 2 === 1 ? value : value + 1;
94
96
  };
95
97
 
96
- let bitIndex = 0;
97
- for (let i = 0; i < imageData.data.length; i += 4) {
98
- // Write to RGB channels
99
- imageData.data[i] = bitsToWrite[bitIndex]
100
- ? setLSB(imageData.data[i])
101
- : clearLSB(imageData.data[i]);
102
- imageData.data[i + 1] = bitsToWrite[bitIndex + 1]
103
- ? setLSB(imageData.data[i + 1])
104
- : clearLSB(imageData.data[i + 1]);
105
- imageData.data[i + 2] = bitsToWrite[bitIndex + 2]
106
- ? setLSB(imageData.data[i + 2])
107
- : clearLSB(imageData.data[i + 2]);
108
- imageData.data[i + 3] = 255; // Keep alpha channel at full opacity
109
- bitIndex += 3;
98
+ const { sparse, obfuscate, totalCapacity } = writeData;
99
+
100
+ // Process each bit position in the image
101
+ for (let bitPos = 0; bitPos < totalCapacity; bitPos++) {
102
+ const pixelIdx = Math.floor(bitPos / 3);
103
+ const channelIdx = bitPos % 3;
104
+ const dataIdx = pixelIdx * 4 + channelIdx; // RGBA offset
105
+
106
+ if (sparse.hasOwnProperty(bitPos)) {
107
+ // Write actual message bit at this position
108
+ imageData.data[dataIdx] = sparse[bitPos]
109
+ ? setLSB(imageData.data[dataIdx])
110
+ : clearLSB(imageData.data[dataIdx]);
111
+ } else if (obfuscate) {
112
+ // Fill unused position with random bit for obfuscation
113
+ const randomBit = Math.floor(Math.random() * 2);
114
+ imageData.data[dataIdx] = randomBit
115
+ ? setLSB(imageData.data[dataIdx])
116
+ : clearLSB(imageData.data[dataIdx]);
117
+ }
118
+ // If obfuscate is false and no data at this position, leave pixel unchanged
110
119
  }
120
+
111
121
  return imageData;
112
122
  };
113
123
 
@@ -156,7 +166,8 @@ const extractMessageFromImage = async (imagePath, encryptionKey) => {
156
166
  if (message == null) {
157
167
  return {
158
168
  success: false,
159
- error: "Decryption failed. Possible causes: wrong password, corrupted file, or no hidden message.",
169
+ error:
170
+ "Decryption failed. Possible causes: wrong password, corrupted file, or no hidden message.",
160
171
  };
161
172
  }
162
173
 
@@ -175,13 +186,15 @@ const extractMessageFromImage = async (imagePath, encryptionKey) => {
175
186
  * @param {string} message - Encrypted message to hide
176
187
  * @param {string} encryptionKey - Key for scrambling the data
177
188
  * @param {string} outputPath - Path for the output image (default: "encoded.png")
189
+ * @param {boolean} obfuscate - Whether to fill unused bits with random data (default: true)
178
190
  * @returns {Promise<{success: boolean, outputPath?: string, capacity?: Object, error?: string}>} Result object
179
191
  */
180
192
  const encodeMessageToImage = async (
181
193
  imagePath,
182
194
  message,
183
195
  encryptionKey,
184
- outputPath = "encoded.png"
196
+ outputPath = "encoded.png",
197
+ obfuscate = true
185
198
  ) => {
186
199
  try {
187
200
  validateImageFile(imagePath);
@@ -203,7 +216,8 @@ const encodeMessageToImage = async (
203
216
  const encryptedBitStream = prepare_write_data(
204
217
  bitStream,
205
218
  encryptionKey,
206
- totalCapacity
219
+ totalCapacity,
220
+ obfuscate
207
221
  );
208
222
 
209
223
  const encryptedImageData = write_lsb(imageData, encryptedBitStream);
@@ -229,3 +243,4 @@ module.exports = {
229
243
  encodeMessageToImage,
230
244
  calculateImageCapacity,
231
245
  };
246
+
@@ -25,9 +25,19 @@ const get_hashed_order = (password, arrayLength) => {
25
25
  };
26
26
 
27
27
  /**
28
- * Decodes a UTF-8 byte array to a string
29
- * @param {number[]} bytes - Array of byte values
30
- * @returns {string} Decoded UTF-8 string
28
+ * Decodes a UTF-8 byte array to a string with bounds checking
29
+ * Handles truncated multi-byte sequences gracefully by stopping at truncation
30
+ * rather than reading undefined values. This is especially important when
31
+ * decrypting with the wrong password, which produces random byte sequences.
32
+ *
33
+ * @param {number[]} bytes - Array of byte values (0-255)
34
+ * @returns {string} Decoded UTF-8 string (stops at first truncated sequence)
35
+ * @example
36
+ * // Valid UTF-8
37
+ * utf8Decode([72, 101, 108, 108, 111]); // "Hello"
38
+ *
39
+ * // Truncated multi-byte sequence (wrong password scenario)
40
+ * utf8Decode([72, 101, 195]); // "He" (stops at truncated byte)
31
41
  */
32
42
  const utf8Decode = (bytes) => {
33
43
  const chars = [];
@@ -37,8 +47,6 @@ const utf8Decode = (bytes) => {
37
47
 
38
48
  while (offset < length) {
39
49
  currentByte = bytes[offset];
40
- secondByte = bytes[offset + 1];
41
- thirdByte = bytes[offset + 2];
42
50
 
43
51
  if (128 > currentByte) {
44
52
  // Single-byte character (ASCII)
@@ -46,10 +54,21 @@ const utf8Decode = (bytes) => {
46
54
  offset += 1;
47
55
  } else if (191 < currentByte && currentByte < 224) {
48
56
  // Two-byte character
57
+ if (offset + 1 >= length) {
58
+ // Truncated multi-byte sequence
59
+ break;
60
+ }
61
+ secondByte = bytes[offset + 1];
49
62
  chars.push(String.fromCharCode(((currentByte & 31) << 6) | (secondByte & 63)));
50
63
  offset += 2;
51
64
  } else {
52
65
  // Three-byte character
66
+ if (offset + 2 >= length) {
67
+ // Truncated multi-byte sequence
68
+ break;
69
+ }
70
+ secondByte = bytes[offset + 1];
71
+ thirdByte = bytes[offset + 2];
53
72
  chars.push(
54
73
  String.fromCharCode(
55
74
  ((currentByte & 15) << 12) | ((secondByte & 63) << 6) | (thirdByte & 63)
@@ -171,3 +190,4 @@ const str_to_bits = (str, numCopies) => {
171
190
  };
172
191
 
173
192
  module.exports = { get_hashed_order, str_to_bits, bits_to_str };
193
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "obscr",
3
- "version": "0.2.1",
4
- "description": "Encrypt and hide your secure data",
3
+ "version": "1.0.0",
4
+ "description": "Encrypt and hide your secure data in images using AES-256-GCM encryption and LSB steganography",
5
5
  "main": "bin/index.js",
6
6
  "bin": {
7
7
  "obscr": "./bin/index.js"
@@ -10,11 +10,19 @@
10
10
  "test": "jest --coverage",
11
11
  "test:watch": "jest --watch",
12
12
  "test:unit": "jest test/unit",
13
- "test:integration": "jest test/integration"
13
+ "test:integration": "jest test/integration",
14
+ "dev": "concurrently -k \"vite\" \"npm:electron-dev\"",
15
+ "electron-dev": "electron electron/main.js",
16
+ "build:react": "vite build",
17
+ "build": "npm run build:react && electron-builder"
14
18
  },
15
19
  "publishConfig": {
16
20
  "registry": "https://registry.npmjs.org"
17
21
  },
22
+ "files": [
23
+ "bin/",
24
+ "lib/"
25
+ ],
18
26
  "author": "Johannes Dragulanescu",
19
27
  "license": "MIT",
20
28
  "repository": "github:jdragulanescu/obscr",
@@ -28,7 +36,6 @@
28
36
  "dependencies": {
29
37
  "boxen": "^5.1.2",
30
38
  "chalk": "^4.1.2",
31
- "cli-progress": "^3.12.0",
32
39
  "crypto-js": "^4.1.1",
33
40
  "inquirer": "^8.0.0",
34
41
  "ora": "^5.4.1",
@@ -36,7 +43,37 @@
36
43
  "yargs": "^17.5.1"
37
44
  },
38
45
  "devDependencies": {
39
- "jest": "^29.7.0"
46
+ "@tailwindcss/postcss": "^4.1.18",
47
+ "@vitejs/plugin-react": "^4.0.0",
48
+ "autoprefixer": "^10.4.23",
49
+ "class-variance-authority": "^0.7.1",
50
+ "cli-progress": "^3.12.0",
51
+ "clsx": "^2.1.1",
52
+ "concurrently": "^9.0.0",
53
+ "electron": "^40.0.0",
54
+ "electron-builder": "^26.4.0",
55
+ "jest": "^29.7.0",
56
+ "lucide-react": "^0.562.0",
57
+ "postcss": "^8.5.6",
58
+ "react": "^18.3.1",
59
+ "react-dom": "^18.3.1",
60
+ "tailwind-merge": "^3.4.0",
61
+ "tailwindcss": "^4.1.18",
62
+ "vite": "^6.0.0",
63
+ "zxcvbn": "^4.4.2"
64
+ },
65
+ "build": {
66
+ "appId": "com.obscr.app",
67
+ "files": [
68
+ "dist/**",
69
+ "electron/**",
70
+ "lib/**",
71
+ "bin/**",
72
+ "package.json"
73
+ ],
74
+ "extraMetadata": {
75
+ "main": "electron/main.js"
76
+ }
40
77
  },
41
78
  "jest": {
42
79
  "testEnvironment": "node",
package/CHANGELOG.md DELETED
@@ -1,92 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [0.2.1] - 2026-01-20
9
-
10
- ### Added
11
- - `.npmignore` file to exclude tests and development files from npm package
12
- - 23 new CLI integration tests for command-line interface validation
13
- - 3 additional steg unit tests for edge case validation
14
-
15
- ### Changed
16
- - Interactive mode now runs by default when no command is provided
17
- - Improved spacing throughout CLI output for better readability
18
- - Package size reduced (only production files included in npm package)
19
-
20
- ### Fixed
21
- - Fixed boxen borderColor compatibility issue (changed from "violet" to "magenta")
22
-
23
- ### Testing
24
- - Total test count: 103 tests (up from 80)
25
- - Overall coverage: 90.45%
26
- - All tests passing
27
-
28
- ## [0.2.0] - 2026-01-20
29
-
30
- ### Added
31
- - **Compression Support**: Optional gzip compression using `--compress` flag to reduce message size by 50-90%
32
- - **Custom Output Filename**: `-o/--output` flag for encrypt command to specify output filename (default: "encoded.png")
33
- - **Image Capacity Validation**: Automatic validation that message fits in image with helpful error messages
34
- - **Capacity Information**: Shows capacity utilization after successful encryption
35
- - **Comprehensive Test Suite**: 80 tests with 90%+ code coverage
36
- - Unit tests for crypto (100% coverage), utils (100% coverage), and steg (97% coverage)
37
- - Integration tests for full workflows
38
- - Automated test fixtures and isolated test outputs
39
- - **JSDoc Documentation**: All exported functions now have detailed JSDoc comments
40
- - **Enhanced README**: Comprehensive documentation with examples, security notes, and technical details
41
-
42
- ### UX Enhancements
43
- - **Interactive Mode**: New `obscr interactive` command with guided menu-driven workflow
44
- - **Progress Indicators**: Real-time spinners showing encryption/decryption progress with ora
45
- - **Visual Feedback**: Color-coded messages with icons (✔, ✖, ℹ, ⚠) and beautiful boxed outputs
46
- - **Verbose Mode**: `-v/--verbose` flag for detailed technical information and debugging
47
- - **Quiet Mode**: `-q/--quiet` flag for minimal output, perfect for scripting and automation
48
- - **Confirmation Prompts**: Asks before overwriting existing files
49
- - **Examples Command**: New `obscr examples` command showing usage examples
50
- - **Better Help Text**: Improved help messages with clearer descriptions
51
- - **Welcome Banner**: Stylish welcome banner with version information (can be suppressed with --quiet)
52
- - **Password Masking**: Visual asterisks when entering passwords
53
- - **Smart Validation**: Real-time input validation with helpful error messages
54
-
55
- ### Changed
56
- - **Async/Await Pattern**: Replaced synchronous crypto operations with async Promise-based approach for better performance
57
- - **Modern JavaScript**: Converted `var` to `const`/`let`, using template literals throughout
58
- - **Better Variable Names**: Improved naming for clarity (e.g., `c` → `currentByte`, `tmp` → `powerOfTwo`)
59
- - **Error Handling**: Consistent error handling with structured return types `{success, data, error}`
60
- - **Return Types**: Changed from tuple returns `[boolean, data]` to objects for better clarity
61
- - **Array Optimizations**: Pre-allocated arrays instead of dynamic growth
62
-
63
- ### Fixed
64
- - **Debug Output Removed**: Removed debug `console.log` statement in encrypt command
65
- - **Error Object Handling**: Now throws proper Error objects instead of strings
66
- - **Fixed Output Filename**: Encrypt command now supports custom output instead of hardcoded "encoded.png"
67
-
68
- ### Security
69
- - **Backward Compatibility Maintained**:
70
- - SECRET_KEY preserved for compatibility with older encrypted images
71
- - Can decrypt images created with previous versions
72
- - Automatically detects compressed vs uncompressed messages
73
- - All encryption parameters unchanged (AES-256-GCM, PBKDF2 with 65,535 iterations)
74
-
75
- ### Technical Notes
76
- - Compression format adds optional 5th field: `salt:nonce:ciphertext:tag[:1]`
77
- - Async operations provide foundation for future streaming implementations
78
- - Test outputs isolated in `test/output/` directory and properly gitignored
79
-
80
- ## [0.1.2] - 2022-09-11
81
-
82
- ### Changed
83
- - Updated dependencies
84
- - Cleaned up codebase
85
-
86
- ## [0.1.1] - 2022-07-08
87
-
88
- ### Added
89
- - Initial release with AES-256-GCM encryption
90
- - LSB steganography for PNG images
91
- - Password-based key derivation with PBKDF2
92
- - CLI interface with encrypt/decrypt commands