¿Por qué usar el System.Random clase C # en vez de System.Security.Cryptography.RandomNumberGenerator?

¿Por qué alguien usaría el generador de números aleatorios “estándar” de System.Random en lugar de usar siempre el generador de números aleatorios criptográficamente seguro de System.Security.Cryptography.RandomNumberGenerator (o sus subclases porque RandomNumberGenerator es abstracto)?

Nate Lawson nos dice en su presentación de Google Tech Talk ” Crypto Strikes Back ” en el minuto 13:11 que no debe usar los generadores de números aleatorios “estándar” de Python, Java y C # y, en su lugar, usar la versión criptográficamente segura.

Sé la diferencia entre las dos versiones de generadores de números aleatorios (vea la pregunta 101337 ).

Pero, ¿qué razón hay para no usar siempre el generador seguro de números aleatorios? ¿Por qué usar System.Random en absoluto? Rendimiento tal vez?

Velocidad e intención Si está generando un número aleatorio y no necesita seguridad, ¿por qué usar una función de criptografía lenta? No necesita seguridad, entonces, ¿por qué hacer que otra persona piense que el número puede usarse para algo seguro cuando no será así?

Además de la velocidad y la interfaz más útil ( NextDouble() etc.) también es posible hacer una secuencia aleatoria repetible utilizando un valor de NextDouble() fijo. Eso es bastante útil, entre otros durante la prueba.

 Random gen1 = new Random(); // auto seeded by the clock Random gen2 = new Random(0); // Next(10) always yields 7,8,7,5,2,.... 

En primer lugar, la presentación que vinculó solo habla de números aleatorios por motivos de seguridad. Por lo tanto, no afirma que Random sea ​​malo para fines no relacionados con la seguridad.

Pero sí afirmo que es. La implementación .net 4 de Random tiene fallas de varias maneras. Recomiendo solo usarlo si no te importa la calidad de tus números aleatorios. Recomiendo usar mejores implementaciones de terceros.

Defecto 1: La siembra

El constructor predeterminado se siembra con la hora actual. Por lo tanto, todas las instancias de Random creadas con el constructor predeterminado dentro de un marco de tiempo corto (aproximadamente 10 ms) devuelven la misma secuencia. Esto está documentado y “por diseño”. Esto es particularmente molesto si quiere unir varias veces su código, ya que no puede simplemente crear una instancia de Random al comienzo de la ejecución de cada subproceso.

La solución alternativa es ser extremadamente cuidadoso al usar el constructor predeterminado y sembrar manualmente cuando sea necesario.

Otro problema aquí es que el espacio de semilla es bastante pequeño (31 bits). Entonces, si genera 50k instancias de Random con semillas perfectamente aleatorias, probablemente obtendrá una secuencia de números aleatorios dos veces (debido a la paradoja del cumpleaños ). Entonces, la siembra manual no es fácil de hacer bien tampoco.

Defecto 2: la distribución de los números aleatorios devueltos por Next(int maxValue) está sesgada

Hay parámetros para los cuales Next(int maxValue) claramente no es uniforme. Por ejemplo, si calcula r.Next(1431655765) % 2 obtendrá 0 en aproximadamente 2/3 de las muestras. (Código de muestra al final de la respuesta).

Defecto 3: El método NextBytes() es ineficiente.

El costo por byte de NextBytes() es casi tan grande como el costo de generar una muestra de entero completo con Next() . De esto, sospecho que de hecho crean una muestra por byte.

Una mejor implementación usando 3 bytes de cada muestra aceleraría NextBytes() en casi un factor 3.

Gracias a este error Random.NextBytes() es solo un 25% más rápido que System.Security.Cryptography.RNGCryptoServiceProvider.GetBytes en mi máquina (Win7, Core i3 2600MHz).

Estoy seguro de que si alguien inspeccionara el código de origen / código decomstackdo encontraría aún más defectos de los que encontré en mi análisis de caja negra.


Ejemplos de código

r.Next(0x55555555) % 2 está muy sesgado:

 Random r = new Random(); const int mod = 2; int[] hist = new int[mod]; for(int i = 0; i < 10000000; i++) { int num = r.Next(0x55555555); int num2 = num % 2; hist[num2]++; } for(int i=0;i 

Actuación:

 byte[] bytes=new byte[8*1024]; var cr=new System.Security.Cryptography.RNGCryptoServiceProvider(); Random r=new Random(); // Random.NextBytes for(int i=0;i<100000;i++) { r.NextBytes(bytes); } //One sample per byte for(int i=0;i<100000;i++) { for(int j=0;j>16); bytes[j+1]=(byte)(num>>8); bytes[j]=(byte)num; } //Yes I know I'm not handling the last few bytes, but that won't have a noticeable impact on performance } //Crypto for(int i=0;i<100000;i++) { cr.GetBytes(bytes); } 

System.Random es mucho más eficaz ya que no genera números aleatorios criptográficamente seguros.

Una simple prueba en mi máquina llenando un buffer de 4 bytes con datos aleatorios 1,000,000 veces toma 49 ms para Random, pero 2845 ms para RNGCryptoServiceProvider. Tenga en cuenta que si aumenta el tamaño del búfer que está rellenando, la diferencia se reduce ya que la sobrecarga de RNGCryptoServiceProvider es menos relevante.

Las razones más obvias ya se han mencionado, así que aquí hay una más oscura: los PRNG criptográficos generalmente necesitan ser resembrados continuamente con entropía “real”. Por lo tanto, si usa un CPRNG con demasiada frecuencia, podría agotar el conjunto de entropía del sistema, lo que (dependiendo de la implementación del CPRNG) o lo debilitará (permitiendo así que un atacante lo prediga) o se bloqueará al intentar llenarlo. su grupo de entropía (convirtiéndose así en un vector de ataque para un ataque DoS).

De cualquier manera, su aplicación se ha convertido en un vector de ataque para otras aplicaciones totalmente independientes que, a diferencia del suyo, realmente dependen de las propiedades criptográficas del CPRNG.

Este es un problema del mundo real, por cierto, que se ha observado en servidores sin cabeza (que naturalmente tienen entropía bastante pequeña porque carecen de fonts de entropía como entrada de mouse y teclado) que ejecutan Linux, donde las aplicaciones utilizan incorrectamente el kernel /dev/random CPRNG para todo tipo de números aleatorios, mientras que el comportamiento correcto sería leer un pequeño valor inicial de /dev/urandom y usarlo para sembrar su propio PRNG.

Si está progtwigndo un juego de cartas en línea o un lotter, querrá asegurarse de que la secuencia sea casi imposible de adivinar. Sin embargo, si muestra usuarios, por ejemplo, una cita del día, el rendimiento es más importante que la seguridad.

Esto se ha discutido con cierto detalle, pero en última instancia, la cuestión del rendimiento es una consideración secundaria al seleccionar un RNG. Hay una gran variedad de RNG disponibles, y el Lehmer LCG enlatado en el que se basa la mayoría de los RNGs del sistema no es el mejor ni incluso necesariamente el más rápido. En sistemas viejos y lentos, era un excelente compromiso. Ese compromiso rara vez es realmente relevante en estos días. La cosa persiste en los sistemas actuales principalmente porque A) la cosa ya está construida, y no hay una razón real para “reinventar la rueda” en este caso, y B) para lo que la gran mayoría de la gente lo va a usar, es ‘suficientemente bueno’.

En última instancia, la selección de un RNG se reduce a la relación riesgo / recompensa. En algunas aplicaciones, por ejemplo, un videojuego, no existe riesgo alguno. Un Lehmer RNG es más que adecuado, y es pequeño, conciso, rápido, bien entendido y “en la caja”.

Si la aplicación es, por ejemplo, un juego de póquer en línea o una lotería donde hay premios reales involucrados y dinero real entra en juego en algún momento de la ecuación, el Lehmer “en la caja” ya no es adecuado. En una versión de 32 bits, solo tiene 2 ^ 32 posibles estados válidos antes de que comience a ciclo en el mejor de los casos . En estos días, esa es una puerta abierta para un ataque de fuerza bruta. En un caso como este, el desarrollador querrá ir a algo así como un RNG de período muy largo de algunas especies, y probablemente lo saque de un proveedor criptográficamente fuerte. Esto proporciona un buen compromiso entre velocidad y seguridad. En tal caso, la persona buscará algo como Mersenne Twister o un Generador Recursivo Múltiple de algún tipo.

Si la aplicación es algo así como la comunicación de grandes cantidades de información financiera a través de una red, ahora existe un gran riesgo, y pesa mucho más que cualquier posible recompensa. Todavía hay carros blindados porque a veces los hombres fuertemente armados son la única seguridad que es adecuada, y créanme, si una brigada de personas de operaciones especiales con tanques, cazas y helicópteros fuera económicamente factible, sería el método de elección. En un caso como este, usar un generador de números aleatorios criptográficamente fuerte tiene sentido, porque cualquiera que sea el nivel de seguridad que pueda obtener, no es tanto como usted desea. Así que tomará todo lo que pueda encontrar, y el costo es un problema de segundo lugar muy, muy remoto, ya sea en tiempo o dinero. Y si eso significa que cada secuencia aleatoria tarda 3 segundos en generar en una computadora muy poderosa, vas a esperar los 3 segundos, porque en el esquema de las cosas, ese es un costo trivial.

Tenga en cuenta que la clase System.Random en C # está codificada incorrectamente, por lo que debe evitarse.

https://connect.microsoft.com/VisualStudio/feedback/details/634761/system-random-serious-bug#tabs

No todo el mundo necesita números aleatorios criptográficamente seguros, y es posible que se beneficien más de una prn simple y más rápida. Quizás lo más importante es que puede controlar la secuencia de los números System.Random.

En una simulación que utiliza números aleatorios que quizás desee recrear, vuelva a ejecutar la simulación con la misma semilla. Puede ser útil para rastrear errores cuando también desea regenerar un escenario defectuoso dado, ejecutando su progtwig con la misma secuencia exacta de números aleatorios que bloquearon el progtwig.

Si no necesito la seguridad, es decir, solo quiero un valor relativamente indeterminado, no uno que sea criptográficamente fuerte, Random tiene una interfaz mucho más fácil de usar.

Las diferentes necesidades requieren diferentes RNG. Para cifrado, desea que sus números aleatorios sean lo más aleatorios posible. Para las simulaciones de Monte Carlo, desea que ocupen el espacio de manera uniforme y que puedan iniciar el RNG desde un estado conocido.

Random no es un generador de números aleatorios, es un generador de secuencias pseudoaleatorio determinista, que toma su nombre por razones históricas.

El motivo para usar System.Random es si desea estas propiedades, a saber, una secuencia determinista, que garantiza que se produce la misma secuencia de resultados cuando se inicializa con la misma semilla.

Si desea mejorar la “aleatoriedad” sin sacrificar la interfaz, puede heredar de System.Random anulando varios métodos.

¿Por qué querrías una secuencia determinista?

Una razón para tener una secuencia determinista en lugar de una verdadera aleatoriedad es porque es repetible.

Por ejemplo, si está ejecutando una simulación numérica, puede inicializar la secuencia con un número aleatorio (verdadero) y registrar qué número se utilizó .

Luego, si desea repetir exactamente la misma simulación, por ejemplo, para la depuración, puede hacerlo inicializando la secuencia con el valor registrado .

¿Por qué querrías esta secuencia particular, no muy buena?

La única razón por la que puedo pensar sería por la compatibilidad con el código existente que usa esta clase.

En resumen, si quieres mejorar la secuencia sin cambiar el rest de tu código, adelante.

Escribí un juego (Crystal Sliders en el iPhone: aquí ) que colocaría una serie “aleatoria” de gems (imágenes) en el mapa y rotarías el mapa como lo querías y las seleccionarías y se irían. – Similar a Bejeweled. Estaba usando Random (), y fue sembrado con la cantidad de 100ns tics desde que el teléfono arrancó, una semilla bastante aleatoria.

Me pareció increíble que generara juegos que fueran casi idénticos entre sí: de las 90 o más gems, de 2 colores, ¡obtendría dos EXACTAMENTE iguales, excepto por 1 a 3 gems! Si giras 90 monedas y obtienes el mismo patrón a excepción de 1-3 volteos, ¡eso es MUY poco probable! Tengo varias capturas de pantalla que muestran lo mismo. ¡Me sorprendió lo malo que era System.Random ()! Supuse que DEBO haber escrito algo terriblemente incorrecto en mi código y lo estaba usando mal. Aunque estaba equivocado, era el generador.

Como experimento, y como solución final, volví al generador de números aleatorios que he estado utilizando desde 1985, que es muchísimo mejor. Es más rápido, tiene un período de 1.3 * 10 ^ 154 (2 ^ 521) antes de que se repita. El algoritmo original fue sembrado con un número de 16 bits, pero lo cambié a un número de 32 bits y mejoré la siembra inicial.

El original está aquí:

ftp://ftp.grnet.gr/pub/lang/algorithms/c/jpl-c/random.c

Con el paso de los años, he echado todas las pruebas de números aleatorios que pude pensar en esto, y las he superado a todas. No espero que tenga algún valor como criptográfico, pero devuelve un número tan rápido como “return * p ++;” hasta que se agote los 521 bits, y luego ejecuta un proceso rápido sobre los bits para crear nuevos aleatorios.

Creé un contenedor C # – lo llamé JPLRandom () implementé la misma interfaz que Random () y cambié todos los lugares donde lo llamé en el código.

La diferencia fue inmensamente mejor, Dios mío, me sorprendió, no debería haber forma de que yo lo notara simplemente mirando las pantallas de 90 gems en un patrón, pero hice un lanzamiento de emergencia de mi juego después de esto.

Y nunca usaría System.Random () para nada nunca más. ¡ME HA SACUDIDO que su versión se haya quedado impresionado por algo que ahora tiene 30 años!

-Traderhut Juegos