Différence entre java.util.Random et java.security.SecureRandom

202

Mon équipe a reçu un code côté serveur (en Java) qui génère des jetons aléatoires et j'ai une question à ce sujet -

Le but de ces jetons est assez sensible - utilisé pour l'identifiant de session, les liens de réinitialisation de mot de passe, etc. Ils doivent donc être cryptographiquement aléatoires pour éviter que quelqu'un ne les devine ou les force brutalement. Le jeton est un "long" donc il est long de 64 bits.

Le code utilise actuellement la java.util.Randomclasse pour générer ces jetons. La documentation de java.util.Randomindique clairement ce qui suit:

Les instances de java.util.Random ne sont pas cryptographiquement sécurisées. Envisagez plutôt d'utiliser SecureRandom pour obtenir un générateur de nombres pseudo-aléatoires cryptographiquement sécurisé à utiliser par les applications sensibles à la sécurité.

Cependant, la façon dont le code utilise actuellement java.util.Randomest la suivante: il instancie la java.security.SecureRandomclasse, puis utilise la SecureRandom.nextLong()méthode pour obtenir la valeur de départ utilisée pour instancier la java.util.Randomclasse. Ensuite, il utilise une java.util.Random.nextLong()méthode pour générer le jeton.

Donc, ma question maintenant - est-il encore peu sûr étant donné que le java.util.Randomsemis est utilisé java.security.SecureRandom? Dois-je modifier le code afin qu'il utilise java.security.SecureRandomexclusivement pour générer les jetons?

Actuellement, la graine de code est Randomune fois au démarrage

user967973
la source
14
Une fois amorcé, la sortie de java.util.Random est une séquence déterministe de nombres. Vous ne voudrez peut-être pas cela.
Peter Štibraný
1
Le code amorce-t-il Randomune fois au démarrage, ou en amène-t-il un nouveau pour chaque jeton? J'espère que c'est une question stupide, mais j'ai pensé vérifier.
Tom Anderson
8
Au hasard a seulement un état interne de 48 bits et répéter après 2 ^ 48 appels à nextLong () ce qui signifie qu'il ne produira pas possible longou les doublevaleurs.
Peter Lawrey
3
Il y a un autre problème grave. 64bits signifie 1,84 * 10 ^ 19 combinaisons possibles, ce qui est trop peu pour résister à une attaque sophistiquée. Il existe des machines qui ont craqué un code DES 56 bits (facteur 256 de moins) avec 90 * 10 ^ 9 clés par seconde en 60 heures. Utilisez 128 bits ou deux longs!
Thorsten S.20

Réponses:

232

L'implémentation standard d'Oracle JDK 7 utilise ce qu'on appelle un générateur linéaire congruentiel pour produire des valeurs aléatoires dans java.util.Random.

Tiré du java.util.Randomcode source (JDK 7u2), d'un commentaire sur la méthode protected int next(int bits), qui est celle qui génère les valeurs aléatoires:

Il s'agit d'un générateur de nombres pseudo-aléatoires linéaires congruents, tel que défini par DH Lehmer et décrit par Donald E. Knuth dans The Art of Computer Programming, Volume 3: Seminumerical Algorithms , section 3.2.1.

Prévisibilité des générateurs linéaires congruentiels

Hugo Krawczyk a écrit un très bon article sur la façon dont ces LCG peuvent être prédits ("Comment prédire les générateurs congruentiels"). Si vous êtes chanceux et intéressé, vous pouvez toujours en trouver une version gratuite et téléchargeable sur le Web. Et il y a beaucoup plus de recherches qui montrent clairement que vous ne devriez jamais utiliser un LCG à des fins critiques pour la sécurité. Cela signifie également que vos nombres aléatoires sont prévisibles en ce moment, ce que vous ne voulez pas pour les ID de session et autres.

Comment casser un générateur linéaire congruentiel

L'hypothèse selon laquelle un attaquant devrait attendre que le LCG se répète après un cycle complet est fausse. Même avec un cycle optimal (le module m dans sa relation de récurrence), il est très facile de prédire les valeurs futures en beaucoup moins de temps qu'un cycle complet. Après tout, ce n'est qu'un tas d'équations modulaires qui doivent être résolues, ce qui devient facile dès que vous avez observé suffisamment de valeurs de sortie du LCG.

La sécurité ne s'améliore pas avec une "meilleure" graine. Peu importe que vous semiez avec une valeur aléatoire générée SecureRandomou même produisiez la valeur en lançant un dé plusieurs fois.

Un attaquant calculera simplement la graine à partir des valeurs de sortie observées. Cela prend beaucoup moins de temps que 2 ^ 48 dans le cas de java.util.Random. Les mécréants peuvent essayer cette expérience , où il est montré que vous pouvez prédire les Randomsorties futures en n'observant que deux (!) Valeurs de sortie dans le temps, environ 2 ^ 16. Il ne faut même pas une seconde sur un ordinateur moderne pour prédire la sortie de vos nombres aléatoires dès maintenant.

Conclusion

Remplacez votre code actuel. Utilisez SecureRandomexclusivement. Ensuite, au moins, vous aurez une petite garantie que le résultat sera difficile à prévoir. Si vous voulez les propriétés d'un PRNG cryptographiquement sécurisé (dans votre cas, c'est ce que vous voulez), alors vous devez y aller SecureRandomuniquement. Être intelligent pour changer la façon dont il était censé être utilisé entraînera presque toujours quelque chose de moins sûr ...

gaufrer
la source
4
Très utile, peut-être pourriez-vous également expliquer comment fonctionne SecureRandom (tout comme vous expliquez comment fonctionne Random) ..
gresdiplitude
4
Cela va à l'encontre du but de secureRandom
Azulflame
Je sais, j'ai appris cette leçon à la dure. Mais un chiffrement difficile et une source difficile à trouver fonctionnent bien. Notch pourrait apprendre quelque chose à ce sujet (il code le mot de passe de son utilisateur dans un fichier .lastlogin, encodé avec un cryptage de base en utilisant "passwordfile" comme clé)
Azulflame
1
La vraie question ici: si java peut produire un prng plus sécurisé avec une API similaire, pourquoi n'a-t-il pas simplement remplacé celui cassé?
Joel Coehoorn
11
@JoelCoehoorn Ce n'est pas ce qui Randomest cassé - il devrait simplement être utilisé dans différents scénarios. Bien sûr, vous pouvez toujours utiliser SecureRandom. Mais en général, SecureRandomest nettement plus lent que pur Random. Et il y a des cas où vous n'êtes intéressé que par de bonnes propriétés statistiques et d'excellentes performances, mais vous ne vous souciez pas vraiment de la sécurité: les simulations Monte-Carlo en sont un bon exemple. J'ai fait des commentaires à ce sujet dans une réponse similaire , vous le trouverez peut-être utile.
graver le
72

Un aléatoire n'a que 48 bits alors que SecureRandom peut avoir jusqu'à 128 bits. Ainsi, les chances de répéter dans securerandom sont très faibles.

Random utilise le system clockcomme graine / ou pour générer la graine. Ils peuvent donc être reproduits facilement si l'attaquant connaît l'heure à laquelle la graine a été générée. Mais SecureRandom prend Random Datade votre os(ils peuvent être un intervalle entre les frappes, etc. - la plupart des OS collectent ces données les stockent dans des fichiers - /dev/random and /dev/urandom in case of linux/solaris) et les utilise comme graine.
Donc, si la petite taille du jeton est correcte (dans le cas de Random), vous pouvez continuer à utiliser votre code sans aucune modification, car vous utilisez SecureRandom pour générer la graine. Mais si vous voulez des jetons plus gros (auxquels vous ne pouvez pas faire face brute force attacks), optez pour SecureRandom -
Dans le cas où des 2^48tentatives aléatoires sont nécessaires, avec les processeurs avancés d'aujourd'hui, il est possible de les casser en temps pratique. Mais pour plus de sécurité, des 2^128tentatives seront nécessaires, ce qui prendra des années et des années pour atteindre l'équilibre avec les machines avancées d'aujourd'hui.

Voir ce lien pour plus de détails.
EDIT
Après avoir lu les liens fournis par @emboss, il est clair que la graine, aussi aléatoire soit-elle, ne doit pas être utilisée avec java.util.Random. Il est très facile de calculer la graine en observant la sortie.

Optez pour SecureRandom - Utilisez Native PRNG (comme indiqué dans le lien ci-dessus) car il prend des valeurs aléatoires du /dev/randomfichier pour chaque appel ànextBytes(). De cette façon, un attaquant observant la sortie ne pourra rien distinguer à moins qu'il ne contrôle le contenu du /dev/randomfichier (ce qui est très peu probable).
L' algorithme sha1 prng calcule la graine une seule fois et si votre machine virtuelle fonctionne pendant des mois en utilisant la même chose graine, il pourrait être craqué par un attaquant qui observe passivement la sortie.

REMARQUE - Si vous appelez le nextBytes()plus rapidement possible, votre système d'exploitation peut écrire des octets aléatoires (entropie) dans le /dev/random, vous pourriez avoir des problèmes lors de l'utilisation de NATIVE PRNG . Dans ce cas, utilisez une instance SHA1 PRNG de SecureRandom et toutes les quelques minutes (ou un certain intervalle), amorcez cette instance avec la valeur denextBytes()d'une instance PRNG NATIVE de SecureRandom. L'exécution de ces deux en parallèle garantira que vous semez régulièrement avec de vraies valeurs aléatoires, tout en n'épuisant pas l'entropie obtenue par le système d'exploitation.

Ashwin
la source
Il faut beaucoup moins de 2 ^ 48 pour prédire un Random, l'OP ne devrait pas être utilisé Randomdu tout.
graver le
@emboss: Je parle de bruteforce.
Ashwin
1
Attention avec Linux: il peut atteindre l'épuisement entropique (plus en VM qu'avec du matériel)! Regardez /proc/sys/kernel/random/entropy_availet vérifiez avec quelques vidages de fils qu'il n'y a pas trop de temps à lire/dev/random
Yves Martin
2
Notez qu'Oracle JRE (au moins 1.7) fonctionne avec / dev / urandom par défaut et non / dev / random donc le suffixe de votre réponse n'est plus correct. pour vérifier le chèque $ JAVA_HOME / lib / security / java.security pour la propriété securerandom.source
Boaz
1
Notre fichier java.security avait securerandom.source = file: / dev / urandom au lieu de file: /// dev / urandom (deux barres obliques après deux points pour le protocole de fichier, puis une barre oblique supplémentaire pour la racine du système de fichiers), ce qui la fait retomber vers / dev / random, ce qui a provoqué des problèmes d'épuisement du pool d'entropie. Impossible de le modifier, nous avons donc dû définir une propriété système java.security.egd sur la bonne au démarrage de l'application.
maxpolk
11

Si vous exécutez deux fois java.util.Random.nextLong()avec la même graine, elle produira le même nombre. Pour des raisons de sécurité, vous souhaitez vous en tenir java.security.SecureRandomcar c'est beaucoup moins prévisible.

Les 2 classes sont similaires, je pense que vous avez juste besoin de changer Randompour SecureRandomun outil de refactoring et la plupart de votre code existant fonctionnera.

Mualig
la source
11
Si vous prenez deux instances d'un PRNG et que vous l'amorcez avec la même valeur, vous obtenez toujours les mêmes nombres aléatoires, même en utilisant SecureRandom ne change rien à cela. Tous les PRNG sont déterministes et donc prévisibles si vous connaissez la graine.
Robert
1
Il existe différentes implémentations de SecureRandom, certaines sont des PRNG, d'autres non. D'un autre côté, java.util.Random est toujours PRNG (tel que défini dans son Javadoc).
Peter Štibraný
3

Si la modification de votre code existant est une tâche abordable, je vous suggère d'utiliser la classe SecureRandom comme suggéré dans Javadoc.

Même si vous trouvez que l'implémentation de la classe Random utilise la classe SecureRandom en interne. vous ne devez pas considérer comme acquis que:

  1. D'autres implémentations de VM font la même chose.
  2. L'implémentation de la classe Random dans les futures versions du JDK utilise toujours la classe SecureRandom

Il est donc préférable de suivre la suggestion de documentation et d'aller directement avec SecureRandom.

Andrea Parodi
la source
Je ne crois pas que la question d'origine indiquait que l' java.util.Randomimplémentation était utilisée en SecureRandominterne, elle disait que leur code utilisait SecureRandompour amorcer le Random. Pourtant, je suis d'accord avec les deux réponses jusqu'à présent; il est préférable d'utiliser SecureRandompour éviter une solution explicitement déterministe.
Palpatim
2

L'implémentation de référence actuelle de java.util.Random.nextLong()effectue deux appels à la méthode next(int)qui expose directement 32 bits de la graine actuelle:

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);
}

Les 32 bits supérieurs du résultat de nextLong()sont les bits de la graine à ce moment. Étant donné que la largeur de la graine est de 48 bits (dit le javadoc), il suffit * d'itérer sur les 16 bits restants (soit seulement 65,536 essais) pour déterminer la graine qui a produit le second 32 bits.

Une fois la graine connue, tous les jetons suivants peuvent être facilement calculés.

En utilisant la sortie de nextLong()directement, en partie le secret du PNG à un degré tel que le secret entier peut être calculé avec très peu d'effort. Dangereux!

* Un effort est nécessaire si les deuxièmes 32 bits sont négatifs, mais on peut le découvrir.

Mat
la source
Correct. Découvrez comment casser rapidement java.util.random sur jazzy.id.au/default/2010/09/20/… !
ingyhere
2

La graine n'a pas de sens. Un bon générateur aléatoire diffère par le nombre choisi. Chaque générateur aléatoire commence à partir d'un nombre et itère à travers un «anneau». Ce qui signifie que vous venez d'un nombre à l'autre, avec l'ancienne valeur interne. Mais après un certain temps, vous atteignez à nouveau le début et recommencez. Vous exécutez donc des cycles. (la valeur de retour d'un générateur aléatoire n'est pas la valeur interne)

Si vous utilisez un nombre premier pour créer un anneau, tous les nombres de cet anneau sont choisis, avant de terminer un cycle complet à travers tous les nombres possibles. Si vous prenez des nombres non premiers, tous les nombres ne sont pas choisis et vous obtenez des cycles plus courts.

Des nombres premiers plus élevés signifient des cycles plus longs, avant de revenir à nouveau au premier élément. Ainsi, le générateur aléatoire sécurisé a juste un cycle plus long, avant d'atteindre à nouveau le début, c'est pourquoi il est plus sûr. Vous ne pouvez pas prédire la génération de nombres aussi facilement qu'avec des cycles plus courts.

En d'autres termes: vous devez tout remplacer.

Nicolas
la source
0

J'essaierai d'utiliser des mots très basiques pour que vous puissiez facilement comprendre la différence entre Random et secureRandom et l'importance de SecureRandom Class.

Vous êtes-vous déjà demandé comment OTP (mot de passe unique) est généré? Pour générer un OTP, nous utilisons également la classe Random et SecureRandom. Maintenant, pour rendre votre OTP fort, SecureRandom est meilleur car il a fallu 2 ^ 128 essais pour casser l'OTP, ce qui est presque impossible par la machine actuelle, mais s'il est utilisé Random Class, votre OTP peut être piraté par quelqu'un qui peut nuire à vos données parce qu'il a pris juste 2 ^ 48 essayer de craquer.

sachin pathak
la source