Digital steganography the modern form of ancient art of concealing messages

 Author - Sivakumar RR

The word “Steganography” comes from Greek “steganographia” which combines the words “steganós”, meaning “covered or concealed” and “grahia” meaning “writing”. Even though most of the places this is considered as part of Cryptography where messages are concealed or encrypted during communication, Steganography conceals the fact that the message is communicated. In this technique, message content will be hidden inside another file which can be an audio, video, image or other format of files.
 

The main advantage of using Steganography over Cryptography alone is that the intended secret message does not attract attention to itself as an object of scrutiny. There are places where encryption is illegal where steganography can be an alternate solution. Although in some cases steganography can be used as an extra layer of security on top of encryption where the encrypted message can be communicated via a stego object. Various forms of steganography which are widely used now a days are

  • Text Steganography 
  • Image Steganography 
  • Video Steganography 
  • Network Steganography 
  • Audio Steganography etc

Since all the above mentioned are self explanatory from their names itself, I’m not going into details of each and every one in this article. 

As you can see in the drawing above, a stego object is generated from the process of embedding message (M) into cover object (C) with the stego key (K). In this article we will be going through the similar implementation for creating a steganographed image using Python programming language.


Images are formed by a group of pixels representing RGB values. The primary colors Red, Green and Blue, based on the intensity of each of these colors in each pixel decides the color of the pixel. As represented in the image RGB values can be represented in binary format as well. We are going to achieve this goal by changing the RGB value in each pixel.

Picture Credit : Edureka Steganography tutorial

When we take this pixel value, we will be able to modify its bit values, as represented in the image above we have MSB (most significant bit) and LSB (least significant bit) in the binary representation. Changing the MSB will be modifying the bytes too much which will have a large impact on the output where LSB modification will have very minimal impact. So in the actual result, the pixel's color will have a very minimal change which in most cases will not be identifiable with human eyes. In this article we are going to modify the LSB in pixels to have minimal impact on the cover object.

In this tutorial, we will be using a default image we call it as “base_image.png” for all the encoding purposes and each time when a new encoding is processed there will be an output file created named “encoded_image.png”. We are mainly using PNG format here since JPG / JPEG image’s pixels will be compressed over transmission in most of the applications for performance and storage optimization, in such cases the data we are trying to embed will be lost or scrambled.

How LSB update process works here?

[(225, 12, 99), (155, 2, 50), (99, 51, 15), (15, 55, 22),(155, 61, 87), (63, 30, 17), (1, 55, 19), (99, 81, 66),(219, 77, 91), (69, 39, 50), (18, 200, 33), (25, 54, 190)]

 
Take above RGB combination array as an image which we are using it as cover object and assume the message we wanted to encode inside this is “hi”. Using ASCII table we can convert the text to decimal and then to binary value where the “hi” will be represented as “0110100 0110101“. We will be iterating over the pixel values and changing the least significant bit (LSB) with the message value in binary. This update will be +1 or -1 operation so this will not make any noticeable impact on the image. Resulting image data will be as follows.


[(224, 13, 99),(154, 3, 50),(98, 50, 15),(15, 54, 23),(154, 61, 87),(63, 30, 17),(1, 55, 19),(99, 81, 66),(219, 77, 91),(69, 39, 50),(18, 200, 33),(25, 54, 190)]

Now let’s try this in python, we will be using a few libraries for better and ease of implementation.

import cv2 #opencv-python for image processing
import types
import numpy as np
import subprocess

We will start with the user prompt for option selection to either choose with encode the data or decode the data.

a = input("\n 1. Encode the data (we will use a default image to encode data)\n 2. Decode the data \n Your input is: ")

userinput = int(a)

if (userinput == 1):

    print("\nEncoding....")

    encode_text() 

          

elif (userinput == 2):

    print("\nDecoding....") 

    print("Decoded message is ==> " + decode_text().decode()) 

    print("")

else: 

    raise Exception("Enter correct input")

 


Let’s start with encoding, so one thing which we need to know here is, if we go ahead with only steganography then the data is actually hidden, not really secured since it's not encrypted and there are possibilities that others can read it by analyzing the pattern of the file.

We can add an extra layer for securing the data here, which is Encryption. As part of the encoding process let’s encrypt the data with a key and encode the encrypted message with the image. Later then the key can be shared with the user so the decoding user can use the key for decrypting the message.

from cryptography.fernet import Fernet

We will be using the cryptography library and Fernet for encryption.

def encode_text(): 

    image = cv2.imread("base_image.png") # Read the input image using OpenCV-Python.

    

    #details of the image

    print("The shape of the image is: ",image.shape) #check the shape of image to calculate the number of bytes in it

      

    data = input("Enter data to be encoded : ") 

    if (len(data) == 0): 

        raise ValueError('Data is empty')

    

    key = Fernet.generate_key()

    print("\n Secret Key ==> " + key.decode())

    print("\n Keep the above mentioned key as secret, since the key is required to decrypt the data.")

    enc_data = encrypt_message(data, key)


    filename = "encoded_image.png"

    encoded_image = hideData(image, enc_data) # call the hideData function to hide the secret message into the selected image

    cv2.imwrite(filename, encoded_image)

    print("\n Process completed, encoded image name 'encoded_image.png'")

 



Here we are using the Fernet to generate a random key for encryption and we are sharing the key with the user as part of the process. We can go further and implement the “hideData” method we use here.

 

def hideData(image, secret_message):

    # calculate the maximum bytes to encode

    n_bytes = image.shape[0] * image.shape[1] * 3 // 8

    print("Maximum bytes to encode:", n_bytes)


    #Let's make sure that we have enough image size to encode data

    if len(secret_message) > n_bytes:

        raise ValueError("Error - insufficient bytes, need bigger image or less data !!")

  

    secret_message = secret_message.decode() + "#####" # using this string as the delimiter

    

    data_index = 0

    binary_secret_msg = messageToBinary(secret_message)


    data_len = len(binary_secret_msg)

    for values in image:

        for pixel in values:

            # convert RGB values to binary format

            r, g, b = messageToBinary(pixel)

            # modify the LSB only if there is still data to store

            if data_index < data_len:

                # red pixel

                pixel[0] = int(r[:-1] + binary_secret_msg[data_index], 2)

                data_index += 1

            if data_index < data_len:

                # green pixel

                pixel[1] = int(g[:-1] + binary_secret_msg[data_index], 2)

                data_index += 1

            if data_index < data_len:

                # blue pixel

                pixel[2] = int(b[:-1] + binary_secret_msg[data_index], 2)

                data_index += 1

            # break out of the loop if the data is over

            if data_index >= data_len:

                break


    return image

 

In this method we are performing multiple validations and a couple of tweaks. One of them is adding a delimiter for marking when the data encoding is completed. This is very important in our implementation since while reading the data, it is important that we have to announce the encoded message is over, otherwise it will also include bits which are not part of the data which can lead to error during decryption. We are also calling a new method for binary conversion “messageToBinary”, this is a helper method for converting different types of data to binary during the process.


def messageToBinary(message):


    if type(message) == str:

        return ''.join([ format(ord(i), "08b") for i in message ])

    elif type(message) == bytes or type(message) == np.ndarray:

        return [ format(i, "08b") for i in message ]

    elif type(message) == int or type(message) == np.uint8:

        return format(message, "08b")

    else:

        raise TypeError("Input type not supported")

 



Another helper method we have added here for the Encryption process. 

def encrypt_message(msg,key):

    cipher_suite = Fernet(key)

    cipher_text = cipher_suite.encrypt(str.encode(msg))

    return cipher_text


That concludes the Encryption and Encoding process, now let’s add other components for Decryption and Decoding.
 

def decrypt_message(cipher_text,key):

    cipher_suite = Fernet(str.encode(key))

    plain_text = cipher_suite.decrypt(str.encode(cipher_text))

    return plain_text


For decoding,
 

def decode_text():

    # read the image that contains the hidden image

    image_name = input("Enter the name of the steganographed image that you want to decode (with extension) :") 

    image = cv2.imread(image_name) #read the image using cv2.imread() 


    text = showData(image)


    key = input("\n Enter the key to decrypt message : ")

    de_text = decrypt_message(text,key)


    return de_text

 
Definition of helper method “showData” where the actual reading of data happening.
 

 

def showData(image):


    binary_data = ""

    for values in image:

        for pixel in values:

            r, g, b = messageToBinary(pixel) #convert the red,green and blue values into binary format

            binary_data += r[-1] #red pixel

            binary_data += g[-1] #green pixel

            binary_data += b[-1] #blue pixel

    # split by 8-bits

    all_bytes = [ binary_data[i: i+8] for i in range(0, len(binary_data), 8) ]

    # convert from bits to characters

    decoded_data = ""

    for byte in all_bytes:

        decoded_data += chr(int(byte, 2))

        if decoded_data[-5:] == "#####": #check if we have reached the delimiter which is "#####"

            break

    #print(decoded_data)

    return decoded_data[:-5] #remove the delimiter to show the original hidden message

 

Our entire process in the script can be mapped as follows

 

  • Data --> Key Generation --> Encrypt -->  Encode --> Image generated & Key shared
  • Image shared --> Decode --> Key sharing --> Decrypt --> Print Message

Following are the actual screenshots of the script generated.

 

Encoding Process


 

Decoding Process


 You can find the full code here :  https://github.com/sivakumar090/image_stegano

 
 

 
 

 

 




Comments