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