How to hide any data in PNG

Have you ever hidden something inside a PNG?

It's time to discover America!

I was really surprised by the extremely small amount of information on this topic. Let's fix it.


So, straight to the point! What do we need to know to hide something inside a PNG image?

We need to know that PNG stores information about each pixel. Each pixel, in turn, has 3 channels (R, G, B) that describe the color and one alpha channel that describes the transparency.

LSB (Least Significant Bit) - the least significant bits that we can use for our dark deeds. Changing them will cause a slight change in color that the human eye cannot detect.


The image demonstrates the process of hiding data in a PNG file using steganography.

We just need to convert the "secret information" to bit form and go through each channel of each pixel, changing the LSB to the one we need.


An example of using a program to embed secret text into a PNG image.

Each pixel can hold 3 bits of information. So, the classic "Hello world" in UTF-8 will require 30 pixels (6x6 image). A text of 100 thousand words will fit in 1000x1000. Want more? Potential 5MB of spontaneous data will fit in 5000x5000.


The theory is clear (I hope). Time for practical examples.

We encode our message inside the PNG:

import { PNG } from 'pngjs';
import fs from 'node:fs';

function writeData(imageBinary, dataBinary) {

   for (let i = 0, dataBitIndex = 0; i < imageBinary.length; i += 4) {

      for (let j = 0; j < 3; j++, dataBitIndex++) {

         if (dataBitIndex >= dataBinary.length * 8) {
            return imageBinary;
         }

         /**
          * Get the current data bit
          **/

         let bit = (dataBinary[Math.floor(dataBitIndex / 8)] >> (7 - (dataBitIndex % 8))) & 1;

         /**
          * Shift the color
          **/
         imageBinary[i + j] = (imageBinary[i + j] & 0xFE) | bit;

      }

   }

   return imageBinary;

}

function async encode(inputPath, outputPath, message) {

   let binaryMessage = Buffer.from(message, 'utf-8');

   return new Promise(resolve => {

      /**
       * Open the image and get its pixels
       **/
      fs.createReadStream(inputPath)
         .pipe(new PNG())
         .on('parsed', function() {

            //this - PNG object
            //this.data - Buffer object, essentially [R, G, B, A, R, G, B, A...]

            /**
             * Write the message length in the first 4 bytes
             **/
            let length = Buffer.alloc(4);
            length.writeUInt32BE(binaryMessage.length, 0);

            let binaryTotalData = Buffer.concat([
               length,
               binaryTotalData
            ]);

            /**
             * Replace the pixels
             **/
            writeData(this.data, binaryTotalData);

            /**
             * Save to file
             **/
            let stream = fs.createWriteStream(outputPath);

            stream.on('finish', resolve);

            this.png.pack().pipe(stream);

         });

   });
}

Get the message from PNG:

function readMessage(dataBinary) {

   let bytes: number[] = [];

   for (let i = 0, dataBitIndex = 0, currentByte = 0; i < pixels.length; i += 4) {

      for (let j = 0; j < 3; j++) {

         let bit = pixels[i + j] & 1;

         currentByte = (currentByte << 1) | bit;
         dataBitIndex++;

         if (dataBitIndex % 8 === 0) {
            bytes.push(currentByte);
            currentByte = 0;
         }

      }

   }

   return Buffer.from(bytes);

}

function async decode(targetPath) {

   return new Promise(resolve => {

      /**
       * Open the image and get its pixels
       **/
      fs.createReadStream(targetPath)
         .pipe(new PNG())
         .on('parsed', function() {

            //this - PNG Object
            //this.data - Buffer Object, essentially [R, G, B, A, R, G, B, A...]

            /**
             * Read the data
             **/
            let binaryTotalData = readData(this.data);

            /**
              * Find out the length of the original message and trim
             **/
            let length = binaryTotalData.readUInt32BE();
            let binaryMessage = binaryTotalData.slice(4, 4 + length);

            resolve(binaryMessage);

         });

   });

}

A graphical representation of the method of hiding information inside a PNG file.

Then it all depends on your imagination. You can write another file inside the PNG, you can encrypt the data using AES, you can hide all your passwords in a photo with your favorite leader uncle.

You can choose pixels not in random order (use elliptic curves for this?), you can add random noise to make it harder to detect the fact of data hiding.

You can find the code for a more detailed solution on GitHub: https://github.com/in4in-dev/png-stenography (using AES, hiding files in the picture)

Thank you all. Enjoy!

Comments