Java 里提供了一些用于生成随机数的工具类,这里分析一下其实现原理,以及他们之间的区别、使用场景。
java.util.Random
Random 是比较常用的随机数生成类,它的基本信息在类的注释里都写到了,下面是 JDK8 里该类的注释:
-
/** * An instance of this class is used to generate a stream of * pseudorandom numbers. The class uses a 48-bit seed, which is * modified using a linear congruential formula. (See Donald Knuth, * <i>The Art of Computer Programming, Volume 2</i>, Section 3.2.1.) * <p> * If two instances of {@code Random} are created with the same * seed, and the same sequence of method calls is made for each, they * will generate and return identical sequences of numbers. In order to * guarantee this property, particular algorithms are specified for the * class {@code Random}. Java implementations must use all the algorithms * shown here for the class {@code Random}, for the sake of absolute * portability of Java code. However, subclasses of class {@code Random} * are permitted to use other algorithms, so long as they adhere to the * general contracts for all the methods. * <p> * The algorithms implemented by class {@code Random} use a * {@code protected} utility method that on each invocation can supply * up to 32 pseudorandomly generated bits. * <p> * Many applications will find the method {@link Math#random} simpler to use. * * <p>Instances of {@code java.util.Random} are threadsafe. * However, the concurrent use of the same {@code java.util.Random} * instance across threads may encounter contention and consequent * poor performance. Consider instead using * {@link java.util.concurrent.ThreadLocalRandom} in multithreaded * designs. * * <p>Instances of {@code java.util.Random} are not cryptographically * secure. Consider instead using {@link java.security.SecureRandom} to * get a cryptographically secure pseudo-random number generator for use * by security-sensitive applications. * * @author Frank Yellin * @since 1.0 */
翻译一下,主要有以下几点:
Random 类使用线性同余法 linear congruential formula 来生成伪随机数。
两个 Random 实例,如果使用相同的种子 seed,那他们产生的随机数序列也是一样的。
Random 是线程安全的,你的程序如果对性能要求比较高的话,推荐使用 ThreadLocalRandom。
Random 不是密码学安全的,加密相关的推荐使用 SecureRandom。
Random 的基本用法如下所示:Random random = new Random(); int r = random.nextInt(); // 生成一个随机数
从下面的源码中可以看到,Random 的默认使用当前系统时钟来生成种子 seed。
private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L); public Random() { this(seedUniquifier() ^ System.nanoTime()); } public Random(long seed) { if (getClass() == Random.class) this.seed = new AtomicLong(initialScramble(seed)); else { // subclass might have overriden setSeed this.seed = new AtomicLong(); setSeed(seed); } } private static long seedUniquifier() { for (;;) { long current = seedUniquifier.get(); long next = current * 181783497276652981L; if (seedUniquifierpareAndSet(current, next)) return next; } }
java.Security.SecureRandom
介绍 Random 类时提到过,要生成加密基本的随机数应该使用 SecureRandom 类,该类信息如下所示:
/** * This class provides a cryptographically strong random number * generator (RNG). * * <p>A cryptographically strong random number * minimally complies with the statistical random number generator tests * specified in <a href="http://csrc.nist.gov/cryptval/140-2.htm"> * <i>FIPS 140-2, Security Requirements for Cryptographic Modules</i></a>, * section 4.9.1. * Additionally, SecureRandom must produce non-deterministic output. * Therefore any seed material passed to a SecureRandom object must be * unpredictable, and all SecureRandom output sequences must be * cryptographically strong, as described in * <a href="http://www.ietf/rfc/rfc1750.txt"> * <i>RFC 1750: Randomness Recommendations for Security</i></a>. * * <p>A caller obtains a SecureRandom instance via the * no-argument constructor or one of the {@code getInstance} methods: * * <pre> * SecureRandom random = new SecureRandom(); * </pre> * * <p> Many SecureRandom implementations are in the form of a pseudo-random * number generator (PRNG), which means they use a deterministic algorithm * to produce a pseudo-random sequence from a true random seed. * Other implementations may produce true random numbers, * and yet others may use a combination of both techniques. * * <p> Typical callers of SecureRandom invoke the following methods * to retrieve random bytes: * * <pre> * SecureRandom random = new SecureRandom(); * byte bytes[] = new byte[20]; * random.nextBytes(bytes); * </pre> * * <p> Callers may also invoke the {@code generateSeed} method * to generate a given number of seed bytes (to seed other random number * generators, for example): * <pre> * byte seed[] = random.generateSeed(20); * </pre> * * Note: Depending on the implementation, the {@code generateSeed} and * {@code nextBytes} methods may block as entropy is being gathered, * for example, if they need to read from /dev/random on various Unix-like * operating systems. */
主要有以下几点:
该类提供了能满足加密要求的强随机数生成器。
传递给 SecureRandom 种子必须是不可预测的,seed 使用不当引发的安全漏洞可以看看 比特币电子钱包漏洞。
一般使用默认的种子生成策略就行,对应 Linux 里面就是 /dev/random 和 /dev/urandom。其实现原理是:操作系统收集了一些随机事件,比如鼠标点击,键盘点击等等,SecureRandom 使用这些随机事件作为种子。
使用 /dev/random 来生成种子时,可能会因为熵不够而阻塞,性能比较差。
SecureRandom 用法如下所示:SecureRandom random = new SecureRandom(); byte[] data = random.nextBytes(16);
下面我们看看其内部实现:
synchronized public void nextBytes(byte[] bytes) { secureRandomSpi.engineNextBytes(bytes); } public SecureRandom() { super(0); getDefaultPRNG(false, null); } private void getDefaultPRNG(boolean setSeed, byte[] seed) { String prng = getPrngAlgorithm(); if (prng == null) { // bummer, get the SUN implementation prng = "SHA1PRNG"; this.secureRandomSpi = new sun.security.provider.SecureRandom(); this.provider = Providers.getSunProvider(); if (setSeed) { this.secureRandomSpi.engineSetSeed(seed); } } else { try { SecureRandom random = SecureRandom.getInstance(prng); this.secureRandomSpi = random.getSecureRandomSpi(); this.provider = random.getProvider(); if (setSeed) { this.secureRandomSpi.engineSetSeed(seed); } } catch (NoSuchAlgorithmException nsae) { // never happens, because we made sure the algorithm exists throw new RuntimeException(nsae); } } if (getClass() == SecureRandom.class) { this.algorithm = prng; } }
在 mac 环境下使用 JDK8 测试时发现,默认使用了 NativePRNG 而非 SHA1PRNG,但是 NativePRNG 其实还是在 sun.security.provider.SecureRandom 的基础上做了一些封装。
在 sun.security.provider.SeedGenerator 类里,可以看到 seed 是利用 /dev/random 或 /dev/urandom 来生成的,启动应用程序时可以通过参数 -Djava.security.egd=file:/dev/urandom 来指定 seed 源。
static { String var0 = SunEntries.getSeedSource(); if (!var0.equals("file:/dev/random") && !var0.equals("file:/dev/urandom")) { if (var0.length() != 0) { try { instance = new SeedGenerator.URLSeedGenerator(var0); if (debug != null) { debug.println("Using URL seed generator reading from " + var0); } } catch (IOException var2) { if (debug != null) { debug.println("Failed to create seed generator with " + var0 + ": " + var2.toString()); } } } } else { try { instance = new NativeSeedGenerator(var0); if (debug != null) { debug.println("Using operating system seed generator" + var0); } } catch (IOException var3) { if (debug != null) { debug.println("Failed to use operating system seed generator: " + var3.toString()); } } } if (instance == null) { if (debug != null) { debug.println("Using default threaded seed generator"); } instance = new SeedGenerator.ThreadedSeedGenerator(); } }
在 Random 类里,多个实例设置相同的seed,产生的随机数序列也是一样的。而 SecureRandom 则不同,运行下面的代码:
public class RandomTest { public static void main(String[] args) { byte[] seed = "hello".getBytes(); for (int i = 0; i < 10; ++i) { SecureRandom secureRandom = new SecureRandom(seed); System.out.println(secureRandom.nextInt()); } } }
输出如下所示,每次运行产生的随机数都不一样。
-2105877601 1151182748 1329080810 -617594950 2094315881 -1649759687 -1360561033 -653424535 -927058354 -1577199965
为什么呢?因为 engineSetSeed 方法设置 seed 时调用的是静态实例 INSTANCE 的 implSetSeed 方法,该方法通过 getMixedRandom 得到的 SecureRandom 来设置 seed,而这个 SecureRandom 初始化种子是系统的。
private static final NativePRNG.RandomIO INSTANCE; // in NativePRNG protected void engineSetSeed(byte[] var1) { INSTANCE.implSetSeed(var1); } private void implSetSeed(byte[] var1) { Object var2 = this.LOCK_SET_SEED; synchronized(this.LOCK_SET_SEED) { if (!this.seedOutInitialized) { this.seedOutInitialized = true; this.seedOut = (OutputStream)AccessController.doPrivileged(new PrivilegedAction<OutputStream>() { public OutputStream run() { try { return new FileOutputStream(RandomIO.this.seedFile, true); } catch (Exception var2) { return null; } } }); } if (this.seedOut != null) { try { this.seedOut.write(var1); } catch (IOException var5) { throw new ProviderException("setSeed() failed", var5); } } this.getMixRandom().engineSetSeed(var1); } } private SecureRandom getMixRandom() { SecureRandom var1 = this.mixRandom; if (var1 == null) { Object var2 = this.LOCK_GET_BYTES; synchronized(this.LOCK_GET_BYTES) { var1 = this.mixRandom; if (var1 == null) { var1 = new SecureRandom(); try { byte[] var3 = new byte[20]; readFully(this.nextIn, var3); var1.engineSetSeed(var3); } catch (IOException var5) { throw new ProviderException("init failed", var5); } this.mixRandom = var1; } } } return var1; }
在 sun.security.provider.SecureRandom.engineSetSeed 方法里,新种子的生成不仅和刚设置的 seed 有关,也和原来的种子(系统产生的 seed)有关。
// in sun.security.provider.SecureRandom public synchronized void engineSetSeed(byte[] var1) { if (this.state != null) { this.digest.update(this.state); for(int var2 = 0; var2 < this.state.length; ++var2) { this.state[var2] = 0; } } this.state = this.digest.digest(var1); }
/dev/random 与 /dev/urandom
在 Linux 操作系统中,有一个特殊的设备文件 /dev/random,可以用作随机数发生器或伪随机数发生器。在读取时,/dev/random 设备会返回小于熵池噪声总数的随机字节。/dev/random 可生成高随机性的公钥或一次性密码本。若熵池空了,对/dev/random的读操作将会被阻塞,直到从别的设备中收集到了足够的环境噪声为止。
当然你也可以设置成不堵塞,当你在 open 的时候设置参数 O_NONBLOCK, 但是当你read 的时候,如果熵池空了,会返回 -1。
/dev/random 的一个副本是 /dev/urandom (“unlocked”,非阻塞的随机数发生器),它会重复使用熵池中的数据以产生伪随机数据。这表示对/dev/urandom的读取操作不会产生阻塞,但其输出的熵可能小于 /dev/random 的。它可以作为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。
更多推荐
Java 随机数生成器 Random & SecureRandom
发布评论