Generación segura de números aleatorios: base de la cripto fuerte
La generación aleatoria segura es crítica para el cifrado. Aprende cómo funciona crypto.getRandomValues y por qué la aleatoriedad débil rompe tu seguridad.
La generación segura de números aleatorios es la base sobre la que descansa todo lo demás en criptografía. En JavaScript, crypto.getRandomValues(buffer) rellena un TypedArray con bytes aleatorios criptográficamente seguros del CSPRNG del sistema operativo (/dev/urandom en Linux/macOS, BCryptGenRandom en Windows, SecRandomCopyBytes en iOS/macOS). No uses Math.random() para nada que toque la seguridad — es un PRNG al estilo Mulberry32 o xorshift diseñado para velocidad, no para imprevisibilidad, y su salida es predecible tras observar unas pocas muestras. Un RNG débil rompe claves AES, handshakes TLS, la unicidad de nonces en GCM, la imposibilidad de adivinar tokens, y cualquier otro primitivo de seguridad que dependa de bits impredecibles.
La diferencia entre aleatorio y aleatorio criptográfico
Un PRNG (generador de números pseudoaleatorios) produce un flujo determinista a partir de una semilla. Dada la semilla y el algoritmo, puedes reproducir cada salida. Perfecto para juegos, simulaciones y métodos de Monte Carlo. Catastrófico para criptografía.
Un CSPRNG (PRNG criptográficamente seguro) se siembra desde una fuente de entropía real (ruido térmico, temporización de interrupciones, instrucciones RNG hardware como RDSEED de Intel) y su diseño garantiza que la salida sea computacionalmente indistinguible de la aleatoriedad real, y que observar la salida pasada no ayude a predecir la futura.
JavaScript te ofrece ambos. Math.random() es un PRNG. crypto.getRandomValues() es un envoltorio sobre el CSPRNG del sistema operativo. Una línea de diferencia en el código, una diferencia enorme en seguridad.
El uso correcto canónico
// Genera bytes aleatorios para una clave AES de 256 bits
const keyBytes = crypto.getRandomValues(new Uint8Array(32));
// Genera un nonce GCM de 96 bits
const nonce = crypto.getRandomValues(new Uint8Array(12));
// Genera una sal de 128 bits
const salt = crypto.getRandomValues(new Uint8Array(16));
// Genera un token aleatorio seguro para URL
const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
const token = btoa(String.fromCharCode(...tokenBytes))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
crypto.getRandomValues() es síncrono, rellena el buffer en su lugar y devuelve el buffer. El tamaño máximo de solicitud es de 65.536 bytes en una sola llamada (una cuota impuesta por la especificación para evitar el bloqueo). Para material aleatorio más grande, llama repetidamente.
Equivalentes en Node.js
const { randomBytes, randomFillSync, webcrypto } = require('crypto');
const keyBytes = randomBytes(32); // Devuelve un Buffer
// O compatible con Web Crypto
const nonce = webcrypto.getRandomValues(new Uint8Array(12));
randomBytes de Node extrae del mismo CSPRNG subyacente que Web Crypto. Usa el estilo de API que mejor se adapte a tu código. Para código isomórfico que se ejecuta en ambos entornos, webcrypto.getRandomValues coincide exactamente con el navegador.
Por qué falla Math.random
V8 (Chrome/Node), SpiderMonkey (Firefox) y JavaScriptCore (Safari) implementan todos Math.random() como un PRNG rápido sin garantías criptográficas. V8 usa una variante xorshift128+. Los investigadores han demostrado que tras observar ~5 salidas, un atacante puede recuperar el estado interno y predecir todas las salidas futuras. En 2015, Mike Pound y sus colegas invirtieron el estado de Math.random de V8 en programas de recompensas por errores reales.
Si usas Math.random() para generar tokens de sesión, enlaces de restablecimiento de contraseña, nonces de cifrado o IDs de compartición, los atacantes que observen unos pocos de ellos pueden predecir el resto. No es teórico — es una clase de error habitual en auditorías.
Usos incorrectos comunes
Sembrar un PRNG de biblioteca con Math.random():
// MAL
const seed = Math.floor(Math.random() * 2**32);
Nada posterior puede ser más aleatorio que la semilla. Usa crypto.getRandomValues(new Uint32Array(1))[0] en su lugar.
Usar Date.now() como entropía: El tiempo es adivinable dentro de ventanas estrechas. Incluso combinado con un pequeño factor aleatorio, las marcas de tiempo filtran suficientes bits para los atacantes.
Construir tu propio PRNG combinando fuentes con XOR: No lo hagas. Los CSPRNG del sistema operativo ya mezclan cada fuente de entropía útil. Añadir tu propia agitación típicamente reduce la entropía en lugar de aumentarla.
Sesgo de módulo al generar rangos: randomBytes[0] % 10 no está distribuido uniformemente sobre 0-9 porque 256 no es múltiplo de 10. Para enteros aleatorios uniformes en un rango, usa muestreo por rechazo:
function randomInt(max) {
const range = new Uint32Array(1);
const threshold = 2**32 - (2**32 % max);
do {
crypto.getRandomValues(range);
} while (range[0] >= threshold);
return range[0] % max;
}
Fuentes de entropía y preocupaciones en el arranque
En Linux, /dev/urandom siempre es seguro después del arranque temprano. Durante los primeros segundos de arranque en sistemas sin RNG hardware, el pool del kernel puede estar subalimentado. Esto fue explotado en el error de OpenSSL de Debian de 2008, donde un parche eliminó la mezcla de entropía y dejó solo el ID de proceso como semilla. Las claves generadas en esa ventana tenían solo 2^15 valores posibles — enumerables en segundos.
Los sistemas modernos siembran el CSPRNG del kernel a partir de: RDSEED en x86-64 (cuando está disponible), instrucciones RNG de ARMv8.5-A, ruido térmico de varios periféricos, temporización de interrupciones, teclado/ratón si es interactivo. En servidores con hardware como Intel Ice Lake o AMD Zen 3+, el CSPRNG se siembra en microsegundos tras el arranque.
Para contenedores Docker: el /dev/urandom del host se pasa por defecto. No se requiere ninguna acción. Para funciones sin servidor (AWS Lambda, Cloudflare Workers), el runtime gestiona la siembra de entropía por invocación.
Tokens de sesión e IDs de compartición
Para un servicio de transferencia de archivos, generas identificadores aleatorios para:
- IDs de archivo en URLs (los atacantes no deben poder adivinar IDs válidas)
- Tokens de compartición para enlaces protegidos con contraseña
- Tokens CSRF
- Claves de cifrado (claves AES por archivo)
- Nonces para GCM
Longitud mínima: 128 bits (16 bytes) para colisión e imposibilidad de adivinar; 256 bits (32 bytes) para claves. La codificación segura para URL mediante base64url añade ~33% de longitud; el hexadecimal añade un 100%.
Un token codificado en base64url de 32 bytes tiene 43 caracteres y es prácticamente libre de colisiones con 2^256.
Detectar un RNG débil
Señales de que tu RNG está roto o es débil:
- Tokens idénticos generados por diferentes solicitudes (colisión en lo que debería ser un espacio enorme)
- La salida pasa pruebas visuales pero falla las baterías estadísticas
dieharderoPractRand - Reutilización de semilla tras el reinicio del proceso — cada despliegue usa el mismo estado inicial
- Las claves generadas caen en patrones (por ejemplo, los primeros 4 bytes varían pero los últimos 28 son idénticos)
En producción, probablemente no verás esto a menos que algo esté catastróficamente mal. El modo de fallo suele ser silencioso: los ataques simplemente se vuelven prácticos sobre lo que debería ser un espacio de búsqueda de 2^256.
Auditoría: cada llamada a Math.random() en una base de código debería revisarse. Un grep de Math.random sobre tu árbol de fuentes es una buena comprobación de higiene semanal. Convertir cualquier sitio de llamada relevante para la seguridad a crypto.getRandomValues lleva minutos y previene vulnerabilidades reales.
Cadenas aleatorias y UUIDs
Para identificadores de cara al usuario, crypto.randomUUID() devuelve un UUID v4 (122 bits de aleatoriedad) en un formato estándar:
const id = crypto.randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Compatible con Chrome 92+, Firefox 95+, Safari 15.4+, Node 14.17+. Bueno para claves primarias de bases de datos, IDs de solicitudes API e identificadores únicos no críticos para la seguridad. Usa getRandomValues explícito para todo lo que requiera formatos personalizados o mayor entropía.
En servidores: evita los pools RNG personalizados
Algunos frameworks de servidor ofrecen sus propios pools aleatorios que afirman "mezclar" el CSPRNG del sistema con entropía a nivel de aplicación. Trátalo con escepticismo. La mezcla personalizada rara vez mejora la salida del kernel y puede reducir silenciosamente la entropía si tiene errores.
Si estás en Node o un runtime importante, el crypto.randomBytes integrado es correcto y rápido. No lo reemplaces con mezcladores de terceros.
La conclusión
Cada pieza de criptografía en una aplicación de transferencia de archivos depende de bytes aleatorios impredecibles. Claves AES, nonces GCM, sales PBKDF2, tokens de compartición, tokens anti-CSRF, IDs de sesión — todos necesitan el mismo primitivo: crypto.getRandomValues() en navegadores, crypto.randomBytes() o webcrypto.getRandomValues en Node. Usa esos. Nunca Math.random(). Nunca marcas de tiempo. Nunca tu propio mezclador.
HexaTransfer deriva cada clave AES por archivo, nonce e identificador de URL a partir de crypto.getRandomValues() en el cliente. Los IDs de compartición del lado del servidor provienen de crypto.randomBytes. Una sola API, comportamiento coherente, sin forma de filtrar accidentalmente bits predecibles al sistema.
El primitivo es aburrido precisamente porque debe serlo. Aburrido, correcto y disponible en todas partes — exactamente lo que deben ser los fundamentos criptográficos.
Pruébalo en hexatransfer.com — gratis, sin cuenta, hasta 10 GB.
Envía archivos grandes de forma segura con cifrado de extremo a extremo
Transfiere archivos de hasta 10 GB gratis con cifrado de extremo a extremo. Sin necesidad de cuenta. Tus archivos se cifran en tu navegador antes de subirlos: nadie más puede leerlos.
Enviar un archivo