Encrypt and Decrypt Data with AES-GCM in Python
Encrypt sensitive data using AES-GCM with the cryptography library. Covers key derivation, nonce generation, authenticated encryption, and file encryption.
Overview
AES-GCM (Advanced Encryption Standard with Galois/Counter Mode) provides authenticated encryption: it encrypts data and verifies integrity in a single operation. Unlike AES-CBC, GCM does not require padding and detects tampering. This recipe uses the cryptography library to encrypt strings, files, and binary data with AES-256-GCM.
When to Use
- You need to encrypt sensitive data at rest (database fields, files, config secrets)
- You need authenticated encryption (confidentiality + integrity)
- You store user passwords or API keys in an encrypted form
- You encrypt files before uploading to cloud storage
Solution
Install the cryptography library
pip install cryptography
Basic AES-256-GCM encrypt and decrypt
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def generate_key() -> bytes:
"""Generate a random 256-bit AES key."""
return AESGCM.generate_key(bit_length=256)
def encrypt(key: bytes, plaintext: bytes, associated_data: bytes = b"") -> tuple[bytes, bytes, bytes]:
"""Encrypt plaintext with AES-GCM. Returns (nonce, ciphertext, tag is embedded)."""
nonce = os.urandom(12) # 96-bit nonce
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
return nonce, ciphertext
def decrypt(key: bytes, nonce: bytes, ciphertext: bytes, associated_data: bytes = b"") -> bytes:
"""Decrypt ciphertext with AES-GCM. Raises InvalidTag if tampered."""
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, associated_data)
# Usage
key = generate_key()
nonce, ciphertext = encrypt(key, b"Sensitive data here")
decrypted = decrypt(key, nonce, ciphertext)
print(decrypted.decode()) # "Sensitive data here"
Key derivation from a password (PBKDF2)
import os
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def derive_key(password: str, salt: bytes = None) -> tuple[bytes, bytes]:
"""Derive a 256-bit key from a password using PBKDF2."""
if salt is None:
salt = os.urandom(16)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=600_000,
)
key = kdf.derive(password.encode())
return key, salt
def encrypt_with_password(password: str, plaintext: bytes) -> dict:
"""Encrypt data using a password-derived key."""
key, salt = derive_key(password)
nonce, ciphertext = encrypt(key, plaintext)
return {
"salt": base64.b64encode(salt).decode(),
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(ciphertext).decode(),
}
def decrypt_with_password(password: str, data: dict) -> bytes:
"""Decrypt data using a password-derived key."""
salt = base64.b64decode(data["salt"])
key, _ = derive_key(password, salt)
nonce = base64.b64decode(data["nonce"])
ciphertext = base64.b64decode(data["ciphertext"])
return decrypt(key, nonce, ciphertext)
# Usage
encrypted = encrypt_with_password("my-password", b"Secret message")
decrypted = decrypt_with_password("my-password", encrypted)
print(decrypted.decode()) # "Secret message"
File encryption
import os
import struct
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def encrypt_file(key: bytes, input_path: str, output_path: str) -> None:
"""Encrypt a file with AES-GCM. Format: [nonce(12)][ciphertext]."""
nonce = os.urandom(12)
aesgcm = AESGCM(key)
with open(input_path, "rb") as f:
plaintext = f.read()
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
with open(output_path, "wb") as f:
f.write(nonce)
f.write(ciphertext)
def decrypt_file(key: bytes, input_path: str, output_path: str) -> None:
"""Decrypt a file encrypted with encrypt_file."""
with open(input_path, "rb") as f:
nonce = f.read(12)
ciphertext = f.read()
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
with open(output_path, "wb") as f:
f.write(plaintext)
# Usage
key = AESGCM.generate_key(bit_length=256)
encrypt_file(key, "document.pdf", "document.pdf.enc")
decrypt_file(key, "document.pdf.enc", "document_decrypted.pdf")
Large file encryption in chunks
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def encrypt_file_stream(key: bytes, input_path: str, output_path: str, chunk_size: int = 64 * 1024) -> None:
"""Encrypt a large file in chunks using separate nonces per chunk."""
aesgcm = AESGCM(key)
with open(input_path, "rb") as fin, open(output_path, "wb") as fout:
chunk_index = 0
while True:
chunk = fin.read(chunk_size)
if not chunk:
break
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, chunk, str(chunk_index).encode())
fout.write(struct.pack("<I", len(nonce) + len(ciphertext)))
fout.write(nonce)
fout.write(ciphertext)
chunk_index += 1
def decrypt_file_stream(key: bytes, input_path: str, output_path: str) -> None:
"""Decrypt a file encrypted with encrypt_file_stream."""
aesgcm = AESGCM(key)
import struct
with open(input_path, "rb") as fin, open(output_path, "wb") as fout:
chunk_index = 0
while True:
size_bytes = fin.read(4)
if not size_bytes or len(size_bytes) < 4:
break
size = struct.unpack("<I", size_bytes)[0]
data = fin.read(size)
nonce = data[:12]
ciphertext = data[12:]
plaintext = aesgcm.decrypt(nonce, ciphertext, str(chunk_index).encode())
fout.write(plaintext)
chunk_index += 1
Associated data (AAD) for context binding
# Bind ciphertext to a specific context (user ID, record ID)
key = AESGCM.generate_key(bit_length=256)
user_id = "user-12345"
associated_data = user_id.encode()
nonce, ciphertext = encrypt(key, b"SSN: 123-45-6789", associated_data)
# Decrypt with correct context — works
decrypted = decrypt(key, nonce, ciphertext, associated_data)
# Decrypt with wrong context — raises InvalidTag
try:
decrypt(key, nonce, ciphertext, b"user-99999")
except Exception as e:
print(f"Decryption failed: {e}") # Tampering detected
Explanation
AES-GCM combines AES counter mode encryption with Galois Mode authentication. It provides:
- Confidentiality: Data is encrypted with a symmetric key.
- Integrity: A GCM tag verifies the ciphertext was not modified.
- Authenticated encryption: Both confidentiality and integrity in one operation.
Key concepts:
- Key: 128, 192, or 256 bits. Use 256 bits for maximum security. Never hardcode keys — derive from passwords or load from a secret manager.
- Nonce: A 96-bit value that must be unique for each encryption with the same key. Use
os.urandom(12)for random nonces. Reusing a nonce with the same key breaks security entirely. - Associated Data (AAD): Optional data that is authenticated but not encrypted. Use it to bind ciphertext to context (user IDs, record IDs) to prevent ciphertext swapping attacks.
- PBKDF2: Key derivation function that converts a password into a cryptographic key. Uses a salt and many iterations to slow down brute-force attacks. Use 600,000+ iterations with SHA-256.
- InvalidTag: Exception raised when decryption fails due to wrong key, tampered ciphertext, or mismatched AAD.
Variants
| Method | Auth | Padding | Use When |
|---|---|---|---|
| AES-GCM | Yes | None | Default choice, authenticated encryption |
| AES-CBC + HMAC | Yes | PKCS7 | Legacy systems, no GCM support |
| AES-CBC | No | PKCS7 | Not recommended (no integrity) |
| ChaCha20-Poly1305 | Yes | None | Alternative to AES-GCM, faster in software |
Guidelines
- Always use AES-GCM or ChaCha20-Poly1305. Never use AES-CBC without a separate MAC.
- Generate a fresh random nonce for every encryption. Never reuse a nonce with the same key.
- Use 256-bit keys for new systems. 128-bit is acceptable but less adaptable.
- Derive keys from passwords with PBKDF2 (600,000+ iterations) or Argon2.
- Store the salt and nonce alongside the ciphertext. They are not secrets.
- Use AAD to bind ciphertext to context and prevent ciphertext swapping.
- Never hardcode encryption keys in source code. Load from environment or secret manager.
- Use
os.urandom()for cryptographic randomness. Never userandom.random(). - For large files, encrypt in chunks with a unique nonce per chunk.
Common Mistakes
- Reusing a nonce with the same key. This completely breaks GCM security.
- Using
random.random()instead ofos.urandom(). Therandommodule is not cryptographically secure. - Hardcoding keys in source code. Keys in Git history are permanently compromised.
- Not authenticating data with AAD. Without AAD, an attacker can swap ciphertexts between records.
- Using too few PBKDF2 iterations. 1,000 iterations is trivially brute-forced. Use 600,000+.
- Storing the key and ciphertext together. If an attacker gets the file, they get the key too.
- Not handling
InvalidTagexceptions. A failed decryption should not crash the app silently.
Additional Common Mistakes
- Using a deterministic nonce. Some implementations derive the nonce from the plaintext or a counter without ensuring uniqueness across processes. Always use
os.urandom(12)for random nonces:
# Bad: deterministic nonce
nonce = hashlib.sha256(plaintext).digest()[:12]
# Good: random nonce
nonce = os.urandom(12)
- Ignoring
InvalidTagexceptions. A failed decryption indicates tampering or wrong key. Handle it explicitly instead of letting it crash:
from cryptography.exceptions import InvalidTag
try:
plaintext = aesgcm.decrypt(nonce, ciphertext, aad)
except InvalidTag:
# Log the incident, return error, do NOT return partial data
logger.warning("Decryption failed: possible tampering detected")
raise ValueError("Data integrity check failed") Frequently Asked Questions
What is the difference between AES-GCM and AES-CBC?
AES-CBC only provides confidentiality (encryption). AES-GCM provides both confidentiality and integrity (authenticated encryption). GCM also does not require padding. Always prefer GCM over CBC.
How do I store the encrypted data?
Store the salt, nonce, and ciphertext together. They are not secrets. Only the key is secret. A common format is JSON with base64-encoded fields, or a binary file with [salt][nonce][ciphertext].
Is AES-256 better than AES-128?
AES-256 provides a larger key space but is slightly slower. For most applications, AES-128 is sufficient. Use AES-256 if you want a security margin against future quantum attacks or if compliance requires it.
Can I use the same key for multiple encryptions?
Yes, as long as each encryption uses a unique nonce. Generate a random 12-byte nonce for each operation. Never reuse a nonce with the same key.
Related Resources
Docker Secrets Management Without Hardcoding Credentials
Inject secrets into containers using Docker secrets, env files, and external secret managers without hardcoding them in images.
RecipeDocker Network Isolation and Inter-Container Security
Secure inter-container communication with custom Docker networks, network segmentation, and access control policies.
GuideWebhook Security — Delivery, Verification, and Protection
A practical guide to securing webhooks: signature verification, replay attack prevention, payload encryption, and endpoint hardening for reliable delivery.
GuideEncryption at Rest: AES-256, KMS, Envelope Encryption
Master encryption at rest: AES-256-GCM, key management services, envelope encryption, key rotation, database encryption, field-level encryption, and production security patterns.
GuideSecrets Management: Vault, Cloud Managers
A practical guide to secrets management: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, and GCP Secret Manager with rotation, access control, and CI/CD integration.
RecipeScan Docker Images for CVEs with Trivy and Grype
Scan Docker images for vulnerabilities before deployment using Trivy and Grype. Covers CI integration, severity filtering, SBOM, and remediation.