随机数安全性:java.util.Random 和 java.security.SecureRandom 之间的区别

伪随机数生成器使用确定的数学算法产生具备良好统计属性的数字序列,但实际上这种数字序列并非具备真正的随机特性。

伪随机数生成器通常以一个种子值为起始,每次计算使用当前种子值生成一个输出及一个新种子,这个新种子会被用于下次计算。

Java 提供的伪随机数生成器 java.util.Random 的不同实例如果使用相同种子值创建,则后续产生的随机数总会重复,如:

@Test
public void test() {
    
    
    long seed = System.currentTimeMillis();
    Random random1 = new Random(seed);
    Random random2 = new Random(seed);
    for (int i = 0; i < 10; i++) {
    
    
        System.out.println(random1.nextInt() + "=" + random2.nextInt());
    }
}

测试结果:

584397915=584397915
1101630039=1101630039
-1214277036=-1214277036
-119968434=-119968434
-262827225=-262827225
-1539565323=-1539565323
1391920860=1391920860
-922846780=-922846780
2080924613=2080924613
2114543521=2114543521

以上场景也正好模拟了 java.util.Random 的常用场景,使用不带任何参数的 Random 构造函数生成 Random 实例时,系统会使用系统时钟的当前时间作为种子值,那么系统在初始化或重启时生成的 Random 实例的种子值可能都是相同的。攻击者可以在系统中植入监听并构建相应的查询表预测将要使用的种子值,因此安全场景中不能使用 java.util.Random

安全场景中建议使用 java.security.SecureRandomSecureRandom 提供加密的强随机数生成器(RNG),实际上 SecureRandom 继承自 java.util.Random,本质上仍然是伪随机数生成器原理,在种子值确定的情况下生成的随机数也一样,区别在于 SecureRandom 使用的种子值不单单是系统时钟的当前时间,增强了种子值的不可预测性,因此比 Random 安全,不过也因此会带来一定的性能开销,因此 RandomSecureRandom 的选择取决于真实使用场景,安全场景中尽量使用 SecureRandom

java.security.SecureRandom 正确使用示例:

  1. 使用无参构建函数生成 SecureRandom 实例。
@Test
public void test() {
    
    
    SecureRandom random1 = new SecureRandom();
    SecureRandom random2 = new SecureRandom();
    for (int i = 0; i < 10; i++) {
    
    
        System.out.println(random1.nextInt() + "!=" + random2.nextInt());
    }
}
  1. SecureRandom 内置两种随机数算法:NativePRNGSHA1PRNG,可以指定随机数算法生成SecureRandom 实例。
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");


java.util.Random 和 java.security.SecureRandom 之间的区别

java.security.SecureRandom 是更安全的版本 java.util.Random,它在 Java 中提供了加密安全伪随机数生成器 (CSPRNG)。

java.security.SecureRandom 总是优先于 java.util.Random 用于生成敏感随机数,例如在密码应用程序中生成加密密钥或在 Web 服务器上或在密码生成器中生成会话 ID 以创建高度安全的密码。

这里有几个原因 java.security.SecureRandom 应在敏感应用中使用:

  1. java.util.Random 实现使用线性同余生成器 (LCG) 来生成高度可预测的伪随机值。

另一方面,大多数 java.security.SecureRandom 实现使用伪随机数生成器 (PRNG),它使用确定性算法从真正的随机种子生成伪随机序列。

  1. java.util.Random 使用种子的系统时间。 java.security.SecureRandom 从底层操作系统获取随机数据。在 Linux/Solaris 中,随机数是通过从熵池中读取来创建的 /dev/random/dev/urandom 文件。

  2. 如果两个实例 java.util.Random 使用相同的种子创建,并且为每个进行相同的方法调用序列,它们将生成并返回相同的数字序列。情况并非如此 java.security.SecureRandom,它从操作系统获得的熵源中播种,例如 I/O 事件的时间,这实际上是无法检测到的。

  3. java.util.Random 类使用 48-位种子,而 java.security.SecureRandom 通常使用一个 128-位或 160位种子。因此,只有 248 需要尝试打破 Random 类,在现代计算机上甚至可能不需要一秒钟。另一方面, 2128 或者 2160 将需要尝试 SecureRandom,这将需要数年才能达到今天的 CPU 计算能力。

  4. java.security.SecureRandom 产生不确定的输出,因为它不依赖于系统时钟的种子值。所以不可能预测之前和未来的随机数。 java.util.Random另一方面,使用系统时钟作为种子,如果生成种子的时间已知,则可以轻松复制。

  5. LCG 速度非常快,通常只需要 32- 或者 64-位来保持状态。另一方面,创建一个 java.security.SecureRandom 实例比创建一个非常昂贵 java.util.Random 实例和关于 30-50 比慢几倍 Random.

此外,该 generateSeed()nextBytes() 的方法 SecureRandom 如果没有足够的熵可用并且熵正在收集,则可能会在 Linux 上被阻止 /dev/random 文件。这是什么 Wikipedia 不得不说:

生成器保持对熵池中噪声总位数的估计。从这个熵池中创建随机数。阅读时, /dev/random 设备将仅返回熵池中估计的噪声位数内的随机字节。 /dev/random 应该适用于需要非常高质量随机性的用途,例如一次性填充或密钥生成。当熵池为空时,从 /dev/random 将阻塞,直到收集到额外的环境噪音。

  1. 值得注意的是 java.security.SecureRandom 是一个子类 java.util.Random 并继承所有 nextX() 方法,其中 X 可以是 Boolean、Double、Float、Gaussian、Int 和 Long。

  2. 初始化 java.util.Randomjava.security.SecureRandom:

Random 可以初始化为:

Random rand = new Random(System.nanoTime());

SecureRandom 可以初始化为:

SecureRandom rand = new SecureRandom(SecureRandom.getSeed(20));

在这里,数 20 表示 160-位种子为 20 字节 = 160 位。

  1. Javadoc 本身建议使用 java.security.SecureRandom 在安全敏感的应用程序中获得密码安全的伪随机数生成器。

猜你喜欢

转载自blog.csdn.net/qq_43842093/article/details/132642237