Skip to content

HidePNGinPNGLSB

stegobox.codec.HidePNGinPNGLSB

Bases: BaseCodec

Hide PNG image in another PNG image using LSB steganography.

  • To encode: This method is to modify the LSB of each RGB channel for every pixel in the cover image.
  • To decode: The least significant bits of the encoded image are read to recover the secret image.

Originally implemented at cstyan/pyStego

Source code in stegobox/codec/hide_pnginpng_lsb.py
class HidePNGinPNGLSB(BaseCodec):
    """Hide PNG image in another PNG image using LSB steganography.

    * To encode: This method is to modify the LSB of each RGB channel for every
      pixel in the cover image.
    * To decode: The least significant bits of the encoded image are read to recover
      the secret image.

    Originally implemented at [cstyan/pyStego](https://github.com/cstyan/pyStego)
    """

    def __init__(self) -> None:
        self.all_secret_data: list = []
        super().__init__()

    def _modify_lsb(self, count, size_of_file, pixel):
        """This function loops through the RGB channels of the current pixel and
        assigns a bit from the secret file data to the LSB of the RGB channel.
        Returns 1 if the counter index reaches the end of the secret data.

        Args:
            count: current count index from the pixels loop in stego
            sizeOfFile: the size of the secret file data, includng the header info
            pixel: a list of the RGB values for the current pixel, mutable object
        """
        run = 0
        # loop 3 times, once for each channel in RGB
        while run < 3:
            # if the count index is larger than the number of bits in the file
            if count >= size_of_file:
                return 1

            pixel[run][-1] = self.all_secret_data[count]
            run = run + 1
            count = count + 1

    def _construct_data(self, secret_file_path):
        """This function builds one large object with all the bits required to
        store the secret file in a cover image by calling constructHeader and
        getBinaryData. Returns that data.

        Args:
            secretFilePath: the path of the secret file
        """
        # append header and file data to all data
        self.all_secret_data = (
            self.all_secret_data
            + self._construct_header(secret_file_path)
            + self._get_binary_data(secret_file_path)
        )
        return self.all_secret_data

    def _construct_header(self, secret_file_path):
        """Constructs a string containing filename\0filesize\0, then turns each
        byte in the string into a binary, returns the header for our stego as a
        list of bits.

        Args:
            secretFilePath: the path of the secret file
        """
        # get the file name from the secretFilePath and append a null
        # terminator
        header = ntpath.basename(secret_file_path) + "\0"
        # get the file size of the secret file and append a null terminator
        header = header + str(os.path.getsize(secret_file_path)) + "\0"
        header = bytearray(bytes(header.encode()))
        header_bits = ""
        # convert each byte of the data in the header to bits
        for byte in header:
            header_bits = header_bits + bin(byte)[2:].zfill(8)

        # return as a list of individual bits
        return list(header_bits)

    def _get_binary_data(self, secret_file_path):
        """Open the secretFile and return it's data as a list of bits by converting
        each byte to a list of bits.

        Args:
            secretFilePath (_type_): the path of the secret file
        """
        file_descriptor = open(secret_file_path, "rb")
        file_data = bytearray(file_descriptor.read())
        binary_data = ""
        # convert each byte of data in the file to bits
        for byte in file_data:
            binary_data = binary_data + bin(byte)[2:].zfill(8)

        # return the binary data as a list of individual bits
        return list(binary_data)

    def _get_bits(self, stego_image):
        """Get the least significant bits out of a stego'd image by looping through
        all of it's pixels, and determining if the value for each RGB channel in
        a pixel is odd or even, and appending the correct value to a list of bits.
        Returns the list of all least significant bits in the stego'd image.

        Args:
            stego_image: encoded image with secret image embeded
        """
        pixels = list(stego_image.getdata())
        bit_list = []
        for pixel in pixels:
            red = pixel[0] % 2
            green = pixel[1] % 2
            blue = pixel[2] % 2
            # check if red was odd or even
            if red == 0:
                bit_list.append("0")
            else:
                bit_list.append("1")
            # check if green was odd or even
            if green == 0:
                bit_list.append("0")
            else:
                bit_list.append("1")
            # check if blue was odd or even
            if blue == 0:
                bit_list.append("0")
            else:
                bit_list.append("1")

        return bit_list

    def encode(self, carrier: str, payload: str) -> Image.Image:
        """LSB: hide png in png.

        Args:
            carrier (str): Path of the cover image in format png.
            payload (str): Path of the secret image in format png.

        Returns:
            Image.Image: The encoded PNG image object with the payload embedded.
        """
        data = self._construct_data(payload)
        # setup file data
        cover_image = Image.open(carrier).convert("RGB")
        cover_image_pixels = list(cover_image.getdata())

        # counter for indexing into the bits of the secretFile
        secret_data_index = 0
        cover_image_index = 0
        break_flag = False

        # loop through the pixels in the cover image
        for pixel in cover_image_pixels:
            if break_flag is True:
                break

            # get a list of all the bits for each byte in the RGB
            # representation of the current pixel
            red = list(bin(pixel[0])[2:])
            green = list(bin(pixel[1])[2:])
            blue = list(bin(pixel[2])[2:])
            # modify the LSB
            if (
                self._modify_lsb(secret_data_index, len(data), list([red, green, blue]))
                == 1
            ):
                break_flag = True
            # secretDataIndex in modifyLSB is immutable, so we need to update
            # it
            secret_data_index = secret_data_index + 3
            # covert each RGB bit represenatation back into an integer value
            red_i = int("".join(red), 2)
            green_i = int("".join(green), 2)
            blue_i = int("".join(blue), 2)
            # assign the RGB values back to the current pixel of the cover
            # image
            cover_image_pixels[cover_image_index] = (red_i, green_i, blue_i)
            cover_image_index = cover_image_index + 1

        stego_image = Image.new(
            "RGB", (cover_image.size[0], cover_image.size[1]), (255, 255, 255)
        )
        stego_image.putdata(cover_image_pixels)
        return stego_image

    def decode(self, stego_image: Image.Image) -> Image.Image:
        """Decode the secret image from the encoded image.

        Args:
            stego_image (Image.Image): The encoded PNG image object with the
                secret image embedded.

        Returns:
            Image.Image: The decoded PNG image.
        """
        least_significant_bits = self._get_bits(stego_image)
        lsb_string = binascii.unhexlify("%x" % int("".join(least_significant_bits), 2))
        _, filesize, data = lsb_string.split(b"\0", 2)
        depayload_image = Image.new(
            "RGB", (stego_image.size[0], stego_image.size[1]), (255, 255, 255)
        )
        depayload_image.putdata(data[: int(filesize)])
        print("Secret file has been saved.")
        return depayload_image

encode(carrier, payload)

LSB: hide png in png.

Parameters:

Name Type Description Default
carrier str

Path of the cover image in format png.

required
payload str

Path of the secret image in format png.

required

Returns:

Type Description
Image

Image.Image: The encoded PNG image object with the payload embedded.

Source code in stegobox/codec/hide_pnginpng_lsb.py
def encode(self, carrier: str, payload: str) -> Image.Image:
    """LSB: hide png in png.

    Args:
        carrier (str): Path of the cover image in format png.
        payload (str): Path of the secret image in format png.

    Returns:
        Image.Image: The encoded PNG image object with the payload embedded.
    """
    data = self._construct_data(payload)
    # setup file data
    cover_image = Image.open(carrier).convert("RGB")
    cover_image_pixels = list(cover_image.getdata())

    # counter for indexing into the bits of the secretFile
    secret_data_index = 0
    cover_image_index = 0
    break_flag = False

    # loop through the pixels in the cover image
    for pixel in cover_image_pixels:
        if break_flag is True:
            break

        # get a list of all the bits for each byte in the RGB
        # representation of the current pixel
        red = list(bin(pixel[0])[2:])
        green = list(bin(pixel[1])[2:])
        blue = list(bin(pixel[2])[2:])
        # modify the LSB
        if (
            self._modify_lsb(secret_data_index, len(data), list([red, green, blue]))
            == 1
        ):
            break_flag = True
        # secretDataIndex in modifyLSB is immutable, so we need to update
        # it
        secret_data_index = secret_data_index + 3
        # covert each RGB bit represenatation back into an integer value
        red_i = int("".join(red), 2)
        green_i = int("".join(green), 2)
        blue_i = int("".join(blue), 2)
        # assign the RGB values back to the current pixel of the cover
        # image
        cover_image_pixels[cover_image_index] = (red_i, green_i, blue_i)
        cover_image_index = cover_image_index + 1

    stego_image = Image.new(
        "RGB", (cover_image.size[0], cover_image.size[1]), (255, 255, 255)
    )
    stego_image.putdata(cover_image_pixels)
    return stego_image

decode(stego_image)

Decode the secret image from the encoded image.

Parameters:

Name Type Description Default
stego_image Image

The encoded PNG image object with the secret image embedded.

required

Returns:

Type Description
Image

Image.Image: The decoded PNG image.

Source code in stegobox/codec/hide_pnginpng_lsb.py
def decode(self, stego_image: Image.Image) -> Image.Image:
    """Decode the secret image from the encoded image.

    Args:
        stego_image (Image.Image): The encoded PNG image object with the
            secret image embedded.

    Returns:
        Image.Image: The decoded PNG image.
    """
    least_significant_bits = self._get_bits(stego_image)
    lsb_string = binascii.unhexlify("%x" % int("".join(least_significant_bits), 2))
    _, filesize, data = lsb_string.split(b"\0", 2)
    depayload_image = Image.new(
        "RGB", (stego_image.size[0], stego_image.size[1]), (255, 255, 255)
    )
    depayload_image.putdata(data[: int(filesize)])
    print("Secret file has been saved.")
    return depayload_image