El módulo hashlib te da acceso a una familia de funciones hash criptográficas, pero su uso correcto depende completamente de para qué estás hasheando. La misma función que es perfecta para verificar la integridad de un archivo es una catástrofe cuando la usas para almacenar contraseñas. El malentendido entre estos dos casos de uso es uno de los errores de seguridad más comunes en Python.
SHA-256 es un algoritmo de hash de propósito general: determinista, rápido y diseñado para serlo. Esa velocidad es una virtud cuando verificas que un archivo de 500 MB no fue corrompido en tránsito — y un problema grave cuando alguien tiene una GPU y tu base de datos de hashes de contraseñas. Una RTX 4090 puede calcular ~10 mil millones de SHA-256 por segundo. Un atacante con tu dump de base de datos puede probar diccionarios enteros en minutos.
Para contraseñas, necesitas funciones de key derivation (KDF): algoritmos diseñados deliberadamente para ser lentos y costosos en memoria. hashlib.scrypt() está en la stdlib desde Python 3.6. Las librerías bcrypt y argon2-cffi son las opciones estándar de la industria cuando necesitas algo battle-tested con parámetros bien documentados. Todas tienen un factor de coste configurable: puedes hacer que la operación tarde 200ms en tu servidor, lo que convierte un ataque de diccionario de “minutos” a “siglos”.
El tercer frente es la cadena de dependencias. Typosquatting en PyPI es real y activo: paquetes como colourama (con ‘ou’) imitan a colorama para ejecutar código malicioso durante la instalación. pip install --require-hashes fuerza la verificación de hashes SHA-256 de cada paquete, haciendo imposible que un paquete sustituto pase inadvertido aunque tenga el mismo nombre.
# requirements.txt con hashes verificados se genera con:
# pip-compile --generate-hashes requirements.in
# o manualmente para dependencias directas.
import hashlib
import hmac
import os
from pathlib import Path
# ── 1. Integridad de archivos: SHA-256 es correcto aquí ──────────────────────
def file_sha256(path: Path) -> str:
"""Calcula SHA-256 de un archivo en bloques para no cargar todo en memoria."""
h = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(65_536), b""):
h.update(chunk)
return h.hexdigest()
def verify_file(path: Path, expected_hex: str) -> bool:
"""Comparación en tiempo constante para evitar timing attacks."""
actual = file_sha256(path)
# hmac.compare_digest previene ataques de timing aunque sea un hash público
return hmac.compare_digest(actual, expected_hex)
# ── 2. Hashing de contraseñas: scrypt desde la stdlib ────────────────────────
def hash_password_scrypt(password: str) -> bytes:
salt = os.urandom(16) # 128 bits de entropía, nunca reutilizar
# n=2^14, r=8, p=1 es el mínimo recomendado; súbelo en producción
# n=2^17 ó 2^18 si tu hardware lo permite
key = hashlib.scrypt(
password.encode(),
salt=salt,
n=2**14, # factor de CPU/memoria — debe ser potencia de 2
r=8, # tamaño de bloque interno
p=1, # paralelismo
dklen=32, # longitud del hash resultante
)
# Guardamos salt + hash juntos; necesitarás ambos para verificar
return salt + key
def verify_password_scrypt(password: str, stored: bytes) -> bool:
salt = stored[:16]
stored_key = stored[16:]
candidate = hashlib.scrypt(
password.encode(),
salt=salt,
n=2**14,
r=8,
p=1,
dklen=32,
)
return hmac.compare_digest(candidate, stored_key)
# ── 3. Hashing con argon2-cffi (pip install argon2-cffi) ─────────────────────
# Argon2id es el ganador de la Password Hashing Competition (2015)
# y la recomendación actual de OWASP.
try:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
# Parámetros siguiendo las recomendaciones OWASP 2023:
# time_cost=2 iteraciones, memory_cost=19MB, parallelism=1
ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
def hash_password_argon2(password: str) -> str:
return ph.hash(password) # salt embebido automáticamente en el string
def verify_password_argon2(password: str, hashed: str) -> bool:
try:
ph.verify(hashed, password)
# rehash automático si los parámetros han cambiado
if ph.check_needs_rehash(hashed):
return True # señal para actualizar el hash en BD
return True
except VerifyMismatchError:
return False
except ImportError:
pass # fallback a scrypt si argon2-cffi no está disponible
# ── 4. Demostración de integridad de archivo ──────────────────────────────────
if __name__ == "__main__":
import tempfile
with tempfile.NamedTemporaryFile(delete=False, suffix=".bin") as tmp:
tmp.write(b"payload de prueba" * 1000)
tmp_path = Path(tmp.name)
digest = file_sha256(tmp_path)
print(f"SHA-256: {digest}")
print(f"Verificación OK: {verify_file(tmp_path, digest)}")
# Simula corrupción
tmp_path.write_bytes(b"datos alterados")
print(f"Tras corrupción: {verify_file(tmp_path, digest)}") # False
# Hashing de contraseña
stored = hash_password_scrypt("mi_contraseña_segura")
print(f"Verificación correcta: {verify_password_scrypt('mi_contraseña_segura', stored)}")
print(f"Verificación incorrecta: {verify_password_scrypt('otra_cosa', stored)}")
Qué está pasando en cada decisión
iter(lambda: f.read(65_536), b"") es el patrón idiomático para leer archivos en bloques hasta EOF sin cargar el archivo completo en memoria. hashlib acepta múltiples llamadas a .update() — el estado interno del hash se acumula correctamente.
hmac.compare_digest merece atención especial. Aunque comparar hashes de archivos no suele ser crítico para timing attacks, el hábito de usar comparación en tiempo constante te salva cuando el contexto cambia. Python’s == en strings puede salir en cuanto encuentra el primer carácter diferente, filtrando información sobre cuántos bytes coinciden.
El patrón salt + key en hash_password_scrypt es mínimo pero funcional: los primeros 16 bytes son siempre el salt, el resto es el hash derivado. En producción querrías también versionar el algoritmo (un byte de versión al inicio) para poder migrar parámetros sin invalidar todos los hashes existentes.
En argon2-cffi, check_needs_rehash es elegante: cuando subes los parámetros de coste, puedes detectar en el login que el hash almacenado usa parámetros viejos y actualizarlo transparentemente, sin forzar un reset masivo de contraseñas.
Errores que debes conocer
Error: Usar SHA-256 directamente para contraseñas porque “es seguro”.
# ❌ Wrong import hashlib hashed = hashlib.sha256(b"password123").hexdigest() # ✅ Right import hashlib, os salt = os.urandom(16) key = hashlib.scrypt(b"password123", salt=salt, n=2**14, r=8, p=1, dklen=32) stored = salt + key
SHA-256 es un hash de propósito general sin coste ajustable; scrypt añade el factor n que hace el ataque computacionalmente prohibitivo.
Error: Instalar dependencias sin verificar hashes en producción, ignorando typosquatting.
# ❌ Wrong pip install colourama # un typo y ejecutas código malicioso # ✅ Right — genera requirements.txt con hashes: # pip install pip-tools && pip-compile --generate-hashes requirements.in # Luego instala con: pip install --require-hashes -r requirements.txt
Con --require-hashes, pip rechaza cualquier paquete cuyo SHA-256 no coincida exactamente con el registrado, sin importar el nombre.
Error: Olvidar auditar dependencias transitivas — el paquete directo es legítimo, pero su dependencia no.
# ❌ Wrong — solo revisas tus imports directos manualmente # ✅ Right pip install pip-audit pip-audit # escanea el grafo completo de dependencias contra CVE databases
pip-audit consulta la PyPA Advisory Database y OSV, reportando vulnerabilidades en todo el árbol de dependencias, no solo en lo que declaras en requirements.txt.
N° 200