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

229

我所在的团队接手了一些服务器端代码(使用Java语言),用于生成随机令牌。我对此有一个问题:

这些令牌的用途相当敏感,用于会话ID、密码重置链接等,因此需要具有加密级别的随机性,以避免有人猜测或尝试暴力破解。这个令牌是一个“long”类型,长度为64位。

目前该代码使用java.util.Random类来生成这些令牌。 java.util.Random文档清楚地说明了以下内容:

java.util.Random的实例不具备加密安全性。考虑改用SecureRandom获取用于安全敏感应用程序的具有加密安全性的伪随机数生成器。

但是,代码目前使用java.util.Random的方式是 - 它实例化了java.security.SecureRandom类,然后使用SecureRandom.nextLong()方法获取种子,用于实例化java.util.Random类。然后它使用java.util.Random.nextLong()方法生成令牌。

那么我的问题是 - 给定java.util.Random使用java.security.SecureRandom进行种子生成,它仍然不安全吗?我需要修改代码,以便仅使用java.security.SecureRandom来生成令牌吗?

目前该代码在启动时对Random进行了一次种子设置。


15
一旦种子确定,java.util.Random生成的输出是一系列确定性数字。或许您不希望如此。 - Peter Štibraný
1
代码在启动时只会种植一次 Random,还是每个令牌都会种植一个新的?希望这不是一个愚蠢的问题,但我想确认一下。 - Tom Anderson
8
随机数生成器只有48位的内部状态,调用nextLong()函数2^48次后会重复,这意味着它无法生成所有可能的long或double类型值。 - Peter Lawrey
3
还有一个严重的问题。64位只意味着1.8410^19种可能的组合,这对于经受复杂攻击来说太少了。已有机器在60小时内每秒使用 9010^9 个密钥破解了一个56位的DES代码(因数小256倍)。请使用128位或两个长整型! - Thorsten S.
7个回答

252

标准的Oracle JDK 7实现使用所谓的线性同余生成器(Linear Congruential Generator)来在java.util.Random中产生随机值。

摘自java.util.Random源代码(JDK 7u2),评论位于方法protected int next(int bits)中,该方法是生成随机值的方法:

这是一个线性同余伪随机数生成器,由D.H. Lehmer定义并由Donald E. Knuth在《计算机程序设计艺术》第3卷:《半数值算法》第3.2.1节描述。

线性同余生成器的可预测性

Hugo Krawczyk撰写了一篇关于如何预测这些LCG的非常好的论文(“How to predict congruential generators”)。如果你很幸运并且感兴趣,你可能仍然可以在网上找到免费可下载的版本。还有更多的研究表明,您绝不应该将LCG用于安全关键目的。这也意味着您的随机数现在是可预测的,对于会话ID等情况,这是您不想要的。

如何破解线性同余生成器

假设攻击者必须等待LCG完整循环后才能重复是错误的。即使使用最优周期(其递归关系中的模数m),也可以在比完整周期更短的时间内预测未来的值。毕竟,这只是需要解决一堆模方程,只要您已经观察了足够的LCG输出值,就变得容易。

安全性不会因为“更好”的种子而改善。使用由SecureRandom生成的随机值或通过多次掷骰子产生的值都无关紧要。

攻击者只需要观察输出值并计算出种子即可。在java.util.Random的情况下,这比2^48要快得多。不信者可以尝试进行这个实验,其中演示了仅观察两个输出值就可以在大约2^16的时间内预测未来的Random输出。在现代计算机上,甚至不到一秒钟就能预测出您随机数的输出结果。

结论

替换你当前的代码。使用SecureRandom。那么,您至少有一个小保证,结果将很难被预测。如果您希望具有密码学安全PRNG的特性(在您的情况下,这就是您想要的),则必须仅使用SecureRandom。聪明地更改它应该使用的方式几乎总会导致不太安全的结果...


5
非常有帮助,也许您可以像解释Random一样解释SecureRandom的工作原理。 - gresdiplitude
4
理解:这违背了安全随机数的目的 翻译:那就达不到安全随机数的目的。 - Azulflame
2
这里真正的问题是:如果Java可以使用类似的API生成更安全的PRNG,为什么他们不直接替换掉那个有缺陷的呢? - Joel Coehoorn
13
并不是说 Random 函数出了问题,它只适用于不同的场景。当然,你可以使用 SecureRandom 函数。但一般来说,相比纯粹的 Random 函数, SecureRandom 函数明显地速度较慢。在某些情况下,你只关心良好的统计特性和卓越的性能,而并不真正关心安全性:蒙特卡罗模拟就是一个很好的例子。我在一个类似的答案中对此进行了评论,也许你会觉得有用。 - emboss
同一主题的最新文章。里面还有一个能够预测双精度值的实现。(http://franklinta.com/2014/08/31/predicting-the-next-math-random-in-java/) - artspb
显示剩余4条评论

80
随机数生成器(Random)只有48位,而SecureRandom可以达到128位。因此,在SecureRandom中重复的概率非常小。

随机数生成器(Random)使用系统时钟作为种子来生成随机数或种子。因此,如果攻击者知道生成种子的时间,就可以轻松地重现它们。但是SecureRandom从操作系统(os)中获取随机数据(例如按键间隔等-大多数操作系统会收集这些数据并将其存储在文件中,对于Linux / Solaris,则是/dev/random和/dev/urandom),并将其用作种子。
因此,如果小令牌大小(在随机数生成器(Random)的情况下)可以接受,您可以继续使用代码而不进行任何更改,因为您正在使用SecureRandom来生成种子。但是,如果您需要更大的令牌(不能受到暴力破解攻击),请使用SecureRandom -
在随机数生成器(Random)的情况下,只需要尝试2^48次,而现今先进的CPU可以在实际时间内破解它。但是对于SecureRandom,需要尝试2^128次,即使使用现今先进的计算机也需要很多年才能破解。

请参见此链接以获取更多详细信息。
编辑
在阅读@emboss提供的链接后,很明显即使种子随机性足够好,也不应该使用java.util.Random。通过观察输出,很容易计算种子。

请使用SecureRandom - 使用原生PRNG(如上面的链接中所述),因为它会从每个调用nextBytes()的/dev/random文件中获取随机值。这样,除非攻击者控制/dev/random文件的内容(这是不太可能的),否则他将无法理解任何输出。SHA1 PRNG算法仅计算一次种子,如果您的虚拟机使用相同种子运行数月,被动观察输出的攻击者可能会破解它。

注意 - 如果您调用nextBytes()比您的操作系统能够写入随机字节(熵)到/dev/random更快,则在使用NATIVE PRNG时可能会遇到麻烦。 在这种情况下,请使用SecureRandom的SHA1 PRNG实例,并且每隔几分钟(或某个间隔),将此实例与SecureRandom的NATIVE PRNG实例的nextBytes()值进行种子处理。 并行运行这两个实例将确保您定期使用真正的随机值进行种植,同时不会耗尽操作系统获得的熵。


预测“Random”所需的数量远远小于2^48,因此OP根本不应该使用“Random”。 - emboss
@emboss:我在谈论暴力破解。 - Ashwin
1
注意 Linux:它可能会达到熵耗尽的状态(在 VM 中比硬件更容易)。查看 /proc/sys/kernel/random/entropy_avail 并检查一些线程转储,确保在读取 /dev/random 时没有太长的等待时间。 - Yves Martin
2
请注意,Oracle JRE(至少1.7版本)默认使用/dev/urandom而不是/dev/random,因此您的答案后缀不再正确。要验证,请检查$JAVA_HOME/lib/security/java.security文件中的securerandom.source属性。 - Boaz
2
我们的java.security文件中,securerandom.source=file:/dev/urandom被写成了file:///dev/urandom(在文件协议后面有两个斜杠,然后再加一个斜杠表示文件系统的根目录),导致它回退到/dev/random,从而导致熵池耗尽的问题。无法编辑它,所以不得不在应用程序启动时设置系统属性java.security.egd为正确的值。 - maxpolk
实际上,new SecureRandom()使用/dev/urandom,因此您不需要设置securerandom.source。几乎永远不应该使用/dev/random。它会大部分时间阻塞很长时间,而额外的熵只是微小的。因此,除非您真的有充分的理由,否则请始终使用/dev/urandom - Charlie Reitzel

13

若使用相同种子(seed)运行两次java.util.Random.nextLong()方法,它将生成相同的数字。出于安全原因,您应该使用java.security.SecureRandom,因为它更难预测。

这2个类很相似,只需使用重构工具将Random替换为SecureRandom,大部分现有代码将仍可正常工作。


12
如果您使用相同的值来给任何伪随机数生成器(PRNG)种子,那么两个实例生成的随机数总是相同的,即使使用SecureRandom也无法改变这一点。所有的PRNG都是确定性的,因此如果您知道它们的种子,就可以预测它们产生的随机数。 - Robert
1
有不同的SecureRandom实现,有些是伪随机数生成器(PRNGs),有些则不是。另一方面,java.util.Random始终是PRNG(如其Javadoc中所定义)。 - Peter Štibraný
在Linux上,如果您使用new SecureRandom()/dev/urandom,则将不断从操作系统设备中提取额外的熵。它严格来说不是伪随机数生成器(PRNG)。可以说它是一种混合PRNG-增强熵的方式(是一种很好的工程设计)。 - Charlie Reitzel

3
如果更改现有代码是可行的任务,我建议按照Javadoc中建议的使用SecureRandom类。即使您发现Random类实现在内部使用了SecureRandom类,也不应该认为:
  1. 其他VM实现也是这样做的。
  2. 将来JDK版本中Random类的实现仍然使用SecureRandom类。
因此,最好还是遵循文档建议并直接使用SecureRandom。

1
我不相信原问题陈述了 java.util.Random 实现内部使用了 SecureRandom,它说 他们的代码 使用 SecureRandom 来种子化 Random。尽管如此,我同意到目前为止的两个答案;最好使用 SecureRandom 来避免明确确定性解决方案。 - Palpatim

2

java.util.Random.nextLong() 的当前参考实现会调用两次 next(int) 方法,该方法直接暴露了当前种子的32位:

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);
}
nextLong() 的结果的高 32 位是种子的位数。由于种子的宽度为 48 位(根据 javadoc 的说法),只需迭代剩余的 16 位(仅有 65,536 次尝试)即可确定生成第二个 32 位的种子。
一旦知道了种子,所有后续的标记都可以轻松计算出来。
直接使用 nextLong() 的输出会在一定程度上暴露 PNG 的机密,以至于可以用非常少的工作量计算出整个机密。 非常危险!
如果第二个 32 位为负数,则需要一些努力才能找出。

正确。请查看如何快速破解java.util.random的方法:http://jazzy.id.au/default/2010/09/20/cracking_random_number_generators_part_1.html! - ingyhere

2
我会尝试使用非常基础的词语,以便您可以轻松理解Random和SecureRandom之间的区别以及SecureRandom类的重要性。
您是否想知道如何生成OTP(一次性密码)?为了生成OTP,我们也使用Random和SecureRandom类。现在,为了使您的OTP更加强大,SecureRandom更好,因为它需要2 ^ 128次尝试才能破解OTP,这几乎是不可能的,但是如果使用Random类,则您的OTP可以被某些人破解,他们可能会损害您的数据,因为只需要2 ^ 48次尝试即可破解。

2
种子是无意义的。一个好的随机生成器在所选的素数方面有所不同。每个随机生成器都从一个数字开始,并通过“环”迭代。这意味着,您从一个数字到达下一个数字,使用旧的内部值。但是过一段时间后,您会再次到达开头并重新开始。因此,您运行循环。(随机生成器的返回值不是内部值)
如果您使用素数创建环,那么在完成所有可能的数字的完整循环之前,该环中的所有数字都将被选择。如果您选择非质数,则不会选择所有数字,并且您将获得较短的周期。
更高的素数意味着在返回到第一个元素之前需要更长的周期。因此,安全的随机生成器只是具有更长的周期,然后才会再次到达开头,这就是为什么它更安全。您无法像使用较短周期那样轻松地预测数字生成。
换句话说:您必须全部替换。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接