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/CHANGELOG.md +92 -0
- package/README.md +214 -26
- package/bin/index.js +417 -85
- package/bin/utils/crypto.js +69 -30
- package/bin/utils/steg.js +189 -74
- package/bin/utils/utils.js +113 -64
- package/package.json +23 -3
- package/.github/workflows/publish.yml +0 -18
package/bin/utils/crypto.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
31
|
-
cipher.
|
|
32
|
-
cipher.
|
|
33
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: err.message || "Failed to extract message from image",
|
|
168
|
+
};
|
|
89
169
|
}
|
|
90
170
|
};
|
|
91
171
|
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
104
|
-
|
|
205
|
+
encryptionKey,
|
|
206
|
+
totalCapacity
|
|
105
207
|
);
|
|
106
208
|
|
|
107
|
-
const
|
|
108
|
-
|
|
209
|
+
const encryptedImageData = write_lsb(imageData, encryptedBitStream);
|
|
210
|
+
const outputBuffer = PNG.sync.write(encryptedImageData);
|
|
109
211
|
|
|
110
|
-
fs.writeFileSync(
|
|
212
|
+
fs.writeFileSync(outputPath, outputBuffer);
|
|
111
213
|
|
|
112
|
-
return
|
|
214
|
+
return {
|
|
215
|
+
success: true,
|
|
216
|
+
outputPath,
|
|
217
|
+
capacity: capacityInfo,
|
|
218
|
+
};
|
|
113
219
|
} catch (err) {
|
|
114
|
-
|
|
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
|
+
};
|
package/bin/utils/utils.js
CHANGED
|
@@ -1,45 +1,59 @@
|
|
|
1
1
|
const { MersenneTwister } = require("./mersenne-twister");
|
|
2
2
|
const SHA512 = require("crypto-js/sha512");
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
9
|
-
const seed = SHA512(password).words.reduce(
|
|
14
|
+
let location;
|
|
15
|
+
const seed = SHA512(password).words.reduce((total, num) => {
|
|
10
16
|
return total + Math.abs(num);
|
|
11
17
|
}, 0);
|
|
12
|
-
const
|
|
13
|
-
for (let i =
|
|
14
|
-
|
|
15
|
-
result.push(orders[
|
|
16
|
-
orders[
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
currentByte = bytes[offset];
|
|
40
|
+
secondByte = bytes[offset + 1];
|
|
41
|
+
thirdByte = bytes[offset + 2];
|
|
33
42
|
|
|
34
|
-
if (128 >
|
|
35
|
-
|
|
43
|
+
if (128 > currentByte) {
|
|
44
|
+
// Single-byte character (ASCII)
|
|
45
|
+
chars.push(String.fromCharCode(currentByte));
|
|
36
46
|
offset += 1;
|
|
37
|
-
} else if (191 <
|
|
38
|
-
|
|
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(
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
for (
|
|
92
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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(
|
|
134
|
+
return utf8Decode(messageArray);
|
|
103
135
|
};
|
|
104
136
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|