Thursday, December 26, 2013

JCA Engine - SecureRandom

SecureRandom 是JCA中最简单的Engine类。它的作用是生成随机数。很多和Java Security相关的操作,都离不开随机数。比如,KeyGenerator需要随机数生成器来随机生成key;Cipher可能需要随机数生成来生成IV(Initialization Vector)。

java.security.SecureRandom vs java.util.Random

注意:为了方便,下文使用SecurityRandom代替java.security.SecureRandom;使用Random代替java.util.Random.

说道SecureRandom就不得不说我们比较熟悉的Random类。这两个类都是随机数生成器,那么它们之间有什么区别呢?Java API为什么要提供两个完成相同功能的类呢?

在Random的API文档中有这样一段话:

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.

大致的意思是说,Random的实例是不安全的。如果想要在安全级别比较高的系统中使用随机数生成器,请考虑使用SecureRandom.

也就是说Random不安全。我在读到这里的时候就产生了一个疑问:随机数生成器有什么不安全的呢?它的“不安全表现在什么地方呢”?经过一番研究,了解到这里所谓的不安全,实际上是说,生成的随机序列是可以被预测的。也就是说,我知道了几个生成的随机数后,就可以推测出接下来将会生成的随机数是什么。

发现有一点可怕了吧… 设想这样一种情况:用户登录系统之后,系统会生成一个随机数作为用户操作系统的token,并提供给用户。这个用户每次操作的时候,都需要提供这个token。此时,如果这个随机数生成器是不安全的,那么黑客则可以预测出来生成的随机数序列,进而预测出将会分配给用户的token。这样一来,也就可以未经过用户允许的情况下,操作用户的数据。

关于如何预测Random生成的随机数,可以参考这篇文章。我从这篇文章中摘录了一些代码,用来演示破解随机数生成器的过程。

这段代码会连续输出10个随机数:
Random random = new Random();
for (int i=0;i<10;i++) {
    int randomValue = random.nextInt();
    System.out.println(String.format("v%d: %s", i + 1, randomValue));
}
本次运行结果是这样的:
v1: -1116240189
v2: 1675702140
v3: -227598907
v4: -452268948
v5: 2006095225
v6: 584972179
v7: -1039927173
v8: -656175616
v9: -704109609
v10: 11273124
作为黑客只需要知道任意两个连续的随机数,就可以破解这个随机数生成器了。为了简单,我假设黑客知道了v1和v2:
long multiplier = 0x5DEECE66DL;
long addend = 0xBL;
long mask = (1L << 48) - 1;

int v1 = -1116240189;
int v2 = 1675702140;

for (int i = 0; i < 65536; i++) {
    long seed = (((long) v1) << 16) + i;
    if (((seed * multiplier + addend) & mask) >>> 16 == v2) {
        System.out.println("Seed found: " + seed);
        break;
    }
}
黑客通过执行这段代码,将得到如下结果:
Seed found: -73153916970526
看到了吗,我们反向计算出来了这个随机数生成器的seed。已经接近成功了不是吗?剩下的我们只要初始化一个Random对象,并且设置它的seed为-73153916970526,就可以得到和系统中一样的随机数序列:
long multiplier = 0x5DEECE66DL;
long mask = (1L << 48) - 1;
/*
这里之所以要对seed的值处理一下,是因为我们通过计算得到的seed,是经过了Random类处理过的,并不是系统创建Random时传入的值,因此这个seed是不能通过Random的构造方法传入的。请参见Random.initialScramble这个方法。这里只是做了这个方法的反向运算,使其最后设置的seed符合我们计算出来的值。
*/
long seedValue = -73153916970526L ^ multiplier;
Random random = new Random(seedValue);
for (int i=0;i<10;i++) {
    int predictValue = random.nextInt();
    System.out.println(String.format("v%d: %s", i + 1, predictValue));
}
运行程序,得到如下结果:
v1: 1675702140
v2: -227598907
v3: -452268948
v4: 2006095225
v5: 584972179
v6: -1039927173
v7: -656175616
v8: -704109609
v9: 11273124
v10: -1299046198
咱们可以对比着看一下,这样更明显些:
系统输出 黑客计算
v1: -1116240189 --
v2: 1675702140 v1: 1675702140
v3: -227598907 v2: -227598907
v4: -452268948 v3: -452268948
v5: 2006095225 v4: 2006095225
v6: 584972179 v5: 584972179
v7: -1039927173 v6: -1039927173
v8: -656175616 v7: -656175616
v9: -704109609 v8: -704109609
v10: 11273124 v9: 11273124
-- v10: -1299046198
看到了吗,黑客通过两个连续的随机值,计算出了之后所有的随机序列。

这就是我们为什么要在安全要求比较高的系统中使用SecureRandom的原因。

那么这里就又有一个疑问了:既然SecureRandom要比Random安全,我们不如把所有使用Random的代码都换成SecureRandom,从而使系统更加安全,不是吗?当然不是啦!!

要知道“有一利,必有一弊”。这里是“以性能换安全”。也就是说,SecureRandom的性能要比Random的性能差,因此在一些与安全无关的情况下,还是建议使用Random,来达到更高的效率。关于为什么会有效率问题,请参考这篇文章

使用方法

好了,是时候说说怎么使用SecureRandom了。其实使用SecureRandom类特别简单,首先说明,这是JCA中的一个Engine类(参考本类主题的Overview)。那么提到Engine类,我们就知道怎么样获得它的实例了:

SecureRandom.getInstance(algorithm);

通过传入一个生成随机数的算法名称,向JCA请求一个实现了这个算法的随机数生成器。默认的随机数生成器算法是SHA1PRNG;也可以选择性地传入一个provider,如果没有传入provider,那么JCA会根据查找策略,从注册到JCA的Provider中查找实现了这个算法的SPI。

接下来可以选择性的调用setSeed方法来设置这个随机数生成器的seed。如果不设置seed,那么SecureRandom的实现类将会完全随机产生随机序列。

如果把两个SecureRandom设置为相同的seed,那么这两个SecureRandom将产生相同的随机序列。

可以随时再次调用setSeed方法,来更新seed。但是请注意,重新设置seed的时候,并不是简单的用新的seed替换原有seed,而是将新的seed作为原有seed的补充。这样做的好处是,当使用一个值多次调用setSeed后,得到的依然是完全随机的序列,不会产生于之前相同的序列。请看下面的演示:
random1.setSeed(160L);
System.out.println(String.format("Random1: %d", random1.nextInt()));
random1.setSeed(160L);
System.out.println(String.format("Random1: %d", random1.nextInt()));
random2.setSeed(160L);
System.out.println(String.format("Random2: %d", random2.nextInt()));
以上程序中random1 和 random2 分别是两个刚刚实例化的SecureRandom对象。注意,这两个random对象都没有设置过seed。执行以上程序,将得到下面的结果:
Random1: 718216388
Random1: 862077552
Random2: 718216388
由此可见,在random1上,第二次将seed设置成160也无法使SecureRandom变回第一次设置完seed时候的状态了,因此证明了重新设置seed不会替换原有的seed,而是作为原有seed的补充。

而random2是第一次设置seed,而且seed与random1 的第一个seed相同,所以能够生成与random1相同的随机序列。

seed设置完毕后就可以调用SecureRandom中的nextXxx方法来获得随机数,这里API中讲解的非常详细,这里不再赘述。

其中有一个方法使用起来比较方便,而且我之前没有注意到。在这里提一下:

synchronized public void nextBytes(byte[] bytes)

通过传入一个byte数组,这个方法将会用随机的byte填充整个byte数组,这个方法用于生成IV或者Key的时候非常方便,值得记住。

No comments :

Post a Comment