Volver al inicio

Generación de contraseñas en Python: secrets en lugar de random

El artículo desglosa por qué el módulo random no es adecuado para la generación de contraseñas debido al determinismo de Mersenne Twister. Comparación con secrets, análisis de entropía e intento fallido de ataque a semilla. Recomendaciones para código seguro.

¿Por qué random es peligroso para contraseñas en Python: secrets en acción
Advertisement 728x90

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:

Google AdInline article slot
  • 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:

Google AdInline article slot
  • 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):

Google AdInline article slot

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

Advertisement 728x90

Leer después