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