随机数生成器Random的安全性问题

深入学习Java基础知识
2022-06-10 13:37 · 阅读时长7分钟
小课

在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}
注意: 这个Java运行环境不支持自定义包名,并且public class name必须是Main

由此可见,如果我们要想破解或者说预测随机数序列,只需要得到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}
注意: 这个Java运行环境不支持自定义包名,并且public class name必须是Main
总结

从上面可以看出,如果我们能够得到Random先后产生的两个随机数就能预测下一个随机数的值,从而引出安全问题,在对安全性要求比较高的业务场景中,我们可以使用SecureRandom来替代Random生成随机数,SecureRandom重写了next方法,即使两个SecureRandom的seed一样,它们产生的随机序列也是不一样的。

javaRandomSecureRandom随机数安全