Por qué una contraseña generada con Math.random puede parecer lo bastante aleatoria sin ser un número aleatorio criptográficamente seguro

Una página acaba de mostrar una cadena que mezcla mayúsculas, minúsculas, cifras y símbolos. Eso solo describe el juego de caracteres. No prueba que la siguiente cadena sea impredecible. No te fíes de las palabras «aleatorio» o «local» en la página. Abre las herramientas para desarrolladores: la ruta de Generar debe llamar a crypto.getRandomValues, y la contraseña que acabas de crear no debe aparecer en Network como dato de negocio.

Primero la conclusión: Math.random() es un generador de números pseudoaleatorios ordinario. MDN escribe que no proporciona números aleatorios criptográficamente seguros y que no debe usarse para nada relacionado con la seguridad. Las contraseñas de acceso, los tokens de un solo uso y las claves necesitan crypto.getRandomValues(). Una cadena que parece aleatoria, un botón Generar que funciona sin conexión y un candado en la barra de direcciones prueban cada uno un límite distinto. Ninguno responde a «¿qué API llamó Generar?».

Responde a la intención de búsqueda en una frase

Math.random() devuelve un flotante en [0, 1) con una distribución aproximadamente uniforme. MDN es claro: ese valor no es criptográficamente seguro; usa en su lugar Crypto.getRandomValues(). «Parecer lo bastante aleatoria a simple vista» describe la mezcla de caracteres en la salida. No dice nada sobre si alguien que ha visto unas salidas — o el código fuente — puede reconstruir el estado interno del generador e imprimir los candidatos siguientes.

Esta nota va dirigida a desarrolladores y operaciones que revisan un «generador de contraseñas en línea», y a quien recibe de un compañero una pestaña Generar para inspeccionar antes de usarla. No sustituye el manual del generador de contraseñas, ni repite el recorrido Network de «¿salió el texto en claro de esta pestaña?». Lo que debes llevarte: la capa que separa un PRNG ordinario de un CSPRNG, por qué un módulo ingenuo puede sesgar aun una buena API, y la ruta exacta desde el clic en Generar hasta la pila de llamadas.

Parecer aleatoria solo dice que los caracteres pueden ser heterogéneos

Juzgar «aleatorio» a ojo es barato y, la mayoría de las veces, incorrecto. aB3$kP9x y Tr0ub4dor&3 parecen contraseñas. El primero puede venir de un juego de caracteres amplio. El segundo puede ser solo una frase memorable con sustituciones habituales. La apariencia responde a «¿parece una contraseña?», no a «¿se puede calcular el siguiente bit?».

Incluso cuando el juego incluye de verdad mayúsculas, minúsculas, cifras y símbolos, una secuencia de índices predecible reduce el trabajo. Un atacante capaz de relanzar el mismo generador — o de recuperar su estado tras unas salidas observadas — enumera los candidatos restantes en lugar de todo el espacio. Para una contraseña de acceso, la impredecibilidad va antes que «tiene un carácter especial».

La longitud sigue siendo otra cuestión. El mismo CSPRNG a 6 caracteres y a 16 cambia el coste de fuerza bruta varios órdenes de magnitud. Por debajo de 8, el espacio ya es pequeño aunque la API sea la correcta. El modo aleatorio de FastPwd permite 6–128 caracteres, 16 por defecto, y avisa por debajo de 8 de una seguridad más baja — es una advertencia de longitud, no un algoritmo más débil. Mantén separadas las dos afirmaciones: la API responde a la impredecibilidad; la longitud responde al espacio de búsqueda.

Qué tipo de azar es realmente Math.random

ECMAScript especifica Math.random() como un Number mayor o igual que 0 y estrictamente menor que 1, extraído con un algoritmo o estrategia definidos por la implementación y aproximadamente uniforme. La especificación del lenguaje no exige resistencia a la predicción ni que la semilla venga del pool de entropía del sistema operativo. Un motor puede elegir un PRNG con excelentes estadísticas y estado reversible.

El motor V8 de Chrome usó MWC1616 hasta finales de 2015. El artículo de V8 recoge los límites: 64 bits de estado interno, calidad sesgada y fallos en la suite TestU01. A partir de V8 4.9.41 (incluido con Chrome 49), pasó a xorshift128+: 128 bits de estado, periodo 2^128 − 1 y éxito en TestU01. Firefox y Safari se unieron después a la misma familia. Son mejoras estadísticas, no una mejora de seguridad.

El mismo artículo de V8 traza el límite en una frase: xorshift128+ es mucho mejor que la implementación antigua y sigue sin ser criptográficamente seguro. Hash, firmas y cifrado/descifrado no son trabajo de un PRNG ordinario; pertenecen a window.crypto.getRandomValues de la API Web Cryptography. «Parecer estadísticamente aleatorio» y «un observador no puede reconstruir a bajo coste las salidas siguientes» son dos barreras distintas. Una contraseña necesita la segunda.

La consulta CodeQL de GitHub Insecure randomness marca Math.random usado para fabricar una contraseña como alerta, por la misma razón: si un atacante puede predecir el valor, puede iniciar sesión en lugar del usuario nuevo. Un generador que solo llama a Math.random() y luego proyecta sobre un juego de caracteres puede pasar una mirada y un control de frecuencias grosero, y fallar en la revisión que supone que alguien leyó el código o interceptó unas salidas. No escribas «usó Math.random, pero el juego es amplio» como validación.

Qué cambia realmente getRandomValues

crypto.getRandomValues(typedArray) rellena un array tipado de enteros — Uint8Array, Uint32Array y otras vistas enteras — con valores aleatorios en sentido criptográfico. MDN es explícito sobre el compromiso de implementación: por rendimiento, los navegadores no leen en general un generador hardware en cada bit. Arrancan un PRNG apto para uso criptográfico desde una fuente de entropía del sistema, como /dev/urandom en Unix o la interfaz aleatoria de Windows. El algoritmo puede cambiar de un navegador a otro. El objetivo de diseño no: una salida ya publicada no debe permitir reconstruir a bajo coste el estado interno.

Las dos API no pertenecen a la misma especificación. Math.random cae bajo ECMA-262. getRandomValues cae bajo Web Cryptography. Una sola llamada de más de 65 536 bytes lanza QuotaExceededError; una contraseña de 16 caracteres está lejos de ese techo. getRandomValues es también el único método de Crypto que puede ejecutarse fuera de contexto seguro; la derivación de claves y subtle.encrypt siguen exigiendo contexto seguro (HTTPS en una página de producción). «Sigue funcionando en HTTP» no es motivo para abandonar TLS.

Separa «esta pestaña extrajo los bytes aleatorios» de «el servidor los extrajo y los devolvió». Aunque el servidor haya usado un CSPRNG de alta calidad, la contraseña ya pasó por la memoria del otro lado, los registros y ese intercambio HTTP — en claro o en forma reversible. El candado de la barra de direcciones solo cifra el trayecto de este navegador hasta el extremo TLS actual. No dice que ese extremo no pueda leer el cuerpo. Si la página afirma «generado en local», Generar no debe enviar la contraseña en POST y volver a mostrarla.

Lo que has visto Lo que puede respaldar Lo que no puede respaldar
La salida parece aleatoria El juego de caracteres puede ser amplio El proceso es impredecible
El código fuente contiene Math.random La página usa un PRNG ordinario en algún sitio Generar lo usa para la contraseña
El código fuente contiene getRandomValues La página importó una API CSPRNG Ese clic usó esa API
Generar sigue funcionando sin conexión Ese clic no esperó una API de fabricación en directo La fuente no es Math.random
La búsqueda en Network no encuentra la contraseña En esta observación no se envió como dato de negocio La API de azar es un CSPRNG

Sesgo de módulo: un CSPRNG puede seguir sesgándose

El segundo error habitual llega después de que el script ya llamó a getRandomValues. Toma un byte en 0–255 y escribe charset[byte % charset.length]. Si la longitud del juego no divide 256, los primeros caracteres reciben una fracción extra del resto y aparecen un poco más a menudo. En una contraseña de 16 caracteres, el sesgo suele ser pequeño, pero muestra que la implementación no terminó «uniforme». Un juego más raro, o proyectar 8 bits sobre una lista grande de palabras, hace el sesgo más visible.

El muestreo por rechazo lo corrige: calcula el mayor múltiplo del tamaño del juego que aún cabe en 256 (o 2^32), descarta cualquier extracción mayor o igual a ese límite y luego toma el módulo. Los caracteres aleatorios y las frases legibles de FastPwd llaman a crypto.getRandomValues, extraen un entero de 32 bits con Uint32Array(1), fijan el límite en Math.floor(0x100000000 / max) * max y repiten cuando la extracción cae en la cola. Si falta getRandomValues, la generación se rechaza — no hay respaldo a Math.random. Cuando revises otro sitio, sigue leyendo «extraer entero → descartar cola → proyectar carácter». Encontrar el nombre de la función no es el final de la revisión.

No experimentes con una contraseña de acceso real, una clave API de producción o un ticket sin enmascarar. Genera una cadena desechable y deséchala. Compruebas la ruta de Generar y el tráfico, no un secreto nuevo expuesto otra vez.

Comprobar ahora: código fuente, consola, Network

Un eslogan no es prueba. Prepara un marcador que no aparezca nunca en tráfico real: pon la longitud en 16, genera una vez y trata la cadena entera como un canario. Los tres pasos siguientes se completan en esta pestaña. No hace falta cuenta.

  1. Abre Sources en las herramientas para desarrolladores, o Ver código fuente de la página. Busca por separado Math.random y getRandomValues. Anota archivos y nombres de función. Aún no concluyas.
  2. Baja desde el manejador de clic del botón Generar hasta que los caracteres se concatenen en el resultado. Esa ruta debe llamar a crypto.getRandomValues. Si solo ves Math.random(), Date.now() o una semilla fija, esa generación no es un CSPRNG.
  3. Pasa a Network, activa Preserve log y deja el filtro en All. Genera una vez más. Pega la cadena entera en el cuadro de búsqueda. La línea de petición, la query, el cuerpo y las cargas de analítica no deben contener esa contraseña.

La consola solo prueba que la API existe

En Console, typeof crypto.getRandomValues === "function" que devuelve true solo significa que el navegador de este documento expone el método. No significa que el script de la página lo haya llamado. Ejecutar tú una línea de getRandomValues y tratar la contraseña de la página como «ya comprobada» son dos fragmentos sin relación.

Sin conexión solo prueba que nada se fabricó a distancia

Deja que la página termine de cargar. Luego pon Network en Offline, o corta la red del sistema, y genera de nuevo. Si sigue apareciendo un resultado, ese clic no dependió de una API en directo. Es una señal fuerte de «generado en esta pestaña». Math.random tampoco necesita red. Sin conexión es un paso que aún debes combinar con la ruta en el código fuente y la búsqueda del canario.

Los scripts empaquetados suelen comprimir los nombres a una sola letra. Si no aparece getRandomValues, busca crypto y Uint32Array, y mira si Generar dispara un POST de negocio nuevo. La minificación no es una exención. Solo significa que la búsqueda da un rodeo más.

Tres formas de leer de más «aleatorio»

Primera: tratar el candado de la barra de direcciones como prueba de que el generador es un CSPRNG. HTTPS / TLS protege el trayecto de este navegador hasta el extremo TLS actual. Un servidor que recibe una contraseña que enviaste en POST sigue mostrando candado. El candado no responde a Math.random frente a getRandomValues.

Segunda: tratar «el paquete menciona getRandomValues» como prueba de que Generar usó un CSPRNG. El mismo archivo puede usar Web Crypto para cifrado de archivos, un enlace autodestructivo o una demo, mientras la ruta de contraseña sigue llamando a Math.random. Lo inverso también vale: Math.random en un anuncio o animación no refuta la ruta de contraseña. La pila del clic es la prueba.

Tercera: tratar «no se subió» como «es impredecible». Un panel Network limpio solo significa que, en esta observación, la contraseña no salió de la pestaña como campo HTTP de negocio. Una extensión puede seguir leyendo el DOM. El historial del portapapeles del sistema puede guardar la cadena que copiaste. Son otros límites. La calidad del azar, si se envió y si una copia queda en algún sitio después de irte, son tres preguntas distintas.

Preguntas frecuentes

Si una contraseña generada parece lo bastante aleatoria, ¿puedo usarla como contraseña de acceso?

No solo por la apariencia. Caracteres mezclados solo sugieren que el juego puede ser amplio. No prueban que el proceso sea impredecible. MDN indica que Math.random no debe usarse para seguridad. Comprueba también si la longitud basta, si reutilizas la cadena en otros sitios y si Generar llama a crypto.getRandomValues.

Si la página genera una contraseña sin conexión, ¿el azar es seguro?

Solo prueba que ese clic no esperó una API en directo y que el servidor no fabricó la cadena por ti. Math.random también funciona sin conexión. Tras el éxito sin conexión, la ruta en el código fuente debe seguir mostrando getRandomValues, y Network no debe contener la contraseña que acabas de crear.

El código fuente contiene Math.random y getRandomValues. ¿Puedo usarlo igualmente?

Depende de la función que llama el botón Generar. Las animaciones y el orden de visualización pueden usar Math.random. Las contraseñas, tokens y claves deben pasar por getRandomValues. Abre Sources y baja desde el manejador de clic. No te quedes con el recuento de apariciones de cada nombre en el paquete.

¿Qué API de azar usa FastPwd y se sube la contraseña?

Los caracteres aleatorios y las frases legibles llaman a crypto.getRandomValues y proyectan un entero de 32 bits sobre el juego de caracteres con muestreo por rechazo, para evitar la cola del módulo. Si falta la API, la generación se rechaza. Longitud 6–128 caracteres, 16 por defecto; por debajo de 8, un aviso indica menor seguridad. El resultado no se sube por defecto. Abre y usa, sin cuenta.

Repetir la misma cadena desechable en una herramienta lista para usar

Si quieres una página que deje claro el límite del azar, abre el generador de contraseñas de FastPwd. Sin registro. Deja el modo aleatorio en 16 caracteres por defecto y genera una cadena desechable. Repite la quinta sección: en Sources, baja desde Generar hasta getRandomValues y el bucle de muestreo por rechazo; en Network, busca la cadena entera para que la línea de petición y el cuerpo no coincidan.

Después, si también quieres saber si la cadena se parece a una contraseña muy usada, lleva otra cadena desechable al comprobador de contraseñas y compárala con la lista pública de alta frecuencia. La contraseña de prueba no se sube. No coincidir no prueba que nunca estuvo en la web; coincidir solo dice que es demasiado común. Ese es el límite de la lista de contraseñas débiles, no el del CSPRNG. No pegues una contraseña que vayas a activar en un comprobador en línea.

Antes de la siguiente pestaña generadora desconocida, recuerda tres hechos. Parecer aleatoria no prueba impredecibilidad. Math.random puede pasar suites estadísticas y seguir sin ser un CSPRNG. Ver getRandomValues en el paquete no basta: necesitas la ruta que clicaste y un canario que confirme que la contraseña no salió como dato de negocio. Las observaciones que puedes repetir merecen entrar en tu propia lista de revisión.

Lecturas relacionadas

«Cifrado local, nada se sube» tampoco es prueba — busca en Network con un canario en Cómo comprobar que el cifrado local del navegador no envió el original. Una lista local de contraseñas débiles puede detectar contraseñas demasiado comunes y no es una búsqueda en toda la web; ver Qué puede demostrar una lista local de contraseñas débiles, y por qué no es un cruce de toda la web. Si después debes entregar la contraseña una sola vez a un compañero, por qué la parte tras # queda fuera de HTTP por defecto está en Por qué el hash de una URL puede guardar una clave, y cuándo esa protección falla.