java隨機數之Random和SecureRandom


一、前言

  在一次項目的安全測試源代碼掃描中,報由random()實施的隨機數生成器不能抵擋加密攻擊。其中報漏洞的源代碼如下:

int number = (int) ((Math.random() * 9 + 1) * Math.pow(10, 6 -1));
String numStr = String.valueOf(number);

   其中的報漏洞的解釋是這樣說的,在對安全性要求較高的環境中,使用能夠生成可預測值的函數作為偽隨機數據源,會產生Insecure Randomness(不安全隨機性)錯誤。電腦是一種具有確定性的機器,因此不可能產生真正的隨機性,偽隨機數生成器(PRNG)近似於隨機算法,始於一個能計算后續數值的種子。PRNG 包括兩種類型: 統計學的 PRNG 和密碼學的 PRNG。 統計學的 PRNG 提供很多有用的統計屬性, 但其輸出結果很容易預測, 因此容易復制數值流。 在安全性所依賴的生成值不可預測的情況下, 這種類型並不適用。 密碼學的 PRNG 生成的輸出結果較難預測, 可解決這一問題。 為保證值的加密安全性, 必須使攻擊者根本無法、 或幾乎不可能鑒別生成的隨機值和真正的隨機值。 通常情況下, 如果並未聲明 PRNG 算法帶有加密保護, 那么它很可能就是統計學的 PRNG, 因此不應在對安全性要求較高的環境中使用, 否則會導致嚴重的漏洞(如易於猜測的密碼、 可預測的加密密鑰、 Session Hijacking 和 DNS Spoofing) 。

示例: 下面的代碼可利用統計學的 PRNG 為購買產品后仍在有效期內的收據創建一個 URL。

String GenerateReceiptURL(String baseUrl) {
  Random ranGen = new Random();
  ranGen.setSeed((new Date()).getTime());
  return (baseUrl + ranGen.nextInt(400000000) + ".html");
}

  這段代碼使用 Random.nextInt() 函數為它生成的收據頁面生成“唯一”的標識符。 由於 Random.nextInt() 是統計學的 PRNG, 攻擊者很容易猜到其生成的字符
串。 盡管收據系統的底層設計並不完善, 但若使用不會生成可預測收據標識符的隨機數生成器(如密碼學的 PRNG),就會更安全些。

二、解決方案
  當不可預測性至關重要時, 如大多數對安全性要求較高的環境都采用隨機性, 這時可以使用密碼學的 PRNG。 不管選擇了哪一種 PRNG, 都要始終使用帶有充足熵的數值作為該算法的種子。 (諸如當前時間之類的數值只提供很小的熵, 因此不應該使用。 )
  Java 語言在 java.security.SecureRandom 中提供了一個加密 PRNG。 就像 java.security 中其他以算法為基礎的類那樣, SecureRandom 提供了與某個特定算法集合相關的包, 該包可以獨立實現。 當使用 SecureRandom.getInstance() 請求一個 SecureRandom 實例時, 您可以申請實現某個特定的算法。 如果算法可行, 那么您可以將它作為 SecureRandom 的對象使用。  如果算法不可行, 或者您沒有為算法明確特定的實現方法, 那么會由系統為您選擇 SecureRandom的實現方法。
  Sun 在名為 SHA1PRNG 的 Java 版本中提供了一種單獨實現 SecureRandom 的方式, Sun 將其描述為計算:
“SHA-1 可以計算一個真實的隨機種子參數的散列值, 同時, 該種子參數帶有一個 64 比特的計算器, 會在每一次操作后加 1。 在 160 比特的 SHA-1 輸出中, 只能使用64 比特的輸出 1。 ”
然而, 文檔中有關 Sun 的 SHA1PRNG 算法實現細節的相關記錄很少, 人們無法了解算法實現中使用的熵的來源, 因此也並不清楚輸出中到底存在多少真實的隨機數值。盡管有關 Sun 的實現方法網絡上有各種各樣的猜測, 但是有一點無庸置疑, 即算法具有很強的加密性, 可以在對安全性極為敏感的各種內容中安全地使用。

三、使用:

案例1:

SecureRandom random1 = SecureRandom.getInstance("SHA1PRNG"); 
SecureRandom random2 = SecureRandom.getInstance("SHA1PRNG"); 
for (int i = 0; i < 5; i++) { 
    System.out.println(random1.nextInt() + " != " + random2.nextInt()); 
}
// 結果
// 704046703 != 2117229935
// 60819811 != 107252259
// 425075610 != -295395347
// 682299589 != -1637998900
// -1147654329 != 1418666937

案例2:獲取隨機字符串,參考微信(wxpay-sdk,java_sdk_v3.0.9)

import java.security.SecureRandom;
import java.util.Random;
public class WXPayUtil {
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* 獲取隨機字符串 Nonce Str
*
* @return String 隨機字符串
*/
public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}

public static void main(String[] args) {
System.out.println(WXPayUtil.generateNonceStr());
}
}
// 結果
// oIjXt5Y9g2drzfzaR4hF9haaiDjSVDCX

 四、常用隨機數的一些匯總

  對於像Math.random()等實現的隨機算法是偽隨機,也就是有規則的隨機。在進行隨機時,隨機算法的起源數字稱為種子數,在種子數的基礎上進行一定的交換,從而產生需要的隨機數字。在實際的項目開發過程中,經常需要產生一些隨機數值,例如網站登錄中的校驗數字等,或者需要以一定的幾率實現某種效果,游戲程序中的物品掉落,抽獎程序等。

1. Math.random() 靜態方法,產生的隨機數是 0 - 1 之間的一個 double,即 0 <= random <= 1.

缺點:結果可預測

舉例:

for (int i = 0; i < 10; i++) {
  System.out.println(Math.random());
}
0.3598613895606426
0.2666778145365811
0.25090731064243355
0.011064998061666276
0.600686228175639
0.9084006027629496
0.12700524654847833
0.6084605849069343
0.7290804782514261
0.9923831908303121

實現原理:

When this method is first called, it creates a single new pseudorandom-number generator, exactly as if by the expression new java.util.Random()
This new pseudorandom-number generator is used thereafter for all calls to this method and is used nowhere else.

當第一次調用 Math.random() 方法時,自動創建了一個偽隨機數生成器,實際上用的是 new java.util.Random()。當接下來繼續調用 Math.random() 方法時,就會使用這個新的偽隨機數生成器。

源碼如下:

public static double random() {
    Random rnd = randomNumberGenerator;
    if (rnd == null) rnd = initRNG(); // 第一次調用,創建一個偽隨機數生成器
    return rnd.nextDouble();
}
private static synchronized Random initRNG() {
    Random rnd = randomNumberGenerator;
    return (rnd == null) ? (randomNumberGenerator = new Random()) : rnd; // 實際上用的是new java.util.Random()
}
This method is properly synchronized to allow correct use by more than one thread. However, 
if many threads need to generate pseudorandom numbers at a great rate, it may reduce contention for each thread to have its own pseudorandom-number generator.

initRNG() 方法是 synchronized 的,因此在多線程情況下,只有一個線程會負責創建偽隨機數生成器(使用當前時間作為種子),其他線程則利用該偽隨機數生成器產生隨機數。

因此 Math.random() 方法是線程安全的。

什么情況下隨機數的生成線程不安全:

  • 線程1在第一次調用 random() 時產生一個生成器 generator1,使用當前時間作為種子。
  • 線程2在第一次調用 random() 時產生一個生成器 generator2,使用當前時間作為種子。
  • 碰巧 generator1 和 generator2 使用相同的種子,導致 generator1 以后產生的隨機數每次都和 generator2 以后產生的隨機數相同。

什么情況下隨機數的生成線程安全: Math.random() 靜態方法使用

  • 線程1在第一次調用 random() 時產生一個生成器 generator1,使用當前時間作為種子。
  • 線程2在第一次調用 random() 時發現已經有一個生成器 generator1,則直接使用生成器 generator1
public class JavaRandom {
    public static void main(String args[]) {
        new MyThread().start();
        new MyThread().start();
    }
}
class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + Math.random());
        }
    }
}

結果:

Thread-1: 0.8043581595645333
Thread-0: 0.9338269554390357
Thread-1: 0.5571569413128877
Thread-0: 0.37484586843392464

 2. 對於其它的一些隨機數相關的工具類可參考java.util.Random 工具類、java.util.concurrent.ThreadLocalRandom 工具類、java.Security.SecureRandom、隨機字符串.(https://www.jianshu.com/p/2f6acd169202)

 

 

參考來源:

作者:專職跑龍套
鏈接:https://www.jianshu.com/p/2f6acd169202
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM