When you see an image file, do you just see, – the visual image it represents? Or is it possible that there’s much more to it? Maybe there’s some secret hidden in the image that you don’t know?
Actually, yes. It is already an established cryptography practice to hide information in something like image files. This is called – Steganography.
What Is Steganography?
Steganography is a way of hiding critical information. Unlike cryptography, which focuses on encrypting data, steganography focuses on concealing the data, and thus the intended secret message does not attract attention to itself as an object of scrutiny.
Today, we will create a simple Python program that hides a secret message in an image file without changing what the image looks like.
The source code for this Steganography tutorial can be found at chen-yumin/steganography-python.
The idea behind this image-based steganography is simple. Image files are composed of digital data that describes what is in the picture, usually the colors of all the pixels. Take a 24-bit color bitmap image for example, each pixel is comprised of 3 channels of 8-bit color. However, the least significant bit (LSB) in these 8 bits carries very little weight, and therefore, even if it is somehow lost, you will barely notice any difference in the visual of the image.
Conceal the Message
In this example, we will conceal a hidden message in a 8-bit grayscale image, replacing each pixel’s LSB to store our message.
def to_bit_generator(msg): for c in (msg): o = ord(c) for i in range(8): yield (o & (1 << i)) >> i
First of all, the secret message gets converted to a Python
generator which returns 1 bit of the message each time. For simplicity we will just read the
README.md file and use it as our secret hidden message. If you’d like to hide something else, feel free to change this to another file.
# Create a generator for the hidden message hidden_message = to_bit_generator(open("README.md", "r").read() * 10) # Read the original image img = cv2.imread('original.png', cv2.IMREAD_GRAYSCALE) for h in range(len(img)): for w in range(len(img)): # Write the hidden message into the least significant bit img[h][w] = (img[h][w] & ~1) | next(hidden_message) # Write out the image with hidden message cv2.imwrite("output.png", img)
To keep things simple, the image file is read as
IMREAD_GRAYSCALE grayscale. This reduces the complexity of the example so we don’t have to worry about the color dimension.
We loop through every pixel of this image, using the
or operation to override its least significant bit. And here we go, we have our secret message hidden in this image.
Note that this file is saved as
.png, which is a lossless compression format. You can also save it as some uncompressed image file formats, such as
.bmp. However, if it is saved as
.jpg, the secret hidden message will be lost due to the lossy compression algorithm.
Run the script, and here we have an image file with hidden data:
Compare the above two image files, can you notice any visual difference? The image on the left is the original one; the right is the processed image with hidden data. They all look exactly the same, but if you read it in a hex editor you will be able to see the actual content has been completely changed.
Restore the Message
Now that we have generated the image file with hidden data, let’s read back from the file and see if we can restore the hidden message.
# Read the image and try to restore the message img = cv2.imread('output.png', cv2.IMREAD_GRAYSCALE) i = 0 bits = '' chars =  for row in img: for pixel in row: bits = str(pixel & 0x01) + bits i += 1 if(i == 8): chars.append(chr(int(bits, 2))) i = 0 bits = '' print(''.join(chars))
Run the script above to restore the hidden message. Can you see it?
Yes, the above textual information was all read from the
output.png image file. Can you believe that? The next time when you see an image file, maybe you won’t take it as it is any more.