The stegify_message function does the following.
- Calls text_to_bits to convert the message string into a sequence of 0s and 1s.
- 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.