Seguridad y Criptografía en Python: hashlib, secrets y cryptography
La seguridad es un requisito, no una opción. Python ofrece módulos estándar para las operaciones más comunes y la biblioteca cryptography para primitivas criptográficas modernas y auditadas.
hashlib — funciones de hash
import hashlib
mensaje = b"Hola mundo"
# Hashes comunes
sha256 = hashlib.sha256(mensaje).hexdigest()
sha512 = hashlib.sha512(mensaje).hexdigest()
md5 = hashlib.md5(mensaje).hexdigest() # NO usar para seguridad
print(f"SHA-256: {sha256}")
print(f"SHA-512: {sha512[:32]}...")
# Hash de archivo (streaming — no carga todo en RAM)
def hash_archivo(ruta: str, algoritmo: str = 'sha256') -> str:
h = hashlib.new(algoritmo)
with open(ruta, 'rb') as f:
while chunk := f.read(65536): # leer en bloques de 64 KB
h.update(chunk)
return h.hexdigest()
digest = hash_archivo('documento.pdf')
print(f"SHA-256 del archivo: {digest}")
# HMAC — autenticación de mensajes con clave
import hmac
clave = b'mi-clave-secreta'
firma = hmac.new(clave, mensaje, hashlib.sha256).hexdigest()
print(f"HMAC-SHA256: {firma}")
# Verificar HMAC de forma segura (timing-safe)
firma_recibida = firma
valida = hmac.compare_digest(
hmac.new(clave, mensaje, hashlib.sha256).digest(),
bytes.fromhex(firma_recibida)
)
print(f"HMAC válido: {valida}")
secrets — generación de tokens seguros
import secrets
import string
# Token de URL (para restablecimiento de contraseña, magic links…)
token_url = secrets.token_urlsafe(32) # 43 chars, base64url
token_hex = secrets.token_hex(32) # 64 chars hexadecimal
token_bytes = secrets.token_bytes(32) # 32 bytes crudos
print(f"URL token: {token_url}")
print(f"Hex token: {token_hex}")
# Contraseña aleatoria segura
alfabeto = string.ascii_letters + string.digits + '!@#$%^&*'
contrasena = ''.join(secrets.choice(alfabeto) for _ in range(20))
print(f"Contraseña: {contrasena}")
# Comparación de tokens a tiempo constante (evita timing attacks)
token_esperado = secrets.token_urlsafe(32)
token_recibido = token_esperado # simular coincidencia
es_valido = secrets.compare_digest(token_esperado, token_recibido)
print(f"Token válido: {es_valido}")
# API key: prefijo + token (facilita revocar y detectar en logs)
def generar_api_key(prefijo: str = 'kj') -> str:
return f"{prefijo}_{secrets.token_urlsafe(32)}"
api_key = generar_api_key()
print(f"API Key: {api_key}")
bcrypt — hash de contraseñas
pip install bcrypt
import bcrypt
def hashear_contrasena(contrasena: str) -> str:
"""Hash seguro con sal aleatoria incorporada (cost factor = 12)."""
sal = bcrypt.gensalt(rounds=12) # rounds: 10-14 son seguros; 12 ≈ 250 ms
hash_bytes = bcrypt.hashpw(contrasena.encode('utf-8'), sal)
return hash_bytes.decode('utf-8')
def verificar_contrasena(contrasena: str, hash_guardado: str) -> bool:
"""Verificación timing-safe — siempre tarda lo mismo."""
return bcrypt.checkpw(
contrasena.encode('utf-8'),
hash_guardado.encode('utf-8')
)
# Uso típico en registro/login
hash_guardado = hashear_contrasena("mi_contrasena_segura")
print(f"Hash: {hash_guardado}")
ok = verificar_contrasena("mi_contrasena_segura", hash_guardado)
ko = verificar_contrasena("contrasena_incorrecta", hash_guardado)
print(f"Correcta: {ok}, Incorrecta: {ko}")
# Nota: bcrypt trunca contraseñas > 72 bytes — usar pre-hash para contraseñas largas
import hashlib, base64
def hashear_largo(contrasena: str) -> str:
"""Soporta contraseñas de longitud arbitraria."""
pre = base64.b64encode(hashlib.sha256(contrasena.encode()).digest())
return bcrypt.hashpw(pre, bcrypt.gensalt(12)).decode()
cryptography — primitivas modernas
pip install cryptography
Cifrado simétrico (Fernet — AES-128-CBC + HMAC-SHA256)
from cryptography.fernet import Fernet, MultiFernet
# Generar clave (guardar en lugar seguro — sin ella no hay descifrado)
clave = Fernet.generate_key()
f = Fernet(clave)
# Cifrar
mensaje = b"Datos confidenciales"
cifrado = f.encrypt(mensaje)
print(f"Cifrado: {cifrado[:40]}...")
# Descifrar
descifrado = f.decrypt(cifrado)
print(f"Descifrado: {descifrado}")
# Con TTL — el token expira automáticamente
import time
cifrado_ttl = f.encrypt(mensaje)
time.sleep(1)
try:
f.decrypt(cifrado_ttl, ttl=10) # válido durante 10 segundos
print("Token válido")
except Exception:
print("Token expirado o inválido")
# Rotación de claves (MultiFernet)
clave_nueva = Fernet.generate_key()
clave_vieja = clave
mf = MultiFernet([Fernet(clave_nueva), Fernet(clave_vieja)])
# Descifra con cualquiera, cifra siempre con la primera (nueva)
rotado = mf.rotate(cifrado)
Cifrado asimétrico (RSA)
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
# Generar par de claves RSA-2048
clave_privada = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
clave_publica = clave_privada.public_key()
# Serializar (guardar en disco)
pem_privada = clave_privada.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.BestAvailableEncryption(b'passphrase'),
)
pem_publica = clave_publica.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
# Cifrar con clave pública (solo la privada puede descifrar)
mensaje = b"Secreto para el destinatario"
cifrado_rsa = clave_publica.encrypt(
mensaje,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
)
)
# Descifrar con clave privada
descifrado = clave_privada.decrypt(
cifrado_rsa,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
)
)
print(f"Descifrado: {descifrado}")
Firmas digitales
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
# Curva elíptica P-256 (más eficiente que RSA para firmas)
clave_privada = ec.generate_private_key(ec.SECP256R1())
clave_publica = clave_privada.public_key()
mensaje = b"Mensaje a firmar"
# Firmar
firma = clave_privada.sign(mensaje, ec.ECDSA(hashes.SHA256()))
print(f"Firma ({len(firma)} bytes): {firma.hex()[:32]}...")
# Verificar
try:
clave_publica.verify(firma, mensaje, ec.ECDSA(hashes.SHA256()))
print("Firma válida")
except InvalidSignature:
print("Firma INVÁLIDA — mensaje manipulado o clave incorrecta")
JWT — JSON Web Tokens
pip install PyJWT
import jwt
from datetime import datetime, timedelta, timezone
CLAVE_SECRETA = "mi-super-clave-jwt-256-bits"
ALGORITMO = "HS256"
def crear_token(user_id: int, roles: list[str]) -> str:
"""Crea un JWT con expiración de 1 hora."""
ahora = datetime.now(timezone.utc)
payload = {
'sub': str(user_id), # subject
'iat': ahora, # issued at
'exp': ahora + timedelta(hours=1),
'roles': roles,
}
return jwt.encode(payload, CLAVE_SECRETA, algorithm=ALGORITMO)
def verificar_token(token: str) -> dict:
"""Decodifica y verifica un JWT. Lanza excepción si es inválido."""
try:
payload = jwt.decode(
token,
CLAVE_SECRETA,
algorithms=[ALGORITMO],
options={'require': ['exp', 'sub']},
)
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token expirado")
except jwt.InvalidTokenError as e:
raise ValueError(f"Token inválido: {e}")
# Ejemplo de uso
token = crear_token(user_id=42, roles=['admin', 'user'])
print(f"Token: {token[:40]}...")
payload = verificar_token(token)
print(f"User ID: {payload['sub']}, Roles: {payload['roles']}")
TOTP — autenticación de dos factores (2FA)
pip install pyotp qrcode[pil]
import pyotp
import qrcode
from io import BytesIO
def configurar_2fa(email: str) -> dict:
"""Genera secreto TOTP y QR para Google Authenticator / Authy."""
secreto = pyotp.random_base32() # guardar en BD (cifrado)
totp = pyotp.TOTP(secreto)
# URI para el QR (formato otpauth://)
uri = totp.provisioning_uri(
name=email,
issuer_name='MiApp'
)
# Generar imagen QR
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 {'secreto': secreto, 'qr_png': buf.getvalue(), 'uri': uri}
def verificar_2fa(secreto: str, codigo: str) -> bool:
"""Verifica el código TOTP de 6 dígitos. Ventana de ±30 segundos."""
totp = pyotp.TOTP(secreto)
return totp.verify(codigo, valid_window=1) # ±1 intervalo de 30s
# Uso
datos = configurar_2fa('usuario@ejemplo.com')
secreto = datos['secreto']
# El usuario escanea el QR con su app autenticadora
# Verificar código (el usuario ingresa el de 6 dígitos)
codigo_usuario = pyotp.TOTP(secreto).now() # simular código actual
ok = verificar_2fa(secreto, codigo_usuario)
print(f"2FA válido: {ok}")
Prácticas seguras de almacenamiento
# ✅ CORRECTO — hash con bcrypt para contraseñas
from bcrypt import hashpw, gensalt, checkpw
hash_guardado = hashpw(contrasena.encode(), gensalt(12))
# Guardar hash_guardado en BD, nunca la contraseña en texto plano
# ✅ CORRECTO — Fernet para datos sensibles reversibles
from cryptography.fernet import Fernet
# Clave desde variable de entorno, nunca hardcodeada
import os
clave = os.environ['ENCRYPTION_KEY'].encode()
f = Fernet(clave)
numero_tarjeta_cifrado = f.encrypt(b'4111111111111111')
# Guardar numero_tarjeta_cifrado en BD
# ❌ INCORRECTO
# hashlib.md5(contrasena.encode()).hexdigest() # MD5 es rompible
# hashlib.sha256(contrasena.encode()).hexdigest() # Sin sal — vulnerable a rainbow tables
# contrasena_en_texto_plano = contrasena # NUNCA
# ✅ Validar entradas antes de procesarlas
import re
def validar_email_seguro(email: str) -> bool:
# Limitar longitud para prevenir DoS con regex
if len(email) > 254:
return False
patron = re.compile(r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$')
return bool(patron.match(email))
Buenas prácticas
- Usa
secretsen lugar derandompara cualquier cosa relacionada con seguridad —randomno es criptográficamente seguro. - bcrypt/argon2 para contraseñas, nunca SHA-256 ni MD5 directamente — sin sal son vulnerables a tablas arcoíris (rainbow tables).
hmac.compare_digestysecrets.compare_digestpara comparar tokens y hashes — previenen ataques de temporización (timing attacks).- La biblioteca
cryptographysobrepycrypto/pycryptodome— está activamente mantenida y auditada. - JWT: usa algoritmos asimétricos (RS256, ES256) en producción si múltiples servicios verifican los tokens; HS256 solo cuando el secreto está centralizado.
- Variables de entorno para secretos — nunca hardcodees claves, tokens o passwords en el código fuente.
Conversiones relacionadas
Conversiones frecuentes del catálogo: