Módulo random y módulo secrets.
Como crear contraseñas seguras.
En el artículo anterior, uno de los módulos que vimos era el módulo random, que usábamos para generar números aleatorios o secuencias al azar, pero, como pudimos ver, no es un módulo útil o seguro si lo que queremos es generar contraseñas o códigos de seguridad seguros.
¿Por qué?
La documentación oficial de Python nos dice que los números generados con el módulo random, son pseudoaleatorios, esto es, no son completamente aleatorios, sino que se generan con un algoritmo determinista basado en una semilla inicial.
¿Qué quiere decir?
Pues básicamente que todo lo generado con el módulo random, se puede predecir. Si, es difícil, pero se puede.
Como he dicho, se puede predecir la misma secuencia de números generada.
¿Cómo es posible?
En primer lugar, porque los números no son realmente aleatorios, por eso decimos que son pseudoaleatorios ya que son generados siguiendo un algoritmo determinista, es decir, siguen un conjunto de instrucciones predecibles.
Y, en segundo lugar, conociendo la semilla podemos predecir los números generados.
Vale, vale, pero oye, ¿qué cojo… es eso de «la semilla»?
La semilla
Una semilla digamos es un punto de partida para el algoritmo que genera los números. A partir de ese punto, es donde el generador empieza a generar números pseudoaleatorios.
Si usamos siempre la misma semilla, obtendremos siempre la misma secuencia de números.
Veamos un ejemplo:
import random random.seed(5) # Establecemos la semilla print(random.randint(1, 100)) # 80 print(random.randint(1, 100)) # 33
Siempre que ejecutemos el código, dado que la semilla es ‘5’, dará como resultado los mismos números: 80 y 33.
Aquí ya vemos que el módulo random no es seguro cuando se necesita aleatoriedad impredecible.
Ahora bien, seguro que has pensado…
“¿Y si no uso ninguna semilla? De esa manera será imposible predecir los números”.
Pues déjame decirte que estas equivocad@. ¡Python lo hará por ti!
Aclaremos esto, sino usas ninguna semilla, el módulo random, internamente producirá una semilla aleatoria, basándose, por ejemplo, en factores como la hora del sistema.
Por ejemplo, si hacemos esto:
import random print(random.randint(1, 100)) print(random.randint(1, 100))
Ahora los números generados varían y «no los podemos predecir». Los números cambian cada vez que se ejecuta el código, pero como he dicho antes, hay una manera de predecirlos (aunque tu no uses ninguna semilla).
Si no especificas la semilla, Python usa la hora actual (época Unix, 1 de enero de 1970).
¿Qué quiere decir todo esto? ¿Cuál es… «la debilidad»?
Pues, por ejemplo, si se basa en la hora del sistema, un posible atacante que conozca cuando se ejecutó el programa, puede hacer una «búsqueda forzada» probando muchas horas cercanas para deducir la semilla.
Veámoslo más claro.
Imagina que un programa genera un número pseudoaleatorio hoy a las 10:00:01 horas. Un posible atacante ha conseguido saber la hora aproximada de cuándo se ha ejecutado el programa y cuando ha sido generado el número, en base a eso, puede probar varias semillas basadas en la hora (por ejemplo, 10:00:00, 10:00:01, 10:00:02…) y encontrar cuál produjo ese número generado.
Por otro lado, también tenemos los ataques por fuerza bruta. Esto es, las semillas no tienen un rango infinito. Por tanto, si alguien conoce que estás usando números pequeños como semillas (del 0 al 100 por ejemplo), puede generar rápidamente todas las secuencias posibles y compararlas con el resultado.
Otro posible caso de «debilidad», puede ser, por ejemplo, alguien que analice los resultados, observa la secuencia [3, 2, 9, 1] generada. Con suficiente conocimiento matemático y fuerza bruta, podría deducir que fue generada con cierta semilla usando el algoritmo de random.
Como estás viendo, el módulo random no es nada seguro, aunque tu no uses ninguna semilla.
Sí, ya sé que es muuuuy difícil pero la posibilidad de… «hackeo» está ahí.
Otros factores
Uno de ellos ya lo hemos dicho, el tiempo; la hora actual del sistema (usada como base principal de la semilla en random cuando no se especifica una con random.seed()).
Sin embargo, Python, a partir de las últimas versiones (Python 3.2 en adelante) también usa otras fuentes de entropía adicionales para «crear la semilla», haciendo algo más difícil predecirla y, por tanto, haciendo que el módulo random sea algo más seguro y algo «más aleatorio».
Estas fuentes o factores son:
- La entropía del sistema: Por ejemplo, datos del proceso que ejecuta el programa (como el ID del proceso o PID). Este número es único para cada proceso que se ejecuta en el sistema.
- Datos del intérprete de Python: direcciones de memoria, variables internas como el estado del programa, etc.
En definitiva, Python combina las fuentes anteriores en una operación matemática compleja para generar una semilla inicial más robusta. Aun así, la aleatoriedad de random sigue siendo realmente no segura.
Entendido esto, puedes estar pensando:
«¿Entonces que hago? ¿Cómo creo contraseñas seguras? ¿Cómo genero números seguros?»
¡Muy fácil! Si necesitas aleatoriedad «real» e impredecible y segura, debes usar el módulo secrets.
Módulo secrets
El módulo secrets es un módulo diseñado para generar números aleatorios completamente seguros.
¿Por qué es más seguro que random?
Evidentemente porque su algoritmo para generar números aleatorios no es predecible, ya que no usa semillas fijas ni su algoritmo es determinista como random. El módulo secrets, a diferencia de random, una otras fuentes de entropía más seguras (recoge datos aleatorios directamente del sistema operativo) haciendo que sea muy muy difícil, por no decir imposible, predecir los números.
Fuentes de entropía que usa secrets
- Movimientos del ratón y eventos del teclado: Cada vez que movemos el ratón, se registran sus coordenadas y cada vez que presionamos alguna tecla, se registra información sobre qué teclas se presionan, orden, intervalo de tiempo… Esto hace que los valores varíen constantemente, dependiendo de la interacción del usuario, lo que los hace impredecibles.
- Acceso a archivos: Cuando accedemos a algún archivo (leemos o escribimos un archivo), se registra información como la hora de acceso; tamaño del archivo, ubicación en el disco, etc. Esto hace que los datos varíen cada vez y, por tanto, una vez más hace que sean impredecibles.
Para no extender demasiado el artículo, pues aún nos quedan algunos ejemplos que ver en código, decirte que también existen otros factores de entropía como la actividad de red (paquetes enviados y recibidos) o, generadores de entropía específicos del sistema operativo que usemos. Por ejemplo, Linux usa /dev/random.
Vale, una vez hemos visto toda la teoría y ya sabemos que el módulo random no es seguro en comparación con el módulo secrets, vayamos a ver algunos ejemplos con código.
Crear números aleatorios seguros e impredecibles
Para ello usamos la función randbelow()
import secrets # Generar un número seguro entre 0 y 99 (inclusive) numero_seguro = secrets.randbelow(100) print(f"Número seguro entre 0 y 99: {numero_seguro}"
También podemos generar una secuencia de números aleatorios y seguros:
import secrets # Generar una secuencia de 5 números aleatorios seguros entre 1 y 100 secuencia_aleatoria = [secrets.randbelow(100) + 1 for _ in range(5)] print(f"Secuencia aleatoria: {secuencia_aleatoria}")
Por cierto, por si no lo sabes, aprovecho para decirte que el uso de «_» en lugar de «x» o «i» en un bucle, es una convención en Python para indicar que no necesitamos usar la variable dentro del bucle. Significa que la variable es solo un contador y no se utilizará en el cuerpo del bucle.
Al hacer:
secuencia_aleatoria1 = [secrets.randbelow(100) + 1 for _ in range(5)] # Convención: no usamos el índice secuencia_aleatoria2 = [secrets.randbelow(100) + 1 for x in range(5)] # Usamos un nombre innecesario
Las dos maneras funcionan, pero el uso de “_” es más elegante y claro e indicamos que no necesitamos usar la variable del bucle.
Ahora bien, si necesitamos el índice, por ejemplo:
for i in range(5): print(f"Número actual: {i}")
Entonces sí que conviene usar un nombre descriptivo como “x”, “i”, “indice” o cualquier otro.
Bueno, sigamos con el tema principal del artículo y perdona por desviarme un poco 😉
Algunos ejemplos más del módulo secrets
Otro ejemplo, sacado de la documentación del módulo:
import secrets import string # Generar una contraseña alfanumérica de ocho caracteres: alphabet = string.ascii_letters + string.digits password = ''.join(secrets.choice(alphabet) for i in range(8)) print(password)
Ampliemos un poco más y ahora generemos una contraseña auténticamente segura combinando letras, números y símbolos.
import secrets import string # Conjunto de caracteres: letras, números y símbolos alfabeto = string.ascii_letters + string.digits + string.punctuation # Longitud de la contraseña longitud = 16 # Generar una contraseña segura contrasena_segura = ''.join(secrets.choice(alfabeto) for _ in range(longitud)) print(f"Contraseña segura generada: {contrasena_segura}")
Tokens
Podemos generar tokens en formato hexadecimal o en base64.
Hexadecimal:
import secrets # Generar un token seguro de 16 bytes codificado en hexadecimal token_hex = secrets.token_hex(16) print(f"Token hexadecimal seguro: {token_hex}")
Salida:
Token hexadecimal seguro: 2b7b1f8ec0bad4600077402374d6884b
base64:
Usaremos la función urlsafe_b64encode()
import secrets import base64 # Generar 16 bytes aleatorios token_bytes = secrets.token_bytes(16) # Codificar en Base64 seguro para URLs token_urlsafe = base64.urlsafe_b64encode(token_bytes).decode('utf-8') print(f"Token seguro para URL: {token_urlsafe}")
Salida:
Token seguro para URL: SG4AdlICSj-Uh0u9tALEQg==
secrets.choice()
En lugar de usar random.choice(), podemos usar secrets.choice() para obtener un elemento aleatorio de una lista:
import secrets # Lista de nombres nombres = ['Oscar', 'Luis', 'Marcos', 'Ana', 'Laura', 'Maria'] # Seleccionar un nombre de forma segura nombre_aleatorio = secrets.choice(nombres) print(f"Color seleccionado de forma segura: {nombre_aleatorio}")
Si, la función random.choice() también hace lo mismo seleccionando un elemento de la lista, pero como comentamos al principio, random utiliza un generador de números pseudoaleatorios, por lo que si alguien averigua la semilla de random, puede saber qué elemento se seleccionará así que… ¡no nos interesa!
¡Ojo! No quiero decir que «olvides» random y uses secrets en todo. El ejemplo que acabo de poner, es útil si quieres realmente aleatoriedad impredecible (como seleccionar un ganador de un sorteo o elegir un premio al azar, por ejemplo).
En definitiva, el módulo secrets es útil para aplicaciones donde la seguridad es crítica y donde necesitamos generar números aleatorios seguros e impredecibles. Es adecuado para contraseñas, claves, tokens… y cualquier situación que requiera aleatoriedad no predecible. Por el contrario, el módulo random puede ser más rápido y útil en situaciones donde la seguridad no es importante.
Espero que te haya servido el post y a partir de ahora sepas elegir uno u otro según tu necesidad.
¡Nos vemos! Y… ¡Feliz navidad! 🎅🏻🎄