当前位置:   article > 正文

Java 随机数生成器 Random & SecureRandom_make sure that using this pseudorandom number gene

make sure that using this pseudorandom number generator is safe here.

Java 里提供了一些用于生成随机数的工具类,这里分析一下其实现原理,以及他们之间的区别、使用场景。

java.util.Random

Random 是比较常用的随机数生成类,它的基本信息在类的注释里都写到了,下面是 JDK8 里该类的注释:

    1. /**
    2. * An instance of this class is used to generate a stream of
    3. * pseudorandom numbers. The class uses a 48-bit seed, which is
    4. * modified using a linear congruential formula. (See Donald Knuth,
    5. * <i>The Art of Computer Programming, Volume 2</i>, Section 3.2.1.)
    6. * <p>
    7. * If two instances of {@code Random} are created with the same
    8. * seed, and the same sequence of method calls is made for each, they
    9. * will generate and return identical sequences of numbers. In order to
    10. * guarantee this property, particular algorithms are specified for the
    11. * class {@code Random}. Java implementations must use all the algorithms
    12. * shown here for the class {@code Random}, for the sake of absolute
    13. * portability of Java code. However, subclasses of class {@code Random}
    14. * are permitted to use other algorithms, so long as they adhere to the
    15. * general contracts for all the methods.
    16. * <p>
    17. * The algorithms implemented by class {@code Random} use a
    18. * {@code protected} utility method that on each invocation can supply
    19. * up to 32 pseudorandomly generated bits.
    20. * <p>
    21. * Many applications will find the method {@link Math#random} simpler to use.
    22. *
    23. * <p>Instances of {@code java.util.Random} are threadsafe.
    24. * However, the concurrent use of the same {@code java.util.Random}
    25. * instance across threads may encounter contention and consequent
    26. * poor performance. Consider instead using
    27. * {@link java.util.concurrent.ThreadLocalRandom} in multithreaded
    28. * designs.
    29. *
    30. * <p>Instances of {@code java.util.Random} are not cryptographically
    31. * secure. Consider instead using {@link java.security.SecureRandom} to
    32. * get a cryptographically secure pseudo-random number generator for use
    33. * by security-sensitive applications.
    34. *
    35. * @author Frank Yellin
    36. * @since 1.0
    37. */

    翻译一下,主要有以下几点:

    Random 类使用线性同余法 linear congruential formula 来生成伪随机数。
    两个 Random 实例,如果使用相同的种子 seed,那他们产生的随机数序列也是一样的。
    Random 是线程安全的,你的程序如果对性能要求比较高的话,推荐使用 ThreadLocalRandom。
    Random 不是密码学安全的,加密相关的推荐使用 SecureRandom。
    Random 的基本用法如下所示:

    1. Random random = new Random();
    2. int r = random.nextInt(); // 生成一个随机数

    从下面的源码中可以看到,Random 的默认使用当前系统时钟来生成种子 seed。

    1. private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
    2. public Random() {
    3. this(seedUniquifier() ^ System.nanoTime());
    4. }
    5. public Random(long seed) {
    6. if (getClass() == Random.class)
    7. this.seed = new AtomicLong(initialScramble(seed));
    8. else {
    9. // subclass might have overriden setSeed
    10. this.seed = new AtomicLong();
    11. setSeed(seed);
    12. }
    13. }
    14. private static long seedUniquifier() {
    15. for (;;) {
    16. long current = seedUniquifier.get();
    17. long next = current * 181783497276652981L;
    18. if (seedUniquifier.compareAndSet(current, next))
    19. return next;
    20. }
    21. }

    java.Security.SecureRandom

    介绍 Random 类时提到过,要生成加密基本的随机数应该使用 SecureRandom 类,该类信息如下所示:

    1. /**
    2. * This class provides a cryptographically strong random number
    3. * generator (RNG).
    4. *
    5. * <p>A cryptographically strong random number
    6. * minimally complies with the statistical random number generator tests
    7. * specified in <a href="http://csrc.nist.gov/cryptval/140-2.htm">
    8. * <i>FIPS 140-2, Security Requirements for Cryptographic Modules</i></a>,
    9. * section 4.9.1.
    10. * Additionally, SecureRandom must produce non-deterministic output.
    11. * Therefore any seed material passed to a SecureRandom object must be
    12. * unpredictable, and all SecureRandom output sequences must be
    13. * cryptographically strong, as described in
    14. * <a href="http://www.ietf.org/rfc/rfc1750.txt">
    15. * <i>RFC 1750: Randomness Recommendations for Security</i></a>.
    16. *
    17. * <p>A caller obtains a SecureRandom instance via the
    18. * no-argument constructor or one of the {@code getInstance} methods:
    19. *
    20. * <pre>
    21. * SecureRandom random = new SecureRandom();
    22. * </pre>
    23. *
    24. * <p> Many SecureRandom implementations are in the form of a pseudo-random
    25. * number generator (PRNG), which means they use a deterministic algorithm
    26. * to produce a pseudo-random sequence from a true random seed.
    27. * Other implementations may produce true random numbers,
    28. * and yet others may use a combination of both techniques.
    29. *
    30. * <p> Typical callers of SecureRandom invoke the following methods
    31. * to retrieve random bytes:
    32. *
    33. * <pre>
    34. * SecureRandom random = new SecureRandom();
    35. * byte bytes[] = new byte[20];
    36. * random.nextBytes(bytes);
    37. * </pre>
    38. *
    39. * <p> Callers may also invoke the {@code generateSeed} method
    40. * to generate a given number of seed bytes (to seed other random number
    41. * generators, for example):
    42. * <pre>
    43. * byte seed[] = random.generateSeed(20);
    44. * </pre>
    45. *
    46. * Note: Depending on the implementation, the {@code generateSeed} and
    47. * {@code nextBytes} methods may block as entropy is being gathered,
    48. * for example, if they need to read from /dev/random on various Unix-like
    49. * operating systems.
    50. */

    主要有以下几点:

    该类提供了能满足加密要求的强随机数生成器。
    传递给 SecureRandom 种子必须是不可预测的,seed 使用不当引发的安全漏洞可以看看 比特币电子钱包漏洞。
    一般使用默认的种子生成策略就行,对应 Linux 里面就是 /dev/random 和 /dev/urandom。其实现原理是:操作系统收集了一些随机事件,比如鼠标点击,键盘点击等等,SecureRandom 使用这些随机事件作为种子。
    使用 /dev/random 来生成种子时,可能会因为熵不够而阻塞,性能比较差。
    SecureRandom 用法如下所示:

    1. SecureRandom random = new SecureRandom();
    2. byte[] data = random.nextBytes(16);

    下面我们看看其内部实现:

    1. synchronized public void nextBytes(byte[] bytes) {
    2. secureRandomSpi.engineNextBytes(bytes);
    3. }
    4. public SecureRandom() {
    5. super(0);
    6. getDefaultPRNG(false, null);
    7. }
    8. private void getDefaultPRNG(boolean setSeed, byte[] seed) {
    9. String prng = getPrngAlgorithm();
    10. if (prng == null) {
    11. // bummer, get the SUN implementation
    12. prng = "SHA1PRNG";
    13. this.secureRandomSpi = new sun.security.provider.SecureRandom();
    14. this.provider = Providers.getSunProvider();
    15. if (setSeed) {
    16. this.secureRandomSpi.engineSetSeed(seed);
    17. }
    18. } else {
    19. try {
    20. SecureRandom random = SecureRandom.getInstance(prng);
    21. this.secureRandomSpi = random.getSecureRandomSpi();
    22. this.provider = random.getProvider();
    23. if (setSeed) {
    24. this.secureRandomSpi.engineSetSeed(seed);
    25. }
    26. } catch (NoSuchAlgorithmException nsae) {
    27. // never happens, because we made sure the algorithm exists
    28. throw new RuntimeException(nsae);
    29. }
    30. }
    31. if (getClass() == SecureRandom.class) {
    32. this.algorithm = prng;
    33. }
    34. }

    在 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 源。

    1. static {
    2. String var0 = SunEntries.getSeedSource();
    3. if (!var0.equals("file:/dev/random") && !var0.equals("file:/dev/urandom")) {
    4. if (var0.length() != 0) {
    5. try {
    6. instance = new SeedGenerator.URLSeedGenerator(var0);
    7. if (debug != null) {
    8. debug.println("Using URL seed generator reading from " + var0);
    9. }
    10. } catch (IOException var2) {
    11. if (debug != null) {
    12. debug.println("Failed to create seed generator with " + var0 + ": " + var2.toString());
    13. }
    14. }
    15. }
    16. } else {
    17. try {
    18. instance = new NativeSeedGenerator(var0);
    19. if (debug != null) {
    20. debug.println("Using operating system seed generator" + var0);
    21. }
    22. } catch (IOException var3) {
    23. if (debug != null) {
    24. debug.println("Failed to use operating system seed generator: " + var3.toString());
    25. }
    26. }
    27. }
    28. if (instance == null) {
    29. if (debug != null) {
    30. debug.println("Using default threaded seed generator");
    31. }
    32. instance = new SeedGenerator.ThreadedSeedGenerator();
    33. }
    34. }

    在 Random 类里,多个实例设置相同的seed,产生的随机数序列也是一样的。而 SecureRandom 则不同,运行下面的代码:

    1. public class RandomTest {
    2. public static void main(String[] args) {
    3. byte[] seed = "hello".getBytes();
    4. for (int i = 0; i < 10; ++i) {
    5. SecureRandom secureRandom = new SecureRandom(seed);
    6. System.out.println(secureRandom.nextInt());
    7. }
    8. }
    9. }

    输出如下所示,每次运行产生的随机数都不一样。

    1. -2105877601
    2. 1151182748
    3. 1329080810
    4. -617594950
    5. 2094315881
    6. -1649759687
    7. -1360561033
    8. -653424535
    9. -927058354
    10. -1577199965

    为什么呢?因为 engineSetSeed 方法设置 seed 时调用的是静态实例 INSTANCE 的 implSetSeed 方法,该方法通过 getMixedRandom 得到的 SecureRandom 来设置 seed,而这个 SecureRandom 初始化种子是系统的。

    1. private static final NativePRNG.RandomIO INSTANCE;
    2. // in NativePRNG
    3. protected void engineSetSeed(byte[] var1) {
    4. INSTANCE.implSetSeed(var1);
    5. }
    6. private void implSetSeed(byte[] var1) {
    7. Object var2 = this.LOCK_SET_SEED;
    8. synchronized(this.LOCK_SET_SEED) {
    9. if (!this.seedOutInitialized) {
    10. this.seedOutInitialized = true;
    11. this.seedOut = (OutputStream)AccessController.doPrivileged(new PrivilegedAction<OutputStream>() {
    12. public OutputStream run() {
    13. try {
    14. return new FileOutputStream(RandomIO.this.seedFile, true);
    15. } catch (Exception var2) {
    16. return null;
    17. }
    18. }
    19. });
    20. }
    21. if (this.seedOut != null) {
    22. try {
    23. this.seedOut.write(var1);
    24. } catch (IOException var5) {
    25. throw new ProviderException("setSeed() failed", var5);
    26. }
    27. }
    28. this.getMixRandom().engineSetSeed(var1);
    29. }
    30. }
    31. private SecureRandom getMixRandom() {
    32. SecureRandom var1 = this.mixRandom;
    33. if (var1 == null) {
    34. Object var2 = this.LOCK_GET_BYTES;
    35. synchronized(this.LOCK_GET_BYTES) {
    36. var1 = this.mixRandom;
    37. if (var1 == null) {
    38. var1 = new SecureRandom();
    39. try {
    40. byte[] var3 = new byte[20];
    41. readFully(this.nextIn, var3);
    42. var1.engineSetSeed(var3);
    43. } catch (IOException var5) {
    44. throw new ProviderException("init failed", var5);
    45. }
    46. this.mixRandom = var1;
    47. }
    48. }
    49. }
    50. return var1;
    51. }

    在 sun.security.provider.SecureRandom.engineSetSeed 方法里,新种子的生成不仅和刚设置的 seed 有关,也和原来的种子(系统产生的 seed)有关。

    1. // in sun.security.provider.SecureRandom
    2. public synchronized void engineSetSeed(byte[] var1) {
    3. if (this.state != null) {
    4. this.digest.update(this.state);
    5. for(int var2 = 0; var2 < this.state.length; ++var2) {
    6. this.state[var2] = 0;
    7. }
    8. }
    9. this.state = this.digest.digest(var1);
    10. }

    /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 的。它可以作为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/413284
推荐阅读
相关标签
  

闽ICP备14008679号