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 };