Generación de Contraseñas Criptográficamente Seguras en Python: random vs secrets
La entropía define la calidad de una contraseña no como una propiedad de la cadena en sí, sino como una característica del proceso de generación. La fórmula H = L × log₂(N) asume una elección independiente y equiprobable de cada carácter de un alfabeto de tamaño N a lo largo de una longitud L. Para 8 letras minúsculas (N=26): ~37,6 bits. Para ASCII completo (N=94): ~79 bits.
Un error común: la aparente aleatoriedad de una contraseña garantiza su fortaleza. Dos contraseñas como f9A$kL2pQzX1 pueden tener la misma entropía formal pero una seguridad muy diferente en el mundo real según el generador utilizado.
El módulo random: un PRNG determinista
El módulo random implementa el Mersenne Twister (MT19937) con un estado interno de ~19937 bits. Cada valor es determinista basado en el anterior. La semilla predeterminada se toma de os.urandom(), pero las propiedades inherentes del PRNG permanecen:
- Determinismo: una semilla produce una secuencia predecible.
- Recuperabilidad: el estado puede reconstruirse a partir de ~624 salidas brutas de 32 bits.
- Falta de seguridad criptográfica: no es resistente a ataques de recuperación de estado.
Ejemplo de código típico:
import random
import string
alphabet = string.ascii_letters + string.digits
def generate_password(length=12):
return ''.join(random.choice(alphabet) for _ in range(length))
La contraseña es un segmento de la secuencia del PRNG. Si se conoce el estado, la secuencia es predecible.
El módulo secrets: acceso directo a la entropía del sistema
Secrets utiliza os.urandom() directamente para cada llamada:
- secrets.randbelow(n) → os.urandom().
- random.SystemRandom() → os.urandom().
No hay un estado fijo, no hay determinismo. Cada byte se extrae de fuentes de hardware: interrupciones, ruido de red, movimientos del ratón.
La diferencia es crítica: random crea una ilusión de seguridad, secrets la proporciona.
Ataque práctico: recuperación de semilla y estado
Experimento con 5 contraseñas consecutivas de GigaChat (longitud 12, Python 3.9.18, fecha 24.03.2026 11:11):
Hipótesis: semilla = marca de tiempo del servidor ±1 hora.
from datetime import datetime, timedelta
base_time = datetime(2026, 3, 24, 11, 11, 0)
window_seconds = 3600
for offset in range(-window_seconds, window_seconds):
t = base_time + timedelta(seconds=offset)
seed = int(t.timestamp())
# Inicializar random.seed(seed) y verificar
Resultado: sin coincidencias. Usar os.urandom() para la semilla derrota ataques de fuerza bruta basados en tiempo.
Un ataque real a MT19937 requiere 624 valores brutos. Las contraseñas son una salida distorsionada (elección → carácter), totalizando solo 60 elecciones para 5 contraseñas. La recuperación es imposible.
Límites del ataque:
- Requiere salidas brutas del PRNG.
- Semilla predecible (tiempo, PID).
- Uso repetido del mismo generador.
- Algoritmo conocido.
Conclusiones clave
- La entropía es una propiedad del proceso, no del resultado: siempre verifica tu generador.
- random (Mersenne Twister) es vulnerable a la recuperación de estado con datos suficientes.
- secrets/os.urandom es la única opción para contraseñas, claves y tokens.
- La ilusión de aleatoriedad de random es peligrosa en producción.
- Los ataques a PRNG solo son realistas con errores de semilla o fugas de estado.
Recomendaciones para desarrolladores de nivel medio/senior
Implementa en tu código:
import secrets
import string
alphabet = string.ascii_letters + string.digits + string.punctuation
def secure_password(length=16):
return ''.join(secrets.choice(alphabet) for _ in range(length))
Evita:
- Usar random para autenticación.
- Semillas fijas.
- Reutilizar generadores.
Prueba: genera 10⁶ contraseñas, verifica distribución uniforme (chi²).
— Editorial Team
Aún no hay comentarios.