我的团队得到了一些产生随机令牌的服务器端代码(Java),我有一个相同的问题。
这些令牌的用途相当敏感-用于会话ID、密码重置链接等,因此它们需要是加密随机的,以避免有人猜测它们,或者可能地强行执行它们。令牌是"长"的,所以它是64位长。
代码当前使用java.util.Random类来生成这些令牌。文档([ http://dos.Oracle .COM/JavaSe/ 7 /DOCS/API/Java/UTL/RAPID.HTML](1))对EDCOX1 0表示清楚如下:
Instances of java.util.Random are not cryptographically secure. Consider instead using SecureRandom to get a cryptographically secure pseudo-random number generator for use by security-sensitive applications.
但是,代码当前使用java.util.Random的方式是:它实例化java.security.SecureRandom类,然后使用SecureRandom.nextLong()方法获取用于实例化java.util.Random类的种子。然后使用java.util.Random.nextLong()方法生成令牌。
所以我现在的问题是,考虑到使用java.security.SecureRandom播种java.util.Random,是否仍然不安全?我是否需要修改代码,以便它专门使用java.security.SecureRandom来生成令牌?
目前,代码种子在启动时是一次Random。
- 一旦设定种子,java.util.random的输出就是确定的数字序列。你可能不想要。
- 代码是在启动时为Random种子一次,还是为每个令牌种子一个新的?希望这是一个愚蠢的问题,但我想我会查一下。
- Random只有一个48位的内部状态,在2^48次调用NextLog()后会重复,这意味着它不会产生所有可能的long或double值。
- 还有一个严重的问题。64位意味着1.84*10^19可能的组合太少,无法抵御复杂的攻击。有一些机器在60小时内以每秒90*10^9键的速度破解了56位DES代码(小于256因子)。使用128位或两个长的!
标准的OracleJDK7实现使用所谓的线性同余生成器在java.util.Random中生成随机值。
摘自java.util.Random源代码(jdk 7u2),摘自对生成随机值的方法protected int next(int bits)的注释:
This is a linear congruential pseudorandom number generator, as
defined by D. H. Lehmer and described by Donald E. Knuth in
The Art of Computer Programming, Volume 3:
Seminumerical Algorithms, section 3.2.1.
线性同余生成器的可预测性
Hugo Krawczyk写了一篇很好的论文,介绍了如何预测这些LCG("如何预测同余生成器")。如果你很幸运也很感兴趣,你仍然可以在网上找到一个免费的,可下载的版本。还有更多的研究清楚地表明,您不应该将LCG用于安全关键的目的。这也意味着您的随机数现在是可预测的,这是您不希望用于会话ID等的。
如何打破线性同余发生器
假设攻击者必须等待LCG在一个完整周期后重复,这是错误的。即使有一个最佳周期(其递推关系中的模量m),也很容易在比完整周期短得多的时间内预测未来值。毕竟,这只是一堆需要求解的模块化方程,只要观察到足够多的LCG输出值,就很容易得到。
"更好"的种子并不能提高安全性。如果您使用SecureRandom生成的随机值进行种子植入,或者甚至通过多次滚动模具来生成该值,这根本不重要。
攻击者只需根据观察到的输出值计算种子。对于java.util.Random来说,这比2^48花费的时间要短得多。怀疑者可能会尝试这个实验,结果表明你可以预测未来的Random输出,只观察到两个(!)输出值在时间上约为2^16。在现代计算机上,现在预测随机数的输出甚至不需要一秒钟。
结论
替换当前代码。仅使用SecureRandom。那么至少你会有一点保证,结果将很难预测。如果您想要密码安全prng的属性(在您的情况下,这就是您想要的),那么您只需要使用SecureRandom。聪明地改变它应该被使用的方式几乎总是会导致一些不安全的东西…
- 很有帮助,也许你也可以解释SecureRandom是如何工作的(就像你解释Random是如何工作的一样)。
- 破坏了安全随机的目的
- @通过默默无闻的阿兹武夫安全很少起作用
- 我知道,这一课很难学到。但一个困难的密码和难以找到的来源工作得很好。Notch可以从中学习到一些东西(他将用户的密码编码在.lastlogin文件中,并使用"密码文件"作为密钥进行基本加密)
- 这里真正的问题是:如果Java可以用类似的API产生更安全的PRNG,为什么他们不替换掉一个?
- @乔尔乔恩,并不是说Random坏了——它应该只在不同的场景中使用。当然,您可以始终使用SecureRandom。但总的来说,SecureRandom比纯Random慢得多。有些情况下,您只对良好的统计特性和出色的性能感兴趣,但实际上并不关心安全性:蒙特卡洛模拟就是一个很好的例子。我在一个类似的答案中对此作了评论,也许你会发现它很有用。
- @瓦伊沙利,谢谢!如果我有时间,我将尝试丢失一些关于默认SecureRandom实现的单词。同时,也许你会发现这个有趣的…
- 关于同一主题的新文章。内部也有能够预测双值的实现。(franklinta.com/2014/08/31/…)
- @浮雕上提供的"实验"在博客上实际上并不起作用-如果条件永远不会成为现实。该博客评论列表上的其他人也发表了这一评论。
一个随机数只有48位,其中SecureRandom最多可以有128位。所以在SecureRandom中重复的机会很小。随机使用system clock作为种子/或生成种子。因此,如果攻击者知道种子的生成时间,就可以很容易地复制它们。但是SecureRandom从你的os中获取Random Data(它们可以是击键之间的间隔时间等—大多数操作系统收集这些数据存储在文件中—/dev/random and /dev/urandom in case of linux/solaris并将其用作种子。因此,如果小令牌大小正常(在随机情况下),您可以继续使用代码而不做任何更改,因为您使用SecureRandom来生成种子。但是,如果您想要更大的令牌(不受brute force attacks的约束),请使用secureRandom——如果是随机的,则只需要2^48次尝试,使用今天的高级CPU,可以在实际时间内将其中断。但是,对于SecureRandom2^128系统,将需要进行多次尝试,这将需要多年时间才能与当今先进的机器抗衡。有关详细信息,请参阅此链接。编辑在阅读了@emboss提供的链接后,很明显种子,不管它是随机的,不应与java.util.random一起使用。通过观察产量很容易计算出种子。转到secureRandom-使用本机prng(如上面链接中所示),因为每次调用nextBytes()时,它都从/dev/random文件中获取随机值。这样,除非攻击者控制/dev/random文件的内容(这是非常不可能的),否则观察输出的攻击者将无法识别任何内容。sha1prng算法只计算一次seed,如果您的虚拟机使用相同的seed运行数月,则可能会被被动观察输出的攻击者破解。注意-如果调用nextBytes()的速度比操作系统向/dev/random中写入随机字节(熵)的速度快,那么在使用本机prng时可能会遇到麻烦。在这种情况下,使用SecureRandom的sha1 prng实例,每隔几分钟(或某些间隔),将该实例的值从SecureRandom的本机prng实例的nextBytes()中设定。运行这两个并行操作将确保定期使用真正的随机值播种,同时也不会耗尽操作系统获得的熵。
- 预测一个Random所需的时间远小于2^48,而OP根本不应该使用Random。
- @浮雕:我说的是野蛮变形。
- 注意Linux:它可以达到熵耗尽(在虚拟机中比在硬件中更多)!查看/proc/sys/kernel/random/entropy_avail并检查一些线程转储,在/dev/random上读取时没有等待太长时间。
- 请注意,OracleJRE(至少1.7)在默认情况下与/dev/urandom一起工作,而不是/dev/random,因此您的答案后缀不再正确。要验证,请检查$java_home/lib/security/java.security以获取secureRandom.source属性
- 我们的java.security文件有securelrandom.source=file:/dev/urandom而不是file:///dev/urandom(文件协议的冒号后面有两个斜杠,文件系统根的斜杠后面还有一个),导致它返回到/dev/random,这导致了熵池耗尽问题。无法编辑,所以必须在应用启动时将系统属性java.security.egd设置为正确的属性。
如果用相同的种子运行两次java.util.Random.nextLong(),它将产生相同的数目。出于安全考虑,您希望继续使用java.security.SecureRandom,因为它的可预测性要差得多。
这两个类是相似的,我认为您只需要使用重构工具将Random更改为SecureRandom,您现有的大多数代码都可以工作。
- 如果您采用任何prng的两个实例并使用相同的值对其进行种子设定,则始终会得到相同的随机数,即使使用secureRandom也不会改变这一点。所有的prng都是确定性的,因此如果你知道种子的话,就可以预测。
- 有不同的SecureRandom实现,有些是prng,有些不是。另一方面,java.util.random始终是prng(在其javadoc中定义)。
如果更改现有代码是一项负担得起的任务,我建议您使用JavaDoc中建议的SecureRandom类。
即使您发现随机类实现在内部使用SecureRandom类。你不应该想当然地认为:
其他虚拟机实现也做同样的事情。
在将来的JDK版本中,随机类的实现仍然使用SecureRandom类
因此,最好遵循文档建议并直接使用SecureRandom。
- 我不认为最初的问题是,java.util.Random实现在内部使用SecureRandom,它说他们的代码使用SecureRandom来为Random播种。尽管如此,我还是同意这两个答案;最好使用SecureRandom来避免显式的确定性解决方案。
- 哦,是的。我的错…
种子毫无意义。一个好的随机发生器在所选素数上是不同的。每个随机生成器都从一个数字开始,并通过一个"环"迭代。也就是说,从一个数字到下一个数字,使用旧的内部值。但过了一会儿,你又回到起点,重新开始。所以你运行循环。(随机生成器的返回值不是内部值)
如果您使用质数创建一个环,那么在您完成一个完整的循环之前,该环中的所有数字都会被选中。如果你采用非质数,并不是所有的数字都会被选择,你会得到更短的周期。
更高的素数意味着,在您再次返回第一个元素之前,周期更长。因此,安全随机生成器只有较长的周期,在再次到达开始之前,这就是它更安全的原因。你不能像用更短的周期那样容易预测数字的产生。
换言之:你必须全部替换。
当前java.util.Random.nextLong()的引用实现对方法next(int)进行了两次调用,该方法直接公开当前种子的32位:
1 2 3 4 5 6 7 8 9 10 11
| 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位为负,则需要做一些努力,但可以发现这一点。
- 对的。看看如何在jazzy.id.au/default/2010/09/20/&hellip;上快速破解java.util.random!
我将尝试使用非常基本的单词,这样您就可以很容易地理解Random和SecureRandom之间的区别以及SecureRandom类的重要性。
有没有想过OTP(一次性密码)是如何生成的?为了生成一个OTP,我们也使用了Random和SecureRandom类。现在,为了使您的OTP更强大,SecureRandom更好,因为它花了2^128次尝试,破解了目前机器几乎不可能破解的OTP,但是如果使用了随机类,那么您的OTP可以被一些人破解,因为它只花了2^48次尝试就破解了。