¿Por qué utilizar la clase C # System.Random en lugar de System.Security.Cryptography.RandomNumberGenerator?


85

¿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 usemos los generadores de números aleatorios "estándar" de Python, Java y C # y que en su lugar usemos la versión criptográficamente segura.

Conozco la diferencia entre las dos versiones de generadores de números aleatorios (consulte la pregunta 101337 ).

Pero, ¿qué razón hay para no utilizar siempre el generador seguro de números aleatorios? ¿Por qué utilizar System.Random en absoluto? ¿Desempeño quizás?


7
¿Qué prefieres escribir?
Macha

13
Demasiadas personas usan eso seriamente como una justificación para lo que hacen (generalmente no en voz alta). El código se lee más de lo que está escrito, ¿a quién le importan las diferencias triviales de longitud?
Mark Sowul

3
Pero de todos modos, ¿por qué debería usar RNG criptográficos si no está haciendo criptografía?
Mark Sowul

3
@Macha, para eso son los alias ->using R = System.Security.Cryptography.RandomNumberGenerator; R.Create();
cchamberlain

Respuestas:


144

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 alguien más piense que el número puede usarse para algo seguro cuando no lo será?


30
Me gusta mucho el argumento de la intención.
Lernkurve

12
Cabe señalar que Random.GetNext está lejos de ser bueno para "distribuir" los números aleatorios en el espectro, especialmente en un entorno de subprocesos. Me encontré con este problema al escribir un programa para probar diferentes soluciones al problema Rand7 de Rand5. En una prueba rápida enhebrada de 100000 números aleatorios entre 0 y 10, 82470 de los números generados eran 0. Vi discrepancias similares en mis pruebas anteriores. La criptografía aleatoria es muy uniforme en su distribución de números. Supongo que la lección es probar siempre sus datos aleatorios para ver que son "suficientemente aleatorios" para sus necesidades.
Kristoffer L

35
@Kristoffer Creo que lo usaste mal Random. Déjame adivinar: creaste una nueva instancia de la Randomclase para cada número, que dado que está sembrado por un temporizador aproximado se sembrará con el mismo valor durante un intervalo de aproximadamente 1-16 ms.
CodesInChaos

15
@CodesInChaos: Además de eso, existe una condición de carrera Randomque hace que devuelva todos los 0 cuando se usa el mismo objeto en varios subprocesos.
BlueRaja - Danny Pflughoeft

3
@KristofferL: Vea el comentario anterior, también vea esta respuesta
BlueRaja - Danny Pflughoeft

65

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 semilla fijo. Eso es bastante útil, entre otros, durante las pruebas.

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

2
Y está el BitConverter.ToInt32 (valor Byte [], int startIndex) que puede ser más fácil de entender. ;)
sisve

7
Ian Bell y David Braben utilizaron un generador aleatorio en el juego de computadora Elite para crear una amplia lista de planetas y sus atributos (tamaño, etc.), con una memoria muy limitada. Esto también depende de que el generador cree un patrón determinista (a partir de una semilla), que obviamente el Crypto no proporciona (por diseño). Hay más información sobre cómo lo hicieron aquí: wiki.alioth.net/index.php / Random_number_generator y el libro "Infinite Game Universe: Mathematical Techniques" ISBN: 1584500581 tiene una discusión más general sobre tales técnicas.
Daniel James Bryars


2
@phoog "Como resultado, el código de su aplicación no debe asumir que la misma semilla dará como resultado la misma secuencia pseudoaleatoria en diferentes versiones de .NET Framework". - No lo sé, me parece bastante claro. Sin embargo, no me sorprendería que no puedan cambiarlo en la práctica sin romper los programas existentes, a pesar de esta advertencia.
Roman Starkov

2
@phoog: Estás diciendo una cosa y luego exactamente lo contrario. Te estás contradiciendo directamente.
Timwi

53

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

Pero afirmo que lo es. La implementación de .net 4 de Randomtiene varias fallas. Recomiendo usarlo solo si no le importa la calidad de sus números aleatorios. Recomiendo utilizar mejores implementaciones de terceros.

Defecto 1: La siembra

El constructor predeterminado se inicia con la hora actual. Por lo tanto, todas las instancias de Randomcreadas con el constructor predeterminado dentro de un período de tiempo corto (aproximadamente 10 ms) devuelven la misma secuencia. Esto está documentado y "por diseño". Esto es particularmente molesto si desea realizar múltiples subprocesos en su código, ya que no puede simplemente crear una instancia de Randomal comienzo de la ejecución de cada subproceso.

La solución es tener mucho cuidado al usar el constructor predeterminado y sembrar manualmente cuando sea necesario.

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

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

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

Defecto 3: el NextBytes()método es ineficaz.

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

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

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

Estoy seguro de que si alguien inspeccionara el código de bytes fuente / descompilado, encontraría aún más fallas de las que encontré con mi análisis de caja negra.


Muestras de código

r.Next(0x55555555) % 2 está fuertemente 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<mod;i++)
    Console.WriteLine(hist[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<bytes.Length;j++)
      bytes[j]=(byte)r.Next();
}

//One sample per 3 bytes
for(int i=0;i<100000;i++)
{
    for(int j=0;j+2<bytes.Length;j+=3)
    {
        int num=r.Next();
        bytes[j+2]=(byte)(num>>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);
}

1
Interesante, puedo confirmar su hallazgo: en mi máquina Next (1431655765) también da 2/3 con cualquier siembra. ¿Cuál es la magia de 1431655765? ¿Cómo llegaste a este número?
citykid

1
@citykid Mira el número como hexadecimal o bits. Su magia surge de la forma dudosa que se Randomutiliza para transformar un entero de 31 bits en un número con el límite superior especificado. Olvidé los detalles, pero es algo así randomValue * max / 2^{31}.
CodesInChaos

1431655765_10 = 101010101010101010101010101010101_2
Tim S.

6
Hm. Entonces, ¿qué implementación de Random para C # recomienda usar?
Arsen Zahray

1
Santo cielo, la falta de uniformidad en la distribución de Next(), demostrada por usted aquí, es un error bastante espectacular, y todavía está presente hoy, 6 años después de que escribió por primera vez sus hallazgos. (Digo "error" en lugar de simplemente "defecto", porque los documentos afirman que "los números pseudoaleatorios se eligen con la misma probabilidad de un conjunto finito de números" . No es así, y su código aquí lo demuestra).
Mark Amery

24

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

Una prueba simple en mi máquina para llenar un búfer de 4 bytes con datos aleatorios 1,000,000 de 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á llenando, la diferencia se reduce a medida que la sobrecarga para RNGCryptoServiceProvider es menos relevante.


2
Gracias por demostrarlo con una prueba real.
Lernkurve

3
Puede pensar que esto es duro, pero -1 para publicar los resultados de una referencia de rendimiento sin incluir el código de la referencia. Incluso si las características de rendimiento de Randomy RNGCryptoServiceProviderno han cambiado en los últimos 8 años (que por lo que sé que podrían haber hecho), he visto suficientes puntos de referencia completamente rotos utilizados en Stack Overflow para no confiar en los resultados de un punto de referencia cuyo código no está disponible públicamente.
Mark Amery

21

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

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

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


Leí el artículo de Wikipedia y algunas otras fuentes de Internet sobre la entropía y el agotamiento de la entropía, y no lo entiendo del todo. ¿Cómo puedo agotar el grupo de entropía cuando el generador de números aleatorios se alimenta con la hora del sistema, el número de bytes libres, etc.? ¿Cómo pueden otros usarlo como vector de ataque para predecir números aleatorios? ¿Puede dar un ejemplo sencillo? Quizás esta discusión deba desconectarse. en.wikipedia.org/wiki/Entropy_%28computing%29
Lernkurve

3
El tiempo del sistema no es una fuente de entropía, porque es predecible. No estoy seguro de la cantidad de bytes libres, pero dudo que sea una fuente de entropía de alta calidad. Al enviar más solicitudes al servidor, el atacante puede hacer que el número de bytes libres disminuya, haciéndolo parcialmente determinista. Su aplicación se convierte en un vector de ataque porque al agotar la reserva de entropía, obliga a la otra aplicación crítica para la seguridad a usar números aleatorios menos aleatorios, o esperar hasta que se reponga la fuente de entropía.
quant_dev

Entiendo que si uno tiene un generador pseudoaleatorio alimentado con, por ejemplo, una semilla de 32 bits, un ataque de fuerza bruta a menudo será bastante fácil; incluso una semilla de 64 bits puede estar sujeta a ataques de cumpleaños. Sin embargo, una vez que la semilla se vuelve mucho más grande, no veo el riesgo. Si uno tiene un generador aleatorio que para cada byte de salida pasa un estado de 128 bits a través de un algoritmo de cifrado de bloques y luego genera los 8 bits inferiores, ¿cómo podría un atacante, incluso con gigas de bytes de salida consecutivos, inferir el estado, sin debilidades en el propio algoritmo de cifrado?
supercat

11

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


9

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. Existe una amplia gama de RNG, y el Lehmer LCG enlatado que contiene la mayoría de los sistemas de RNG no es el mejor ni necesariamente el más rápido. En sistemas antiguos y lentos, era un compromiso excelente. 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 qué la usará la gran mayoría de personas, 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 ningún riesgo. 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 de lotería en el que hay premios reales involucrados y el dinero real entra en juego en algún punto 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 circular en el mejor de los casos . En estos días, esa es una puerta abierta a 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 sembrarlo de un proveedor criptográficamente fuerte. Esto proporciona un buen compromiso entre velocidad y seguridad. En tal caso, la persona estará buscando algo como el Mersenne Twister o un generador recursivo múltiple de algún tipo.

Si la aplicación es algo así como comunicar grandes cantidades de información financiera a través de una red, ahora existe un gran riesgo y supera con creces cualquier posible recompensa. Todavía hay carros blindados porque a veces hombres fuertemente armados es la única seguridad que es adecuada, y créanme, si una brigada de personas de operaciones especiales con tanques, cazas y helicópteros fuera financieramente viable, sería el método de elección. En un caso como este, tiene sentido usar un RNG criptográficamente fuerte, porque sea cual sea el nivel de seguridad que pueda obtener, no es tanto como desea. Por lo tanto, tomará todo lo que pueda encontrar y el costo es un problema de segundo lugar muy, muy remoto, ya sea en tiempo o en dinero. Y si eso significa que cada secuencia aleatoria tarda 3 segundos en generarse en una computadora muy potente, esperará los 3 segundos,


3
Creo que estás equivocado acerca de tus magnitudes; el envío de datos financieros debe ser extremadamente rápido; Si su algoritmo de negociación puede llegar al resultado 0.1 ms más rápido que la competencia, terminará mejor en la cola de comandos de compra / venta / stop-loss / cotización. 3 segundos es una eternidad. Es por eso que los comerciantes invierten en computadoras increíblemente buenas. Vea la respuesta anterior; Crypt.RNG tarda solo 0,0028 ms por nuevo número; 0.0000028 segundos, por lo que tiene una diferencia de 9 órdenes de magnitud en términos de la cantidad de procesamiento que requiere y también de la importancia de la velocidad.
Henrik


4

No todo el mundo necesita números aleatorios criptográficamente seguros, y podrían beneficiarse más de un prng sencillo más rápido. 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 volver a crear, 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 programa con exactamente la misma secuencia de números aleatorios que bloqueó el programa.


2

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.


2

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


1
Si sólo System.Random hiciera cualquiera de las dos ... oh, bueno.
user2864740

2

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

La razón para usarlo System.Randomes si desea estas propiedades, es decir, una secuencia determinista, que está garantizada para producir 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.Randominvalidar 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, con fines de depuración, puede hacerlo inicializando la secuencia con el valor registrado .

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

La única razón que se me ocurre es la compatibilidad con versiones anteriores del código existente que usa esta clase.

En resumen, si desea mejorar la secuencia sin cambiar el resto de su código, adelante.


1

Escribí un juego (Crystal Sliders en el iPhone: aquí ) que pondría una serie "aleatoria" de gemas (imágenes) en el mapa y tú rotarías el mapa como querías y las seleccionarías y desaparecieron. - Similar a Bejeweled. Estaba usando Random (), y fue sembrado con el número de ticks de 100ns desde que se inició el teléfono, una semilla bastante aleatoria.

Me pareció sorprendente que generara juegos que eran casi idénticos entre sí; de las 90 gemas o más, de 2 colores, obtendría dos EXACTAMENTE iguales, ¡excepto por 1 a 3 gemas! Si lanza 90 monedas y obtiene el mismo patrón, excepto 1-3, ¡es MUY improbable! Tengo varias capturas de pantalla que muestran lo mismo. ¡Me sorprendió lo malo que era System.Random ()! Supuse que DEBO haber escrito algo horriblemente mal 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 usando desde 1985 aproximadamente, que es MUY 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é el sembrado inicial.

El original está aquí:

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

A lo largo de los años, he lanzado todas las pruebas de números aleatorios que pude pensar en esto, y las superó a todas. No espero que tenga ningún valor como criptográfico, pero devuelve un número tan rápido como "return * p ++;" hasta que se agoten los 521 bits, y luego ejecuta un proceso rápido sobre los bits para crear nuevos aleatorios.

Creé un contenedor de 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 MUY mejor - Dios mío, estaba asombrado - no debería haber forma de que pudiera decirlo con solo mirar las pantallas de aproximadamente 90 gemas en un patrón, pero hice un lanzamiento de emergencia de mi juego después de esto.

Y nunca volvería a usar System.Random () para nada. ¡Me sorprende que su versión esté impresionado por algo que ahora tiene 30 años!

-Juegos de Traderhut


3
Mi primera suposición es que recreaste con Randomdemasiada frecuencia. Solo debe crearse una vez invocando Nextesa instancia muchas veces. Randomes malo, pero no tan malo. ¿Puede publicar un programa de muestra junto con un par de semillas que presenten este problema?
CodesInChaos

El código crearía un Random () al comienzo de cada nivel (pero era un problema mayor con el nivel 1 más que con los posteriores) El código era aproximadamente el siguiente:
Traderhut Games

Rnd = new Random ((uint) GameSeed); NextGameSeed = Rnd.Next (2000000000); Cada nivel usaba un nuevo Aleatorio que se creó con una nueva semilla: la Semilla se guardó para cada nivel para poder recrear el mapa y también confirmar la secuencia de semillas aleatorias coincidentes. Esto me permite confirmar que el juego es una serie válida de mapas que se han resuelto y recrear el juego.
Traderhut Games

E Inicialmente, Random se creó en base a System.DateTime.Now.Ticks (o 0), y luego GameSeed se eligió usando la misma llamada que Rnd.Next () anterior. Si no puedo hacer esto, entonces hay un problema serio con la siembra del generador de números aleatorios.
Traderhut Games

¡esta no es una respuesta a la pregunta original!
Mike Dinescu

-1

Dado que System.Random es criticado aquí por su "incorrección" y sesgo, me revisé.

distribución

Este código f # demuestra que se comporta realmente bien, en mi máquina promedio:

let r = System.Random()
Seq.init 1000000 (fun _ -> r.Next(0,10))
|> Seq.toList
|> Seq.groupBy id
|> Seq.map (fun (v,ls) -> v, ls |> Seq.length)
|> Seq.sortBy fst
|> Seq.iter (printfn "%A")

(0, 100208)
(1, 99744)
(2, 99929)
(3, 99827)
(4, 100273)
(5, 100280)
(6, 100041)
(7, 100001)
(8, 100175)
(9, 99522)    

Las versiones del marco, la máquina y el sistema operativo pueden marcar la diferencia. Ingrese el código en F # interactivo en su máquina y pruébelo usted mismo. Para la cirptografía leí

let arr = [| 0uy |]
let rr = System. Security.Cryptography.RandomNumberGenerator.Create()
Seq.init 1000000 (fun _ -> rr.GetBytes(arr); arr.[0])
|> Seq.toList
|> Seq.groupBy id
|> Seq.map (fun (v,ls) -> v, ls |> Seq.length)
|> Seq.sortBy fst
|> Seq.take 10 // show first 10 bytes
|> Seq.iter (printfn "%A")

// distribution of first 10 bytes
(0uy, 3862)
(1uy, 3888)
(2uy, 3921)
(3uy, 3926)
(4uy, 3948)
(5uy, 3889)
(6uy, 3922)
(7uy, 3797)
(8uy, 3861)
(9uy, 3874)

actuación

#time

let arr = [| 0uy |]

let r = System.Random()
Seq.init 1000000 (fun _ -> r.NextBytes(arr); arr.[0] |> int64) |> Seq.sum

Real: 00:00:00.204, CPU: 00:00:00.203, GC gen0: 45, gen1: 1, gen2: 1
val it : int64 = 127503467L

let rr = System. Security.Cryptography.RandomNumberGenerator.Create()
Seq.init 1000000 (fun _ -> rr.GetBytes(arr); arr.[0] |> int64) |> Seq.sum

Real: 00:00:00.365, CPU: 00:00:00.359, GC gen0: 44, gen1: 0, gen2: 0
val it : int64 = 127460809L

lo que sugiere una relación 1: 2 y un comportamiento de memoria algo más agradable de la versión criptográfica.

conclusión

Principalmente por su API mucho más agradable, algo por su rendimiento y bastante buena distribución, se prefiere System.Random. System.Random también podría reducir las dependencias de la biblioteca y, si se porta un marco, es probable que System.Random esté disponible antes que la variante Crypto.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.