Diferencia entre java.util.Random y java.security.SecureRandom

202

Mi equipo recibió un código del lado del servidor (en Java) que genera tokens aleatorios y tengo una pregunta sobre lo mismo:

El propósito de estos tokens es bastante sensible: se usa para identificación de sesión, enlaces de restablecimiento de contraseña, etc. Por lo tanto, deben ser criptográficamente aleatorios para evitar que alguien los adivine o los fuerce de manera bruta. El token es "largo", por lo que tiene 64 bits de largo.

El código actualmente usa la java.util.Randomclase para generar estos tokens. La documentación para java.util.Randomestablece claramente lo siguiente:

Las instancias de java.util.Random no son criptográficamente seguras. Considere en su lugar utilizar SecureRandom para obtener un generador de números pseudoaleatorios criptográficamente seguro para uso de aplicaciones sensibles a la seguridad.

Sin embargo, la forma en que el código está usando actualmente java.util.Randomes la siguiente: crea una instancia de la java.security.SecureRandomclase y luego usa el SecureRandom.nextLong()método para obtener la semilla que se usa para crear instancias de la java.util.Randomclase. Luego usa el java.util.Random.nextLong()método para generar el token.

Entonces mi pregunta ahora: ¿sigue siendo inseguro dado que java.util.Randomse está sembrando java.security.SecureRandom? ¿Necesito modificar el código para que se use java.security.SecureRandomexclusivamente para generar los tokens?

Actualmente, la semilla del código es la Randomúnica en el inicio

usuario967973
fuente
14
Una vez sembrado, la salida de java.util.Random es una secuencia determinista de números. Puede que no quieras eso.
Peter Štibraný
1
¿El código genera Randomuna vez al inicio, o genera una nueva para cada token? Con suerte, esta es una pregunta estúpida, pero pensé en comprobarlo.
Tom Anderson
8
Random sólo tiene un estado interno de 48 bits y se repetirá después de 2 ^ 48 llamadas a nextLong () lo que significa que no va a producir toda posible longo doublevalores.
Peter Lawrey
3
Hay otro problema grave. 64 bits significa 1.84 * 10 ^ 19 combinaciones posibles, que son muy pocas para resistir un ataque sofisticado. Hay máquinas que descifraron un código DES de 56 bits (factor 256 menos) con 90 * 10 ^ 9 claves por segundo en 60 horas. ¡Use 128 bits o dos largos!
Thorsten S.

Respuestas:

232

La implementación estándar de Oracle JDK 7 usa lo que se llama un Generador Congruencial Lineal para producir valores aleatorios java.util.Random.

Tomado del java.util.Randomcódigo fuente (JDK 7u2), de un comentario sobre el método protected int next(int bits), que es el que genera los valores aleatorios:

Este es un generador de números pseudoaleatorios congruentes lineales, según lo definido por DH Lehmer y descrito por Donald E. Knuth en The Art of Computer Programming, Volumen 3: Algoritmos Seminuméricos , sección 3.2.1.

Previsibilidad de generadores congruenciales lineales

Hugo Krawczyk escribió un artículo bastante bueno sobre cómo se pueden predecir estos LCG ("Cómo predecir generadores congruentes"). Si tiene suerte e interés, aún puede encontrar una versión gratuita y descargable en la web. Y hay mucha más investigación que muestra claramente que nunca debe usar un LCG para fines críticos de seguridad. Esto también significa que sus números aleatorios son predecibles en este momento, algo que no desea para ID de sesión y similares.

Cómo romper un generador congruencial lineal

La suposición de que un atacante tendría que esperar a que se repita el LCG después de un ciclo completo es incorrecta. Incluso con un ciclo óptimo (el módulo m en su relación de recurrencia) es muy fácil predecir valores futuros en mucho menos tiempo que un ciclo completo. Después de todo, es solo un montón de ecuaciones modulares que deben resolverse, lo que se vuelve fácil tan pronto como haya observado suficientes valores de salida del LCG.

La seguridad no mejora con una semilla "mejor". Simplemente no importa si siembras con un valor aleatorio generado por SecureRandomo incluso si produces el valor tirando un dado varias veces.

Un atacante simplemente calculará la semilla a partir de los valores de salida observados. Esto lleva mucho menos tiempo que 2 ^ 48 en el caso de java.util.Random. Los incrédulos pueden probar este experimento , donde se muestra que puede predecir Randomresultados futuros observando solo dos (!) Valores de salida en el tiempo aproximadamente 2 ^ 16. No toma ni un segundo en una computadora moderna para predecir la salida de sus números aleatorios en este momento.

Conclusión

Reemplace su código actual. Uso SecureRandomexclusivo. Entonces, al menos, tendrá una pequeña garantía de que el resultado será difícil de predecir. Si desea las propiedades de un PRNG criptográficamente seguro (en su caso, eso es lo que desea), debe elegir SecureRandomsolo. Ser inteligente para cambiar la forma en que se suponía que debía usarse casi siempre resultará en algo menos seguro ...

realzar
fuente
44
Muy útil, puede ser que también podría explicar cómo funciona SecureRandom (como se explica cómo funciona aleatorio) ..
gresdiplitude
44
Eso derrota el propósito de secureRandom
Azulflame
Lo sé, aprendí esa lección de la manera difícil. Pero un cifrador resistente y una fuente difícil de encontrar funciona bien. Notch podría aprender algo al respecto (codifica la contraseña de su usuario en un archivo .lastlogin, codificado con cifrado básico utilizando "contraseña" como clave)
Azulflame
1
La verdadera pregunta aquí: si Java puede producir un prng más seguro con una API similar, ¿por qué no reemplazaron el roto?
Joel Coehoorn
11
@JoelCoehoorn No es que Randomesté roto, solo debe usarse en diferentes escenarios. Por supuesto, siempre puedes usar SecureRandom. Pero en general, SecureRandomes notablemente más lento que puro Random. Y hay casos en los que solo le interesan las buenas propiedades estadísticas y el excelente rendimiento, pero realmente no le importa la seguridad: las simulaciones de Monte-Carlo son un buen ejemplo. Hice comentarios sobre eso en una respuesta similar , tal vez lo encuentres útil.
relieve
72

Un aleatorio tiene solo 48 bits, mientras que SecureRandom puede tener hasta 128 bits. Por lo tanto, las posibilidades de repetir en securerandom son muy pequeñas.

Random usa el system clockcomo semilla / o para generar la semilla. Por lo tanto, pueden reproducirse fácilmente si el atacante sabe la hora en que se generó la semilla. Pero SecureRandom toma de su (pueden ser intervalos entre pulsaciones de teclas, etc., la mayoría de los recopilan estos datos y los almacenan en archivos ) y los utiliza como semilla. Entonces, si el tamaño del token pequeño está bien (en caso de Aleatorio), puede continuar usando su código sin ningún cambio, ya que está utilizando SecureRandom para generar la semilla. Pero si desea tokens más grandes (a los que no puede estar sujeto ), use SecureRandom: En caso de que se requieran solo intentos aleatorios , con las CPU avanzadas de hoy en día es posible romperlo en el tiempo práctico. Pero para que se requieran intentos aleatorios seguros , lo que llevará años y años alcanzar el equilibrio con las máquinas avanzadas de hoy. Vea este enlace para más detalles. EDITARRandom Dataos/dev/random and /dev/urandom in case of linux/solaris
brute force attacks
2^482^128



Después de leer los enlaces proporcionados por @emboss, está claro que la semilla, por aleatoria que sea, no debería usarse con java.util.Random. Es muy fácil calcular la semilla observando la salida.

Vaya a SecureRandom : use PRNG nativo (como se indica en el enlace anterior) porque toma valores aleatorios del /dev/randomarchivo para cada llamada anextBytes(). De esta forma, un atacante que observe la salida no podrá distinguir nada a menos que esté controlando el contenido del /dev/randomarchivo (lo cual es muy poco probable).
El algoritmo sha1 prng calcula la semilla solo una vez y si su VM está funcionando durante meses usando el mismo semilla, podría ser rajado por un atacante que observa pasivamente la salida

NOTA : Si está llamando nextBytes()más rápido de lo que su sistema operativo puede escribir bytes aleatorios (entropía) en el /dev/random, puede tener problemas al usar NATIVE PRNG . En ese caso, use una instancia SHA1 PRNG de SecureRandom y cada pocos minutos (o algún intervalo), siembre esta instancia con el valor denextBytes()de una instancia NATIVE PRNG de SecureRandom. Ejecutar estos dos paralelos asegurará que esté sembrando regularmente con valores aleatorios verdaderos, sin agotar la entropía obtenida por el sistema operativo.

Ashwin
fuente
Se requiere mucho menos de 2 ^ 48 para predecir a Random, el OP no debería usarse Randomen absoluto.
relieve
@emboss: estoy hablando de fuerza bruta.
Ashwin
1
Tenga cuidado con Linux: ¡puede alcanzar el agotamiento de la entropía (más en VM que con hardware)! Mire /proc/sys/kernel/random/entropy_availy verifique con algunos volcados de hilos que no haya demasiado tiempo de espera al seguir leyendo/dev/random
Yves Martin
2
Tenga en cuenta que Oracle JRE (al menos 1.7) funciona con / dev / urandom de forma predeterminada y no con / dev / random, por lo que el sufijo de su respuesta ya no es correcto. para verificar, verifique $ JAVA_HOME / lib / security / java.security para la propiedad securerandom.source
Boaz
1
Nuestro archivo java.security tenía securerandom.source = file: / dev / urandom en lugar de file: /// dev / urandom (dos barras después de dos puntos para el protocolo de archivo, luego una barra más para la raíz del sistema de archivos), lo que hace que retroceda a / dev / random, que causó problemas con el agotamiento del grupo de entropía. No se pudo editar, así que tuve que establecer una propiedad del sistema java.security.egd en la correcta al iniciar la aplicación.
maxpolk
11

Si corre dos veces java.util.Random.nextLong()con la misma semilla, producirá el mismo número. Por razones de seguridad que desea seguirjava.security.SecureRandom porque es mucho menos predecible.

Las 2 clases son similares, creo que sólo tiene que cambiar Randoma SecureRandomuna herramienta de refactorización y la mayor parte de su código existente funcionará.

Mualig
fuente
11
Si toma dos instancias de cualquier PRNG y lo inicializa con el mismo valor, siempre obtiene los mismos números aleatorios, incluso el uso de SecureRandom no cambia eso. Todos los PRNG son deterministas y, por lo tanto, predecibles si conoce la semilla.
Robert
1
Existen diferentes implementaciones de SecureRandom, algunas son PRNG, otras no. Por otro lado, java.util.Random siempre es PRNG (como se define en su Javadoc).
Peter Štibraný
3

Si cambiar su código existente es una tarea asequible, le sugiero que use la clase SecureRandom como se sugiere en Javadoc.

Incluso si encuentra que la implementación de la clase Random usa la clase SecureRandom internamente. no debes dar por sentado que:

  1. Otras implementaciones de VM hacen lo mismo.
  2. La implementación de la clase Random en futuras versiones de JDK todavía usa la clase SecureRandom

Por lo tanto, es una mejor opción seguir la sugerencia de documentación e ir directamente con SecureRandom.

Andrea Parodi
fuente
No creo que la pregunta original indicara que la java.util.Randomimplementación se usa SecureRandominternamente, dice que su código se usa SecureRandompara sembrar el Random. Aún así, estoy de acuerdo con ambas respuestas hasta ahora; Es mejor usar SecureRandompara evitar una solución explícitamente determinista.
Palpatim
2

La implementación de referencia actual de java.util.Random.nextLong()hace dos llamadas al método next(int)que expone directamente 32 bits de la semilla actual:

protected int next(int bits) {
    long nextseed;
    // calculate next seed: ...
    // and store it in the private "seed" field.
    return (int)(nextseed >>> (48 - bits));
}

public long nextLong() {
    // it's okay that the bottom word remains signed.
    return ((long)(next(32)) << 32) + next(32);
}

Los 32 bits superiores del resultado de nextLong() son los bits de la semilla en ese momento. Como el ancho de la semilla es de 48 bits (dice el javadoc), es suficiente * iterar sobre los 16 bits restantes (eso es solo 65.536 intentos) para determinar la semilla que produjo el segundo 32 bits.

Una vez que se conoce la semilla, todos los siguientes tokens se pueden calcular fácilmente.

Usando la salida de nextLong()directamente, en parte el secreto de PNG en un grado tal que todo el secreto se puede calcular con muy poco esfuerzo. ¡Peligroso!

* Se necesita cierto esfuerzo si los segundos 32 bits son negativos, pero uno puede descubrirlo.

Mate
fuente
Correcto. ¡Vea cómo descifrar rápidamente java.util.random en jazzy.id.au/default/2010/09/20/… !
ingyhere
2

La semilla no tiene sentido. Un buen generador aleatorio difiere en el número primario elegido. Cada generador aleatorio comienza desde un número e itera a través de un 'anillo'. Lo que significa que vienes de un número al siguiente, con el antiguo valor interno. Pero después de un tiempo vuelves al principio y comienzas de nuevo. Entonces corres ciclos. (el valor de retorno de un generador aleatorio no es el valor interno)

Si usa un número primo para crear un anillo, se eligen todos los números en ese anillo, antes de completar un ciclo completo a través de todos los números posibles. Si toma números no primos, no se eligen todos los números y se obtienen ciclos más cortos.

Los números primos más altos significan ciclos más largos antes de volver al primer elemento nuevamente. Entonces, el generador aleatorio seguro solo tiene un ciclo más largo, antes de llegar al principio nuevamente, por eso es más seguro. No puede predecir la generación de números tan fácil como con ciclos más cortos.

Con otras palabras: tienes que reemplazar todo.

Nicolas
fuente
0

Intentaré usar palabras muy básicas para que puedas entender fácilmente la diferencia entre Random y secureRandom y la importancia de SecureRandom Class.

¿Alguna vez se preguntó cómo se genera la OTP (contraseña única)? Para generar una OTP, también usamos las clases Random y SecureRandom. Ahora para fortalecer su OTP, SecureRandom es mejor porque tomó 2 ^ 128 intentos, descifrar el OTP que es casi imposible con la máquina actual, pero si se usa Random Class, entonces su OTP puede ser descifrado por alguien que puede dañar sus datos porque se tomó solo 2 ^ 48 intento, para romper.

Sachin Pathak
fuente