obscr 0.1.1 → 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.
@@ -1,4 +1,10 @@
1
1
  const crypto = require("crypto");
2
+ const zlib = require("zlib");
3
+ const { promisify } = require("util");
4
+
5
+ const gzipAsync = promisify(zlib.gzip);
6
+ const gunzipAsync = promisify(zlib.gunzip);
7
+ const pbkdf2Async = promisify(crypto.pbkdf2);
2
8
 
3
9
  const cryptoConfig = {
4
10
  cipherAlgorithm: "aes-256-gcm",
@@ -10,9 +16,16 @@ const cryptoConfig = {
10
16
  digest: "sha512",
11
17
  };
12
18
 
13
- const encrypt = (message, password) => {
19
+ /**
20
+ * Encrypts a message using AES-256-GCM with optional compression
21
+ * @param {string} message - The message to encrypt
22
+ * @param {string} password - The password to use for encryption
23
+ * @param {boolean} compress - Whether to compress the message before encryption
24
+ * @returns {Promise<string>} The encrypted message in format: salt:nonce:ciphertext:tag[:compressed]
25
+ */
26
+ const encrypt = async (message, password, compress = false) => {
14
27
  const salt = crypto.randomBytes(cryptoConfig.saltLength);
15
- const key = crypto.pbkdf2Sync(
28
+ const key = await pbkdf2Async(
16
29
  password,
17
30
  salt,
18
31
  cryptoConfig.iterations,
@@ -20,6 +33,13 @@ const encrypt = (message, password) => {
20
33
  cryptoConfig.digest
21
34
  );
22
35
 
36
+ let dataToEncrypt = Buffer.from(message, "utf8");
37
+
38
+ // Compress if requested
39
+ if (compress) {
40
+ dataToEncrypt = await gzipAsync(dataToEncrypt);
41
+ }
42
+
23
43
  const nonce = crypto.randomBytes(cryptoConfig.nonceLength);
24
44
  const cipher = crypto.createCipheriv(
25
45
  cryptoConfig.cipherAlgorithm,
@@ -27,31 +47,40 @@ const encrypt = (message, password) => {
27
47
  nonce
28
48
  );
29
49
 
30
- let encryptedBase64 = "";
31
- cipher.setEncoding("base64");
32
- cipher.on("data", (chunk) => (encryptedBase64 += chunk));
33
- cipher.on("end", () => {
34
- // do nothing console.log(encryptedBase64);
35
- // Prints: some clear text data
36
- });
37
- cipher.write(message);
38
- cipher.end();
50
+ const encryptedChunks = [];
51
+ encryptedChunks.push(cipher.update(dataToEncrypt));
52
+ encryptedChunks.push(cipher.final());
53
+ const encrypted = Buffer.concat(encryptedChunks);
39
54
 
40
55
  const saltBase64 = base64Encoding(salt);
41
56
  const nonceBase64 = base64Encoding(nonce);
57
+ const encryptedBase64 = base64Encoding(encrypted);
42
58
  const gcmTagBase64 = base64Encoding(cipher.getAuthTag());
43
- return (
44
- saltBase64 + ":" + nonceBase64 + ":" + encryptedBase64 + ":" + gcmTagBase64
45
- );
59
+
60
+ // Add compression flag to the end for backward compatibility
61
+ const compressionFlag = compress ? ":1" : "";
62
+ return `${saltBase64}:${nonceBase64}:${encryptedBase64}:${gcmTagBase64}${compressionFlag}`;
46
63
  };
47
64
 
48
- const decrypt = (encrypted, password) => {
65
+ /**
66
+ * Decrypts an encrypted message
67
+ * @param {string} encrypted - The encrypted message string
68
+ * @param {string} password - The password to use for decryption
69
+ * @returns {Promise<string>} The decrypted message
70
+ * @throws {Error} If decryption fails
71
+ */
72
+ const decrypt = async (encrypted, password) => {
49
73
  const dataSplit = encrypted.split(":");
74
+
75
+ // Check if compression flag is present (backward compatible)
76
+ const isCompressed = dataSplit.length === 5 && dataSplit[4] === "1";
77
+
50
78
  const salt = base64Decoding(dataSplit[0]);
51
79
  const nonce = base64Decoding(dataSplit[1]);
52
80
  const ciphertext = dataSplit[2];
53
81
  const gcmTag = base64Decoding(dataSplit[3]);
54
- const key = crypto.pbkdf2Sync(
82
+
83
+ const key = await pbkdf2Async(
55
84
  password,
56
85
  salt,
57
86
  cryptoConfig.iterations,
@@ -66,25 +95,35 @@ const decrypt = (encrypted, password) => {
66
95
  );
67
96
  decipher.setAuthTag(gcmTag);
68
97
 
69
- let decrypted = "";
70
- decipher.on("readable", () => {
71
- while (null !== (chunk = decipher.read())) {
72
- decrypted += chunk.toString("utf8");
98
+ try {
99
+ const decryptedChunks = [];
100
+ decryptedChunks.push(decipher.update(Buffer.from(ciphertext, "base64")));
101
+ decryptedChunks.push(decipher.final());
102
+ let decrypted = Buffer.concat(decryptedChunks);
103
+
104
+ // Decompress if it was compressed
105
+ if (isCompressed) {
106
+ decrypted = await gunzipAsync(decrypted);
73
107
  }
74
- });
75
- decipher.on("end", () => {
76
- // do nothing console.log(decrypted);
77
- });
78
- decipher.on("error", (err) => {
79
- throw err.message;
80
- });
81
- decipher.write(ciphertext, "base64");
82
- decipher.end();
83
- return decrypted;
108
+
109
+ return decrypted.toString("utf8");
110
+ } catch (err) {
111
+ throw new Error(`Decryption failed: ${err.message}`);
112
+ }
84
113
  };
85
114
 
115
+ /**
116
+ * Encodes a buffer to base64 string
117
+ * @param {Buffer} input - The buffer to encode
118
+ * @returns {string} Base64 encoded string
119
+ */
86
120
  const base64Encoding = (input) => input.toString("base64");
87
121
 
122
+ /**
123
+ * Decodes a base64 string to buffer
124
+ * @param {string} input - The base64 string to decode
125
+ * @returns {Buffer} Decoded buffer
126
+ */
88
127
  const base64Decoding = (input) => Buffer.from(input, "base64");
89
128
 
90
129
  module.exports = { encrypt, decrypt };
package/bin/utils/steg.js CHANGED
@@ -1,124 +1,231 @@
1
1
  const fs = require("fs");
2
- const { loadImage, createCanvas } = require("canvas");
3
2
  const { get_hashed_order, str_to_bits, bits_to_str } = require("./utils");
3
+ const { PNG } = require("pngjs");
4
+
5
+ /**
6
+ * Prepares data bits for writing by scrambling them with a password-based key
7
+ * @param {number[]} dataBits - Array of data bits to write
8
+ * @param {string} encryptionKey - Key for scrambling the data
9
+ * @param {number} totalCapacity - Total capacity of the image in bits
10
+ * @returns {number[]} Scrambled bit array with obfuscation
11
+ * @throws {Error} If data is too large for the image
12
+ */
13
+ const prepare_write_data = (dataBits, encryptionKey, totalCapacity) => {
14
+ const dataBitsLength = dataBits.length;
15
+ if (dataBitsLength > totalCapacity) {
16
+ throw new Error(
17
+ `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
+ );
20
+ }
4
21
 
5
- const prepare_write_data = (data_bits, enc_key, encode_len) => {
6
- const data_bits_len = data_bits.length;
7
- if (data_bits.length > encode_len) throw "Can not hold this much data!";
8
- const result = Array(encode_len);
9
- for (let i = 0; i < encode_len; i++) {
10
- result[i] = Math.floor(Math.random() * 2); //obfuscation
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);
11
26
  }
12
27
 
13
- const order = get_hashed_order(enc_key, encode_len);
14
- for (let i = 0; i < data_bits_len; i++) result[order[i]] = data_bits[i];
28
+ // Scramble actual data into random positions
29
+ const scrambledOrder = get_hashed_order(encryptionKey, totalCapacity);
30
+ for (let i = 0; i < dataBitsLength; i++) {
31
+ result[scrambledOrder[i]] = dataBits[i];
32
+ }
15
33
 
16
34
  return result;
17
35
  };
18
36
 
19
- const prepare_read_data = (data_bits, enc_key) => {
20
- const data_bits_len = data_bits.length;
21
- const result = Array(data_bits_len);
22
- const order = get_hashed_order(enc_key, data_bits_len);
23
-
24
- for (let i = 0; i < data_bits_len; i++) result[i] = data_bits[order[i]];
37
+ /**
38
+ * Extracts and unscrambles data bits using password-based key
39
+ * @param {number[]} dataBits - Array of scrambled bits from image
40
+ * @param {string} encryptionKey - Key for unscrambling the data
41
+ * @returns {number[]} Unscrambled bit array
42
+ */
43
+ const prepare_read_data = (dataBits, encryptionKey) => {
44
+ const dataBitsLength = dataBits.length;
45
+ const result = new Array(dataBitsLength);
46
+ const scrambledOrder = get_hashed_order(encryptionKey, dataBitsLength);
47
+
48
+ for (let i = 0; i < dataBitsLength; i++) {
49
+ result[i] = dataBits[scrambledOrder[i]];
50
+ }
25
51
 
26
52
  return result;
27
53
  };
28
54
 
29
- const get_bits_lsb = (imgData) => {
30
- const result = Array();
31
- for (let i = 0; i < imgData.data.length; i += 4) {
32
- result.push(imgData.data[i] % 2 == 1 ? 1 : 0);
33
- result.push(imgData.data[i + 1] % 2 == 1 ? 1 : 0);
34
- result.push(imgData.data[i + 2] % 2 == 1 ? 1 : 0);
55
+ /**
56
+ * Extracts the least significant bits from image RGB channels
57
+ * @param {Object} imageData - PNG image data object
58
+ * @returns {number[]} Array of extracted LSB bits
59
+ */
60
+ const get_bits_lsb = (imageData) => {
61
+ const result = [];
62
+ // Process RGB channels (skip alpha at i+3)
63
+ 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
35
67
  }
36
68
  return result;
37
69
  };
38
70
 
39
- const write_lsb = (imgData, setdata) => {
40
- function unsetbit(k) {
41
- return k % 2 == 1 ? k - 1 : k;
71
+ /**
72
+ * Writes bits to the least significant bits of image RGB channels
73
+ * @param {Object} imageData - PNG image data object
74
+ * @param {number[]} bitsToWrite - Array of bits to write
75
+ * @returns {Object} Modified image data
76
+ */
77
+ const write_lsb = (imageData, bitsToWrite) => {
78
+ /**
79
+ * Clears the LSB of a byte value
80
+ * @param {number} value - Byte value
81
+ * @returns {number} Value with LSB cleared
82
+ */
83
+ const clearLSB = (value) => {
84
+ return value % 2 === 1 ? value - 1 : value;
85
+ };
86
+
87
+ /**
88
+ * Sets the LSB of a byte value
89
+ * @param {number} value - Byte value
90
+ * @returns {number} Value with LSB set
91
+ */
92
+ const setLSB = (value) => {
93
+ return value % 2 === 1 ? value : value + 1;
94
+ };
95
+
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;
42
110
  }
111
+ return imageData;
112
+ };
43
113
 
44
- function setbit(k) {
45
- return k % 2 == 1 ? k : k + 1;
46
- }
47
- let j = 0;
48
- for (let i = 0; i < imgData.data.length; i += 4) {
49
- imgData.data[i] = setdata[j]
50
- ? setbit(imgData.data[i])
51
- : unsetbit(imgData.data[i]);
52
- imgData.data[i + 1] = setdata[j + 1]
53
- ? setbit(imgData.data[i + 1])
54
- : unsetbit(imgData.data[i + 1]);
55
- imgData.data[i + 2] = setdata[j + 2]
56
- ? setbit(imgData.data[i + 2])
57
- : unsetbit(imgData.data[i + 2]);
58
- imgData.data[i + 3] = 255;
59
- j += 3;
60
- }
61
- return imgData;
114
+ /**
115
+ * Calculates the bit capacity of an image
116
+ * @param {Object} imageData - PNG image data object
117
+ * @returns {number} Capacity in bits
118
+ */
119
+ const calculateImageCapacity = (imageData) => {
120
+ return Math.floor(imageData.data.length / 4) * 3; // 3 bits per pixel (RGB)
62
121
  };
63
122
 
64
- exports.extractMessageFromImage = async (imagepath, encKey) => {
65
- let c, ctx, imgData;
123
+ /**
124
+ * Validates that an image file exists and is readable
125
+ * @param {string} imagePath - Path to the image file
126
+ * @throws {Error} If file doesn't exist or isn't readable
127
+ */
128
+ const validateImageFile = (imagePath) => {
129
+ if (!fs.existsSync(imagePath)) {
130
+ throw new Error(`Image file not found: ${imagePath}`);
131
+ }
66
132
 
67
- try {
68
- const img = await loadImage(imagepath);
69
- c = createCanvas(img.width, img.height);
70
- ctx = c.getContext("2d");
71
- ctx.drawImage(img, 0, 0, img.width, img.height);
72
- imgData = ctx.getImageData(0, 0, c.width, c.height);
73
- } catch (err) {
74
- return [false, err];
133
+ const stats = fs.statSync(imagePath);
134
+ if (!stats.isFile()) {
135
+ throw new Error(`Path is not a file: ${imagePath}`);
75
136
  }
137
+ };
76
138
 
139
+ /**
140
+ * Extracts and decrypts a hidden message from a PNG image
141
+ * @param {string} imagePath - Path to the PNG image
142
+ * @param {string} encryptionKey - Key for unscrambling the data
143
+ * @returns {Promise<{success: boolean, data?: string, error?: string}>} Result object
144
+ */
145
+ const extractMessageFromImage = async (imagePath, encryptionKey) => {
77
146
  try {
78
- const bitsStream = get_bits_lsb(imgData);
79
- const decryptedBitsStream = prepare_read_data(bitsStream, encKey);
80
- const msg = bits_to_str(decryptedBitsStream, 1);
81
- if (msg == null)
82
- return [
83
- false,
84
- "Message does not decrypt. Maybe due to (1) wrong password / enc method. (2) corrupted file",
85
- ];
86
- return [true, msg];
147
+ validateImageFile(imagePath);
148
+
149
+ const imageBuffer = fs.readFileSync(imagePath);
150
+ const imageData = PNG.sync.read(imageBuffer);
151
+
152
+ const bitsStream = get_bits_lsb(imageData);
153
+ const decryptedBitsStream = prepare_read_data(bitsStream, encryptionKey);
154
+ const message = bits_to_str(decryptedBitsStream, 1);
155
+
156
+ if (message == null) {
157
+ return {
158
+ success: false,
159
+ error: "Decryption failed. Possible causes: wrong password, corrupted file, or no hidden message.",
160
+ };
161
+ }
162
+
163
+ return { success: true, data: message };
87
164
  } catch (err) {
88
- return [
89
- false,
90
- "Message does not decrypt. Maybe due to (1) wrong password / enc method. (2) corrupted file",
91
- ];
165
+ return {
166
+ success: false,
167
+ error: err.message || "Failed to extract message from image",
168
+ };
92
169
  }
93
170
  };
94
171
 
95
- exports.encodeMessageToImage = async (imagepath, msg, encKey) => {
172
+ /**
173
+ * Encodes and hides an encrypted message into a PNG image
174
+ * @param {string} imagePath - Path to the source PNG image
175
+ * @param {string} message - Encrypted message to hide
176
+ * @param {string} encryptionKey - Key for scrambling the data
177
+ * @param {string} outputPath - Path for the output image (default: "encoded.png")
178
+ * @returns {Promise<{success: boolean, outputPath?: string, capacity?: Object, error?: string}>} Result object
179
+ */
180
+ const encodeMessageToImage = async (
181
+ imagePath,
182
+ message,
183
+ encryptionKey,
184
+ outputPath = "encoded.png"
185
+ ) => {
96
186
  try {
97
- // const imageBuffer = fs.readFileSync(imagepath);
98
-
99
- const img = await loadImage(imagepath);
100
- const c = createCanvas(img.width, img.height);
101
- const ctx = c.getContext("2d");
102
- ctx.drawImage(img, 0, 0, img.width, img.height);
103
- // console.log(c.toDataURL);
104
- const imgData = ctx.getImageData(0, 0, c.width, c.height);
105
- const encode_len = Math.floor(imgData.data.length / 4) * 3;
106
-
107
- // prepare data
108
- const bitStream = str_to_bits(msg, 1);
187
+ validateImageFile(imagePath);
188
+
189
+ const imageBuffer = fs.readFileSync(imagePath);
190
+ const imageData = PNG.sync.read(imageBuffer);
191
+
192
+ const totalCapacity = calculateImageCapacity(imageData);
193
+ const bitStream = str_to_bits(message, 1);
194
+
195
+ // Prepare capacity info for user
196
+ const capacityInfo = {
197
+ totalBits: totalCapacity,
198
+ usedBits: bitStream.length,
199
+ utilization: ((bitStream.length / totalCapacity) * 100).toFixed(2) + "%",
200
+ };
201
+
202
+ // This will throw if message is too large
109
203
  const encryptedBitStream = prepare_write_data(
110
204
  bitStream,
111
- encKey,
112
- encode_len
205
+ encryptionKey,
206
+ totalCapacity
113
207
  );
114
208
 
115
- const encryptedImgData = write_lsb(imgData, encryptedBitStream);
116
- ctx.putImageData(encryptedImgData, 0, 0);
117
- const encodedImageBuffer = c.toBuffer("image/png");
118
- fs.writeFileSync("encoded.png", encodedImageBuffer);
209
+ const encryptedImageData = write_lsb(imageData, encryptedBitStream);
210
+ const outputBuffer = PNG.sync.write(encryptedImageData);
211
+
212
+ fs.writeFileSync(outputPath, outputBuffer);
119
213
 
120
- return true;
214
+ return {
215
+ success: true,
216
+ outputPath,
217
+ capacity: capacityInfo,
218
+ };
121
219
  } catch (err) {
122
- throw err;
220
+ return {
221
+ success: false,
222
+ error: err.message || "Failed to encode message to image",
223
+ };
123
224
  }
124
225
  };
226
+
227
+ module.exports = {
228
+ extractMessageFromImage,
229
+ encodeMessageToImage,
230
+ calculateImageCapacity,
231
+ };