一文详解安全随机数
本文分享自华为云社区《【安全攻防】深入浅出实战系列专题-安全随机数》,作者: MDKing 。
随机数的使用场景
使用随机数可分类安全场景跟非安全场景。非安全场景需要生成的越快越好。安全场景使用的随机数必须足够安全,保证不能被预测到。
常见的非安全场景:
-
数据的索引号、标识;
-
文件的名称或目录;
-
UUID、用户ID、随机填充字节;
常见安全场景包括但不限于以下场景:
-
用于密码算法用途,如生成IV、盐值、密钥等;
-
会话标识(sessionId)的生成;
-
挑战算法中的随机数生成;
-
验证码的随机数生成;
密码学意义上的安全随机数
安全场景下使用的随机数必须是密码学意义上的安全随机数。
-
真随机数产生器产生的随机数;
-
以真随机数产生器产生的少量随机数作为种子的密码学安全的伪随机数产生器产生的大量随机数。
已知的可供产品使用的密码学安全的非物理真随机数产生器有:
-
Linux操作系统的/dev/random设备接口(存在阻塞问题)
-
Windows操作系统的 CryptGenRandom() 接口
我们可以看到,密码学意义上的安全随机数非指必须都为真随机数生成器生成,因为真随机数生成器产生随机数是需要资源(熵)消耗的,当资源不足时会阻塞。所以在业务使用随机数频次较低的时候可以考虑全部使用真随机数。在业务使用随机数的频次高、数量大时就必须使用兼顾安全跟效率的方式:使用伪随机数生成器作为主体生成器进行随机数生成,使用真随机数周期为其设置种子(seed)。
SecureRandom对象的生成随机数有两类方法:生成下一个随机数的方法,比如nextInt、nextBytes等;生成种子的方法,如generateSeed。
使用new SecureRandom()创建的对象的nextXXX、generateSeed方法在linux系统下随机数来源均为/dev/urandom,生成的随机数都是不安全随机数。
使用SecureRandom.getInstance("SHA1PRNG", "SUN")创建的对象nextXXX生成的为不安全的随机数,generateSeed生成的为安全随机数(linux下来源为/dev/random),所以使用SHA1PRNG算法合理设置好周期补种的逻辑理论上是可行的。但是根据公司密码算法相关要求,从2023年开始将禁止使用SHA1PRNG算法生成安全随机数。
常用推荐的安全随机数用法
大的原则上不建议自己实现补种、刷新熵源等逻辑,最好直接使用JDK封装好的方法。
密码学安全的随机数用法主要有3种:
1. SecureRandom.getInstance("NativePRNGBlocking")
参数要配置NativePRNGBlocking,好处是非常安全,因为nextXXX、generateSeed方法都是生成的真随机数,缺点是因为每次都是真随机数,性能较低,会有阻塞性问题。常见几个参数的对比如下:
参数取值 | 说明 | nextXXX的随机数源 | generateSeed的随机数源 | 随机数质量 | 是否阻塞(没实际验证,读者自酌) |
---|---|---|---|---|---|
SHA1PRNG
|
通过系统属性和java.security的熵收集设备完成随机数生成器的初始播种。完成初始播种后,不再依赖熵收集设备 | NA | NA | 不安全 | 不阻塞 |
NativePRNG
|
完成初始播种后,仍然要从系统的熵收集设备获取随机数,这就是Native的含义。nextBytes()从/dev/urandom设备获取随机数, generateSeed()从/dev/random获取随机数。 | /dev/urandom | /dev/random | 较安全 | 取随机数不阻塞,取种可能阻塞 |
NativePRNGBlocking
|
nextBytes()和generateSeed()均从/dev/random获取随机数 | /dev/random | /dev/random | 最安全 | 很可能阻塞 |
NativePRNGNonBlocking
|
nextBytes()和generateSeed()均从/dev/urandom获取随机数 | /dev/urandom | /dev/urandom | 不安全 | 不阻塞 |
2. SecureRandom.getInstanceStrong()
在JDK 8中引入,主要解决getInstance在应用某些参数时不能保证取到的随机数质量足够安全的问题,简化编程。它返回每个平台上可用的最强SecureRandom实现的实例。它会调用系统上可用的最强算法,该算法由$JAVA_HOME/jre/lib/security/java.security中的securerandom.strongAlgorithms来指定。
3. SecureRandom.getInstance("DRBG",...)
从JDK 9开始,新增了由Sun提供的符合NIST SP 800 90-A标准的DRBG伪随机数产生器,包括HASH-DRBG、HMAC-DRBG、CTR-DRBG三种,且适用于各种OS,符合公司密码算法相关要求,在使用JDK 9
及以上版本时,推荐优先使用该方式生成安全随机数。该方式生成随机数是兼顾安全跟效率周期补种真随机数方式的封装,所以在高频生成安全随机数的场景中可以使用该方式。
推荐写法可参考编程规范:
小结一下:
低频使用安全随机数的业务场景(对性能没有要求)可以使用SecureRandom.getInstance("NativePRNGBlocking")或者SecureRandom.getInstanceStrong()方式;
高频使用安全随机数的业务场景(对性能有要求)只能使用SecureRandom.getInstance("DRBG",...)方式;
上述方式都不能满足业务场景需求时,再考虑自己实现补种、补熵等逻辑(不推荐、一般也不会有)。
实战验证
我们使用SHA1PRNG类型的随机数生成器,验证设置同样的种子seed后,是否能产生同样的随机数序列。
执行结果如下:可以看到,不管重复执行多少次,只要初始设置的种子相同,后面的序列一定是相同、且固定的。
所以如果使用了不安全的随机数生成器实现的代码逻辑,一旦攻击者掌握了一定数量的随机数序列,就有可能推测出初始种子,从而完全预测到后续生成的随机数序列的具体内容。
如果使用真随机数设置种子(当前写法仅为示例,并非推荐写法)
可以看到,每次执行都有随机性,由于设置的seed是通过真随机数生成器生成的,所以不可预测。
有同学肯定好奇,真随机数生成器的速度真的有那么慢吗?我们实际验证下,使用真随机数生成器、伪随机数生成器的性能对比如下: