Source code for steganographer

"""Given an image and a message or file steganographer will hide the message or file in the bits of the image."""
import sys
import os.path
from PIL import Image


def _unpack_image(pixels):
    """Flatten out pixels and returns a tuple. The first entry is the size of each pixel."""
    unpacked_pixels = []

    try:
        for pix in pixels:
            for val in pix:
                unpacked_pixels.append(val)

        return len(pixels[0]), bytes(unpacked_pixels)
    except TypeError:
        return 1, bytes(pixels)


def _pack_image(pixels):
    """Do create 2d list of pixels and return the list."""
    packed_pixels = []
    pixel_length = pixels[0]

    for i in range(0, len(pixels[1]), pixel_length):
        packed_pixels.append(tuple(pixels[1][i:i + pixel_length]))

    return packed_pixels


def _open_bin_file(fname):
    """Reads the file fname and returns bytes for all of its data."""
    try:
        with open(fname, 'rb') as fimage:
            image_bytes = fimage.read()

        return image_bytes

    except FileNotFoundError:
        print("Could not find file", fname)
        sys.exit()


def _write_bin_file(fname, data):
    """Create a file fname and writes the passed in data to it."""
    try:
        with open(fname, 'wb') as fdirty:
            fdirty.write(data)

    except IOError:  # pragma: no cover
        print("Could not create file", fname)
        sys.exit()


def _open_image_file(fname):
    """Reads the file fname and returns bytes for all it's data."""
    try:
        with Image.open(fname) as img:
            pixels = list(img.getdata())
            return _unpack_image(pixels)

    except FileNotFoundError:
        print("Could not read file", fname)
        sys.exit()


def _write_image_file(fname, og_fname, data):
    """Create a image fname and writes the passed in data to it. Returns name of image created."""
    try:
        with Image.open(og_fname) as ogim:
            img = Image.new(ogim.mode, ogim.size)
            img.putdata(_pack_image(data))
            fname_no_ext, _ = os.path.splitext(fname)
            img.save(fname_no_ext + '.png', 'png')
            return fname_no_ext + '.png'

    except FileNotFoundError:
        print("Could not read file", og_fname)
        sys.exit()





[docs]class Steganographer: """Takes care of hiding and revealing messages and files in an image.""" _BYTELEN = 8 _header = Header() def __init__(self): """Setting header data_len, so retrieving the header knows how much data to grab.""" self._header.data_len = self._header.header_length # The only data is the header. self._header.bits_used = 1 def _generate_header(self, data_size, bits_to_use, file_name): """ Generates the header that will be placed at the beginning of the image. Returns header as bytes. """ self._header = Header(data_size, bits_to_use, file_name) return self._header.header_as_bytes def _retrieve_header(self, data): """ Retrieves the header from the data passed in and sets the appropriate attributes. Returns if there is a valid header or not. """ bytes_to_hide_header = self._header.header_length * self._BYTELEN self._header.data_len = bytes_to_hide_header # The only data is the header. header_as_bytes = self._reveal_data(data[:bytes_to_hide_header]) is_header_valid = self._header.retrieve_header(header_as_bytes) # Getting the file name if one exist and updating the header. if self._header.file_name_len > 0: bytes_to_hide_header = self._header.header_length * self._BYTELEN self._header.data_len = bytes_to_hide_header # The only data is the header. header_as_bytes = self._reveal_data(data[:bytes_to_hide_header]) is_header_valid = self._header.retrieve_header(header_as_bytes) return is_header_valid def _hide_byte(self, clean_data, val): """ Hides a byte val in clean_data. Returns bytes. Expects a bytes of length 8 and a character value. Will return a bytes with the character's bits hidden in the least significant bits of clean_data. """ hidden_data = bytearray(len(clean_data)) mask = 1 << (self._BYTELEN - 1) for i in range(len(hidden_data)): masked_bit = val & (mask >> i) if masked_bit > 0: masked_bit >>= self._BYTELEN - 1 - i hidden_data[i] = clean_data[i] | masked_bit else: masked_bit = ~(mask >> (self._BYTELEN - 1)) hidden_data[i] = clean_data[i] & masked_bit return bytes(hidden_data) def _reveal_byte(self, hidden_data): """Expects a bytes of length 8. Will pull out the least significant bit from each byte and return them.""" if len(hidden_data) == 0: return bytes() revealed_data = bytearray(1) for i in range(len(hidden_data)): least_sig_bit = hidden_data[i] & 1 revealed_data[0] |= least_sig_bit << (self._BYTELEN - 1 - i) return bytes(revealed_data) def _hide_string(self, clean_data, val): """ Hides a string val in clean_data. Returns a bytes. Expects a bytes of any length and a string value. Will return a bytes with the string's bits hidden in the least significant bits. """ return self._hide_data(clean_data, bytes(val, 'utf-8')) def _reveal_string(self, hidden_data): """ Returns a string hidden in hidden_data. Expects a bytes of any length. Will pull out the least significant bits from each byte and return them as a string. """ revealed_data = self._reveal_data(hidden_data) try: revealed_string = revealed_data.decode('utf-8') except UnicodeDecodeError: # pragma: no cover print("The hidden message could not be decoded.") sys.exit() return revealed_string def _hide_data(self, clean_data, val): """ Hides val inside clean_data. Returns a bytes. Expects a bytes clean_data of any length and another bytes val. Will return a bytes with the val's bits hidden in the least significant bits of clean_data. """ hidden_data = bytearray() for data_index, str_index in zip(range(0, len(clean_data), self._BYTELEN), range(len(val))): clean_byte = clean_data[data_index:data_index + self._BYTELEN] hidden_byte = self._hide_byte(clean_byte, val[str_index]) hidden_data.extend(hidden_byte) hidden_data = hidden_data + clean_data[len(hidden_data):] return bytes(hidden_data) def _reveal_data(self, hidden_data): """ Returns the data hidden in hidden_data. Expects a bytes hidden_data of any length. Will pull out the least significant bits from each byte and return them as a bytes. """ revealed_data_len = self._header.data_len revealed_data = bytearray() for i in range(0, revealed_data_len * self._BYTELEN, self._BYTELEN): revealed_data.extend(self._reveal_byte(hidden_data[i:i + self._BYTELEN])) return bytes(revealed_data)
[docs] def steganographer_hide(self, clean_image_file, text, dirty_image_file=''): """ Hides text inside clean_image_file and outputs dirty_image_file. Takes in a clean image file name, a dirty image file name and text that will be hidden. Hides the text in clean_image_file and outputs it to dirty_image_file. """ header = self._generate_header(len(text.encode('utf-8')), 1, "") clean_data = _open_image_file(clean_image_file) # Is a tuple with the size of a pixel and the pixels. dirty_data = self._hide_data(clean_data[1][:len(header) * self._BYTELEN], header) dirty_data += self._hide_string(clean_data[1][len(header) * self._BYTELEN:], text) dirty_image_data = (clean_data[0], dirty_data) if dirty_image_file == '': clean_name = clean_image_file.split('.')[0] clean_extension = clean_image_file.split('.')[1] dirty_image_file = clean_name + "Steganogrified." + clean_extension output_file = _write_image_file(dirty_image_file, clean_image_file, dirty_image_data) return output_file
[docs] def steganographer_hide_file(self, clean_image_file, file_to_hide, dirty_image_file=''): """Hides file_to_hide inside clean_image_file and outputs to dirty_image_file.""" with open(file_to_hide, 'rb') as input_file: header = self._generate_header(len(input_file.read()), 1, file_to_hide) input_file.seek(0) clean_data = _open_image_file(clean_image_file) # Is a tuple with the size of a pixel and the pixels. dirty_data = self._hide_data(clean_data[1][:self._header.header_length * self._BYTELEN], header) dirty_data += self._hide_data(clean_data[1][self._header.header_length * self._BYTELEN:], input_file.read()) dirty_image_data = (clean_data[0], dirty_data) if dirty_image_file == '': clean_name = clean_image_file.split('.')[0] clean_extension = clean_image_file.split('.')[1] dirty_image_file = clean_name + "Steganogrified." + clean_extension output_file = _write_image_file(dirty_image_file, clean_image_file, dirty_image_data) return output_file
[docs] def steganographer_reveal(self, fimage): """Reveals whatever data is hidden in the fimage file least significant bits.""" dirty_data = _open_image_file(fimage) if self._retrieve_header(dirty_data[1]) is False: print("This file %s has no hidden message." % fimage) sys.exit() revealed_data = self._reveal_data(dirty_data[1][self._header.header_length * self._BYTELEN:]) return revealed_data, self._header.file_name.decode('utf-8')