我的团队交出了一些服务器端代码(在Java中),生成随机令牌,我有一个关于相同的问题
这些令牌的用途相当敏感——用于会话id、密码重置链接等。所以它们确实需要加密随机,以避免有人猜测它们或强行使用它们。令牌是一个“长”,所以它是64位长。
代码目前使用java.util.Random类来生成这些令牌。java.util.Random的文档清楚地说明了以下内容:
java.util.Random的实例不是加密安全的。相反,可以考虑使用securerrandom来获得一个加密安全的伪随机数生成器,以供对安全敏感的应用程序使用。
然而,代码目前使用java.util.Random的方式是这样的——它实例化java.security. securerrandom类,然后使用securerrandom . nextlong()方法来获得用于实例化java.util.Randomclass的种子。然后它使用java.util.Random.nextLong()方法生成令牌。
所以我现在的问题是——考虑到java.util.Random是使用java.security.SecureRandom进行播种的,它仍然是不安全的吗?我是否需要修改代码,以便它专门使用java.security. securerrandom来生成令牌?
目前,代码种子在启动时是随机的
标准的Oracle JDK 7实现使用所谓的线性同余生成器在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的很好的论文(“如何预测同分生成器”)。如果你足够幸运并且感兴趣,你还可以在网上找到一个免费的、可下载的版本。还有更多的研究清楚地表明,您永远不应该将LCG用于安全关键目的。这也意味着你的随机数现在是可预测的,这是你不希望会话id之类的东西。
如何打破一个线性同余发生器
攻击者必须等待LCG在一个完整的周期后重复的假设是错误的。即使有一个最优周期(递归关系中的模量m),也很容易在比完整周期更短的时间内预测未来的值。毕竟,这只是一堆需要解决的模方程,只要你观察到LCG的足够输出值,这就变得很容易了。
安全性不会因为“更好”的种子而提高。不管你是使用SecureRandom生成的随机值,还是通过多次掷骰子生成该值,这都无关紧要。
攻击者只需根据观察到的输出值计算种子。在java.util.Random的情况下,这比2^48的时间要少得多。不相信的人可以试试这个实验,它表明你可以预测未来的随机输出,在大约2^16的时间内只观察到两个(!)输出值。现在,在现代计算机上预测随机数的输出甚至不需要一秒钟。
结论
替换当前代码。专门使用securerrandom。那么至少你有一点保证,结果将是难以预测的。如果您想要加密安全的PRNG的属性(在您的情况下,这就是您想要的),那么您必须只使用SecureRandom。聪明地改变它应该使用的方式几乎总是会导致一些不太安全的事情……
A random has only 48 bits where as SecureRandom can have upto 128 bits. So the chances of repeating in securerandom is very small. Random uses the system clock as the seed/or to generate the seed. So they can be reproduced easily if the attacker knows the time at which the seed was generated. But SecureRandom takes Random Data from your os(they can be interval between keystrokes etc - most os collect these data store them in files - /dev/random and /dev/urandom in case of linux/solaris) and uses that as the seed. So if the small token size is okay(in case of Random), you can continue using your code without any changes, since you are using SecureRandom to generate the seed. But if you want larger tokens(which cannot be subject to brute force attacks) go with SecureRandom - In case of random just 2^48 attempts are required, with todays advanced cpu's it is possible to break it in practical time. But for securerandom 2^128 attempts will be required, which will take years and years to break even with today's advanced machines.
See this link for more details.
EDIT
After reading the links provided by @emboss, it is clear that the seed, however random it maybe,
should not be used with java.util.Random. It is very easy to calculate the seed by observing the output.
Go for SecureRandom - Use Native PRNG (as given in the link above) because it takes random values from the /dev/random file for each call to nextBytes(). This way an attacker observing the output will not be able to make out anything unless he is controlling the contents of the /dev/random file(which is very unlikely)
The sha1 prng algorithm calculates the seed only once and if your VM is running for months using the same seed, it might be cracked by an attacker who is passively observing the output.
NOTE - If you are calling the nextBytes() faster than your os is able to write random bytes(entropy) into the /dev/random, you might land into trouble when using NATIVE PRNG. In that case use a SHA1 PRNG instance of SecureRandom and every few minutes(or some interval), seed this instance with the value from nextBytes() of a NATIVE PRNG instance of SecureRandom. Running these two parallely will ensure that you are seeding regularly with true random values, while also not exhausting the entropy obtained by the Operating System.
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位是负的,需要一些努力,但人们可以发现这一点。