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:

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:

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:

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:

This structural separation ensures that developers can safely handle storage, data transmission, and encryption operations without exposing raw key bytes.