math/rand es un generador de números pseudoaleatorios (PRNG) que utiliza un algoritmo determinista para calcular el siguiente valor basándose en un estado interno. Si conoces ese estado inicial, llamado seed, puedes predecir con total exactitud toda la secuencia de números que el generador producirá. En contraste, crypto/rand es un generador criptográficamente seguro (CSPRNG) que no depende de una fórmula matemática cerrada, sino que consume entropía directamente del kernel del sistema operativo (utilizando llamadas al sistema como getrandom en Linux o /dev/urandom en sistemas Unix).
Esta arquitectura responde a una necesidad de rendimiento. math/rand es extremadamente rápido porque sus operaciones son puramente aritméticas, lo que lo hace ideal para simulaciones estadísticas, juegos o pruebas de carga donde la velocidad es crítica y la predictibilidad no es un problema. Sin embargo, crypto/rand sacrifica esa velocidad para garantizar la impredecibilidad; al depender de ruido físico y eventos del hardware gestionados por el kernel, es virtualmente imposible para un observador externo predecir el siguiente valor, incluso si ha analizado miles de valores anteriores.
Debes usar math/rand para lógica de juegos, algoritmos de ordenamiento o simulaciones donde la aleatoriedad sea solo una herramienta de variabilidad. Debes usar crypto/rand siempre que el valor generado tenga cualquier implicación de seguridad: tokens de sesión, passwords, salts, nonces, claves de cifrado o cualquier identificador que deba ser secreto y único.
Si utilizas math/rand para propósitos de seguridad, estás creando una vulnerabilidad crítica. Un atacante que logre estimar el momento en que se inició tu servidor (el seed) o que recolecte suficiente información de la secuencia puede reconstruir el estado interno del generador y predecir tus próximos tokens o claves, permitiendo ataques de secuestro de sesión o descifrado de datos sin necesidad de romper la criptografía en sí.
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"math/rand"
"time"
)
// GenerateSecureToken utiliza crypto/rand para obtener bytes impredecibles del sistema operativo.
func GenerateSecureToken(n int) (string, error) {
b := make([]byte, n)
// rand.Read de crypto/rand es una llamada al sistema que consume entropía del kernel.
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
// GenerateInsecureToken utiliza math/rand, que es determinista si se conoce el seed.
func GenerateInsecureToken(r *rand.Rand, n int) string {
b := make([]byte, n)
r.Read(b)
return hex.EncodeToString(b)
}
func main() {
const size = 16
// --- ESCENARIO SEGURO ---
// El token es impredecible incluso si conozco el tiempo exacto de creación.
tokenSeguro, _ := GenerateSecureToken(size)
fmt.Printf("Token Seguro (crypto/rand): %s\n", tokenSeguro)
// --- ESCENARIO VULNERABLE ---
// Un atacante puede adivinar el seed si conoce el timestamp aproximado del servidor.
now := time.Now().UnixNano()
source := rand.NewSource(now)
rInseguro := rand.New(source)
tokenVulnerable := GenerateInsecureToken(rInseguro, size)
fmt.Printf("Token Vulnerable (math/rand): %s\n", tokenVulnerable)
// El atacante que sabe que el token se generó en este instante puede recrear la secuencia.
rAtacante := rand.New(rand.NewSource(now))
tokenRecreado := GenerateInsecureToken(rAtacante, size)
fmt.Printf("Token Recreado por Atacante: %s\n", tokenRecreado)
if tokenVulnerable == tokenRecreado {
fmt.Println("¡ALERTA: El token inseguro fue predicho con éxito!")
} else {
fmt.Println("Nota: El atacante falló porque el seed no coincidía exactamente.")
}
}
En el ejemplo anterior, observamos la diferencia fundamental en la naturaleza de la entropía. La función GenerateSecureToken llama a rand.Read del paquete crypto/rand, lo que significa que el valor resultante no depende de una semilla matemática, sino de la entropía del sistema, haciendo imposible la reproducción del token.
En cambio, GenerateInsecureToken depende de un objeto *rand.Rand que fue inicializado con un seed específico a través de rand.NewSource(now). En el código, simulamos al atacante usando exactamente el mismo valor de now (el timestamp en nanosegundos). Debido a que math/rand es un generador determinista, el tokenRecreado coincide exactamente con el tokenVulnerable. En un entorno real, un atacante solo necesita reducir el espacio de búsqueda del tiempo (por ejemplo, probando todos los nanosegundos de la última hora) para encontrar la semilla y predecir tus claves.
Incluso en Go 1.20+, donde math/rand implementa un seeding automático global para evitar que los valores sean iguales en cada ejecución, el generador sigue siendo un PRNG determinista. Si alguien obtiene acceso al estado interno del generador, la seguridad desaparece.
El error frecuente es intentar “mejorar” la aleatoriedad de math/rand usando el tiempo como semilla:
// ERROR: Vulnerabilidad crítica de seguridad seed := time.Now().Unix() r := rand.New(rand.NewSource(seed)) // Un atacante puede realizar un ataque de fuerza bruta sobre el timestamp // de forma extremadamente eficiente.
La solución siempre es delegar la generación de valores sensibles al kernel a través de crypto/rand.
N° 215