1. 程式人生 > 實用技巧 >Java 隨機數生成器 Random & SecureRandom 原理分析

Java 隨機數生成器 Random & SecureRandom 原理分析

文章目錄
java.util.Random
java.Security.SecureRandom
/dev/random 與 /dev/urandom
資料

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,
  • The Art of Computer Programming, Volume 2, Section 3.2.1.)
  • 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.
  • 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.
  • Many applications will find the method {@link Math#random} simpler to use.
  • 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.
  • 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 (seedUniquifier.compareAndSet(current, next))
            return next;
    }
} 

java.Security.SecureRandom
介紹 Random 類時提到過,要生成加密基本的隨機數應該使用 SecureRandom 類,該類資訊如下所示:

/**

  • This class provides a cryptographically strong random number
  • generator (RNG).
  • A cryptographically strong random number

  • minimally complies with the statistical random number generator tests
  • specified in
  • FIPS 140-2, Security Requirements for Cryptographic Modules,
  • 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
  • RFC 1750: Randomness Recommendations for Security.
  • A caller obtains a SecureRandom instance via the

  • no-argument constructor or one of the {@code getInstance} methods:
  •  SecureRandom random = new SecureRandom();
    
  • 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.
  • Typical callers of SecureRandom invoke the following methods

  • to retrieve random bytes:
  •  SecureRandom random = new SecureRandom();
    
  •  byte bytes[] = new byte[20];
    
  •  random.nextBytes(bytes);
    
  • Callers may also invoke the {@code generateSeed} method

  • to generate a given number of seed bytes (to seed other random number
  • generators, for example):
  •  byte seed[] = random.generateSeed(20);
    
  • 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);
1
2
下面我們看看其內部實現:

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 的。它可以作為生成較低強度密碼的偽隨機數生成器,不建議用於生成高強度長期密碼。