SecureRandom的江湖偏方与真实效果

SecureRandom,我们一般都知道江湖偏方 -Djava.security=file:/dev/./urandom,但往往不求甚解。一年前,在那个有点暗的办公室里,我就是这么做的。

一年后,又有同学说JDK8下Thread Dump出很多SecureRandom的BLOCKING。這回怒翻JDK代码,并配合JMH写的测试,总结出这么一篇。

1. /dev/random 与 /dev/urandom

Linux的两个随机数源, 从IO中断,网卡传输包这些外部入侵者不可预测的随机源中获取熵,混合后使用CSPRNG生成熵池。

当熵池估值为0时,/dev/random 会block住请求,而/dev/urandom 则会继续输出随机数。

 

2. JDK7的SecureRandom

2.1 generateSeed()与next*()

generateSeed()可以为其他安全算法生成种子,随机度需求更高些。
nextInt(), nextLong()则是我们更关心的生成随机数,最后都是调用nextBytes()。

 

2.2 seedSource

seedSource由两者决定,首先看 -Djava.security.egd ,
没设置则看jre/lib/security/java.security,JDK7中securerandom.source=file:/dev/urandom

 

2.3 SHA1PRNG 与 NativePRNG

从名字就知道,两者都是伪随机算法。另外,两种算法都会有synchronized关键字,都会有阻塞,只有时间长短的不同。

 

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 中取值,再异或 SHA1PRNG生成的随机值而成。

可见NativePRGN的性能一定会比单纯SHA1PRNG差,synchronized的代码也多一倍。

那为什么要异或 SHA1PRNG呢?为了支持setSeed(),/dev/[u]random都是不可写的,只好再引入一个可设置seed的SHA1PRNG。

/dev/[u]random不需要Java应用来給种子,而SHA1PRNG则从/dev/urandom中获得种子并显式设置,也就不需要2.3.1中所述的种子四步曲,所以不会阻塞。

 

2.3.3 算法的选择

如果用getInstance()获取,则返回的是特定算法的实现。

如果用new SecureRandom(), 则看seedSource的设置,如果是/dev/[u]random 之一则是NativePRNG,否则是SHA1PRNG,比如-Djava.security=file:/dev/./urandom,多了个./在中间, 就成了SHA1PRNG了。

 

3. 江湖偏方的诞生

在JDK7,默认算法是NativePRNG,里面/dev/urandom本身不用seed,而用到的SHA1PRNG的初始seed也从/dev/urandom 读取,不存在启动慢的问题。就是消耗和延时比纯SHA1PRNG大。

然后Tomcat 生成sessionId时显式使用了SHA1PRNG,因为NativeSeedGenerator的bug,此时初始seed要从/dev/random读取,就存在启动慢的可能(见2.3.1)。所以要设置seedSoure而且要加个./在中间,绕过NativeSeedGenerator改为用URLSeedGenderator。

如果一个不明真莫道不消魂相的群众,也跟着设置-Djava.security=file:/dev/./urandom, 一个意外的效果就是,默认的算法也变成SHA1PRNG了。

 

4. JDK8的SecureRandom

首先,Native算法多了两种子类型。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,只是不用猥琐的加个. 在中间了。不过依然加上也无不可。

如果你想把默认算法搞成SHA1PRNG,那还是要继续江湖偏方。

 

5. 结论

  • SHA1PRNG 比 NativePRNG消耗小一半,synchronized的代码少一半,不与系统/dev/urandom交互所以偶发高延时也更少一些,所以没特殊安全要求的话建议用SHA1,比如生成sessionId,traceId的场景
  • 如果想用SHA1, 设成-Djava.security=file:/dev/./urandom总是对的
  • 如果想用Native,什么都不设置就好了。
  • 如果自己能控制,在应用或框架启动时,先调用一下相应SecureRandom算法实例的nextInt()函数,总能减少一点首次服务调用所花的时间。

 
附录:基于JMH的测试结果, 24核机器上,48线程并发获取secureRandom.nextLong()的测试。

App.randomWithNative 379862 QPS
App.randomWithNative:randomWithNative·p0.00 ≈ 10⁻⁴ ms/op
App.randomWithNative:randomWithNative·p0.50 0.001 ms/op
App.randomWithNative:randomWithNative·p0.90 0.007 ms/op
App.randomWithNative:randomWithNative·p0.95 0.007 ms/op
App.randomWithNative:randomWithNative·p0.99 0.008 ms/op
App.randomWithNative:randomWithNative·p0.999 sample 56.254 ms/op
App.randomWithNative:randomWithNative·p0.9999 sample 186.945 ms/op
App.randomWithNative:randomWithNative·p1.00 sample 1235.223 ms/op

App.randomWithSHA1 668574 QPS
App.randomWithSHA1:randomWithSHA1·p0.00 ≈ 10⁻⁴ ms/op
App.randomWithSHA1:randomWithSHA1·p0.50 0.002 ms/op
App.randomWithSHA1:randomWithSHA1·p0.90 0.006 ms/op
App.randomWithSHA1:randomWithSHA1·p0.95 0.321 ms/op
App.randomWithSHA1:randomWithSHA1·p0.99 1.701 ms/op
App.randomWithSHA1:randomWithSHA1·p0.999 2.245 ms/op
App.randomWithSHA1:randomWithSHA1·p0.9999 12.325 ms/op
App.randomWithSHA1:randomWithSHA1·p1.00 138.936 ms/op

 
中秋寫的文章,修啊改啊才發出來,但這麼偏的題材,估計也就只有Google的爬蟲會看了。

This entry was posted in 技术. Bookmark the permalink.

6 Responses to SecureRandom的江湖偏方与真实效果

  1. 晨勃 says:

    看完了,果然很偏

  2. test says:

    test

  3. humyna says:

    看到最后的图片震惊了
    没有一丝丝防备

  4. 五山 says:

    博客中“春天的旁边”链接已经失效了==

  5. 钱进 says:

    这个偏方一直在用~stackoverflow 上有人说过~

发表评论

您的电子邮箱不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>