一文詳解編程中的隨機數

隨機數,相信大家都不陌生,網上有很多生成隨機數的小工具。直觀來看,隨機數就是一串雜亂無章的數字、字母、以及符號的組合, 比如pSTkKIiZMOlDxOgwpIQGdlZwrJCRiHRK。但隨機數真的就隨機嗎?真的就無法預測嗎?什麼場景下可以用什麼方式來生成隨機數呢? 這篇文章將爲大家介紹隨機數的類型,在程序中如何使用隨機數,以及隨機數在密碼學中使用場景。希望能儘量地將在開發過程中需要用到的隨機數知識都收納在這裏,方便大家進行查閱!

隨機數的類型

在知乎上看到過一個說法,認爲這個世界沒有真正意義上的隨機,比如扔骰子。如果能算對扔出時的轉速、方向,並測出空氣中的阻力,桌面的阻尼係數,骰子的質量 等等因素,那麼就有機會算出骰子落地時的點數。我猜想賭神大概率也是基於這種原理吧。物理科學上是有“真正”意義的隨機的,那就是量子力學的不可測原理。它是由德國著名物理學家海森堡在1927年發表的論文《論量子理論運動學與力學的物理內涵》中提出來的。不是特別理解其中的內容,但是從字面上簡單的理解,就是對於微觀粒子,它的速度和位置不能準確測量,當對其中一個物理量測量得越準確時,另一個物理量就越模糊。

從編程角度看,我們的隨機數生成器分爲兩種大類型,一種是真隨機數生成器,一種是僞隨機數生成器。

真隨機數生成器 TRNG - True Random Number Generator

前面說了實際上基本沒有真正意義隨機,那程序和算法本身就更加不能產生真隨機,但是我們可以想辦法迂迴地產生統計意義上的真隨機。比如Linux內核的隨機數發生器: Linux維持一個熵池,不斷地收集非確定性的事件,比如時鐘,鼠標的移動,鍵盤的敲擊, IO的響應時間,磁盤的速度,wifi的強弱,內存的變化等等,然後基於一定的算法給出一個數。

僞隨機數生成器 PRNG - Pseudo Random Number Genrator

如果需要快速生成大量的隨機數,那麼真隨機數生成器可能由於收集不到那麼多的隨機事件而產生阻塞行爲。在不需要那麼高安全級別的隨機數需求下,我們可以採用僞隨機數生成器來生成隨機數。僞隨機數生成器一般是基於一個給定的初始值,也就是種子 - seed,用一定的算法來算出一個數。且算法內部維持一個內部狀態,每次生成一個新的隨機數,這個值都會跟着變化,這樣就能產生不一樣的隨機數來。常見的僞隨機數生成器的算法有:

  • 線性同餘法 - Linear Congruential Generator (簡稱LCG)
  • 馬特賽特旋轉演算法 - Mersenne Twister.

Java中的Random() 用的就是線性同餘法。線性同餘方法是目前應用廣泛的僞隨機數生成算法,其基本思想是通過對前一個數進行線性運算並取模從而得到下一個數,遞歸公式爲:

在這裏插入圖片描述

其中A,B,M是產生器所用到的常量

隨機數的使用

真隨機數

我們可以通過下面這個命令得到操作系統內核提供的外部熵隨機數生成器:

λ head -c 32 /dev/random | openssl enc -base64
zLvAZ2vfFTUQ+ENPLdbG2F8B3wv86LM9X2s3DeymN28=

這個命令將會從Linux內核的熵池中讀取一個32位的隨機數,並用64進制展示出來。我們也可以選擇用數字的形式展示出來:

λ cat /dev/random| tr -dc '0-9' | fold -w 10| head -n 4
0231488700
4599846604
7629411051
4199097655

上面這個命令從熵池中4個10位的隨機數,並用0-9展示出來。

但用這個命令的時候要小心,由於熵池中的值通過記錄系統的隨機事件得來的,那麼就有可能有用完的時候,那麼這時這個命令就會阻塞在這裏,直到有系統隨機事件進到熵池中才會繼續。這樣對程序來說不是很友好,於是操作系統的隨機數生成器一般都提供另外一個工具,在熵池的隨機事件用完之後,能用僞隨機算法產生一個隨機數給你:

λ cat /dev/urandom| tr -dc 'a-zA-Z0-9' | fold -w 10| head -n 4
EK0Z3g49By
csziDZeWtO
EhHu30IcM4
PyDyY47Ah5

Golang的內置隨機數生成器rand就是基於 /dec/urandom來實現的。

開發中常見的隨機數生成器

這裏我們以Java語言爲例,介紹一下常見的隨機數生成器的用法。

  • Random()

首先一起來看一下這個最常見的Random. Random實現了基於線性同餘法的僞隨機數生成器,其構造函數接收作爲種子的參數seed,如果不給定seed,則默認採用當前時間戳作爲種子。 下面的函數可以生成指定位數的隨機字母串:

public static String ALPHA = "abcdefghijkllmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";   
public static String generate_alphabetic_using_Random(int length) {
        Random random = new Random();
        StringBuffer buffer = new StringBuffer();
        int bound = ALPHA.length();
        for(int i=0; i< length; i++){
            buffer.append(ALPHA.charAt(random.nextInt(bound)));
        }
        return buffer.toString();
    }
  • RandomStringUtil

如果大家只是偶爾需要用到隨機字符串,不要什麼特別的隨機字符集的話,那麼可以用Apache Common提供的RandomStringUtil 輔助類,可以方便地生成常見的隨機字符串:

    public static String generate_alphabetic_using_RandomUtils(int length){
       return RandomStringUtils.randomAlphabetic(length);
    }

作爲一款良心包,Apache Common一般都會有一系列的實現方法可供選擇,比如:

在這裏插入圖片描述

  • Math.random()

    如果要生成的是隨機數,那麼也可以使用Math類的random方法來實現。

      System.out.println("\n--- using Math.random ---");
            for (int i = 0; i < 20; i++) {
                System.out.println(Math.random() * 100);
            }
    

    這裏提一下,Math.random()的實現其實是調用了Random.nextDouble()的,使用它本質上是Random的一個包裝

隨機數生成的併發性能問題

前面我們提到Random隨機數生成器裏面維護着一個內部狀態,每次隨機數的生成這個內部狀態都需要跟着改變,這樣才能生成不同的隨機數。我們跟一下源碼就可以發現Random的seed是一個AtomicLong型,當計算下一個隨機數的時候,會用到CompareAndSwap (CAS) 操作,如果切換失敗的話,那麼就重新計算下一個隨機數:

   private final AtomicLong seed;
    
   protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

當併發有很多個線程都在獲取下一個種子的時候,那麼性能就會降下來,因爲有很多failed-retry的。

有坑就填坑! 既然有併發問題,那麼我們就來ThreadLocal吧。Java內置了一個ThreadLocalRandom類,這個類繼承了Random, 但是每一個線程都有一個ThreadLocal的seed,這樣當併發計算next()的時候,CAS操作就不會有太多衝突了。ThreadLocalRandom的用法也是很簡單的, 跟Random基本一致:

public static String ALPHA = "abcdefghijkllmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";   
public static String generate_alphabetic_using_ThreadLocalRandom(int length) {
        Random random = ThreadLocalRandom.current();
        StringBuffer buffer = new StringBuffer();
        int bound = ALPHA.length();
        for(int i=0; i< length; i++){
            buffer.append(ALPHA.charAt(random.nextInt(bound)));
        }
        return buffer.toString();
    }

Random真的隨機嗎?

我們來看下面這個用法:

  public static void sameSeed_generate_sameResult(long seed){
        Random random1 = new Random(seed);
        Random random2 = new Random(seed);
        System.out.println(String.format("random1: %d, random2: %d", random1.nextInt(100), random2.nextInt(100)));
    }

  public static void main(String[] args) {
        System.out.println("\n--- sameSeed_generate_sameResult ---");
        sameSeed_generate_sameResult(3000);
    }

你種子相同的Random生成的隨機數是一樣的:

--- sameSeed_generate_sameResult ---
random1: 17, random2: 17

這也可以理解,初始值seed一樣,算法一樣,那麼生成的結果也會是一樣的。 這種情況對於大多數需要隨機數的場景來說,也是可以接受的。但是對於安全要求比較高的場景,比如密碼學中,這樣就容易被人攻擊猜到隨機數,比如Java的Random(),默認的種子是當前時間戳, 同餘算法是公開的nextseed = (oldseed * multiplier + addend) & mask;,算法中的A,B,M這三個常量也是固定的

    private static final long multiplier = 0x5DEECE66DL;
    private static final long addend = 0xBL;
    private static final long mask = (1L << 48) - 1;

那麼就有可能通過窮舉一段時間內的所有seed,來求得隨機數。

還是那句話,有坑填坑!

Java在security這個包裏面提供了一個SecureRandom的類,以操作系統隨機數生成器生成seed,再用Hash算法SHA1計算出摘要值作爲最終結果。這樣以操作系統隨機數生成器生成seed 加上 一動有蝴蝶效應的 hash算法,計算出來的隨機數更加能以被猜測出來。

    public static String generate_alphaString_using_SecureRandom(int length)
        throws NoSuchAlgorithmException {
//        Random random =new SecureRandom(); // default is SHA1PRNG
        Random random =SecureRandom.getInstance("SHA1PRNG");
        StringBuffer buffer = new StringBuffer();
        int bound = ALPHA.length();
        for(int i=0; i< length; i++){
            buffer.append(ALPHA.charAt(random.nextInt(bound)));
        }
        return buffer.toString();
    }

用SecureRandom的注意事項

前面我們說到真隨機數的生成有兩個方式,一個是/dev/random, 一個是/dev/urandom,不同的是/dev/random是阻塞的。而SHA1PRNG 會根據JRE裏面的配置選擇/dec/random 還是/dev/urandom,如果裏面配置的是securerandom.source=file:/dev/random, 那麼當操作系統的熵池用完之後,你的程序在求隨機數的時候會被阻塞住,直到有隨機事件到來。據說Tomcat裏面就是使用SecuredRandom.getInstance('SHA1PRNG') 的,使用有時初始化很慢很慢。具體的解決方案就是, 打開$JAVA_PATH/jre/lib/security/java.security , 確保裏面的securerandom.source配置爲file:/dev/random .

隨機數的應用場景

隨機數的應用場景有很多,尤其是在密碼學應用中,基本上大部分的密碼學算法實際應用中都用到了隨機數。下面列舉一些常見的使用場景:

  • UUID: UUID v4 裏面是由 6個固定位 + 122個隨機數來組成的
  • 密碼學算法應用中用到的隨機數
    • 密鑰:對稱加密算法,公開密鑰算法,Message Authentication Code算法都會用到密鑰,而大部分情況下,密鑰就是隨機數
    • IV: 塊密碼加密中CBC迭代模式會用隨機數作爲IV,這樣可以使得相同的明文加密出來的密文都不同,提高密碼猜測的難度
    • nonce: 塊密碼算法的CTR模式後用到nonce
    • salt: 基於口令的加密算法會用到,很多Hash算法也會用到隨機數作爲鹽

以上內容介紹了隨機數的類型,隨機數的使用,重點是在Java裏面是怎麼用的,以及多線程開發下的性能,密碼學隨機數生成,最後介紹了隨機數在密碼學中的使用場景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章