Skip to content

PNGStego

stegobox.codec.PNGStego

Bases: BaseCodec

PNG steganography - simple LSB steganography in PNG images

  • To encode: The input image and message are both converted into binary, and the message is embedded into the least significant bits (LSB) of the input image. Once the message is encoded, a trailing delimiter is written. The resulting image is saved as a PNG file.
  • To decode: The least significant bits of the input image are read off until the delimiter is hit. After which the secret message would be recovered.

Source: nzimm/png-stego

Source code in stegobox/codec/png_stego.py
class PNGStego(BaseCodec):
    """PNG steganography - simple LSB steganography in PNG images

    * To encode: The input image and message are both converted into binary, and the
      message is embedded into the least significant bits (LSB) of the input image. Once
      the message is encoded, a trailing delimiter is written. The resulting image is
      saved as a PNG file.
    * To decode: The least significant bits of the input image are read off until the
      delimiter is hit. After which the secret message would be recovered.

    Source: [nzimm/png-stego](https://github.com/nzimm/png-stego)
    """

    def __init__(self) -> None:
        super().__init__()

    def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
        """Encoder requires the carrier image to be PNG and the payload to be a string.

        Args:
            carrier: Carrier image in format PNG. Read with `stegobox.io.image.read()`.
            payload: Payload (secret message) to be encoded.

        Returns:
            The encoded PNG image object with the payload embedded.
        """
        self._check_png(carrier)
        self._check_empty_payload(payload)

        # Maximum number of bytes that can be encoded
        max_payload_length = carrier.size[0] * carrier.size[1] * 3

        # Convert payload to binary (removes leading '0b')
        payloadb = to_binary(payload)[2:]
        if len(payloadb) > max_payload_length:
            raise Exception(f"Payload must be shorter than {max_payload_length} bits.")

        bits_encoded = 0
        delimiter_bits = 0
        image_data = []

        # Iterate over all pixels in the image
        for y in range(carrier.size[1]):
            for x in range(carrier.size[0]):
                for color in carrier.getpixel((x, y)):
                    if bits_encoded < len(payloadb):
                        # Set LSB of color to one bit of the payload. Mask color with
                        # 254 to guarantee LSB is 0, then set LSB to the payload bit
                        image_data.append((color & 254) | int(payloadb[bits_encoded]))
                        bits_encoded += 1

                    elif delimiter_bits < 8:
                        # Zero out 8 LSB as delimiter for the payload
                        image_data.append(color & 254)
                        delimiter_bits += 1

                    else:
                        # Finally, append the rest of the image data
                        image_data.append(color)

        # Returned encoded image - must be saved as PNG
        return Image.frombytes("RGB", carrier.size, bytes(image_data))

    def decode(self, carrier: Image.Image) -> str:
        """Decode the secret payload from the carrier image.

        Args:
            carrier: Carrier image in format PNG. Read with `stegobox.io.image.read()`.

        Returns:
            The decoded payload (secret message).
        """
        self._check_png(carrier)

        # Buffer for the payload
        payload = ""

        # Return message after finding the first \x00 byte
        bit_count = 1

        # Byte list to convert back to ascii string
        byte_list = []
        byte_str = "0"

        for y in range(carrier.size[1]):
            for x in range(carrier.size[0]):
                for color in carrier.getpixel((x, y)):
                    byte_str += bin(color)[-1]
                    bit_count += 1

                    if bit_count == 8:
                        if byte_str == "00000000":
                            break

                        byte_list.append(byte_str)
                        bit_count = 0
                        byte_str = ""

        # Convert byte list to ascii string
        try:
            for byte in byte_list:
                payload += to_string(byte)
        except TypeError:
            return ""

        return payload

    def _check_png(self, image: Image.Image) -> None:
        if image.format != "PNG":
            raise Exception("Carrier image must be PNG in PNGStego.")

    def _check_empty_payload(self, payload: str) -> None:
        if not payload:
            raise Exception("Payload must not be empty.")

encode(carrier, payload)

Encoder requires the carrier image to be PNG and the payload to be a string.

Parameters:

Name Type Description Default
carrier Image

Carrier image in format PNG. Read with stegobox.io.image.read().

required
payload str

Payload (secret message) to be encoded.

required

Returns:

Type Description
Image

The encoded PNG image object with the payload embedded.

Source code in stegobox/codec/png_stego.py
def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
    """Encoder requires the carrier image to be PNG and the payload to be a string.

    Args:
        carrier: Carrier image in format PNG. Read with `stegobox.io.image.read()`.
        payload: Payload (secret message) to be encoded.

    Returns:
        The encoded PNG image object with the payload embedded.
    """
    self._check_png(carrier)
    self._check_empty_payload(payload)

    # Maximum number of bytes that can be encoded
    max_payload_length = carrier.size[0] * carrier.size[1] * 3

    # Convert payload to binary (removes leading '0b')
    payloadb = to_binary(payload)[2:]
    if len(payloadb) > max_payload_length:
        raise Exception(f"Payload must be shorter than {max_payload_length} bits.")

    bits_encoded = 0
    delimiter_bits = 0
    image_data = []

    # Iterate over all pixels in the image
    for y in range(carrier.size[1]):
        for x in range(carrier.size[0]):
            for color in carrier.getpixel((x, y)):
                if bits_encoded < len(payloadb):
                    # Set LSB of color to one bit of the payload. Mask color with
                    # 254 to guarantee LSB is 0, then set LSB to the payload bit
                    image_data.append((color & 254) | int(payloadb[bits_encoded]))
                    bits_encoded += 1

                elif delimiter_bits < 8:
                    # Zero out 8 LSB as delimiter for the payload
                    image_data.append(color & 254)
                    delimiter_bits += 1

                else:
                    # Finally, append the rest of the image data
                    image_data.append(color)

    # Returned encoded image - must be saved as PNG
    return Image.frombytes("RGB", carrier.size, bytes(image_data))

decode(carrier)

Decode the secret payload from the carrier image.

Parameters:

Name Type Description Default
carrier Image

Carrier image in format PNG. Read with stegobox.io.image.read().

required

Returns:

Type Description
str

The decoded payload (secret message).

Source code in stegobox/codec/png_stego.py
def decode(self, carrier: Image.Image) -> str:
    """Decode the secret payload from the carrier image.

    Args:
        carrier: Carrier image in format PNG. Read with `stegobox.io.image.read()`.

    Returns:
        The decoded payload (secret message).
    """
    self._check_png(carrier)

    # Buffer for the payload
    payload = ""

    # Return message after finding the first \x00 byte
    bit_count = 1

    # Byte list to convert back to ascii string
    byte_list = []
    byte_str = "0"

    for y in range(carrier.size[1]):
        for x in range(carrier.size[0]):
            for color in carrier.getpixel((x, y)):
                byte_str += bin(color)[-1]
                bit_count += 1

                if bit_count == 8:
                    if byte_str == "00000000":
                        break

                    byte_list.append(byte_str)
                    bit_count = 0
                    byte_str = ""

    # Convert byte list to ascii string
    try:
        for byte in byte_list:
            payload += to_string(byte)
    except TypeError:
        return ""

    return payload