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.
@@ -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
@@ -2,115 +2,230 @@ const fs = require("fs");
2
2
  const { get_hashed_order, str_to_bits, bits_to_str } = require("./utils");
3
3
  const { PNG } = require("pngjs");
4
4
 
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
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
+ }
21
+
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 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 imageBuffer = fs.readFileSync(imagepath);
69
- imgData = PNG.sync.read(imageBuffer);
70
- } catch (err) {
71
- return [false, err];
133
+ const stats = fs.statSync(imagePath);
134
+ if (!stats.isFile()) {
135
+ throw new Error(`Path is not a file: ${imagePath}`);
72
136
  }
137
+ };
73
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) => {
74
146
  try {
75
- const bitsStream = get_bits_lsb(imgData);
76
- const decryptedBitsStream = prepare_read_data(bitsStream, encKey);
77
- const msg = bits_to_str(decryptedBitsStream, 1);
78
- if (msg == null)
79
- return [
80
- false,
81
- "Message does not decrypt. Maybe due to (1) wrong password / enc method. (2) corrupted file",
82
- ];
83
- 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 };
84
164
  } catch (err) {
85
- return [
86
- false,
87
- "Message does not decrypt. Maybe due to (1) wrong password / enc method. (2) corrupted file",
88
- ];
165
+ return {
166
+ success: false,
167
+ error: err.message || "Failed to extract message from image",
168
+ };
89
169
  }
90
170
  };
91
171
 
92
- 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
+ ) => {
93
186
  try {
94
- const imageBuffer = fs.readFileSync(imagepath);
95
- const imgData = PNG.sync.read(imageBuffer);
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);
96
194
 
97
- const encode_len = Math.floor(imgData.data.length / 4) * 3;
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
+ };
98
201
 
99
- // prepare data
100
- const bitStream = str_to_bits(msg, 1);
202
+ // This will throw if message is too large
101
203
  const encryptedBitStream = prepare_write_data(
102
204
  bitStream,
103
- encKey,
104
- encode_len
205
+ encryptionKey,
206
+ totalCapacity
105
207
  );
106
208
 
107
- const encryptedImgData = write_lsb(imgData, encryptedBitStream);
108
- let buff = PNG.sync.write(encryptedImgData);
209
+ const encryptedImageData = write_lsb(imageData, encryptedBitStream);
210
+ const outputBuffer = PNG.sync.write(encryptedImageData);
109
211
 
110
- fs.writeFileSync("encoded.png", buff);
212
+ fs.writeFileSync(outputPath, outputBuffer);
111
213
 
112
- return true;
214
+ return {
215
+ success: true,
216
+ outputPath,
217
+ capacity: capacityInfo,
218
+ };
113
219
  } catch (err) {
114
- throw err;
220
+ return {
221
+ success: false,
222
+ error: err.message || "Failed to encode message to image",
223
+ };
115
224
  }
116
225
  };
226
+
227
+ module.exports = {
228
+ extractMessageFromImage,
229
+ encodeMessageToImage,
230
+ calculateImageCapacity,
231
+ };
@@ -1,45 +1,59 @@
1
1
  const { MersenneTwister } = require("./mersenne-twister");
2
2
  const SHA512 = require("crypto-js/sha512");
3
3
 
4
- const get_hashed_order = (password, arr_len) => {
5
- // O(arr_len) algorithm
6
- const orders = Array.from(Array(arr_len).keys());
4
+ /**
5
+ * Generates a hashed order array for data scrambling using password-based seeding
6
+ * @param {string} password - The password to generate the seed from
7
+ * @param {number} arrayLength - The length of the order array to generate
8
+ * @returns {number[]} Array of indices in scrambled order
9
+ */
10
+ const get_hashed_order = (password, arrayLength) => {
11
+ // O(arrayLength) algorithm using Fisher-Yates shuffle
12
+ const orders = Array.from(Array(arrayLength).keys());
7
13
  const result = [];
8
- let loc;
9
- const seed = SHA512(password).words.reduce(function (total, num) {
14
+ let location;
15
+ const seed = SHA512(password).words.reduce((total, num) => {
10
16
  return total + Math.abs(num);
11
17
  }, 0);
12
- const rnd = new MersenneTwister(seed);
13
- for (let i = arr_len; i > 0; i--) {
14
- loc = rnd.genrand_int32() % i;
15
- result.push(orders[loc]);
16
- orders[loc] = orders[i - 1];
18
+ const rng = new MersenneTwister(seed);
19
+ for (let i = arrayLength; i > 0; i--) {
20
+ location = rng.genrand_int32() % i;
21
+ result.push(orders[location]);
22
+ orders[location] = orders[i - 1];
17
23
  }
18
24
  return result;
19
25
  };
20
26
 
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
31
+ */
21
32
  const utf8Decode = (bytes) => {
22
- var chars = [],
23
- offset = 0,
24
- length = bytes.length,
25
- c,
26
- c2,
27
- c3;
33
+ const chars = [];
34
+ let offset = 0;
35
+ const length = bytes.length;
36
+ let currentByte, secondByte, thirdByte;
28
37
 
29
38
  while (offset < length) {
30
- c = bytes[offset];
31
- c2 = bytes[offset + 1];
32
- c3 = bytes[offset + 2];
39
+ currentByte = bytes[offset];
40
+ secondByte = bytes[offset + 1];
41
+ thirdByte = bytes[offset + 2];
33
42
 
34
- if (128 > c) {
35
- chars.push(String.fromCharCode(c));
43
+ if (128 > currentByte) {
44
+ // Single-byte character (ASCII)
45
+ chars.push(String.fromCharCode(currentByte));
36
46
  offset += 1;
37
- } else if (191 < c && c < 224) {
38
- chars.push(String.fromCharCode(((c & 31) << 6) | (c2 & 63)));
47
+ } else if (191 < currentByte && currentByte < 224) {
48
+ // Two-byte character
49
+ chars.push(String.fromCharCode(((currentByte & 31) << 6) | (secondByte & 63)));
39
50
  offset += 2;
40
51
  } else {
52
+ // Three-byte character
41
53
  chars.push(
42
- String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))
54
+ String.fromCharCode(
55
+ ((currentByte & 15) << 12) | ((secondByte & 63) << 6) | (thirdByte & 63)
56
+ )
43
57
  );
44
58
  offset += 3;
45
59
  }
@@ -48,11 +62,16 @@ const utf8Decode = (bytes) => {
48
62
  return chars.join("");
49
63
  };
50
64
 
65
+ /**
66
+ * Encodes a string to a UTF-8 byte array
67
+ * @param {string} str - The string to encode
68
+ * @returns {number[]} Array of byte values
69
+ */
51
70
  const utf8Encode = (str) => {
52
- var bytes = [],
53
- offset = 0,
54
- length,
55
- char;
71
+ const bytes = [];
72
+ let offset = 0;
73
+ let length;
74
+ let char;
56
75
 
57
76
  str = encodeURI(str);
58
77
  length = str.length;
@@ -73,51 +92,81 @@ const utf8Encode = (str) => {
73
92
  return bytes;
74
93
  };
75
94
 
76
- const bits_to_str = (bitarray, num_copy) => {
77
- function merge_bits(bits) {
78
- var bits_len = bits.length;
79
- var bits_sum = 0;
80
- for (var i = 0; i < bits_len; i++) bits_sum += bits[i];
81
- return Math.round(bits_sum / bits_len);
82
- }
83
-
84
- var msg_array = Array();
85
- var data, tmp;
86
-
87
- var msg_array_len = Math.floor(Math.floor(bitarray.length / num_copy) / 8);
88
- for (var i = 0; i < msg_array_len; i++) {
89
- data = 0;
90
- tmp = 128;
91
- for (var j = 0; j < 8; j++) {
92
- data +=
93
- merge_bits(
94
- bitarray.slice((i * 8 + j) * num_copy, (i * 8 + j + 1) * num_copy)
95
- ) * tmp;
96
- tmp = Math.floor(tmp / 2);
95
+ /**
96
+ * Converts a bit array to a string using majority voting for error correction
97
+ * @param {number[]} bitArray - Array of bits (0s and 1s)
98
+ * @param {number} numCopies - Number of redundant copies per bit for error correction
99
+ * @returns {string} Decoded string
100
+ */
101
+ const bits_to_str = (bitArray, numCopies) => {
102
+ /**
103
+ * Merges multiple copies of a bit using majority voting
104
+ * @param {number[]} bits - Array of bit copies
105
+ * @returns {number} 0 or 1 based on majority
106
+ */
107
+ const mergeBits = (bits) => {
108
+ const bitsLength = bits.length;
109
+ let bitsSum = 0;
110
+ for (let i = 0; i < bitsLength; i++) {
111
+ bitsSum += bits[i];
97
112
  }
98
- if (data == 255) break; //END NOTATION
99
- msg_array.push(data);
113
+ return Math.round(bitsSum / bitsLength);
114
+ };
115
+
116
+ const messageArray = [];
117
+ let byteValue, powerOfTwo;
118
+
119
+ const messageArrayLength = Math.floor(Math.floor(bitArray.length / numCopies) / 8);
120
+ for (let i = 0; i < messageArrayLength; i++) {
121
+ byteValue = 0;
122
+ powerOfTwo = 128;
123
+ for (let j = 0; j < 8; j++) {
124
+ byteValue +=
125
+ mergeBits(
126
+ bitArray.slice((i * 8 + j) * numCopies, (i * 8 + j + 1) * numCopies)
127
+ ) * powerOfTwo;
128
+ powerOfTwo = Math.floor(powerOfTwo / 2);
129
+ }
130
+ if (byteValue === 255) break; // END NOTATION
131
+ messageArray.push(byteValue);
100
132
  }
101
133
 
102
- return utf8Decode(msg_array);
134
+ return utf8Decode(messageArray);
103
135
  };
104
136
 
105
- const str_to_bits = (str, num_copy) => {
106
- const utf8array = utf8Encode(str);
107
- const result = Array();
108
- const utf8strlen = utf8array.length;
109
- for (let i = 0; i < utf8strlen; i++) {
110
- for (let j = 128; j > 0; j = Math.floor(j / 2)) {
111
- if (Math.floor(utf8array[i] / j)) {
112
- for (let cp = 0; cp < num_copy; cp++) result.push(1);
113
- utf8array[i] -= j;
114
- } else for (let cp = 0; cp < num_copy; cp++) result.push(0);
137
+ /**
138
+ * Converts a string to a bit array with redundant copies for error correction
139
+ * @param {string} str - The string to convert
140
+ * @param {number} numCopies - Number of redundant copies per bit
141
+ * @returns {number[]} Array of bits with redundancy
142
+ */
143
+ const str_to_bits = (str, numCopies) => {
144
+ const utf8Array = utf8Encode(str);
145
+ const result = [];
146
+ const utf8Length = utf8Array.length;
147
+
148
+ for (let i = 0; i < utf8Length; i++) {
149
+ for (let powerOfTwo = 128; powerOfTwo > 0; powerOfTwo = Math.floor(powerOfTwo / 2)) {
150
+ if (Math.floor(utf8Array[i] / powerOfTwo)) {
151
+ for (let copy = 0; copy < numCopies; copy++) {
152
+ result.push(1);
153
+ }
154
+ utf8Array[i] -= powerOfTwo;
155
+ } else {
156
+ for (let copy = 0; copy < numCopies; copy++) {
157
+ result.push(0);
158
+ }
159
+ }
115
160
  }
116
161
  }
117
- for (let j = 0; j < 24; j++)
118
- for (let i = 0; i < num_copy; i++) {
162
+
163
+ // Add end marker (24 bits of 1s)
164
+ for (let j = 0; j < 24; j++) {
165
+ for (let i = 0; i < numCopies; i++) {
119
166
  result.push(1);
120
167
  }
168
+ }
169
+
121
170
  return result;
122
171
  };
123
172