Python Cryptography: Symmetric and Asymmetric Encryption
Python’s cryptography library is the standard tool for
secure data encryption, offering both high-level, safe interfaces for
everyday tasks and low-level primitives for complex security
requirements. This article examines how the library implements symmetric
encryption—where a single shared key handles both encryption and
decryption—and asymmetric encryption—which relies on mathematically
linked public and private key pairs for confidentiality and digital
signatures.
Symmetric Encryption Primitives
Symmetric encryption uses identical cryptographic keys for both
plaintext encryption and ciphertext decryption. The
cryptography library divides symmetric encryption into two
tiers: high-level "recipes" and low-level "Hazardous Materials"
(hazmat).
High-Level: Fernet
The primary symmetric abstraction is Fernet, located
under cryptography.fernet. Fernet provides authenticated
symmetric encryption built on top of standard primitives:
- AES-128 in CBC mode.
- PKCS7 padding for block alignment.
- HMAC-SHA256 for message integrity and authenticity.
- Timestamping to support token expiration.
Because it includes message authentication, ciphertext cannot be
tampered with or modified without triggering an
InvalidToken exception.
from cryptography.fernet import Fernet
# Key generation
key = Fernet.generate_key()
cipher = Fernet(key)
# Encryption and Decryption
token = cipher.encrypt(b"Confidential payload")
original_message = cipher.decrypt(token)Low-Level: The
hazmat Cipher Layer
For protocols requiring custom ciphers or modes,
cryptography.hazmat.primitives.ciphers exposes low-level
access to block and stream ciphers:
- Ciphers: AES, ChaCha20, 3DES, CAST5.
- Modes: GCM, CBC, CTR, XTS, CFB, OFB.
Modern secure designs favor Authenticated Encryption with Associated Data (AEAD) modes like AES-GCM or ChaCha20-Poly1305 over unauthenticated modes like CBC, eliminating the need to manually construct HMAC checks.
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, b"Authenticated data", b"associated metadata")
plaintext = aesgcm.decrypt(nonce, ciphertext, b"associated metadata")Asymmetric Encryption Primitives
Asymmetric cryptography uses key pairs: a public key for encryption
or signature verification, and a private key for decryption or signing.
The cryptography library provides asymmetric primitives
within cryptography.hazmat.primitives.asymmetric.
Key Generation and Algorithms
The library supports several primary asymmetric families:
- RSA: Configurable key sizes (typically 2048, 3072, or 4096 bits) and public exponents.
- Elliptic Curves (ECC): Standard curves including SECP256R1, SECP384R1, and SECP521R1.
- Edwards-curve Digital Signature Algorithm (EdDSA): Modern, high-speed curves such as Ed25519 and X25519 for key exchange.
Encryption and Decryption (RSA)
For public-key encryption, the library strictly mandates proper padding algorithms to prevent mathematical attacks. The standard choice is Optimal Asymmetric Encryption Padding (OAEP):
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
# Generate an RSA private/public key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# Encrypt with public key using OAEP
message = b"Secret asymmetric message"
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
# Decrypt with private key
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)Digital Signatures
Asymmetric keys also provide authentication and non-repudiation through digital signatures. RSA uses the Probabilistic Signature Scheme (PSS) or PKCS1v15, while ECC uses ECDSA:
signature = private_key.sign(
b"Message to sign",
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256(),
)
public_key.verify(
signature,
b"Message to sign",
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256(),
)Key Management and Serialization
To persist or transmit keys,
cryptography.hazmat.primitives.serialization converts keys
to standard formats:
- Encodings: PEM (Base64 ASCII) and DER (binary).
- Formats: PKCS#8 for private keys, SubjectPublicKeyInfo (X.509) for public keys.
- Encryption: Symmetric key-wrap mechanisms (e.g.,
BestAvailableEncryption) to password-protect serialized private keys.
This structural separation ensures that developers can safely handle storage, data transmission, and encryption operations without exposing raw key bytes.