Steganography in Python

How I Felt Compelled to Find a Hidden Message Without Realizing How Interesting It Would End Up Being

When a coworker invited me to a feedback call, I found a hidden message in the email. Join me as I go down a rabbit hole to decode a message that turned out to be hidden in an image. I used Python to extract the message and learned a lot about steganography in the process.

July 2024

My cowroker invited me to a call and made the email intriguing

One day, I received an email from a coworker with a kind invitation to a feedback call. It was a friendly and welcoming request that we review our collaborations so far and see if there are any areas we can improve. I am sure you know this kind of message.

But this email was different! At the very end of his message, he stated that there was a secret message hidden in the email.

This tickled my curiosity. I decided to find the message... come what may! This is my quite short yet surprisingly eventful journey.

Finding the Hidden Message

At the end of the email, I found a few blank spaces tagged as a hyperlink with a fake email address "try again" and the subject line "Too easy, try again." This was a red herring to mislead us from the real hidden message.

The red herring

I pulled the email out of the corporately mandated Outlook application (I know...) onto the macOS desktop and opened the resulting file in a text editor. It turns out that Outlook hashes the contents of an email. After running the hashes through 10015.io (though you can use any other tool that encodes and decodes), I made the mildly interesting but not very useful discovery that Outlook provides the email's content twice, each in a different format: as plain text and as HTML.

Outlook encodes the email contents in Base64

Now, I won't mince words here. Outlook's HTML is distressingly ugly. It has inline styling and looks sickeningly convoluted. I seem to remember that the only way to get reliable layouts in the HTML of an Outlook email used to be to use tables for layout purposes. I don't know if this is still true nowadays, but I wouldn't be surprised if it were.

Very ugly and convoluted HTML code

Alas, I could not find any indication of a hidden message there. Though, I can't hide my satisfaction upon learning that machine-generated HTML is still far worse than carefully hand-crafted HTML.

I then looked at the recipients of the email. They included other coworkers but also another obviously fake email address.
RGlkIHlvdSByZWFsbHkgdGhpbmsgdGhhdCB0aGlzIHdhcyBnb2luZyB0byBiZSBlYXN5PyAiYUhSMGNITTZMeTlwYldkMWNpNWpiMjB2WVM5YVJYbERaek5XIg==@notreallydomain.com.

This must be the hidden message!

This fake email address might well contain the hidden message

Decoding the Hidden Message

The string before the @ sign looked like a hash of some sort. I assumed that the end of the fake email address @notreallydomain.com should not be included in any attempts to decode it. The trailing == looked familiar, and I felt that they terminate some kind of encoding. Again, I went to 10015.io and ran the string through some of the decoders. It turned out that it is Base64 encoded.

The decoded message reads:
Did you really think that this was going to be easy? "aHR0cHM6Ly9pWnd1ci5j2vYS9aRXlDazNW"

It turns out the string aHR0cHM6Ly9pWnd1ci5j2vYS9aRXlDazNW is also Base64 encoded. After decoding it, you get an actual link: https://imgur.com/a/ZEyCg3V.

The image that the double Base64 encoded string revealed

Based on the contents of the image, I thought that this is another red herring. (I didn't notice that the heading above it says "Where is the message?") Nevertheless, I sent it to my coworker. Even though I did not believe that this was the hidden message I was looking for, it was at least some kind of hidden message.

My coworker replied with a cryptic statement: Did you know? Within digital imagery, hidden whispers await. Pay close attention—they may reveal the concealed truth.

Maybe living in Greece has already made an impression on me, but that statement sounds a bit like a proclamation of the Oracle of Delphi from ancient Greek times. On the bright side, it certainly confirmed that I was on the right track.

I decided to look at the upload date on Imgur to see how old the image is, and it looked quite fresh at only a few days prior to the current date. Taking into account the time that I left the email unanswered and the time it might have taken my coworker to write the email, it was very likely that he had uploaded the image himself. Therefore, the hidden message must be in the image!

Image Madness

Even though I have no idea how to encode a message into an image, I thought it might be a good idea to download the actual file to analyze it in detail.

There are two ways of downloading images from Imgur. I first dragged the image from Safari to the local file system. This gave me a .webp file. Call me old-fashioned, but I kinda dislike .webp files. Please note that this happens when you use Safari. Google Chrome gives you a .png file if you drag it from the browser. So instead, I clicked on the little download icon Imgur provides at the top of images. This gave me a .png file. I compared the file sizes. The .webp file I had dragged directly from Safari had a considerably smaller file size than the .png file. While I can't say for certain, I assume that Imgur automatically converts files to .webp to reduce their file size and conserve bandwidth. This allowed me to conclude that the original format of the uploaded file is .png. If the image has a hidden message, then I'd better look for it in the larger file.

I opened the file with Graphic Converter, a macOS app that acts as a true Swiss army knife for extracting as much metadata as possible from image files in any format (including some obscure medical image formats and most historic image formats all the way back to 8-bit machines from the early 1980s).

No luck there... none of the EXIF data or other metadata looked useful. I couldn't find a hidden message using Graphic Converter.

The image file opened in Graphic Converter

Pillow to the Rescue

Like any person lost for the right answer, I simply googled for "hiding messages in a png image." This looked promising. I started to familiarize myself with the concepts of Steganography. I had heard of that term in conjunction with hidden messages in ancient Greece but had to learn some of the details. Again, this might be my extended stay in Greece playing mind games with me.

Among the results of the web search, I bumped into a post on Medium about decoding a hidden message from a PNG using Python. I knew that while my coworker is impressively adept in many programming languages, I think that he prefers Python. At least that is my impression because when we review JavaScript code together, he uses sophisticated but alien terms like dictionary and tuple that do not apply to rickety, old JavaScript.

Anyway, it was worth a try. It might well be the solution to finding the hidden message. You can find the Medium post with the solution by Dayanand Shah here on Medium.

I'm not going to pretend that I know anything about steganography in Python. According to the Medium post, you can encode a binary message in a .png file in the least significant bit of each color channel (red, green, and blue). Fortunately, the post not only provided the Python code for encoding a message in an image but also how to decode it.

I installed Pillow and ran the Python code on the image file. And I was astounded... I actually got a clear text message printed to the console:
Congratulations! You're as awesome as me :-)

I don't really think that I am awesome... but my coworker sure is! I sent him the message that I had found, and he confirmed that it was correct.

I was so happy that I nearly forgot to accept his invitation to the feedback call he originally asked for in his email.

Running the Code

Please find the repository for this here on GitHub. To decode a hidden message in an image, you can use Python with the Pillow library. The hidden message is encoded in the least significant bit of each color channel (red, green, and blue) of the image. The Python code for encoding and decoding the message can be found in a Medium post.

  1. Install the Pillow library with pip install Pillow.
  2. Run the Python code with python steganography.py. By default, it reads the file imgur.png and decodes the hidden message outputting it to the console.

I was very much tempted to encode secret messages into every image in this article. But I refrained from that, so you can enjoy the images without any distractions.