Back to Blog
Python

Python Cryptography: AES Symmetric Encryption

Implement AES symmetric encryption in Python with the cryptography library: key generation, CBC and GCM modes, padding, authentication, and key management.

PythonAESsymmetric encryptioncryptographyFernetsecurity
Illustration of AES symmetric encryption in Python showing a key and encrypted data.

AES (Advanced Encryption Standard) is a symmetric block cipher widely used to encrypt data. In symmetric encryption, the same key encrypts and decrypts data. Python's cryptography library exposes AES through its hazmat primitives, giving you control over encryption modes and parameters. This article shows how to generate AES keys, encrypt and decrypt data in CBC and GCM modes, and handle the security considerations that matter in production.

Setting Up the cryptography Library

Install the library with pip:

pip install cryptography

The cryptography library provides both a high-level API (like Fernet) and low-level primitives. For AES, you will typically use the hazmat layer, which requires careful handling of keys, IVs, and padding. The high-level Fernet class is easier for many use cases, but raw AES gives you control over the mode and parameters.

Generating and Handling AES Keys

AES supports key sizes of 128, 192, and 256 bits. For most applications, AES-256 is recommended. Generate a secure random key with os.urandom:

import os key = os.urandom(32) # 256-bit key

The key must be kept secret. Store it in a secure location such as a secrets manager, a key management service, or an environment variable used only by the running application. Never hardcode keys in source code. For production, consider using a dedicated key management system to handle rotation and access control.

Encrypting Data with AES in CBC Mode

Cipher Block Chaining (CBC) is a common mode that requires an initialization vector (IV) and padding. The IV must be unique for each encryption operation and is typically stored alongside the ciphertext. Use PKCS7 padding to make the plaintext a multiple of the block size (16 bytes for AES).

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding import os def encrypt_cbc(key, plaintext): iv = os.urandom(16) padder = padding.PKCS7(128).padder() padded_data = padder.update(plaintext) + padder.finalize() cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) encryptor = cipher.encryptor() ciphertext = encryptor.update(padded_data) + encryptor.finalize() return iv + ciphertext # prepend IV for storage

The IV is not secret, but it must be unpredictable and unique per key. Reusing an IV with the same key undermines confidentiality. The ciphertext alone is not authenticated; an attacker can modify it without detection. For authenticated encryption, use GCM mode instead.

Decrypting Data and Handling Padding

Decryption reverses the process: split the IV from the ciphertext, decrypt, then remove padding.

def decrypt_cbc(key, data): iv = data[:16] ciphertext = data[16:] cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) decryptor = cipher.decryptor() padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize() unpadder = padding.PKCS7(128).unpadder() plaintext = unpadder.update(padded_plaintext) + unpadder.finalize() return plaintext

If the key or IV is incorrect, decryption will produce garbage or raise a padding error. Do not catch padding errors and continue; they can indicate tampering. For data integrity, use an authenticated mode like GCM.

Using Authenticated Encryption with AES-GCM

Galois/Counter Mode (GCM) provides both confidentiality and authenticity. It produces a tag that verifies the ciphertext has not been altered. GCM does not require padding because it is a counter-based mode that can handle arbitrary plaintext lengths.

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes import os def encrypt_gcm(key, plaintext, associated_data=None): nonce = os.urandom(12) # 96-bit nonce recommended cipher = Cipher(algorithms.AES(key), modes.GCM(nonce)) encryptor = cipher.encryptor() if associated_data is not None: encryptor.authenticate_additional_data(associated_data) ciphertext = encryptor.update(plaintext) + encryptor.finalize() return nonce + ciphertext + encryptor.tag def decrypt_gcm(key, data, associated_data=None): nonce = data[:12] tag = data[-16:] ciphertext = data[12:-16] cipher = Cipher(algorithms.AES(key), modes.GCM(nonce, tag)) decryptor = cipher.decryptor() if associated_data is not None: decryptor.authenticate_additional_data(associated_data) plaintext = decryptor.update(ciphertext) + decryptor.finalize() return plaintext

The nonce must be unique for every encryption with the same key. GCM is preferred over CBC for most new applications because it provides authentication without a separate HMAC step. The cryptography library also offers the high-level Fernet class, which uses AES in CBC mode with HMAC authentication, but GCM gives you more control over nonce and tag handling.

Key Management and Operational Security

The security of AES encryption depends entirely on key secrecy. Use a key management service or a hardware security module for production workloads. Rotate keys regularly and enforce access controls. When storing encrypted data, always store the IV or nonce and, for GCM, the tag alongside the ciphertext. These values are not secret but are required for decryption.

Fernet has a simple API: you generate a key with Fernet.generate_key(), and it handles padding and appends an HMAC for integrity. However, Fernet uses AES-128-CBC, which may be less appropriate if you require AES-256.

Common Mistakes and How to Avoid Them

One of the most frequent errors is reusing an IV or nonce. With CBC, a reused IV leaks information about the first block. With GCM, reusing a nonce can completely break confidentiality. Always generate a fresh IV/nonce for each encryption.

Another mistake is using ECB mode, which does not hide patterns in the plaintext. Never use ECB for real data.

Failing to authenticate ciphertext is also common. CBC mode without an HMAC allows an attacker to modify ciphertext, potentially altering the decrypted plaintext. Use GCM or add an HMAC separately.

Finally, do not roll your own cryptographic primitives. Rely on the cryptography library's well-tested implementations.

Python Cryptography: AES Symmetric Encryption in Practice | RYUSLOG DEV