在Java开发中经常使用Random作为随机数的生成器,它能满足绝大多数随机数生成的需求,但是在安全性比较高的业务中要慎重使用。我们知道Random内部有一个seed属性,如果在构造方法中传入则使用传入的,否则会自动生成,seed对随机数的生成非常重要,如果两个Random的seed一样,则它们产生的随机数序列也是一样的。
1import java.util.Random;
2
3public class Main {
4
5 public static void main(String[] args) {
6 Random random1 = new Random(123L);
7 Random random2 = new Random(123L);
8 System.out.println(random1.nextInt() + ", " + random2.nextInt());
9 System.out.println(random1.nextInt() + ", " + random2.nextInt());
10 }
11}
由此可见,如果我们要想破解或者说预测随机数序列,只需要得到Random对象的seed属性即可,上面提到了seed属性如果不设置的话会自动生成,下面是Random默认的构造方法。
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
seedUniquifier方法会返回一个常数,而System.nanoTime()则是返回当前纳秒级别的时间戳,也就是说如果得到这个时间戳就能够得到seed值,但是这种风险还是比较小的。
Random不安全的主要是它的next方法,我们常用的nextInt,nextLong这些都是通过它生成的,next和nextInt的源码如下
1private static final long multiplier = 0x5DEECE66DL;
2private static final long addend = 0xBL;
3private static final long mask = (1L << 48) - 1;
4
5public int nextInt() {
6 return next(32);
7}
8
9protected int next(int bits) {
10 long oldseed, nextseed;
11 AtomicLong seed = this.seed;
12 do {
13 oldseed = seed.get();
14 nextseed = (oldseed * multiplier + addend) & mask;
15 } while (!seed.compareAndSet(oldseed, nextseed));
16 return (int)(nextseed >>> (48 - bits));
17}
从源码中可以看出,相邻的两个随机数之间是存在某种关系,首先是oldseed和nextseed存在线性关系,然后是seed和最终生成的int随机数也存在关系。
以nextInt为例,它最终生成的int随机数是通过nextSeed右移16位得到的,我们可以将int随机数左移16位得到一个基数baseSeed,由于nextSeed在右移过程中丢失了低16位,所以baseSeed<=nextSeed<=baseSeed + 216,再通过oldSeed和nextSeed的关系,我们就可以得到上次产生随机数的seed,核心代码如下。
1public static long getPreviousSeed(int r1, int r2) {
2 long oldSeed = ((long) r1 << 16);
3 for (int i = 0; i < (2 << 16); i++) {
4 long nextSeed = (oldSeed * multiplier + addend) & mask;
5 if ((int) (nextSeed >>> 16) == r2) {
6 return nextSeed;
7 }
8 oldSeed++;
9 }
10 throw new RuntimeException("Not found");
11}
下面是一个实例测试,我们可以通过Random先后得到两个int随机数,然后通过这两个随机数获取Random对象目前的seed属性值,从而推断出下一次将会产生的int随机数。
1import java.util.Random;
2
3public class Main {
4
5 public static void main(String[] args) {
6 Random random = new Random();
7 int r1 = random.nextInt();
8 int r2 = random.nextInt();
9 long seed = getPreviousSeed(r1, r2);
10 int nextInt = nextInt(seed);
11 System.out.println(random.nextInt() + ", " + nextInt);
12 }
13
14 private static final long multiplier = 0x5DEECE66DL;
15 private static final long addend = 0xBL;
16 private static final long mask = (1L << 48) - 1;
17
18 public static int nextInt(long seed) {
19 return (int) (((seed * multiplier + addend) & mask) >>> 16);
20 }
21
22 public static long getPreviousSeed(int r1, int r2) {
23 long oldSeed = ((long) r1 << 16);
24 for (int i = 0; i < (2 << 16); i++) {
25 long nextSeed = (oldSeed * multiplier + addend) & mask;
26 if ((int) (nextSeed >>> 16) == r2) {
27 return nextSeed;
28 }
29 oldSeed++;
30 }
31 throw new RuntimeException("Not found");
32 }
33}
从上面可以看出,如果我们能够得到Random先后产生的两个随机数就能预测下一个随机数的值,从而引出安全问题,在对安全性要求比较高的业务场景中,我们可以使用SecureRandom来替代Random生成随机数,SecureRandom重写了next方法,即使两个SecureRandom的seed一样,它们产生的随机序列也是不一样的。