[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky] [Facebook]
[Build Your Own Python Action Arcade!]

[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Use steganography to hide encrypted messages in an image in Python

[Recovering an encrypted message hidden inside an image]

My post Use steganography to hide messages in an image in Python shows how you can hide a message inside an image. Unfortunately, anyone can recover the message if they know it is there. The attacker can simply use the technique described in that post. If the image contains a message, the attacker will find it.

This approach of hoping an attacker just doesn't know how the message is hidden is called security through obscurity and it's not very secure. It basically relies on your attacker being dumb. For example, the mafia crime boss Bernardo Provenzano was caught in part because he used a variation of a simple Caesar cipher to encrypt messages. That cipher is easy to crack if you know that's the cipher being used and Italian police investigators broke it.

Modern cryptography assumes the attacker always knows the way the message was encrypted and relies on the strength of the algorithm to protect the message. The only information the attacker doesn't know is the password or other information used by the encryption algorithm.

Steganography Plus Encryption

This example combines the steganography described in that post with the encryption described in my last post Encrypt and decrypt messages in Python.

The approach is simple: encrypt the message and then use steganography to store the encrypted message in the image. My previous cryptography post uses fernet encryption generates a salt, a sequence of bytes used to protect the message from certain kinds of attacks. That means you need the password and salt to decrypt messages.

You can make the message more secure if you require the people sending and receiving the message to remember the salt, but its bytes are pseudorandom, so you're not going to be able to remember them unless you have a next-level memory. If you want to use that approach, you'll probably need to save the salt on a flash drive or something and then give it to the recipient.

This example takes a different approach: it saves the salt with the message. That makes the encryption a bit less secure, but an attacker will still need to perform a brute-force attack to recover the message. Pick a good password and the message should be safe to quite a while.

Stegifying Messages

Here's the basic approach that my previous example used to hide a message in an image.

  1. You pass the image and message to the stegify_message function.
  2. The stegify_message function does the following.
    1. Calls text_to_bits to convert the message string into a sequence of 0s and 1s.
    2. Passes the image and bits to the stegify_bits function, which writes the bits into the image.

    This example adds one twist: it adds cryptography to the text_to_bits function. Here's how it works.

    def text_to_bits(password_text, message_text): '''Convert the text into a string of 0s and 1s.''' # Encrypt the text into bytes. password_bytes = password_text.encode('utf-8', errors='replace') message_bytes = message_text.encode('utf-8', errors='replace') cipher_bytes, salt_bytes = encrypt_bytes(message_bytes, password_bytes) # Add the salt (16 bytes) at the beginning. all_bytes = salt_bytes + cipher_bytes # Convert to bits. return ''.join(f'{byte:08b}' for byte in all_bytes)

    The function uses encode to convert the password and message into sequences of bytes. It passes those into the encrypt_bytes function to encrypt the message. That function returns the encrypted message bytes and the salt bytes.

    Next, the code attaches the salt's bytes at the beginning of the encrypted message bytes, converts the whole list into a sequence of 0s and 1s, and returns the result.

    Destegifying Messages

    The following destegify_image function recovers a message from an image.

    def destegify_image(password_text, image): '''Extract the bits from this image.''' # Get a bit extractor. extractor = bit_extractor(image) # Get the 32-bit stored message length. num_length_bits = 8 * 4 bits = ''.join(next(extractor) for i in range(num_length_bits)) num_message_bits = int(bits, 2) # Get the rest of the bits. all_bits = ''.join(next(extractor) for i in range(num_message_bits)) all_bytes = bits_to_bytes(all_bits) # Separate the salt (16 bytes) and the message. salt_bytes = all_bytes[:16] cipher_bytes = all_bytes[16:] # Convert the password to bytes. password_bytes = password_text.encode('UTF-8', errors='replace') # Decrypt the message bytes. plain_bytes = decrypt_bytes(cipher_bytes, password_bytes, salt_bytes) return plain_bytes.decode('UTF-8')

    The function creates a bit_extractor as described in the previous steganography post.

    Next, it uses the extractor to get the first 32 bits stored in the image. It joins the bits together and converts them into an integer giving the length of the message plus the salt in pixels.

    The code then uses the extractor to get the salt and message bits, and uses bits_to_bytes (described shortly) to convert the bits into bytes. It peels off the first 16 bytes for the salt and leaves the remaining bytes for the encrypted message.

    The function converts the password into bytes and passes the encrypted message, the password bytes, and the salt bytes to the decrypt_bytes function to decrypt the message.

    Finally, the function converts the decrypted message from bytes to a string and returns the result.

    bits_to_bytes The following bits_to_bytes helper function converts a list of bits into a list of bytes.

    def bits_to_bytes(bits): '''Convert a string of bits into bytes.''' num_bytes = (len(bits) + 7) // 8 return int(bits, 2).to_bytes(num_bytes, byteorder='big')

    The code calculates the number of bytes needed to represent the bits. It uses int to convert the bits into a (possibly large) integer and then calls to_bytes to convert the integer into bytes.

    Conclusion

    I know this post is somewhat confusing. It explains how the program adds encryption to the previous steganography example, but you need to know how that example and the encryption example work. See those posts for details.

    I'm planning at least two more posts in this series: one to make using this example easier in practice and one to show how you can store an image inside another image. Meanwhile, download the example to experiment with it and to see additional details.

© 2025 - 2026 Rocky Mountain Computer Consulting, Inc. All rights reserved.