上一篇文章中介紹了消息驗證碼,這篇文章咱們來聊聊隨機數(shù)。隨機數(shù)看起來是一個很簡單的概念,不論哪種編程語言都提供了簡單的生成隨機數(shù)的方法,有必要單獨寫一篇文章么?
隨機數(shù)看起來簡單,但在密碼學(xué)中的用途非常大。比如用于加解密的密鑰本質(zhì)上就是一個隨機數(shù),密碼學(xué)算法內(nèi)部也會用到隨機數(shù)。從開發(fā)者直觀的角度看,隨機數(shù)就是一串雜亂無序的字母、數(shù)字、符號組合,但如何生成隨機數(shù)很重要,也就是說如何正確使用隨機數(shù)生成算法非常重要。
反映二戰(zhàn)期間歷史的諜戰(zhàn)片中,經(jīng)常有破譯密電的情節(jié)。一種常用的手段就是截獲密報,從眾多密報中找出規(guī)律,從而破解密碼。這種破解方式在現(xiàn)代計算機面前太容易了。首先,因為信息技術(shù)的廣泛使用,密文的收集非常容易,其次,計算機運算速度快,遍歷、迭代都非常容易做到。所以現(xiàn)代密碼學(xué)的首要要求是不可預(yù)測,這也是隨機數(shù)為什么如此重要。比如加密密鑰,應(yīng)該是其他任何人都不能生成,或者以相同的密鑰生成方式生成。
下面討論計算機科學(xué)中的隨機數(shù)及其在密碼學(xué)中的作用,以及偽隨機數(shù)生成器(Preudo Random Number Generator,PRNG)、密碼學(xué)偽隨機生成器(Cryptography secure Preudo Random Number Generator,CSPRNG)以及開發(fā)人員應(yīng)如何生成和使用隨機數(shù)的一些準(zhǔn)則。
偽隨機數(shù)生成器(PRNG)
PRNG是從某個初始熵(種子)開始,并通過某種計算來計算下一個隨機數(shù)的函數(shù),而這些計算在不知道種子的情況下是無法預(yù)測的。這種計算稱為偽隨機函數(shù)。
偽隨機函數(shù)內(nèi)部會維護一個狀態(tài)(internal state)。首先,通過初始種子初始化狀態(tài)。當(dāng)生成下一個隨機數(shù)時,它是從內(nèi)部狀態(tài)(使用某種計算或公式)計算出來的,然后更改偽隨機函數(shù)的內(nèi)部狀態(tài)(使用某種計算或公式)。當(dāng)生成下一個隨機數(shù)時,將再次根據(jù)函數(shù)的內(nèi)部狀態(tài)進(jìn)行計算,并再次更改此狀態(tài),依此類推。以最簡單的形式可以執(zhí)行以下過程:
偽隨機數(shù)生成
如果每次熵(或種子)是一樣的,生成的隨機數(shù)也是相同的,所以熵(或種子)對于隨機數(shù)生成器非常重要。
好的隨機數(shù)生成器應(yīng)該是快速的,并且應(yīng)該具有統(tǒng)計隨機性(請參閱Diehard測試),即在一段時間內(nèi)所有數(shù)字的生成機會均應(yīng)相同。而CSPRNG有更高的要求,還要求不可預(yù)測性和不可重現(xiàn)性。所謂不可重現(xiàn)性(unrepeat)就是不管經(jīng)過多長時間,不會產(chǎn)生完全相同的隨機數(shù)。當(dāng)然,在軟件層面不可能生成完全不一樣的隨機數(shù),在一定周期內(nèi),密碼學(xué)隨機數(shù)算法最終會生成兩個完全相同的隨機數(shù),只是周期長短的問題,在密碼學(xué)中應(yīng)該盡量使用周期相對長的隨機數(shù)。
初始熵(種子)
為了安全起見,PRNG應(yīng)該從真正隨機的初始種子開始,這絕對是不可預(yù)測的。如果種子是可預(yù)測的,它將生成可預(yù)測的隨機數(shù)序列,并且整個隨機生成過程將是不安全的。這就是為什么在開始時擁有不可預(yù)測的隨機性(安全種子)非常重要的原因。
如何以安全的方式初始化偽隨機生成器?答案很簡單:收集隨機性(熵)。
熵
在計算機科學(xué)中,“熵”是指不可預(yù)測的隨機性,通常以位為單位進(jìn)行度量。例如,如果您移動計算機的鼠標(biāo),它將生成一些難以預(yù)測的事件,例如鼠標(biāo)光標(biāo)的開始位置和結(jié)束位置。如果我們假設(shè)鼠標(biāo)在[0...255]像素范圍內(nèi)更改了位置,則從此鼠標(biāo)移動收集的熵應(yīng)約為8位(因為2^8=255)。另一個示例:如果要求用戶考慮一個[0...1000]范圍內(nèi)的數(shù)字,則該數(shù)字將會是大約9-10位的熵(因為2^10=1024)。為了收集256位的熵(例如安全地生成256位的整數(shù)),您將需要考慮一系列此類事件的序列(例如用戶的鼠標(biāo)移動和鍵盤輸入)。
收集熵
我們可以從計算機中許多難以預(yù)測的事件中收集熵:鍵盤點擊、鼠標(biāo)移動、網(wǎng)絡(luò)活動、相機、麥克風(fēng)輸入等,以及它們發(fā)生的時間。初始隨機性的收集通常是由操作系統(tǒng)(OS)在內(nèi)部執(zhí)行的,操作系統(tǒng)提供了標(biāo)準(zhǔn)API來訪問它(例如,從Linux中的/dev/random文件讀取)。
應(yīng)用軟件也可以通過要求用戶移動鼠標(biāo)、在鍵盤上鍵入內(nèi)容、在麥克風(fēng)上說某些內(nèi)容或在相機前面移動一會兒來明確地收集熵。一個很好的例子是bitaddress.org錢包應(yīng)用程序,該應(yīng)用程序?qū)⑹髽?biāo)移動與鍵盤事件結(jié)合在一起以收集熵:
收集熵
另外一個例子就是openssl命令行工具,如果生成密鑰,會讓您敲擊鍵盤,且要敲一定數(shù)量的按鍵,然后才會生成密鑰。
CSPRNG(密碼學(xué)安全隨機數(shù)生成器)
根據(jù)定義,CSPRNG是一種偽隨機數(shù)發(fā)生器(PRNG),要使PRNG成為CSPRNG,有兩個主要要求:
●滿足下一個比特測試:如果某人從PRNG開始就知道所有k個比特,那么他將無法使用合理的計算資源來預(yù)測k+1個比特。
●抗惡意播種:即便某一攻擊能獲得一段時間上對CSPRNG的輸入的完全或部分控制,要預(yù)測或再現(xiàn)來自CSPRNG的任何隨機輸出仍然是不可行的。
操作系統(tǒng)中的熵通常數(shù)量有限,并且等待更多的熵是緩慢且不切實際的。大多數(shù)密碼應(yīng)用程序使用CSPRNG,CSPRNG會將來自操作系統(tǒng)的可用熵“擴展”到更多位,這是加密目的所必需的,并且符合上述CSPRNG要求。
大多數(shù)CSPRNG結(jié)合使用來自操作系統(tǒng)和高質(zhì)量PRNG生成器的熵,它們經(jīng)常“重置”,這意味著當(dāng)新的熵來自操作系統(tǒng)時(例如,來自用戶輸入、系統(tǒng)中斷、磁盤I/O或硬件隨機產(chǎn)生),基礎(chǔ)PRNG根據(jù)即將到來的新熵位來更改其內(nèi)部狀態(tài)。隨著時間的推移,這種不斷的播種使CSPRNG變得非常難以預(yù)測和分析。
通常,現(xiàn)代OS CSPRNG API將來自環(huán)境的不斷收集的熵與其內(nèi)置偽隨機算法的內(nèi)部狀態(tài)結(jié)合起來,并進(jìn)行連續(xù)重新播種,以確保生成的隨機性具有最大的不可預(yù)測性,同時具有高速和無阻塞行為。
硬件隨機發(fā)生器
硬件隨機發(fā)生器,稱為真隨機數(shù)發(fā)生器(True Random Number Generator,TRNG),通常捕獲物理過程或現(xiàn)象,例如光的可見光譜、環(huán)境的熱噪聲、大氣噪聲等。物理環(huán)境的隨機性是通過專門的傳感器收集,然后由設(shè)備進(jìn)行放大和處理,最后通過USB、PCI Express或其他標(biāo)準(zhǔn)接口傳輸?shù)接嬎銠C。
現(xiàn)代微處理器(CPU)提供了內(nèi)置的硬件隨機發(fā)生器,可通過特殊的CPU指令RdRand訪問該發(fā)生器,該指令將隨機整數(shù)返回到CPU寄存器之一。
如今,大多數(shù)加密應(yīng)用程序都不需要硬件隨機數(shù)生成器,因為操作系統(tǒng)中的熵對于常規(guī)加密目的而言足夠安全。對于具有較高安全性要求的系統(tǒng)(例如銀行和金融應(yīng)用程序、證書頒發(fā)機構(gòu)和大額付款處理等),需要使用TRNG。
開發(fā)人員如何正確使用CSPRNG?
通常,開發(fā)人員通過加密庫訪問其操作系統(tǒng)的密碼學(xué)隨機數(shù)生成器(CSPRNG)。
●在Linux和macOS中,可以認(rèn)為/dev/random和/dev/urandom隨機性源對于大多數(shù)加密目的而言都是足夠安全的,并且大多數(shù)加密庫都在內(nèi)部訪問它們。
●在Windows中,可以使用來自下一代(CNG)的Crypto API或更高級的密碼庫中的BCryptGenRandom函數(shù)安全地生成用于加密目的的隨機數(shù)。
●在C#中,使用.NET Framework或.NET Core中的System.Security.Cryptography.RandomNumberGenerator.Create()。
●在Python中,請使用os.urandom()或secrets庫。
●在Java中,請使用java.security.SecureRandom系統(tǒng)類。
●在JavaScript中,將window.crypto.getRandomValues(Uint8Array)用于客戶端(在Web瀏覽器中)或crypto.randomBytes()用于服務(wù)器端(在Node.js中)。
切勿將Math.random()或類似的不安全RNG函數(shù)用于加密目的!
小結(jié)
看到上面的介紹,是否有些暈。其實在開發(fā)中我們并不需要理解隨機數(shù)是如何生成的,但我們需要時刻牢記在心的是,隨機數(shù)生成非常重要,一定要使用安全的API生成安全的隨機數(shù)。在大多數(shù)情況下,我們只需要掌握系統(tǒng)提供了哪些安全的隨機數(shù)生成API,知道如何使用即可。
密碼學(xué)應(yīng)用中很多場景會涉及隨機數(shù),不同的用途有不同的稱呼,比如密鑰、初始化向量(IV)、nonce、鹽(salt)等,目前可以不用關(guān)心這些概念,后續(xù)用到會進(jìn)行講解。