Security and Cryptography in Python: hashlib, secrets and cryptography
Security is a requirement, not an option. Python ships with standard-library modules for the most common operations, and the cryptography library provides modern, audited cryptographic primitives.
hashlib — cryptographic hash functions
import hashlib
message = b"Hello world"
# Common algorithms
sha256 = hashlib.sha256(message).hexdigest()
sha512 = hashlib.sha512(message).hexdigest()
md5 = hashlib.md5(message).hexdigest() # DO NOT use for security
print(f"SHA-256: {sha256}")
# File hash — streaming, no full RAM load
def hash_file(path: str, algorithm: str = 'sha256') -> str:
h = hashlib.new(algorithm)
with open(path, 'rb') as f:
while chunk := f.read(65536): # 64 KB chunks
h.update(chunk)
return h.hexdigest()
digest = hash_file('document.pdf')
print(f"SHA-256 of file: {digest}")
# HMAC — message authentication with a shared key
import hmac
key = b'my-secret-key'
mac = hmac.new(key, message, hashlib.sha256).hexdigest()
print(f"HMAC-SHA256: {mac}")
# Verify HMAC in constant time (prevents timing attacks)
received_mac = mac
valid = hmac.compare_digest(
hmac.new(key, message, hashlib.sha256).digest(),
bytes.fromhex(received_mac)
)
print(f"HMAC valid: {valid}")
secrets — cryptographically secure random values
import secrets
import string
# URL-safe token (password resets, magic links…)
token_url = secrets.token_urlsafe(32) # 43 chars base64url
token_hex = secrets.token_hex(32) # 64 hex chars
token_bytes = secrets.token_bytes(32) # 32 raw bytes
print(f"URL token: {token_url}")
# Secure random password
alphabet = string.ascii_letters + string.digits + '!@#$%^&*'
password = ''.join(secrets.choice(alphabet) for _ in range(20))
print(f"Password: {password}")
# Constant-time token comparison (prevents timing attacks)
expected = secrets.token_urlsafe(32)
received = expected # simulating a match
is_valid = secrets.compare_digest(expected, received)
print(f"Token valid: {is_valid}")
# API key with prefix (easy to spot in logs and revoke)
def generate_api_key(prefix: str = 'kj') -> str:
return f"{prefix}_{secrets.token_urlsafe(32)}"
print(f"API Key: {generate_api_key()}")
bcrypt — password hashing
pip install bcrypt
import bcrypt
def hash_password(password: str) -> str:
"""Secure hash with random salt built-in (cost factor 12)."""
salt = bcrypt.gensalt(rounds=12) # 10-14 safe; 12 ≈ 250 ms
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
def verify_password(password: str, stored_hash: str) -> bool:
"""Timing-safe verification — always takes the same time."""
return bcrypt.checkpw(
password.encode('utf-8'),
stored_hash.encode('utf-8')
)
stored = hash_password("my_secure_password")
print(f"Hash: {stored}")
print(f"Correct: {verify_password('my_secure_password', stored)}")
print(f"Wrong: {verify_password('wrong_password', stored)}")
# Note: bcrypt truncates passwords > 72 bytes — pre-hash for long passwords
import hashlib, base64
def hash_long_password(password: str) -> str:
pre = base64.b64encode(hashlib.sha256(password.encode()).digest())
return bcrypt.hashpw(pre, bcrypt.gensalt(12)).decode()
cryptography — modern cryptographic primitives
pip install cryptography
Symmetric encryption (Fernet — AES-128-CBC + HMAC-SHA256)
from cryptography.fernet import Fernet, MultiFernet
# Generate a key (store securely — without it decryption is impossible)
key = Fernet.generate_key()
f = Fernet(key)
# Encrypt
message = b"Confidential data"
encrypted = f.encrypt(message)
print(f"Encrypted: {encrypted[:40]}...")
# Decrypt
decrypted = f.decrypt(encrypted)
print(f"Decrypted: {decrypted}")
# With TTL — token expires automatically
import time
token = f.encrypt(message)
time.sleep(1)
try:
f.decrypt(token, ttl=10) # valid for 10 seconds
print("Token still valid")
except Exception:
print("Token expired or invalid")
# Key rotation (MultiFernet)
new_key = Fernet.generate_key()
mf = MultiFernet([Fernet(new_key), Fernet(key)])
# Decrypts with either, always encrypts with the first (new) key
rotated = mf.rotate(encrypted)
Asymmetric encryption (RSA)
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
# Generate RSA-2048 key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# Serialize (save to disk)
pem_private = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.BestAvailableEncryption(b'passphrase'),
)
pem_public = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
# Encrypt with public key (only private key can decrypt)
message = b"Secret for the recipient"
encrypted = public_key.encrypt(
message,
padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)
# Decrypt with private key
decrypted = private_key.decrypt(
encrypted,
padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)
print(f"Decrypted: {decrypted}")
Digital signatures
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
# ECDSA P-256 (more efficient than RSA for signing)
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
message = b"Message to sign"
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
try:
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
print("Signature valid")
except InvalidSignature:
print("INVALID signature — message tampered or wrong key")
JWT — JSON Web Tokens
pip install PyJWT
import jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = "my-256-bit-jwt-secret-key"
ALGORITHM = "HS256"
def create_token(user_id: int, roles: list[str]) -> str:
now = datetime.now(timezone.utc)
payload = {
'sub': str(user_id),
'iat': now,
'exp': now + timedelta(hours=1),
'roles': roles,
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str) -> dict:
try:
return jwt.decode(
token, SECRET_KEY,
algorithms=[ALGORITHM],
options={'require': ['exp', 'sub']},
)
except jwt.ExpiredSignatureError:
raise ValueError("Token expired")
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {e}")
token = create_token(user_id=42, roles=['admin', 'user'])
payload = verify_token(token)
print(f"User ID: {payload['sub']}, Roles: {payload['roles']}")
TOTP — two-factor authentication (2FA)
pip install pyotp qrcode[pil]
import pyotp
import qrcode
from io import BytesIO
def setup_2fa(email: str) -> dict:
secret = pyotp.random_base32() # store encrypted in DB
totp = pyotp.TOTP(secret)
uri = totp.provisioning_uri(name=email, issuer_name='MyApp')
qr = qrcode.QRCode(box_size=10, border=4)
qr.add_data(uri)
qr.make(fit=True)
img = qr.make_image(fill_color='black', back_color='white')
buf = BytesIO()
img.save(buf, format='PNG')
return {'secret': secret, 'qr_png': buf.getvalue(), 'uri': uri}
def verify_2fa(secret: str, code: str) -> bool:
"""Verify a 6-digit TOTP code. Allows ±30 second window."""
return pyotp.TOTP(secret).verify(code, valid_window=1)
# Usage
data = setup_2fa('user@example.com')
secret = data['secret']
current_code = pyotp.TOTP(secret).now() # simulate user's app
print(f"2FA valid: {verify_2fa(secret, current_code)}")
Secure storage rules
# ✅ CORRECT — bcrypt for passwords
stored_hash = hash_password("user_password")
# ✅ CORRECT — Fernet for reversible sensitive data
import os
from cryptography.fernet import Fernet
key = os.environ['ENCRYPTION_KEY'].encode() # from env var, never hardcoded
f = Fernet(key)
encrypted_card = f.encrypt(b'4111111111111111')
# ❌ WRONG — these are all insecure
# hashlib.md5(password.encode()).hexdigest() # MD5 is broken
# hashlib.sha256(password.encode()).hexdigest() # no salt → rainbow table attack
# password_plaintext = password # NEVER store plaintext
# ✅ Input validation to prevent regex DoS
import re
def validate_email(email: str) -> bool:
if len(email) > 254: # RFC 5321 max
return False
pattern = re.compile(r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$')
return bool(pattern.match(email))
Best practices
- Use
secretsinstead ofrandomfor anything security-related —randomis not cryptographically secure. - bcrypt/argon2 for passwords, never raw SHA-256 or MD5 — without salt they are vulnerable to rainbow table attacks.
hmac.compare_digestandsecrets.compare_digestto compare tokens and hashes — prevents timing attacks.- The
cryptographylibrary overpycrypto/pycryptodome— actively maintained and audited by security professionals. - JWT: use asymmetric algorithms (RS256, ES256) in production when multiple services verify tokens; HS256 only when the secret is centralized.
- Environment variables for secrets — never hardcode keys, tokens, or passwords in source code or version control.
Related conversions
Frequent conversions across the catalogue: