Implementing LSB Image Steganography

Implementing LSB Image Steganography

·

4 min read

Steganography is the study and practice of concealing information within objects in such a way that it deceives the viewer as if there is no information hidden within the object.

For example, we can use the famous Least Significant Bit (LSB) algorithm to conceil information in a image file while ensuring that the difference is not visible to the naked eye.

specifically, say we have a png file, which consists of RGBA values. Each value is a 8-bit unsigned number ranging from 0-255. We can use the least significant bit of this value to save our information.

pixel values:
23, 42, 178, ...

express in 2 base:
00010111, 00101010, 10110010, ...
_______x, _______x, _______x, ...

In above example, don't change _ part, just use the least significant bit the x part as a storage to save our secret data. Note that the first seven bits is not changed, we only use the last one bit. In this case, the data of this image is changed indeed, but each value is only changed by one. This tiny change of color should not be detected by naked eye.

This way the goal is achieved. Now let's see how to do this in JavaScript.

First, let's define the basic code flow for this.

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <canvas id="c1"></canvas>
    <canvas id="c2"></canvas>

    <script>
        const c1 = document.getElementById("c1");
        const c2 = document.getElementById("c2");

        const ctx1 = c1.getContext("2d");
        const ctx2 = c2.getContext("2d");

        const secret = "こんにちわ";
        const maxLen = 16; // secret buffer length, max bytes

        function loadImg(src) {
            return new Promise(resolve => {
                const img = new Image();
                img.onload = () => {
                    resolve(img);
                };
                img.src = src;
            });
        }

        function encode(imgData, secretBuf) {
        }

        function decode(imgData) {
        }

        loadImg("./123.png").then(img => {
            c1.width = img.width;
            c1.height = img.height;

            c2.width = img.width;
            c2.height = img.height;

            ctx1.drawImage(img, 0, 0);
            const data = ctx1.getImageData(0, 0, img.width, img.height);

            const encoder = new TextEncoder();
            const secretBuf = encoder.encode(secret);

            encode(data.data, secretBuf);
            ctx2.putImageData(data, 0, 0);

            const decodedSecretBuf = decode(data.data);
            const decoder = new TextDecoder();
            const decodedSecret = decoder.decode(secretBuf);
            console.log({ decodedSecret });
        })


    </script>
</body>

As you can see, we define two canvas element, one for original image, one for the changed one. And we use the canvas api to get image data, the pixel values of the image. We also use the TextEncoder and TextDecoder to encode/decode secret message into UTF8 encoding binary format. What's left to do is implement the steganography encode/decode function.

To encode/decode easily, we define a simple scheme here. we use first 16 pixel values to express the length of the secret. Because each pixel can only hold 1 bit real data, so 16 pixel values can hold a 16 bits unsigned value, which can express 65535 bits of secret.

bit
0 1 2 3 ..., 16 - ....
[data length] [ data ]

Now comes the encode/decode function. I made a lot of comment, so it should be easy to read.

function encode(imgData, secretBuf) {
    // check if number of pixels enough to hold secret data
    if ((maxLen + secretBuf.length * 8) > imgData.length) {
        throw new Error("not enough image space to encode");
    }

    // write secret length
    const secretBufBitsLen = secretBuf.length * 8;
    for (let i = 0; i < maxLen; i++) {
        // clear least significant bit
        imgData[i] &= 0b111111110;
        // set it
        imgData[i] |= ((secretBufBitsLen >> (maxLen - i - 1)) & 1);
    }

    // write secret data
    let i = maxLen; // pixel index
    for (const byte of secretBuf) {
        for (let j = 7; j >= 0; j--) {
            // clear least significant bit
            imgData[i] &= 0b111111110;
            // set it
            imgData[i] |= ((byte >> j) & 1);
            i += 1;
        }
    }
}

function decode(imgData) {
    // read secret length
    let secretBufBitsLen = 0;
    for (let i = 0; i < maxLen; i++) {
        secretBufBitsLen |= (imgData[i] & 1) << (maxLen - i - 1);
    }

    // read secret data
    let secretBuf = new Uint8Array(secretBufBitsLen / 8);
    for (let i = maxLen; i < maxLen + secretBufBitsLen; i++) {
        const j = Math.floor((i - maxLen) / 8);
        const k = 7 - ((i - maxLen) % 8);
        secretBuf[j] |= ((imgData[i] & 1) << k);
    }
    return secretBuf;
}

Lastly, run above code in browser. You should see the two images in page and decoded result in console.