¿Por qué Math.random () no está diseñado para ser criptográficamente seguro?

209

La función Math.random() de JavaScript está diseñada para devuelva un solo valor de coma flotante n de manera que 0 ≤ n < 1. Es (o al menos debería ser) ampliamente conocido que el resultado es no criptográficamente seguro. La mayoría de las implementaciones modernas utilizan el algoritmo XorShift128 + que puede ser se rompe fácilmente . Como no es raro que la gente lo use por error cuando lo necesiten mejor aleatoriedad, ¿por qué los navegadores no lo reemplazan con un CSPRNG? Sé que Opera hace eso *, al menos. El único razonamiento en el que podría pensar sería que XorShift128 + es más rápido que un CSPRNG, pero en computadoras modernas (e incluso no tan modernas), sería trivial producir cientos de megabytes por segundo utilizando ChaCha8 o AES-CTR. Estos a menudo son lo suficientemente rápidos como para que una implementación bien optimizada pueda verse obstaculizada solo por la velocidad de la memoria del sistema. Incluso una implementación no optimizada de ChaCha20 es extremadamente rápida en todas las arquitecturas, y ChaCha8 es más del doble de rápida.

Comprendo que no se puede redefinir como un CSPRNG, ya que el estándar no ofrece explícitamente ninguna garantía de uso criptográfico, pero parece que no hay inconvenientes para que los proveedores de navegadores lo hagan voluntariamente. Reduciría el impacto de los errores en una gran cantidad de aplicaciones web sin violar el estándar (solo requiere que la salida sea redonda a la más cercana, incluso IEEE 754 ), lo que reduce el rendimiento o rompe la compatibilidad con las aplicaciones web.

EDITAR: algunas personas han señalado que esto podría hacer que las personas abusen de esta función, incluso si el estándar dice que no puede confiar en ella para la seguridad criptográfica. En mi opinión, hay dos factores opuestos que determinan si el uso de un CSPRNG sería o no un beneficio de seguridad neto:

  1. Falsa sensación de seguridad : la cantidad de personas que de lo contrario usarían una función diseñada para este propósito, como window.crypto , en su lugar, decida utilizar Math.random() porque resulta criptográficamente seguro en su plataforma de destino prevista.

  2. Seguridad oportunista : la cantidad de personas que no conocen nada y usan Math.random() de todas formas para aplicaciones sensibles que estarían protegidas de su propio error. Obviamente, sería mejor educarlos, pero esto no siempre es posible.

Parece seguro suponer que la cantidad de personas que se protegerían de sus propios errores superaría con creces la cantidad de personas que sufren una falsa sensación de seguridad.

* Como señala CodesInChaos, esto ya no es cierto ahora que Opera se basa en Chromium.

Varios navegadores principales han tenido informes de errores que sugieren que se reemplace esta función con una alternativa segura criptográficamente, pero ninguno de los cambios seguros sugeridos aterrizó:

Los argumentos para el cambio esencialmente coinciden con los míos. Los argumentos en contra varían desde un rendimiento reducido en microbenchmarks (con poco impacto en el mundo real) hasta malentendidos y mitos, como la idea incorrecta de que un CSPRNG se debilita con el tiempo a medida que se genera más aleatoriedad. Al final, Chromium creó un objeto criptográfico completamente nuevo y Firefox reemplazó su RNG con el algoritmo XorShift128 +. La función Math.random() sigue siendo totalmente predecible.

    
pregunta forest 15.03.2018 - 07:11
fuente

11 respuestas

427

Fui uno de los implementadores de JScript y formé parte del comité de ECMA desde mediados hasta finales de la década de 1990, por lo que puedo proporcionar una perspectiva histórica aquí.

  

La función JavaScript Math.random () está diseñada para devolver un valor de punto flotante entre 0 y 1. Es ampliamente conocido (o al menos debería ser) que la salida no es criptográficamente segura

Primero que nada: el diseño de muchas API RNG es horrible . El hecho de que la clase .NET Random pueda ser mal utilizada de varias maneras para producir secuencias largas del mismo número es terrible. Una API en la que la forma natural de usarla también es una forma incorrecta es una API de "fallas de falla"; Queremos que nuestras API sean un pozo de éxito, donde la forma natural y la correcta sean las mismas.

Creo que es justo decir que si supiéramos lo que sabemos ahora, la API aleatoria JS sería diferente. Incluso cosas simples como cambiar el nombre a "pseudoaleatorio" ayudaría, porque, como se nota, en algunos casos los detalles de la implementación son importantes. A nivel arquitectónico, hay buenas razones por las que desea que random() sea una fábrica que devuelva un objeto que represente una secuencia aleatoria o pseudoaleatoria, en lugar de simplemente devolver números. Y así. Lecciones aprendidas.

Segundo, recordemos cuál fue el propósito fundamental del diseño de JS en la década de 1990. Haz que el mono baile cuando muevas el ratón . Pensamos que los guiones de expresión en línea eran normales, pensamos que los bloques de guiones de dos a diez líneas eran comunes, y la idea de que alguien podría escribir un cien líneas de guiones en una página era realmente muy inusual. Recuerdo la primera vez que vi un programa de JS de diez mil líneas y mi primera pregunta para las personas que me pedían ayuda porque era muy lenta en comparación con su versión de C ++ fue alguna versión de "¿estás loco ?! 10KLOC JS ?! "

La noción de que cualquier persona necesitaría aleatoriedad de criptografía en JS era igualmente insana. ¿Necesitas que los movimientos de tus monos sean impredecibles con la fuerza criptográfica? Improbable.

Además, recuerda que fue a mediados de la década de 1990. Si no estuvieras allí para ello, puedo decirte que era un mundo muy diferente al actual en lo que respecta a crypto ... Ver export of cryptography .

Ni siquiera habría considerado poner la aleatoriedad de la fuerza de cifrado en nada que se incluyera en el navegador sin obtener una gran cantidad de asesoramiento legal del equipo de MSLegal. No quería tocar criptografía con un poste de diez pies en un mundo donde el código de envío se consideraba exportar municiones a los enemigos del estado . Esto parece una locura desde la perspectiva de hoy, pero ese era el mundo que era .

  

¿Por qué los navegadores no lo reemplazan con un CSPRNG?

Los autores del navegador no tienen que proporcionar una razón para NO realizar un cambio. Los cambios cuestan dinero y quitan el esfuerzo de mejores cambios; Cada cambio tiene un enorme costo de oportunidad .

Más bien, debe proporcionar un argumento no solo por qué hacer el cambio es una buena idea, sino por qué es el mejor uso posible de su tiempo. Este es un pequeño cambio por poco dinero.

  

Entiendo que no se puede redefinir como un CSPRNG, ya que el estándar explícitamente no garantiza la idoneidad para el uso criptográfico, pero parece que no hay inconveniente en hacerlo de todos modos

La desventaja es que los desarrolladores están todavía en una situación en la que no pueden saber de manera confiable si su aleatoriedad es criptográfica o no, y aún más fácilmente pueden caer en la trampa de confiar en una propiedad que es No garantizado por la norma. El cambio propuesto en realidad no soluciona el problema, que es un problema de diseño.

    
respondido por el Eric Lippert 15.03.2018 - 16:10
fuente
115

Porque en realidad hay una alternativa segura criptográficamente a Math.random() :

window.crypto.getRandomValues(typedArray)

Esto le permite al desarrollador usar la herramienta adecuada para el trabajo. Si quieres generar imágenes bonitas o botines para tu juego, usa el rápido Math.random() . Cuando necesite números aleatorios seguros criptográficamente, use el window.crypto más caro.

    
respondido por el Philipp 15.03.2018 - 11:22
fuente
61

JavaScript (JS) se inventó en 1995.

  1. Potencialmente ilegal: la criptografía aún estaba bajo un estricto control de exportación en 1995, por lo que un buen CSPRNG ni siquiera podría haber sido legal distribuirlo en un navegador.
  2. Rendimiento: históricamente, los CSPRNG (generadores de números pseudoaleatorios seguros criptográficamente) son mucho más lentos que los PRNG, así que ¿por qué usar un CSPRNG de forma predeterminada?
  3. Sin una mentalidad de seguridad: en 1995, SSL (lo que ahora conocemos como TLS) se lanzó hace un año. La idea completa de seguridad, y particularmente la seguridad del navegador, no existía realmente.
  4. No es necesario: en 1995, la criptografía segura disponible públicamente era muy nueva en primer lugar; las aplicaciones web aún no existían porque JS estaba siendo inventado; y JS fue diseñado como un lenguaje lento e interpretado para ayudar a las páginas a ser dinámicas. Nadie pensó que "hagamos un CSPRNG lento por defecto para una función aleatoria simple" era necesario en absoluto.
  5. Tan poca necesidad, de hecho, de que no hubiera otra alternativa: JS ni siquiera tenía una API generalmente soportada para un CSPRNG hasta diciembre de 2013, por lo que la criptografía adecuada en aplicaciones web no era posible hasta hace unos años. .
  6. Consistencia: en lugar de cambiar una función existente para que tenga un significado diferente, crearon una nueva función con un nombre diferente. Puede acceder al CSPRNG ahora a través de crypto.getRandomValues .

En resumen: legado, pero también velocidad y consistencia . Los PRNG inseguros siguen siendo mucho más rápidos porque no puede asumir que todo el hardware es compatible con AES, ni depende de la disponibilidad o seguridad de RDRAND.

Personalmente, creo que es hora de cambiar todas las funciones aleatorias con CSPRNG y cambiar el nombre de las funciones inseguras más rápidas a algo como fast_insecure_random() . Solo deberían ser necesarios para los científicos u otras personas que realicen simulaciones que necesiten muchos números aleatorios, pero donde la previsibilidad del RNG no sea un problema. Pero para una función con dos décadas de historia, donde ha existido una alternativa por solo cuatro años (en 2018), puedo entender por qué todavía no estamos en ese punto.

    
respondido por el Luc 15.03.2018 - 12:13
fuente
19

Esto es demasiado largo para un comentario.

Creo que hay una premisa errónea en tu pregunta:

  

en computadoras modernas (e incluso no tan modernas), sería trivial producir cientos de megabytes por segundo usando ChaCha8 o AES-CTR

Está pensando en un navegador de escritorio en una máquina conectada a CA o en una computadora portátil con una batería grande de 10Ah.

Vivimos en un mundo cada vez más móvil, y aunque los dispositivos móviles en estos días son bastante potentes, tienen dos limitaciones importantes: el calor y la duración de la batería. A diferencia de un procesador de escritorio que puede alcanzar fácilmente los 100 ° C, no puede quemar la mano del usuario del teléfono inteligente. Y las baterías de los teléfonos normalmente solo contienen tal vez 1/3 tanto como una computadora portátil (si tienes suerte). Simplemente no hay una buena razón para agregar la generación adicional de calor / consumo de energía si no la necesita.

    
respondido por el Jared Smith 15.03.2018 - 12:56
fuente
16

El motivo principal es que existe una alternativa a Math.random() : vea la respuesta de Philipp . Entonces, quien necesite un criptográfico fuerte puede tenerlo, y aquellos que no lo hacen pueden ahorrar tiempo y (batería).

Pero suponiendo que hubieras preguntado, "por qué, incluso si hay una alternativa más sólida, los desarrolladores no actualizaron Math.random () de la misma forma, es decir, hicieron al azar () una derivada de getRandomValues () - en orden ¿Fortalecer automáticamente muchas aplicaciones? " - Entonces no creo que esto sea realmente confiable, excepto por aquellos desarrolladores que tomaron la decisión (actualización: y como Fate lo tendría, tenemos una respuesta ).

En principio, como ya ha dicho, no hay una razón sólida .

Además, la mayoría de los equipos de desarrollo tienen una acumulación significativa de cosas que son más urgentes de hacer; e incluso un cambio aparentemente pequeño como este requiere pruebas, regresión e ir en contra de la regla dorada " Si no está roto, no la arregle ", una forma más fuerte del criterio YAGNI.

    
respondido por el LSerni 15.03.2018 - 08:15
fuente
14

Los números aleatorios y los bits cryptorandom son animales completamente diferentes. Ni siquiera se utilizan para el mismo propósito. Si desea un número aleatorio distribuido uniformemente entre 0 y 42, entonces desea una distribución uniforme sin un patrón obvio. Tenga en cuenta que si modifica un número mayor con uno más pequeño, entonces no es exactamente una distribución uniforme. Este ejemplo es fácil de ver para un número aleatorio de 0 a 31. Mod. 27. De 0 a 4 aparecen dos veces más a menudo que de 5 a 31.

Hasta que se habla de cryptorandom, la noción de entropía ni siquiera se discute. Un poco de entropía duplica el espacio de búsqueda para adivinar el número (para el usuario previsto de los números).

Cuando pides bits criptoreatorios, pides N bits de entropía. No es lo suficientemente bueno como para tener un patrón no obvio, porque si se descubre una función que la genera (no importa cuán complicada), en realidad hay 0 bits de entropía desde el punto de vista de quienquiera que conozca esta función.

Un buen ejemplo de esto es un generador de números pseudoaleatorios similar a Fortuna. Cifra un número 1 con una clave para el primer número aleatorio (donde el bloque de cifrado es un número grande), luego cifra el número 2 con una clave para el segundo número aleatorio, y así sucesivamente. Con respecto al usuario que no conoce la clave (de K bits) para el cifrado, un bloque de cifrado de N bits perfecto tendrá N bits de entropía para ese bloque.

Si expande a un millón de bits de datos pseudoaleatorios, entonces solo tendrá K bits de entropía si continúa con la misma tecla K. En otras palabras: si tenía un libro de 1 millón los bits que sabe que se generaron con un solo cifrado bajo K, entonces no intente adivinar todos los bits de flujo de cifrado. Simplemente apégate a adivinar la clave y genera la secuencia de cifrado a partir de ella.

Por lo tanto, un generador de números aleatorios es a menudo un cifrado que se vuelve a sembrar con más aleatoriedad, como se puede lograr. En comparación, un simple [0,1] generador de números aleatorios no puede tener más entropía que el número de bits en el número; y normalmente tendrá una distribución extraña que no es exactamente lo que usted quiere también. Crypto necesita cientos de bits, cuando los números de punto flotante son solo 32 o 64 bits, y el algoritmo en sí elimina gran parte de la entropía ... suponiendo que se quiere distribuir algo de manera uniforme desde [0..1], en lugar de decir un Representación de punto flotante hecha de bits aleatorios. Ni siquiera sé qué distribución tendría.

    
respondido por el Rob 16.03.2018 - 04:29
fuente
8

Usted mismo respondió a la pregunta:

  

el estándar explícitamente no garantiza la idoneidad para el uso criptográfico

Entonces, en lugar de cambiar la implementación, el enfoque debe ser educar a los desarrolladores para que escojan la 'herramienta correcta para el trabajo' ™.

Dado eso, y la sobrecarga técnica de alterar la implementación de una función de uso común, así como el hecho de que ya existen soluciones específicas para este problema (consulte la respuesta de @Philipps), no hay ninguna razón convincente para hacer el cambio.

    
respondido por el richzilla 15.03.2018 - 12:39
fuente
5

El diseño del lenguaje de programación debe considerar muchas cosas. Los navegadores son muy potentes y optimizan mucho javascript hoy. Pero cuando considera sistemas integrados, es posible que no tenga una buena fuente de aleatoriedad. Por ejemplo, hay microcontroladores que ejecutan un entorno nodeJS (como).

Este microcontrolador no tiene fuentes aleatorias que garanticen números aleatorios criptográficamente seguros. Por lo tanto, debería requerir la conexión de algún dispositivo que pueda proporcionar una entrada aleatoria a un pin para poder implementar un lenguaje de programación que ofrezca garantías sólidas sobre los números aleatorios. Y necesitaría bastante conocimiento para construir un dispositivo que proporcione suficiente aleatoriedad y para procesar la entrada del dispositivo de una manera adecuada.

    
respondido por el allo 15.03.2018 - 11:10
fuente
5

Como otros, me gustaría señalar que Math.random() no es criptográficamente seguro porque generalmente no es necesario. Pero iría más lejos y argumentaría que no es aconsejable escribir un algoritmo criptográficamente seguro en una especificación a menos que tenga una buena razón.

¿Qué significa ser criptográficamente seguro? Bueno, siempre existe la definición aburrida de "nadie sabe cómo romperlo todavía". ¿Pero qué pasa cuando alguien lo rompe? Si ha especificado un CSPRNG, también tiene que incluir una forma de consultar qué algoritmo está en uso, o hacerlo de otra manera para que el usuario final pueda estar seguro de lo que está obteniendo.

Esto también probablemente lleva a la necesidad de poder admitir múltiples generadores, de modo que el usuario pueda seleccionar uno en el que confíe. Esto añade una enorme complejidad. De repente, una función de 1 línea en una API se convirtió en una suite.

Además, cuando empiezas a hablar criptografía, empiezas a hablar de intentar estar seguro en el generador. Usted menciona el uso de AES para generar números aleatorios: ¿su implementación de AES debe ser inmune a los ataques de canal lateral? Cuando estás escribiendo una biblioteca con el propósito específico de proporcionar garantías criptográficas, no es tan irrazonable tener que hacer esa pregunta. Para una especificación, podría ser terriblemente irrazonable. La inmunidad a los ataques de canal lateral es muy difícil de expresar en el lenguaje de las especificaciones.

¿Y qué has logrado al ponerlo en una especificación? La mayoría de los usuarios de PRNG no necesitan garantías criptográficas en absoluto, por lo que solo desperdicia ciclos de CPU por ellos. Quienes quieran garantías criptográficas probablemente buscarán una biblioteca que admita el conjunto completo de funciones necesarias para sentirse cómodo con dicha criptografía, por lo que no confiarán en Math.random() de todos modos. Todo lo que queda es la demografía que menciona: personas que cometieron un error y usaron una herramienta cuando no deberían hacerlo. Bueno, puedo decirle por experiencia que los principales lenguajes de programación son no un lugar para buscar una API que no pueda usar incorrectamente por error. Están llenos de frases "si haces esto, es tu culpa".

También, considere esto: si usa Math.random() y asume garantías criptográficas, ¿cuáles son las probabilidades de que cometa otro error criptográfico fatal en algún lugar de su algoritmo? ¡Un CSPRNG Math.random() puede proporcionar una falsa sensación de seguridad, y podemos encontrar aún más errores!

    
respondido por el Cort Ammon 16.03.2018 - 23:04
fuente
3

Todo el mundo parece haberse perdido un poco de matiz aquí: los algoritmos criptográficos requieren que un número sea matemático y estadísticamente aleatorio en todas las ejecuciones del algoritmo. Esto significa, por ejemplo, durante un juego o una animación, que podría utilizar una secuencia de números psuedorandomear y esto sería perfectamente correcto para un número "al azar".

Sin embargo, si este número se puede manipular o predecir, por ejemplo, un número aleatorio sembrado (que es el comportamiento predeterminado de las funciones aleatorias de Windows), entonces esta semilla es realmente predecible. Si puedo manipular su aplicación para reiniciarla y luego usar un número aleatorio sembrado, puedo predecir qué número "aleatorio" elegirá. Si esto es posible, entonces la criptografía puede ser derrotada. Una preocupación secundaria también puede ser que algunos algoritmos requieren una distribución de números garantizada en todo el espectro, lo que ciertos generadores psuedorandom no pueden garantizar.

Los generadores de números aleatorios criptográficos tienen un gran conjunto de entradas para crear entropía, por ejemplo, la medición del ruido de disparo desde la entrada del micrófono, las marcas de la hora del día, la suma de comprobación de los registros de RAM, los números de serie, etc. No es imposible, entonces es increíblemente difícil de manipular y predecir. En un sentido criptográfico, el rendimiento no es el objetivo, sino la aleatoriedad "verdadera".

Entonces, dependiendo de su caso de uso, es posible que desee una implementación aleatoria y aleatoria de un número aleatorio, pero si está realizando un intercambio de claves diffie-hellman necesita un algoritmo criptográficamente seguro.

    
respondido por el Spence 22.03.2018 - 14:51
fuente
-1

Otra consideración que no vi que nadie mencionara (o que simplemente pasara por alto) es que algunos usos de la función Math.random() realmente dependen de la repetibilidad cuando se siembran igual que en un momento anterior. Cambiarlo ahora rompería esos usos.

    
respondido por el Bobby Baucom 24.03.2018 - 00:02
fuente

Lea otras preguntas en las etiquetas