broadfield-dev commited on
Commit
046cd2c
·
verified ·
1 Parent(s): e34feda

Update decoder.js

Browse files
Files changed (1) hide show
  1. decoder.js +76 -21
decoder.js CHANGED
@@ -10,49 +10,104 @@ function pemToDer(pem) {
10
  return buffer;
11
  }
12
 
13
- async function decodeFromImageBuffer(imageBuffer, privateKeyPem) {
14
- // 1. Extract data from image using Sharp
15
- const { data: pixelData } = await sharp(imageBuffer).raw().toBuffer({ resolveWithObject: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  const HEADER_BITS = 32;
 
 
 
 
17
  let headerBinaryString = '';
18
- for (let i = 0; headerBinaryString.length < HEADER_BITS; i++) {
19
- const pixelIndex = i * 4;
20
- if (pixelData.length < pixelIndex + 3) throw new Error("Image too small for header.");
21
- headerBinaryString += pixelData[pixelIndex] & 1;
22
- if (headerBinaryString.length < HEADER_BITS) headerBinaryString += pixelData[pixelIndex + 1] & 1;
23
- if (headerBinaryString.length < HEADER_BITS) headerBinaryString += pixelData[pixelIndex + 2] & 1;
24
  }
 
25
  const headerBytes = new Uint8Array(HEADER_BITS / 8);
26
- for (let i = 0; i < HEADER_BITS / 8; i++) { headerBytes[i] = parseInt(headerBinaryString.substring(i * 8, (i + 1) * 8), 2); }
27
- const dataLengthInBytes = new DataView(headerBytes.buffer).getUint32(0, false);
28
- if (dataLengthInBytes === 0) return {};
 
 
 
29
 
 
30
  const dataLengthInBits = dataLengthInBytes * 8;
31
- const bitOffset = HEADER_BITS;
 
 
 
 
 
 
32
  let dataBinaryString = '';
33
- for (let i = 0; dataBinaryString.length < dataLengthInBits; i++) {
34
- const bitPosition = bitOffset + i;
35
- const pixelIndex = Math.floor(bitPosition / 3) * 4;
36
- const channelOffset = bitPosition % 3;
37
- if (pixelData.length < pixelIndex + channelOffset) throw new Error("Image data is corrupt or truncated.");
38
- dataBinaryString += pixelData[pixelIndex + channelOffset] & 1;
39
  }
 
40
  const cryptoPayload = new Uint8Array(dataLengthInBytes);
41
- for (let i = 0; i < dataLengthInBytes; i++) { cryptoPayload[i] = parseInt(dataBinaryString.substring(i * 8, (i + 1) * 8), 2); }
 
 
 
 
 
 
42
 
43
- // 2. Decrypt payload using Web Crypto API
 
 
 
 
 
 
44
  const privateKey = await webcrypto.subtle.importKey('pkcs8', pemToDer(privateKeyPem), { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['decrypt']);
45
  const encryptedAesKeyLen = new DataView(cryptoPayload.buffer, 0, 4).getUint32(0, false);
 
46
  let offset = 4;
47
  const encryptedAesKey = cryptoPayload.slice(offset, offset + encryptedAesKeyLen);
48
  offset += encryptedAesKeyLen;
49
  const nonce = cryptoPayload.slice(offset, offset + 12);
50
  offset += 12;
51
  const ciphertextWithTag = cryptoPayload.slice(offset);
 
52
  const decryptedAesKeyBytes = await webcrypto.subtle.decrypt({ name: 'RSA-OAEP' }, privateKey, encryptedAesKey);
53
  const aesKey = await webcrypto.subtle.importKey('raw', decryptedAesKeyBytes, { name: 'AES-GCM', length: 256 }, true, ['decrypt']);
54
  const decryptedDataBuffer = await webcrypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, aesKey, ciphertextWithTag);
 
55
  return JSON.parse(new TextDecoder().decode(decryptedDataBuffer));
56
  }
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  module.exports = { decodeFromImageBuffer };
 
10
  return buffer;
11
  }
12
 
13
+ /**
14
+ * Extracts the hidden data from an image buffer by perfectly mirroring the Python LSB embedding logic.
15
+ * @param {Buffer} imageBuffer - The raw buffer of the PNG image.
16
+ * @returns {Promise<Uint8Array>} A promise that resolves with the extracted crypto payload.
17
+ */
18
+ async function extractDataFromImage(imageBuffer) {
19
+ // 1. Ensure the image is processed as 3-channel RGB, just like the Python encryptor.
20
+ // .removeAlpha() converts RGBA to RGB. .toColourspace('srgb') handles other formats.
21
+ const { data: pixelData, info } = await sharp(imageBuffer)
22
+ .removeAlpha()
23
+ .toColourspace('srgb')
24
+ .raw()
25
+ .toBuffer({ resolveWithObject: true });
26
+
27
+ if (info.channels !== 3) {
28
+ throw new Error(`Image processing error: Expected 3 (RGB) channels, but got ${info.channels}.`);
29
+ }
30
+
31
+ // 2. Read the first 32 bits (4 bytes) from the LSB stream to get the data length.
32
  const HEADER_BITS = 32;
33
+ if (pixelData.length < HEADER_BITS) {
34
+ throw new Error("Image is too small to contain a valid LSB header.");
35
+ }
36
+
37
  let headerBinaryString = '';
38
+ for (let i = 0; i < HEADER_BITS; i++) {
39
+ headerBinaryString += pixelData[i] & 1;
 
 
 
 
40
  }
41
+
42
  const headerBytes = new Uint8Array(HEADER_BITS / 8);
43
+ for (let i = 0; i < HEADER_BITS / 8; i++) {
44
+ headerBytes[i] = parseInt(headerBinaryString.substring(i * 8, (i + 1) * 8), 2);
45
+ }
46
+ const dataLengthInBytes = new DataView(headerBytes.buffer).getUint32(0, false); // Big-endian
47
+
48
+ if (dataLengthInBytes === 0) return new Uint8Array(0);
49
 
50
+ // 3. Read the main data payload from the LSB stream.
51
  const dataLengthInBits = dataLengthInBytes * 8;
52
+ const startOffset = HEADER_BITS;
53
+ const endOffset = startOffset + dataLengthInBits;
54
+
55
+ if (pixelData.length < endOffset) {
56
+ throw new Error("Image data is corrupt or truncated: Header specifies a length greater than the available pixels.");
57
+ }
58
+
59
  let dataBinaryString = '';
60
+ for (let i = startOffset; i < endOffset; i++) {
61
+ dataBinaryString += pixelData[i] & 1;
 
 
 
 
62
  }
63
+
64
  const cryptoPayload = new Uint8Array(dataLengthInBytes);
65
+ for (let i = 0; i < dataLengthInBytes; i++) {
66
+ cryptoPayload[i] = parseInt(dataBinaryString.substring(i * 8, (i + 1) * 8), 2);
67
+ }
68
+
69
+ return cryptoPayload;
70
+ }
71
+
72
 
73
+ /**
74
+ * Decrypts the hybrid RSA-AES payload. This function remains the same as it was correct.
75
+ * @param {Uint8Array} cryptoPayload - The extracted encrypted payload.
76
+ * @param {string} privateKeyPem - The server's private key.
77
+ * @returns {Promise<object>} A promise that resolves with the decrypted data.
78
+ */
79
+ async function decryptHybridPayload(cryptoPayload, privateKeyPem) {
80
  const privateKey = await webcrypto.subtle.importKey('pkcs8', pemToDer(privateKeyPem), { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['decrypt']);
81
  const encryptedAesKeyLen = new DataView(cryptoPayload.buffer, 0, 4).getUint32(0, false);
82
+
83
  let offset = 4;
84
  const encryptedAesKey = cryptoPayload.slice(offset, offset + encryptedAesKeyLen);
85
  offset += encryptedAesKeyLen;
86
  const nonce = cryptoPayload.slice(offset, offset + 12);
87
  offset += 12;
88
  const ciphertextWithTag = cryptoPayload.slice(offset);
89
+
90
  const decryptedAesKeyBytes = await webcrypto.subtle.decrypt({ name: 'RSA-OAEP' }, privateKey, encryptedAesKey);
91
  const aesKey = await webcrypto.subtle.importKey('raw', decryptedAesKeyBytes, { name: 'AES-GCM', length: 256 }, true, ['decrypt']);
92
  const decryptedDataBuffer = await webcrypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, aesKey, ciphertextWithTag);
93
+
94
  return JSON.parse(new TextDecoder().decode(decryptedDataBuffer));
95
  }
96
 
97
+
98
+ /**
99
+ * Main public function. Decodes auth data from an image buffer.
100
+ * @param {Buffer} imageBuffer - The raw buffer of the uploaded image.
101
+ * @param {string} privateKeyPem - The PEM-formatted private key.
102
+ * @returns {Promise<object>} A promise that resolves to the decoded credentials object.
103
+ */
104
+ async function decodeFromImageBuffer(imageBuffer, privateKeyPem) {
105
+ const cryptoPayload = await extractDataFromImage(imageBuffer);
106
+ if (cryptoPayload.length === 0) {
107
+ // This is a valid case if an empty message was embedded.
108
+ return {};
109
+ }
110
+ return await decryptHybridPayload(cryptoPayload, privateKeyPem);
111
+ }
112
+
113
  module.exports = { decodeFromImageBuffer };