SecureRandom,我们一般都知道江湖偏方 -Djava.security=file:/dev/./urandom,但往往不求甚解,一年前,在那个有点暗的办公室里,我就是这么做的。
一年后,又有同学说JDK8下,好像Thread Dump出来了很多SecureRandom的BLOCKING。现在已习惯彻底解决问题了,于是怒翻JDK代码,并配合JMH写的测试,总结出这么一篇。
-
/dev/random 与 /dev/urandom
Linux的两个随机数源, 从IO中断,网卡传输包这些外部入侵者不可预测的随机源中获取熵,混合后使用CSPRNG生成熵池。当熵池估值为0时,/dev/random 会block住请求,而/dev/urandom 则会继续输出随机数。 -
JDK7的SecureRandom
2.1 generateSeed()与next*()
generateSeed()可以为其他安全算法生成种子,随机度需求更高些。
nextInt(), nextLong()则是我们更关心的生成随机数,最后都是调用nextBytes。在某些情况
2.2 seedSource
seedSource由两者决定,首先看 -Djava.security.egd ,
没设置则看$JAVA_HOME/jre/lib/security/java.security,JDK7中securerandom.source=file:/dev/urandom
2.3 SHA1PRNG 与 NativePRNG
从名字就知道,两者都是伪随机算法。另外,两种算法都会有synchronized关键字,都会有阻塞,只有时间长短的不同。
在JMH的测试里,SHA1PRNG比Native快一倍,并且偶发的高延时更少一点。
2.3.1 SHA1PRNG
generateSeed的实现:只要配置的seedSource是/dev/random 或 /dev/urandom,就会使用NativeSeedGenerator,而此君在JDK7里有bug,只从/dev/random 取值,可能被阻塞。
nextBytes的实现: 纯Java实现,通过不断的对当前Hash值进行再一次SHA1哈希而成。
那Hash的初始值怎么来?如果没有被外部显式设置,则用下面比较复杂的算法生成。
先有个SecureRandom seeder,并且用java从系统收集到一些噪音作为这个SR的初始seed。
调用generateSeed() , 获得一个seed,可能被阻塞(见上)。
调用seeder.setSeed(seed) ,合并1和2的seed。
最后调用seeder.nextBytes(),生成最后的seed。
2.3.2 NativePRNG
generateSeed的实现:从/dev/random中取值,可能阻塞。
nextBytes的实现:从/dev/urandom 中取值,再XOR SHA1PRNG生成的随机值而成。
可见NativePRGN的性能一定会比单纯SHA1PRNG差。
那为什么要XOR SHA1PRNG呢?为了支持setSeed(),/dev/[u]random都是不可写的,只好再引入一个可设置seed的SHA1PRNG。
/dev/[u]random不需要Java应用来給种子,而SHA1PRNG则从/dev/urandom中获得种子并显式设置,也就不需要2.3.1中所述的种子四步曲,所以不会阻塞。
2.2.3 算法的选择
如果用getInstance()获取,则返回的是特定算法的实现,比如SecureRandom.getInstance(”SHA1PRNG”)
如果用new SecureRandom(), 则看seedSource的设置,如果是/dev/[u]random 之一则是NativePRNG,否则是SHA1PRNG,比如-Djava.security=file:/dev/./urandom,比/dev/urandom 多了个./在中间, 就成了SHA1PRNG了。
- 江湖偏方的诞生
在JDK7,默认算法是NativePRNG,里面/dev/urandom本身不用seed,而用到的SHA1PRNG的初始seed从/dev/urandom 读取,不存在启动慢的问题。就是消耗比纯SHA1PRNG大一倍。
然后Tomcat 生成sessionId时显式使用了SHA1PRNG,因为NativeSeedGenerator的bug(见2.3.1),此时初始seed要从/dev/random读取,就会启动慢,所以要设置seedSoure而且要加个./在中间,绕过NativeSeedGenerator改为用URLSeedGenderator。
如果一个不明真相的群众,也跟着设置-Djava.security=file:/dev/./urandom, 一个意外的效果就是,默认的算法也变成SHA1PRNG了。
- JDK8的SecureRandom
首先,Native算法多了两种子类型。原来的generateSeed从/dev/random,nextBytes从/dev/urandom, 而NativeBlocking则generateSeed 和 nextBytes 都从/dev/random中读,NativeNonBlocking则两者都从/dev/urandom中读。不过Native里nextBytes并不需要调用generateSeed,所以对于主要用SecureRandom来生成随机数的应用来说,这个区别不大。
其次,SHA1PRNG用到的SeedGenerator,终于改好了,原来NativeSeedGenerator无论设什么都是读/dev/random,现在改为设什么就读什么,所以jre/lib/security/java.security 里的 securerandom.source变为了 file:/dev/random
所以,JDK8里,如果你显式获得的SHA1PRNG以后启动不想被阻塞,还是要设成-Djava.security=file:/dev/urandom,只是不用猥琐的加个. 在中间了。不过为了兼容JDK7,依然加上也无不可。
如果你想把默认算法搞成SHA1PRNG,那还是要继续江湖偏方,-Djava.security=file:/dev/./urandom
- 结论
SHA1PRNG 比 NativePRNG消耗小一半,偶发高延时也更少,没特殊安全要求的话可以用SHA1。
如果想用SHA1, 设成-Djava.security=file:/dev/./urandom总是对的
如果想用Native,什么都不设置就好了。如果你会用SecureRandom()为其他算法生成seed又不想被堵塞,则创建NativeNonBlocking