How to Encrypt an Image in Python using AES Algorithm

Introduction

In this article, we’ll explore a secure way to encrypt images using Advanced Encryption Standard (AES). AES is a widely trusted and globally recognized symmetric key encryption standard. It works by scrambling the visual data of an image and makes it unreadable for anyone without the correct decryption key.

We will achieve this AES encryption process in Python to encrypt and decrypt an image. After encryption, we’ll build another program for the decryption process using the same logic.

How does the AES Algorithm Work?

AES works by performing a series of operations on blocks of data (128 bits at a time) using a specific key (128, 192, or 256 bits). Here’s a simplified explanation of the process:

  1. Preparation:
    • The plain text (original data) is divided into fixed-size blocks (16 bytes each).
    • A key schedule is generated, expanding the main key into multiple round keys used throughout the encryption process.
  2. Encryption rounds:
    • Each data block goes through a series of rounds (10, 12, or 14 depending on key length). Each round consists of four basic transformations:
      • SubBytes: Substitutes each byte in the block with a different byte according to a predefined lookup table. This shuffles the data and introduces “confusion.”
      • ShiftRows: Shifts the rows of the byte matrix cyclically by different offsets. This disrupts any patterns in the data.
      • MixColumns: Performs a complex mixing operation on the data columns, ensuring diffusion of any remaining data patterns.
      • AddRoundKey: XORs the current block state with a round key derived from the key schedule. This incorporates the key material into each round.
  3. Finalization:
    • After all rounds, the final block becomes the encrypted ciphertext (unreadable data).

The key strength and multiple rounds make AES highly secure. Even a small change in the plain text or key will result in a completely different ciphertext, making it difficult to crack the code.

Why use the AES Algorithm?

There are several advantages to using the Advanced Encryption Standard (AES) algorithm for encryption:

  • High Security: AES is considered one of the most secure encryption algorithms currently available. It uses a combination of factors, including long key lengths (128, 192, or 256 bits) and a robust cipher design, to make it extremely difficult to crack. With a sufficiently long key, brute-force attacks (trying every single possible key) become practically impossible.
  • Efficiency: AES is relatively efficient to implement in both hardware and software. This makes it suitable for a wide range of applications, from resource-constrained devices to high-performance computing environments.
  • Flexibility: AES is a symmetric block cipher, meaning the same key is used for both encryption and decryption. This can be simpler to manage than some other encryption schemes. Additionally, AES can be used in various modes of operation, allowing it to be adapted to different security requirements.
  • Widespread Adoption: AES is the encryption standard endorsed by the National Institute of Standards and Technology (NIST) and many other government agencies around the world. This widespread adoption means there is a large pool of expertise and a strong track record for security.

Requirements and Installations

Before we get started, ensure you have Python installed on your system. You can download it from their official website (https://www.python.org/downloads/).

Now install the pycryptodome library to perform encryption and decryption on an image using the AES algorithm.

pip install pycryptodome

Additionally, install the Pillow library using the following command. This library will allow us to work with images using Python programming.

pip install Pillow

Original Image

In this program, we will use the following image (‘original.jpg’) to encrypt and decrypt using the AES algorithm. You can download it through the Download button below.

original.jpg

encrypt.py

Let’s create a Python file named ‘encrypt.py’ and copy the code below.

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
from Crypto.Util import Counter
from PIL import Image
import io
import os
from Crypto.Hash import SHA256, HMAC
def encrypt_image(input_image_path, output_image_path, key):
  # Load the image
  image = Image.open(input_image_path)
  # Generate a random IV
  iv = get_random_bytes(AES.block_size)
  # Get a unique identifier from the filename
  image_hash = SHA256.new(os.path.basename(input_image_path).encode("utf-8")).hexdigest()
  # Combine key-specific value with random string (nonce) using HMAC
  key_specific = HMAC.new(key, msg=image_hash.encode("utf-8"), digestmod=SHA256).digest()
  unique_iv = HMAC.new(key_specific, msg=os.urandom(16), digestmod=SHA256).digest()[:16]
  if len(unique_iv) != 16:
    raise ValueError("Unexpected IV length. Encryption aborted.")
  # Convert the image to bytes
  img_byte_array = io.BytesIO()
  image.save(img_byte_array, format=image.format)
  img_bytes = img_byte_array.getvalue()
  # Initialize AES cipher
  cipher = AES.new(key, AES.MODE_CBC, unique_iv)
  # Encrypt the image data with padding
  padded_data = pad(img_bytes, AES.block_size)
  encrypted_data = iv + cipher.encrypt(padded_data)
  # Write the encrypted data to the output image file
  with open(output_image_path, 'wb') as f:
    f.write(encrypted_data)
  # Save the IV to a separate file with a filename related to the image
  iv_path = os.path.join(os.path.dirname(output_image_path), f"{os.path.basename(input_image_path)}.iv")
  with open(iv_path, 'wb') as f:
      f.write(unique_iv)
  print(f"Encryption successful. Encrypted image saved to '{output_image_path}'.")
  print(f"IV file saved to '{iv_path}'.")
if __name__ == "__main__":
  input_image_path = 'original.jpg'
  output_image_path = 'encrypted_image.jpg'
  key = 'UZ4i59vPgLRT16s8FZ4i81vPgLRT16qk'
  key = bytes(key, encoding="utf-8")
  # Encrypt the image
  encrypt_image(input_image_path, output_image_path, key)

Explanation

The above program takes an input image named ‘original.jpg’ and outputs an image file – ‘encrypted_image.jpg’ and an iv file – ‘original.jpg.iv’. Let’s understand encrypt_image function line-by-line.

The encrypt_image function takes three arguments: ‘input_image_path’, ‘output_image_path’, and ‘key’. The ‘key’ is a byte array (generated from a string using bytes.encode("utf-8")) containing the secret key used for encryption.

The function encrypts the image at the ‘input_image_path’ and saves the encrypted version at the ‘output_image_path’.

Important Breakdown

  • image = Image.open(input_image_path): This line uses the Pillow (PIL) library to open the image file specified by ‘input_image_path’.
  • iv = get_random_bytes(AES.block_size): This line generates a random Initialization Vector (IV) using the ‘Crypto.Random‘ library. The IV is a crucial part of the encryption process and should be unique for each encryption. The AES encryption algorithm used here requires the IV size to be equal to its block size.
  • image_hash = SHA256.new(os.path.basename(input_image_path).encode("utf-8")).hexdigest(): This line calculates a unique identifier for the image based on its filename.
    • os.path.basename(input_image_path) extracts the filename from the provided path.
    • .encode("utf-8") converts the filename to bytes for hashing.
    • SHA256.new creates a SHA-256 hash object.
    • .hexdigest() gets the hexadecimal digest of the hash, providing a unique fingerprint for the filename.
  • key_specific = HMAC.new(key, msg=image_hash.encode("utf-8"), digestmod=SHA256).digest(): This creates a key-specific value by applying the HMAC (Hash-based Message Authentication Code) algorithm with SHA-256 as the digest method. It uses the ‘key’ and the image hash as inputs.
  • unique_iv = HMAC.new(key_specific, msg=os.urandom(16), digestmod=SHA256).digest()[:16]: This line creates a unique IV by applying HMAC again. Here, the ‘key_specific’ value acts as a key, and a random 16-byte string (generated by os.urandom(16)) is the message. The result is truncated to 16 bytes (AES block size) to form the ‘unique_iv’.
  • img_byte_array = io.BytesIO(): This line creates a virtual in-memory file-like object using io.BytesIO.
  • image.save(img_byte_array, format=image.format): This line saves the loaded image to the in-memory byte array, preserving its original format.
  • img_bytes = img_byte_array.getvalue(): This line retrieves the actual byte data of the image from the in-memory buffer.
  • cipher = AES.new(key, AES.MODE_CBC, unique_iv): This line creates an AES cipher object using the Crypto.Cipher library.
    • ‘key’: The secret key for encryption (assumed to be already converted to bytes).
    • AES.MODE_CBC: This specifies Cipher Block Chaining (CBC) mode for AES, a common and secure mode of operation.
    • ‘unique_iv’: The unique initialization vector generated earlier.
  • padded_data = pad(img_bytes, AES.block_size): AES encryption works on blocks of data. If the image data size isn’t a perfect multiple of the block size, it needs padding. The pad function adds extra bytes to make the data fit the block size.
  • encrypted_data = iv + cipher.encrypt(padded_data): Finally, the encrypt method of the cipher object encrypts the padded image data.
  • with open(output_image_path, 'wb') as f: The encrypted data (including the IV at the beginning) is written to a new image file specified by output_image_path.
  • with open(iv_path, 'wb') as f: The unique IV is also saved in a separate file with a filename based on the original image file for decryption purposes.

Output

Normally, the encrypted image (‘encrypted_image.jpg’) is not visible.

decrypt.py

Now, let’s define the ‘decrypt.py’ script, which reverses the encryption process to recover the original image.

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
from Crypto.Util import Counter
from PIL import Image
import io
import os
from Crypto.Hash import SHA256, HMAC, SHA1
def decrypt_image(input_image_path, output_image_path, key, iv_path):
  # Read the IV from the separate file
  with open(iv_path, 'rb') as f:
    unique_iv = f.read()
  # Read the encrypted data
  with open(input_image_path, 'rb') as f:
    encrypted_data = f.read()
  # Separate the IV from the encrypted data
  iv = encrypted_data[:AES.block_size]
  encrypted_data = encrypted_data[AES.block_size:]
  # Initialize AES cipher
  cipher = AES.new(key, AES.MODE_CBC, unique_iv)
  # Decrypt the image data
  decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)
  # Convert decrypted bytes to image
  decrypted_image = Image.open(io.BytesIO(decrypted_data))
  # Save the decrypted image
  decrypted_image.save(output_image_path, format=decrypted_image.format)
  print(f"Decryption successful. Decrypted image saved to '{output_image_path}'.")
# Example usage
if __name__ == "__main__":
  input_image_path = "encrypted_image.jpg"
  iv_file = 'original.jpg.iv'
  key = 'UZ4i59vPgLRT16s8FZ4i81vPgLRT16qk'
  key = bytes(key, encoding="utf-8")
  decrypt_image(input_image_path, 'decrypted_image.jpg', key, iv_file)

Explanation

The decrypt_image function takes an encrypted image, its corresponding IV file, and a decryption key, and reverses the encryption process to retrieve the original image. Here’s a breakdown of the key lines:

with open(iv_path, 'rb') as f:
    unique_iv = f.read()
  • This line reads the Initialization Vector (IV) from a separate file specified by ‘iv_path’. Remember, the IV was saved alongside the encrypted image during encryption.
  • It opens the file in binary reading mode (‘rb’) and reads the entire content into the ‘unique_iv’ variable.
  with open(input_image_path, 'rb') as f:
    encrypted_data = f.read()
  • Similar to the IV retrieval, this line reads the encrypted image data from the file specified by ‘input_image_path’.
  • It opens the file in binary reading mode (‘rb’) and stores the entire content in the ‘encrypted_data’ variable.
 iv = encrypted_data[:AES.block_size]
 encrypted_data = encrypted_data[AES.block_size:]
  • Since the encrypted data includes the IV prepended at the beginning, this section carefully separates them.
  • It extracts the first AES.block_size bytes (typically 16 bytes) as the iv and assigns the remaining bytes to the encrypted_data variable.
 cipher = AES.new(key, AES.MODE_CBC, unique_iv)

Here, the decryption process is set up. A new AES cipher object is created using the ‘key’, the encryption mode (CBC – Cipher Block Chaining, which must match the encryption mode used), and the retrieved ‘unique_iv’.

 decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)
  • The core decryption happens here. The decrypt method of the cipher object is used with the ‘encrypted_data’ to retrieve the original image data in its encrypted format.
  • Remember the padding added during encryption to ensure a perfect block size? This part removes those extra bytes using the unpad function with the AES block size.
decrypted_image = Image.open(io.BytesIO(decrypted_data))
  • The decrypted data is now a byte stream. To view the image, it needs to be converted back to an image format.
  • This section uses io.BytesIO to create a temporary in-memory file-like object from the ‘decrypted_data’ bytes.
  • Finally, the Image.open function from PIL opens this temporary buffer as the decrypted image (‘decrypted_image’).
decrypted_image.save(output_image_path, format=decrypted_image.format)

The decrypted image is saved as a new image file specified by ‘output_image_path’. The decrypted_image object itself automatically determines the format of the image.

Output

‘decrypted_image.jpg’

Summary

In this article, we learned image encryption in Python using the AES-256 encryption. We explored how the AES algorithm scrambles the data within an image and makes it unreadable without the secret key (256-bit key) along with the Initialization Vector (IV) file. This extra layer of security ensures the confidentiality of your private images even if they are shared or stored electronically.

Remember, it’s not possible to decrypt the encrypted image using the AES algorithm in the provided program without having the secret key and the IV file. So, always keep your secret key and iv file safe.

Recommended Article: Create an Image Encryption App in Python with AES-256 Encryption

References

What Is the Advanced Encryption Standard (AES)?

Everything You Need to Know About AES-256 Encryption

Share your love
Subhankar Rakshit
Subhankar Rakshit

Hey there! I’m Subhankar Rakshit, the brains behind PySeek. I’m a Post Graduate in Computer Science. PySeek is where I channel my love for Python programming and share it with the world through engaging and informative blogs.

Articles: 144

Leave a Reply

Your email address will not be published. Required fields are marked *