加鹽hash儲存密碼的正確方式

wyzsk發表於2020-08-19
作者: D&G · 2014/03/13 14:05

0x00 背景


大多數的web開發者都會遇到設計使用者賬號系統的需求。賬號系統最重要的一個方面就是如何保護使用者的密碼。一些大公司的使用者資料庫洩露事件也時有發生,所以我們必須採取一些措施來保護使用者的密碼,即使網站被攻破的情況下也不會造成較大的危害。保護密碼最好的的方式就是使用帶鹽的密碼hash(salted password hashing).對密碼進行hash操作是一件很簡單的事情,但是很多人都犯了錯。接下來我希望可以詳細的闡述如何恰當的對密碼進行hash,以及為什麼要這樣做。

0x01 重要提醒


如果你打算自己寫一段程式碼來進行密碼hash,那麼趕緊停下吧。這樣太容易犯錯了。這個提醒適用於每一個人,不要自己寫密碼的hash演算法 !關於儲存密碼的問題已經有了成熟的方案,那就是使用phpass或者本文提供的原始碼。

0x02 什麼是hash


#!python
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69afe7dd238f3364946366
hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542

Hash演算法是一種單向的函式。它可以把任意數量的資料轉換成固定長度的“指紋”,這個過程是不可逆的。而且只要輸入發生改變,哪怕只有一個bit,輸出的hash值也會有很大不同。這種特性恰好合適用來用來儲存密碼。因為我們希望使用一種不可逆的演算法來加密儲存的密碼,同時又需要在使用者登陸的時候驗證密碼是否正確。

在一個使用hash的賬號系統中,使用者註冊和認證的大致流程如下:

1. 使用者建立自己的賬號
2. 使用者密碼經過hash操作之後儲存在資料庫中。沒有任何明文的密碼儲存在伺服器的硬碟上。
3. 使用者登陸的時候,將使用者輸入的密碼進行hash操作後與資料庫裡儲存的密碼hash值進行對比。
4. 如果hash值完全一樣,則認為使用者輸入的密碼是正確的。否則就認為使用者輸入了無效的密碼。
5. 每次使用者嘗試登陸的時候就重複步驟3和步驟4。

在步驟4的時候不要告訴使用者是賬號還是密碼錯了。只需要顯示一個通用的提示,比如賬號或密碼不正確就可以了。這樣可以防止攻擊者列舉有效的使用者名稱。

還需要注意的是用來保護密碼的hash函式跟資料結構課上見過的hash函式不完全一樣。比如實現hash表的hash函式設計的目的是快速,但是不夠安全。只有加密hash函式(cryptographic hash functions)可以用來進行密碼的hash。這樣的函式有SHA256, SHA512, RipeMD, WHIRLPOOL等。

一個常見的觀念就是密碼經過hash之後儲存就安全了。這顯然是不正確的。有很多方式可以快速的從hash恢復明文的密碼。還記得那些md5破解網站吧,只需要提交一個hash,不到一秒鐘就能知道結果。顯然,單純的對密碼進行hash還是遠遠達不到我們的安全需求。下一部分先討論一下破解密碼hash,獲取明文常見的手段。

0x03 如何破解hash


字典和暴力破解攻擊(Dictionary and Brute Force Attacks)

最常見的破解hash手段就是猜測密碼。然後對每一個可能的密碼進行hash,對比需要破解的hash和猜測的密碼hash值,如果兩個值一樣,那麼之前猜測的密碼就是正確的密碼明文。猜測密碼攻擊常用的方式就是字典攻擊和暴力攻擊。

Dictionary Attack

Trying apple        : failed
Trying blueberry    : failed
Trying justinbeiber : failed
...
Trying letmein      : failed
Trying s3cr3t       : success!

字典攻擊是將常用的密碼,單詞,短語和其他可能用來做密碼的字串放到一個檔案中,然後對檔案中的每一個詞進行hash,將這些hash與需要破解的密碼hash比較。這種方式的成功率取決於密碼字典的大小以及字典的是否合適。

Brute Force Attack

Trying aaaa : failed
Trying aaab : failed
Trying aaac : failed
...
Trying acdb : failed
Trying acdc : success!

暴力攻擊就是對於給定的密碼長度,嘗試每一種可能的字元組合。這種方式需要花費大量的計算機時間。但是理論上只要時間足夠,最後密碼一定能夠破解出來。只是如果密碼太長,破解花費的時間就會大到無法承受。

目前沒有方式可以阻止字典攻擊和暴力攻擊。只能想辦法讓它們變的低效。如果你的密碼hash系統設計的是安全的,那麼破解hash唯一的方式就是進行字典或者暴力攻擊了。

查表破解(Lookup Tables)

對於特定的hash型別,如果需要破解大量hash的話,查表是一種非常有效而且快速的方式。它的理念就是預先計算(pre-compute)出密碼字典中每一個密碼的hash。然後把hash和對應的密碼儲存在一個表裡。一個設計良好的查詢表結構,即使儲存了數十億個hash,每秒鐘仍然可以查詢成百上千個hash。

如果你想感受下查表破解hash的話可以嘗試一下在CraskStation上破解下下面的sha256 hash。

#!python
c11083b4b0a7743af748c85d343dfee9fbb8b2576c05f3a7f0d632b0926aadfc
08eac03b80adc33dc7d8fbe44b7c7b05d3a2c511166bdb43fcb710b03ba919e7
e4ba5cbd251c98e6cd1c23f126a3b81d8d8328abc95387229850952b3ef9f904
5206b8b8a996cf5320cb12ca91c7b790fba9f030408efe83ebb83548dc3007bd

反向查表破解(Reverse Lookup Tables)

#!bash
Searching for hash(apple) in users' hash list...     : Matches [alice3, 0bob0, charles8]
Searching for hash(blueberry) in users' hash list... : Matches [usr10101, timmy, john91]
Searching for hash(letmein) in users' hash list...   : Matches [wilson10, dragonslayerX, joe1984]
Searching for hash(s3cr3t) in users' hash list...    : Matches [bruce19, knuth1337, john87]
Searching for hash([email protected]) in users' hash list...  : No users used this password

這種方式可以讓攻擊者不預先計算一個查詢表的情況下同時對大量hash進行字典和暴力破解攻擊。

首先,攻擊者會根據獲取到的資料庫資料製作一個使用者名稱和對應的hash表。然後將常見的字典密碼進行hash之後,跟這個表的hash進行對比,就可以知道用哪些使用者使用了這個密碼。這種攻擊方式很有效果,因為通常情況下很多使用者都會有使用相同的密碼。

彩虹表 (Rainbow Tables)

彩虹表是一種使用空間換取時間的技術。跟查表破解很相似。只是它犧牲了一些破解時間來達到更小的儲存空間的目的。因為彩虹表使用的儲存空間更小,所以單位空間就可以儲存更多的hash。彩虹表已經能夠破解8位長度的任意md5hash。彩虹表具體的原理可以參考http://www.project-rainbowcrack.com/

下一章節我們會討論一種叫做“鹽”(salting)的技術。透過這種技術可以讓查表和彩虹表的方式無法破解hash。

0x04 加鹽(Adding Salt)


#!python
hash("hello")                    = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007

查表和彩虹表的方式之所以有效是因為每一個密碼的都是透過同樣的方式來進行hash的。如果兩個使用者使用了同樣的密碼,那麼一定他們的密碼hash也一定相同。我們可以透過讓每一個hash隨機化,同一個密碼hash兩次,得到的不同的hash來避免這種攻擊。

具體的操作就是給密碼加一個隨即的字首或者字尾,然後再進行hash。這個隨即的字尾或者字首成為“鹽”。正如上面給出的例子一樣,透過加鹽,相同的密碼每次hash都是完全不一樣的字串了。檢查使用者輸入的密碼是否正確的時候,我們也還需要這個鹽,所以鹽一般都是跟hash一起儲存在資料庫裡,或者作為hash字串的一部分。

鹽不需要保密,只要鹽是隨機的話,查表,彩虹表都會失效。因為攻擊者無法事先知道鹽是什麼,也就沒有辦法預先計算出查詢表和彩虹表。如果每個使用者都是使用了不同的鹽,那麼反向查表攻擊也沒法成功。

下一節,我們會介紹一些鹽的常見的錯誤實現。

0x05 錯誤的方式:短的鹽和鹽的複用


最常見的錯誤實現就是一個鹽在多個hash中使用或者使用的鹽很短。

鹽的複用(Salt Reuse)

不管是將鹽硬編碼在程式裡還是隨機一次生成的,在每一個密碼hash裡使用相同的鹽會使這種防禦方法失效。因為相同的密碼hash兩次得到的結果還是相同的。攻擊者就可以使用反向查表的方式進行字典和暴力攻擊。只要在對字典中每一個密碼進行hash之前加上這個固定的鹽就可以了。如果是流行的程式的使用了硬編碼的鹽,那麼也可能出現針對這種程式的這個鹽的查詢表和彩虹表,從而實現快速破解hash。

使用者每次建立或者修改密碼一定要使用一個新的隨機的鹽

短的鹽

如果鹽的位數太短的話,攻擊者也可以預先製作針對所有可能的鹽的查詢表。比如,3位ASCII字元的鹽,一共有95x95x95 = 857,375種可能性。看起來好像很多。假如每一個鹽製作一個1MB的包含常見密碼的查詢表,857,375個鹽才是837GB。現在買個1TB的硬碟都只要幾百塊而已。

基於同樣的理由,千萬不要用使用者名稱做為鹽。雖然對於每一個使用者來說使用者名稱可能是不同的,但是使用者名稱是可預測的,並不是完全隨機的。攻擊者完全可以用常見的使用者名稱作為鹽來製作查詢表和彩虹表破解hash。

根據一些經驗得出來的規則就是鹽的大小要跟hash函式的輸出一致。比如,SHA256的輸出是256bits(32bytes),鹽的長度也應該是32個位元組的隨機資料。

0x06 錯誤的方式:雙重hash和古怪的hash函式


這一節討論另外一個常見的hash密碼的誤解:古怪的hash演算法組合。人們可能解決的將不同的hash函式組合在一起用可以讓資料更安全。但實際上,這種方式帶來的效果很微小。反而可能帶來一些互通性的問題,甚至有時候會讓hash更加的不安全。本文一開始就提到過,永遠不要嘗試自己寫hash演算法,要使用專家們設計的標準演算法。有些人會覺得透過使用多個hash函式可以降低計算hash的速度,從而增加破解的難度。透過減慢hash計算速度來防禦攻擊有更好的方法,這個下文會詳細介紹。

下面是一些網上找到的古怪的hash函式組合的樣例。

#!python
md5(sha1(password))
md5(md5(salt) + md5(password))
sha1(sha1(password))
sha1(str_rot13(password + salt))
md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))

不要使用他們!

注意:這部分的內容其實是存在爭議的!我收到過大量郵件說組合hash函式是有意義的。因為如果攻擊者不知道我們用了哪個函式,就不可能事先計算出彩虹表,並且組合hash函式需要更多的計算時間。

攻擊者如果不知道hash演算法的話自然是無法破解hash的。但是考慮到Kerckhoffs's principle,攻擊者通常都是能夠接觸到原始碼的(尤其是免費軟體和開源軟體)。透過一些目標系統的密碼--hash對應關係來逆向出演算法也不是非常困難。

如果你想使用一個標準的"古怪"的hash函式,比如HMAC,是可以的。但是如果你的目的是想減慢hash的計算速度,那麼可以讀一下後面討論的慢速hash函式部分。基於上面討論的因素,最好的做法是使用標準的經過嚴格測試的hash演算法。

0x07 hash碰撞(Hash Collisions)


因為hash函式是將任意數量的資料對映成一個固定長度的字串,所以一定存在不同的輸入經過hash之後變成相同的字串的情況。加密hash函式(Cryptographic hash function)在設計的時候希望使這種碰撞攻擊實現起來成本難以置信的高。但時不時的就有密碼學家發現快速實現hash碰撞的方法。最近的一個例子就是MD5,它的碰撞攻擊已經實現了。

碰撞攻擊是找到另外一個跟原密碼不一樣,但是具有相同hash的字串。但是,即使在相對弱的hash演算法,比如MD5,要實現碰撞攻擊也需要大量的算力(computing power),所以在實際使用中偶然出現hash碰撞的情況幾乎不太可能。一個使用加鹽MD5的密碼hash在實際使用中跟使用其他演算法比如SHA256一樣安全。不過如果可以的話,使用更安全的hash函式,比如SHA256, SHA512, RipeMD, WHIRLPOOL等是更好的選擇。

0x08 正確的方式:如何恰當的進行hash


這部分會詳細討論如何恰當的進行密碼hash。第一個章節是最基礎的,這章節的內容是必須的。後面一個章節是闡述如何繼續增強安全性,讓hash破解變得異常困難。

基礎:使用加鹽hash

我們已經知道惡意駭客可以透過查表和彩虹表的方式快速的獲得hash對應的明文密碼,我們也知道了透過使用隨機的鹽可以解決這個問題。但是我們怎麼生成鹽,怎麼在hash的過程中使用鹽呢?

鹽要使用密碼學上可靠安全的偽隨機數生成器(Cryptographically Secure Pseudo-Random Number Generator (CSPRNG))來產生。CSPRNG跟普通的偽隨機數生成器比如C語言中的rand(),有很大不同。正如它的名字說明的那樣,CSPRNG提供一個高標準的隨機數,是完全無法預測的。我們不希望我們的鹽能夠被預測到,所以一定要使用CSPRNG。下表提供了一些常用語言中的CSPRNG。

Platform CSPRNG
PHP mcrypt_create_iv, openssl_random_pseudo_bytes
Java java.security.SecureRandom
Dot NET (C#, VB) System.Security.Cryptography.RNGCryptoServiceProvider
Ruby SecureRandom
Python os.urandom
Perl Math::Random::Secure
C/C++ (Windows API) CryptGenRandom
Any language on GNU/Linux or Unix Read from /dev/random or /dev/urandom

每一個使用者,每一個密碼都要使用不同的鹽。使用者每次建立賬戶或者修改密碼都要使用一個新的隨機鹽。永遠不要重複使用鹽。鹽的長度要足夠,一個經驗規則就是鹽的至少要跟hash函式輸出的長度一致。鹽應該跟hash一起儲存在使用者資訊表裡。

儲存一個密碼:

1. 使用CSPRNG生成一個長的隨機鹽。
2. 將密碼和鹽拼接在一起,使用標準的加密hash函式比如SHA256進行hash
3. 將鹽和hash記錄在使用者資料庫中

驗證一個密碼:

1. 從資料庫中取出使用者的鹽和hash
2. 將使用者輸入的密碼和鹽按相同方式拼接在一起,使用相同的hash函式進行hash
3. 比較計算出的hash跟儲存的hash是否相同。如果相同則密碼正確。反之則密碼錯誤。

在本文的最後,給出了php,C#,Java,Ruby的加鹽密碼hash的實現程式碼。

在web應用中,要在服務端進行hash:

如果你在寫一個web應用,可能會有在客戶端還是服務端進行hash的疑惑。是將密碼在瀏覽器裡使用javascript進行hash,還是將明文傳給服務端,在服務端進行hash呢?

即使在客戶端用javascript進行了hash,在服務端依然需要將得到的密碼hash再進行hash。如果不這麼做的話,認證使用者的時候,服務端是獲取了瀏覽器傳過來的hash跟資料庫裡的hash比較。這樣子看起來是更安全了,因為沒有明文密碼傳送到服務端。但是事實上卻不是這樣。

問題在於這樣的話,如果惡意的駭客獲取了使用者的hash,就可以直接用來登陸使用者的賬號了。甚至都不需要知道使用者的明文密碼!也就不需要破解hash了。

這並不是說你完全不能在瀏覽器端進行hash。只是如果你要這樣做的話,一定要在服務端再hash一次。在瀏覽器端進行hash是一個不錯的想法,但是在實現的時候一定要考慮到以下幾點:

1, 客戶端密碼hash並不是HTTPS(SSL/TLS)的替代品。如果瀏覽器和伺服器之間的連線是不安全的,中間人(man-in-the-middle)可能透過修改網頁的載入的javascript移除掉hash函式來得到使用者的明文密碼。

2, 有些瀏覽器可能不支援javascript,有些使用者也會禁用javascript。為了更好的相容性,需要檢測使用者的瀏覽器是否支援javascript,如果不支援的話就需要在服務端模擬客戶端hash的邏輯。

3, 客戶端的hash也需要加鹽。一個很容想到的方式就是使用客戶端指令碼請求伺服器或得使用者的鹽。記住,不要使用這種方式。因為這樣惡意攻擊者就可以透過這個邏輯來判斷一個使用者名稱是否有效。因為我們已經在服務端進行了恰當的加鹽的hash。所以這裡使用使用者名稱跟特定的字串(比如域名)拼接作為客戶端的鹽是可以的。

**使用慢速hash函式讓破解更加困難: **

加鹽可以讓攻擊者無法使用查表和彩虹表的方式對大量hash進行破解。但是依然無法避免對單個hash的字典和暴力攻擊。高階的顯示卡(GPUs)和一些定製的硬體每秒可以計算數十億的hash,所以針對單個hash的攻擊依然有效。為了避免字典和暴力攻擊,我們可以採用一種稱為key擴充套件(key stretching)的技術。

思路就是讓hash的過程便得非常緩慢,即使使用高速GPU和特定的硬體,字典和暴力破解的速度也慢到沒有實用價值。透過減慢hash的過程來防禦攻擊,但是hash速度依然可以保證使用者使用的時候沒有明顯的延遲。

key擴充套件的實現是使用一種大量消耗cpu資源的hash函式。不要去使用自己創造的迭代hash函式,那是不夠的。要使用標準演算法的hash函式,比如PBKDF2或者bcrypt。PHP實現可以在這裡找到

這些演算法採用了一個安全變數或者迭代次數作為引數。這個值決定了hash的過程具體有多慢。對於桌面軟體和手機APP,確定這個引數的最好方式是在裝置上執行一個標準測試程式得到hash時間大概在半秒左右的值。這樣就可以避免暴力攻擊,也不會影響使用者體驗。

如果是在web應用中使用key擴充套件hash函式,需要考慮可能有大量的計算資源用來處理使用者認證請求。攻擊者可能透過這種方式來進行拒絕服務攻擊。不過我依然推薦使用key擴充套件hash函式,只是迭代次數設定的小一點。這個次數需要根據自己伺服器的計算能力和預計每秒需要處理的認證請求次數來設定。對於拒絕服務攻擊可以透過讓使用者登陸的時候輸入驗證碼的方式來防禦。系統設計的時候一定要考慮到這個迭代次數將來可以方便的增加或降低。

如果你擔心計算機的能力不夠強,而又希望在自己的web應用中使用key擴充套件hash函式,可以考慮在使用者的瀏覽器執行hash函式。Stanford JavaScript Crypto Library包含了PBKDF2演算法。在瀏覽器中進行hash需要考慮上面提到的幾個方面。

理論上不可能破解的hash:使用加密的key和密碼hash硬體

只要攻擊者能夠驗證一個猜測的密碼是正確還是錯誤,他們都可以使用字典或者暴力攻擊破解hash。更深度的防禦方法是加入一個保密的key(secret key)進行hash,這樣只有知道這個key的人才能驗證密碼是否正確。這個可以透過兩種方式來實現。一種是hash透過加密演算法加密比如AES,或者使用基於key的hash函式(HMAC)。

這個實現起來並不容易。key一定要做到保密,即使系統被攻破也不能洩露才行。但是如果攻擊者獲取了系統許可權,無論key儲存在哪裡,都可能被獲取到。所以這個key一定要儲存在一個外部系統中,比如專門用來進行密碼驗證的物理隔離的伺服器。或是使用安裝在伺服器上特殊硬體,比如YubiHSM

強烈建議所有大型的服務(超過10萬使用者)的公司使用這種方式。對於超過100萬使用者的服務商一定得采用這種方式保護使用者資訊。

如果條件不允許使用專用驗證的伺服器和特殊的硬體,依然從這種方式中受益。大部分資料庫洩露都是利用了SQL隱碼攻擊技術。sql注入大部分情況下,攻擊者都沒法讀取伺服器上的任意檔案(關閉資料庫伺服器的檔案許可權)。如果你生成了一個隨機的key,把它儲存在了一個檔案裡。並且密碼使用了加密key的加鹽hash,單單sql注入攻擊導致的hash洩露並不會影響使用者的密碼。雖然這種方式不如使用獨立的系統來儲存key安全,因為如果系統存在檔案包含漏洞的話,攻擊者就可能讀取這個秘密檔案了。不過,使用了加密key總歸好過沒有使用吧。

需要注意使用key的hash並不是不需要加鹽,聰明的攻擊者總是會找到辦法獲取到key的。所以讓hash在鹽和key擴充套件的保護下非常重要。

0x09 其他的安全措施


密碼hash僅僅是在發生安全事故的時候保護密碼。它並不能讓應用程式更加安全。對於保護使用者密碼hash更多的是需要保護密碼hash不被偷走。

即使經驗豐富的程式也需要經過安全培訓才能寫出安全的應用。一個不錯的學習web應用漏洞的資源是OWASP。除非你理解了OWASP Top Ten Vulnerability List,否則不要去寫關係到敏感資料的程式。公司有責任確保所有的開發者都經過了足夠的安全開發的培訓。

透過第三方的滲透測試也是不錯的方式。即使最好的程式設計師也會犯錯,所以讓安全專家來審計程式碼總是有意義的。尋找一個可信賴的第三方或者自己招聘一個安全人員來機型定期的程式碼審計。安全評審要在應用生命週期的早期就開始並且貫穿整個開發過程。

對網站進行入侵監控也十分重要。我建議至少招聘一名全職的安全人員進行入侵檢測和安全事件響應。如果入侵沒有檢測到,攻擊者可能讓在你的網站上掛馬影響你的使用者。所以迅速的入侵檢測和響應也很重要。

0x0A 經常提問的問題


我應該使用什麼hash演算法

可以使用

  1. 本文最後介紹的程式碼
  2. OpenWall的Portable PHP password hashing framework
  3. 經過充分測試的加密hash函式,比如SHA256, SHA512, RipeMD, WHIRLPOOL, SHA3等
  4. 設計良好的key擴充套件hash演算法,比如PBKDF2bcryptscrypt
  5. crypt的安全版本。($2y$, $5$, $6$)

不要使用

  1. 過時的hash函式,比如MD5,SHA1
  2. crypt的不安全版本。($1$, $2$, $2x$, $3$)
  3. 任何自己設計的演算法。

儘管MD5和SHA1並沒有密碼學方面的攻擊導致它們生成的hash很容易被破解,但是它們年代很古老了,通常都認為(可能有一些不恰當)它們不合適用來進行密碼的儲存。所以我不推薦使用它們。對於這個規則有個例外就是PBKDF2,它使用SHA1作為它的基礎演算法。

當使用者忘記密碼的時候我應該怎樣讓他們重置

在我個人看來現在外面廣泛使用的密碼重置機制都是不安全的,如果你有很高的安全需求,比如重要的加密服務,那麼不要讓使用者重置他們的密碼。

大多數網站使用繫結的email來進行密碼找回。透過生成一個隨機的只使用一次的token,這個token必須跟賬戶繫結,然後把密碼重置的連結傳送到使用者郵箱中。當使用者點選密碼重置連結的時候,提示他們輸入新的密碼。需要注意token一定要繫結到使用者以免攻擊者使用傳送給自己的token來修改別人的密碼。

token一定要設定成15分鐘後或者使用一次後作廢。當使用者登陸或者請求了一個新的token的時候,之前傳送的token都作廢也是不錯的主意。如果token不失效的話,那麼就可以用來永久控制這個賬戶了。Email(SMTP)是明文傳輸的協議,而網際網路上可能有很多惡意的路由器記錄email流量。並且使用者的email賬號也可能被盜。使token儘可能快的失效可以降低上面提到的這些風險。

使用者可能嘗試去修改token,所以不要在token裡儲存任何賬戶資訊。token應該是一個不能被預測的隨機的二進位制塊(binary blob),僅僅用來進行識別的一條記錄。

永遠不要透過email傳送使用者的新密碼。記得使用者重置密碼的時候要重新生成鹽,不要使用之前舊密碼使用的鹽。

如果我的使用者資料庫洩露了,我應該怎麼辦

第一要做的就是弄明白資訊是怎麼洩露的,然後把漏洞修補好。

人們可能會想辦法掩蓋這次安全事件,希望沒有人知道。但是,嘗試掩蓋安全事件會讓你的處境變得更糟。因為你不告知你的使用者他的資訊和密碼可能洩露了會給使用者帶來更大的風險。一定要第一時間通知使用者發生了安全事件,即使你還沒有完全搞明白駭客到底滲透到了什麼程度。在首頁上放一個提醒,然後連結到詳細說明的頁面。如果可能的話給每一個使用者傳送email提醒。

向你的使用者詳細的說明他的密碼是如何被保護的,希望是加鹽的hash,即使密碼進行了加鹽hash保護,攻擊者依然會進行字典和暴力攻擊嘗試破解hash。攻擊者會使用發現的密碼嘗試登陸其他網站,因為使用者可能在不同的網站都使用了相同的密碼(所謂的撞庫攻擊)。告知你的使用者存在的這些風險,建議他們修改使用了相同密碼的地方。在自己的網站上,下次使用者登陸的時候強制他們修改密碼。大部分使用者可能會嘗試使用相同的密碼,為了方便。要設計足夠的邏輯避免這樣的情況發生。

即使有了加鹽的hash,攻擊者也可能快速破解一些很弱的弱密碼。為了降低這種風險,可以在使用正確密碼的前提下,加一個郵件認證,直到使用者修改密碼。

還要告知你的使用者有哪些個人資訊儲存在網站上。如果資料庫包含信用卡資訊,你需要通知你的使用者注意自己近期的賬單,並且最好登出掉這個信用卡。

應該使用怎樣的密碼策略,需要強制使用強密碼麼

如果你的服務不是有很嚴格的安全需求,那麼不要限制你的使用者。我建議在使用者輸入密碼的時候顯示它的強度等級。讓使用者自己決定使用什麼強度的密碼。如果你的系統有很強的安全需求,那麼強制使用者使用12位以上的密碼,至少包含2個數字,2個字母,2個字元。

每6個月最多強制使用者修改一次密碼。超過這個次數,使用者就會感到疲勞。他們更傾向於選擇一個弱密碼。更應該做的是教育你的使用者,當他們感到自己的密碼可能洩露的時候主動修改密碼。

如果攻擊者獲取了資料庫許可權,他不能直接替換hash登陸任意賬戶麼

當然,不過如果他已經或得了資料庫許可權,很可能已經可以獲得伺服器上的所有資訊了。所以沒有什麼必要去修改hash登陸別人賬戶。進行密碼hash的目的不是保護網站不被入侵,而是如果入侵發生了,可以更好的保護使用者的密碼。

在SQL隱碼攻擊中,保護hash不被替換的方式使用兩個使用者不同許可權的使用者連線資料庫。一個具有寫許可權,另外一個只具有隻讀的許可權。

為什麼需要一些特別的演算法比如HMAC,而不是直接把密碼和加密key拼接在一起

(這部分講一些密碼學的原理,翻譯的不好請見諒)

hash函式,比如MD5,SHA1,SHA2使用了Merkle–Damgård construction,這導致演算法可能長度擴充套件攻擊(length extension attacks)。意思就是說給定一個hash H(X),攻擊者可以在不知道X的情況下,可以找到一個H(pad(X)+Y)的值,Y是個其他的字串。pad(X)是hash函式使用的填充函式(padding function)。

這就意味者,對於hash H(key + message),攻擊者可以計算 H(pad(key + message) + extension),並不需要知道加密key。如果這個hash是用在訊息認證過程中,使用key為了避免訊息被修改。這樣的話這個系統就可能失效了,因為攻擊者掌握了一個有效的基於 message+extension的hash。

這種攻擊對於如何快速破解hash還不是很清楚。但是,基於一些風險的考慮,不建議使用單純的hash函式進行加密key的hash。也許一個聰明的密碼學家一天就可以找到使用這種攻擊快速破解hash的方法。所以記得使用HMAC。

鹽應該拼在密碼的前面還是後面

這個不重要。選擇一個並且保持風格一致就行了。實際中,把鹽放在前面更常見一點。

為什麼本文最後提供的hash程式碼使用了固定執行時間的函式來比較hash(length-constant)

使用固定的時間來比較hash是為了防止攻擊者線上上的系統中使用基於時間差的攻擊。這樣攻擊者就只能線下破解了。

比較兩個字串是否相同,標準的方式是先比較第一個位元組,然後比較第二個位元組,一次類推。只要發現有一個位元組不同,那麼這兩個字串就是不同了。可以返回false的訊息了。如果所有位元組比較下來都一樣,那麼這兩個字串就是相同的,可以返回true。這就意味了比較兩個字串,如果他們相同的長度不一樣,花費的時間不一樣。開始部分相同的長度越長,花費的時間也就越長。

基於這個原理,攻擊者可以先找256個字串,他們的hash都是以不同的位元組開頭。然後傳送到目標伺服器,計算伺服器返回的時間。時間最長的那一個就是第一個位元組hash是正確的。依次類推。攻擊者就可能得到hash更多的位元組。

這種攻擊聽起來好像在網路上實現起來比較困難。但是已經有人實現過了。所以我們在比較hash的時候採用了花費時間固定的函式。

本文提供的程式碼中 slowequals 函式是怎麼工作的

上一回答講到了我們需要比較時間固定的函式,這部分詳細講一下程式碼的實現。

#!cpp
private static boolean slowEquals(byte[] a, byte[] b)
{
    int diff = a.length ^ b.length;
    for(int i = 0; i < a.length && i < b.length; i++)
    diff |= a[i] ^ b[i];
    return diff == 0;
}

這段程式碼使用了異或(XOR)運算子"^"來比較整數是否相等,而沒有使用"=="運算子。原因在於如果兩個數完全一致,異或之後的值為零。因為 0 XOR 0 = 0, 1 XOR 1 = 0, 0 XOR 1 = 1, 1 XOR 0 = 1

所以,第一行程式碼如果a.length等於b.length,變數diff等於0,否則的話diff就是一個非零的值。然後,讓a,b的每一個位元組XOR之後再跟diff OR。這樣,只有diff一開始是0,並且,a,b的每一個位元組XOR的結果也是零,最後迴圈完成後diff的值才是0,這種情況是a,b完全一樣。否則最後diff是一個非零的值。

我們使用XOR而不適用"=="的原因是"=="通常編譯成分支的形式。比如C程式碼"diff &= a == b" 可能編譯成下面的X86彙編。

MOV EAX, [A]
CMP [B], EAX
JZ equal
JMP done
equal:
AND [VALID], 1
done:
AND [VALID], 0

分支會導致程式碼執行的時間出現差異。

C程式碼的"diff |= a ^ b"編譯之後類似於,

MOV EAX, [A]
XOR EAX, [B]
OR [DIFF], EAX 

執行時間跟兩個變數是否相等沒有關係。

為什麼要討論這麼多關於hash的東西

使用者在你的網站上輸入密碼,是相信你的安全性。如果你的資料庫被黑了。而使用者密碼又沒有恰當的保護,那麼惡意的攻擊者就可以利用這些密碼嘗試登陸其他的網站和服務。進行撞庫攻擊。(很多使用者在所有的地方都是使用相同的密碼)這不僅僅是你的網站安全,是你的所有使用者的安全。你要對你使用者的安全負責。

0x0B PHP PBKDF2 密碼hash程式碼


程式碼下載

#!php
<?php
/*
 * Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm).
 * Copyright (c) 2013, Taylor Hornby
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 */

// These constants may be changed without breaking existing hashes.
define("PBKDF2_HASH_ALGORITHM", "sha256");
define("PBKDF2_ITERATIONS", 1000);
define("PBKDF2_SALT_BYTE_SIZE", 24);
define("PBKDF2_HASH_BYTE_SIZE", 24);

define("HASH_SECTIONS", 4);
define("HASH_ALGORITHM_INDEX", 0);
define("HASH_ITERATION_INDEX", 1);
define("HASH_SALT_INDEX", 2);
define("HASH_PBKDF2_INDEX", 3);

function create_hash($password)
{
    // format: algorithm:iterations:salt:hash
    $salt = base64_encode(mcrypt_create_iv(PBKDF2_SALT_BYTE_SIZE, MCRYPT_DEV_URANDOM));
    return PBKDF2_HASH_ALGORITHM . ":" . PBKDF2_ITERATIONS . ":" .  $salt . ":" .
        base64_encode(pbkdf2(
            PBKDF2_HASH_ALGORITHM,
            $password,
            $salt,
            PBKDF2_ITERATIONS,
            PBKDF2_HASH_BYTE_SIZE,
            true
        ));
}

function validate_password($password, $correct_hash)
{
    $params = explode(":", $correct_hash);
    if(count($params) < HASH_SECTIONS)
       return false;
    $pbkdf2 = base64_decode($params[HASH_PBKDF2_INDEX]);
    return slow_equals(
        $pbkdf2,
        pbkdf2(
            $params[HASH_ALGORITHM_INDEX],
            $password,
            $params[HASH_SALT_INDEX],
            (int)$params[HASH_ITERATION_INDEX],
            strlen($pbkdf2),
            true
        )
    );
}

// Compares two strings $a and $b in length-constant time.
function slow_equals($a, $b)
{
    $diff = strlen($a) ^ strlen($b);
    for($i = 0; $i < strlen($a) && $i < strlen($b); $i++)
    {
        $diff |= ord($a[$i]) ^ ord($b[$i]);
    }
    return $diff === 0;
}

/*
 * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
 * $algorithm - The hash algorithm to use. Recommended: SHA256
 * $password - The password.
 * $salt - A salt that is unique to the password.
 * $count - Iteration count. Higher is better, but slower. Recommended: At least 1000.
 * $key_length - The length of the derived key in bytes.
 * $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise.
 * Returns: A $key_length-byte key derived from the password and salt.
 *
 * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
 *
 * This implementation of PBKDF2 was originally created by https://defuse.ca
 * With improvements by http://www.variations-of-shadow.com
 */
function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
    $algorithm = strtolower($algorithm);
    if(!in_array($algorithm, hash_algos(), true))
        trigger_error('PBKDF2 ERROR: Invalid hash algorithm.', E_USER_ERROR);
    if($count <= 0 || $key_length <= 0)
        trigger_error('PBKDF2 ERROR: Invalid parameters.', E_USER_ERROR);

    if (function_exists("hash_pbkdf2")) {
        // The output length is in NIBBLES (4-bits) if $raw_output is false!
        if (!$raw_output) {
            $key_length = $key_length * 2;
        }
        return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
    }

    $hash_length = strlen(hash($algorithm, "", true));
    $block_count = ceil($key_length / $hash_length);

    $output = "";
    for($i = 1; $i <= $block_count; $i++) {
        // $i encoded as 4 bytes, big endian.
        $last = $salt . pack("N", $i);
        // first iteration
        $last = $xorsum = hash_hmac($algorithm, $last, $password, true);
        // perform the other $count - 1 iterations
        for ($j = 1; $j < $count; $j++) {
            $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
        }
        $output .= $xorsum;
    }

    if($raw_output)
        return substr($output, 0, $key_length);
    else
        return bin2hex(substr($output, 0, $key_length));
}
?>

0x0C java PBKDF2 密碼hash程式碼


程式碼下載

#!java
/* 
 * Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm).
 * Copyright (c) 2013, Taylor Hornby
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 */

import java.security.SecureRandom;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.SecretKeyFactory;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

/*
 * PBKDF2 salted password hashing.
 * Author: havoc AT defuse.ca
 * www: http://crackstation.net/hashing-security.htm
 */
public class PasswordHash
{
    public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";

    // The following constants may be changed without breaking existing hashes.
    public static final int SALT_BYTE_SIZE = 24;
    public static final int HASH_BYTE_SIZE = 24;
    public static final int PBKDF2_ITERATIONS = 1000;

    public static final int ITERATION_INDEX = 0;
    public static final int SALT_INDEX = 1;
    public static final int PBKDF2_INDEX = 2;

    /**
     * Returns a salted PBKDF2 hash of the password.
     *
     * @param   password    the password to hash
     * @return              a salted PBKDF2 hash of the password
     */
    public static String createHash(String password)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        return createHash(password.toCharArray());
    }

    /**
     * Returns a salted PBKDF2 hash of the password.
     *
     * @param   password    the password to hash
     * @return              a salted PBKDF2 hash of the password
     */
    public static String createHash(char[] password)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        // Generate a random salt
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[SALT_BYTE_SIZE];
        random.nextBytes(salt);

        // Hash the password
        byte[] hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
        // format iterations:salt:hash
        return PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" +  toHex(hash);
    }

    /**
     * Validates a password using a hash.
     *
     * @param   password        the password to check
     * @param   correctHash     the hash of the valid password
     * @return                  true if the password is correct, false if not
     */
    public static boolean validatePassword(String password, String correctHash)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        return validatePassword(password.toCharArray(), correctHash);
    }

    /**
     * Validates a password using a hash.
     *
     * @param   password        the password to check
     * @param   correctHash     the hash of the valid password
     * @return                  true if the password is correct, false if not
     */
    public static boolean validatePassword(char[] password, String correctHash)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        // Decode the hash into its parameters
        String[] params = correctHash.split(":");
        int iterations = Integer.parseInt(params[ITERATION_INDEX]);
        byte[] salt = fromHex(params[SALT_INDEX]);
        byte[] hash = fromHex(params[PBKDF2_INDEX]);
        // Compute the hash of the provided password, using the same salt, 
        // iteration count, and hash length
        byte[] testHash = pbkdf2(password, salt, iterations, hash.length);
        // Compare the hashes in constant time. The password is correct if
        // both hashes match.
        return slowEquals(hash, testHash);
    }

    /**
     * Compares two byte arrays in length-constant time. This comparison method
     * is used so that password hashes cannot be extracted from an on-line 
     * system using a timing attack and then attacked off-line.
     * 
     * @param   a       the first byte array
     * @param   b       the second byte array 
     * @return          true if both byte arrays are the same, false if not
     */
    private static boolean slowEquals(byte[] a, byte[] b)
    {
        int diff = a.length ^ b.length;
        for(int i = 0; i < a.length && i < b.length; i++)
            diff |= a[i] ^ b[i];
        return diff == 0;
    }

    /**
     *  Computes the PBKDF2 hash of a password.
     *
     * @param   password    the password to hash.
     * @param   salt        the salt
     * @param   iterations  the iteration count (slowness factor)
     * @param   bytes       the length of the hash to compute in bytes
     * @return              the PBDKF2 hash of the password
     */
    private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
        return skf.generateSecret(spec).getEncoded();
    }

    /**
     * Converts a string of hexadecimal characters into a byte array.
     *
     * @param   hex         the hex string
     * @return              the hex string decoded into a byte array
     */
    private static byte[] fromHex(String hex)
    {
        byte[] binary = new byte[hex.length() / 2];
        for(int i = 0; i < binary.length; i++)
        {
            binary[i] = (byte)Integer.parseInt(hex.substring(2*i, 2*i+2), 16);
        }
        return binary;
    }

    /**
     * Converts a byte array into a hexadecimal string.
     *
     * @param   array       the byte array to convert
     * @return              a length*2 character string encoding the byte array
     */
    private static String toHex(byte[] array)
    {
        BigInteger bi = new BigInteger(1, array);
        String hex = bi.toString(16);
        int paddingLength = (array.length * 2) - hex.length();
        if(paddingLength > 0)
            return String.format("%0" + paddingLength + "d", 0) + hex;
        else
            return hex;
    }

    /**
     * Tests the basic functionality of the PasswordHash class
     *
     * @param   args        ignored
     */
    public static void main(String[] args)
    {
        try
        {
            // Print out 10 hashes
            for(int i = 0; i < 10; i++)
                System.out.println(PasswordHash.createHash("p\r\nassw0Rd!"));

            // Test password validation
            boolean failure = false;
            System.out.println("Running tests...");
            for(int i = 0; i < 100; i++)
            {
                String password = ""+i;
                String hash = createHash(password);
                String secondHash = createHash(password);
                if(hash.equals(secondHash)) {
                    System.out.println("FAILURE: TWO HASHES ARE EQUAL!");
                    failure = true;
                }
                String wrongPassword = ""+(i+1);
                if(validatePassword(wrongPassword, hash)) {
                    System.out.println("FAILURE: WRONG PASSWORD ACCEPTED!");
                    failure = true;
                }
                if(!validatePassword(password, hash)) {
                    System.out.println("FAILURE: GOOD PASSWORD NOT ACCEPTED!");
                    failure = true;
                }
            }
            if(failure)
                System.out.println("TESTS FAILED!");
            else
                System.out.println("TESTS PASSED!");
        }
        catch(Exception ex)
        {
            System.out.println("ERROR: " + ex);
        }
    }

}

0x0D ASP.NET (C#)密碼hash程式碼


程式碼下載

#!csharp
/* 
 * Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm).
 * Copyright (c) 2013, Taylor Hornby
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Text;
using System.Security.Cryptography;

namespace PasswordHash
{
    /// <summary>
    /// Salted password hashing with PBKDF2-SHA1.
    /// Author: havoc AT defuse.ca
    /// www: http://crackstation.net/hashing-security.htm
    /// Compatibility: .NET 3.0 and later.
    /// </summary>
    public class PasswordHash
    {
        // The following constants may be changed without breaking existing hashes.
        public const int SALT_BYTE_SIZE = 24;
        public const int HASH_BYTE_SIZE = 24;
        public const int PBKDF2_ITERATIONS = 1000;

        public const int ITERATION_INDEX = 0;
        public const int SALT_INDEX = 1;
        public const int PBKDF2_INDEX = 2;

        /// <summary>
        /// Creates a salted PBKDF2 hash of the password.
        /// </summary>
        /// <param name="password">The password to hash.</param>
        /// <returns>The hash of the password.</returns>
        public static string CreateHash(string password)
        {
            // Generate a random salt
            RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider();
            byte[] salt = new byte[SALT_BYTE_SIZE];
            csprng.GetBytes(salt);

            // Hash the password and encode the parameters
            byte[] hash = PBKDF2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
            return PBKDF2_ITERATIONS + ":" +
                Convert.ToBase64String(salt) + ":" +
                Convert.ToBase64String(hash);
        }

        /// <summary>
        /// Validates a password given a hash of the correct one.
        /// </summary>
        /// <param name="password">The password to check.</param>
        /// <param name="correctHash">A hash of the correct password.</param>
        /// <returns>True if the password is correct. False otherwise.</returns>
        public static bool ValidatePassword(string password, string correctHash)
        {
            // Extract the parameters from the hash
            char[] delimiter = { ':' };
            string[] split = correctHash.Split(delimiter);
            int iterations = Int32.Parse(split[ITERATION_INDEX]);
            byte[] salt = Convert.FromBase64String(split[SALT_INDEX]);
            byte[] hash = Convert.FromBase64String(split[PBKDF2_INDEX]);

            byte[] testHash = PBKDF2(password, salt, iterations, hash.Length);
            return SlowEquals(hash, testHash);
        }

        /// <summary>
        /// Compares two byte arrays in length-constant time. This comparison
        /// method is used so that password hashes cannot be extracted from
        /// on-line systems using a timing attack and then attacked off-line.
        /// </summary>
        /// <param name="a">The first byte array.</param>
        /// <param name="b">The second byte array.</param>
        /// <returns>True if both byte arrays are equal. False otherwise.</returns>
        private static bool SlowEquals(byte[] a, byte[] b)
        {
            uint diff = (uint)a.Length ^ (uint)b.Length;
            for (int i = 0; i < a.Length && i < b.Length; i++)
                diff |= (uint)(a[i] ^ b[i]);
            return diff == 0;
        }

        /// <summary>
        /// Computes the PBKDF2-SHA1 hash of a password.
        /// </summary>
        /// <param name="password">The password to hash.</param>
        /// <param name="salt">The salt.</param>
        /// <param name="iterations">The PBKDF2 iteration count.</param>
        /// <param name="outputBytes">The length of the hash to generate, in bytes.</param>
        /// <returns>A hash of the password.</returns>
        private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes)
        {
            Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt);
            pbkdf2.IterationCount = iterations;
            return pbkdf2.GetBytes(outputBytes);
        }
    }
}

0x0E Ruby (on Rails) 密碼hash程式碼


程式碼下載

#!ruby
# Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm).
# Copyright (c) 2013, Taylor Hornby
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions are met:
# 
# 1. Redistributions of source code must retain the above copyright notice, 
# this list of conditions and the following disclaimer.
# 
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation 
# and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
# POSSIBILITY OF SUCH DAMAGE.

require 'securerandom'
require 'openssl'
require 'base64'

# Salted password hashing with PBKDF2-SHA1.
# Authors: @RedragonX (dicesoft.net), havoc AT defuse.ca 
# www: http://crackstation.net/hashing-security.htm
module PasswordHash

  # The following constants can be changed without breaking existing hashes.
  PBKDF2_ITERATIONS = 1000
  SALT_BYTE_SIZE = 24
  HASH_BYTE_SIZE = 24

  HASH_SECTIONS = 4
  SECTION_DELIMITER = ':'
  ITERATIONS_INDEX = 1
  SALT_INDEX = 2
  HASH_INDEX = 3

  # Returns a salted PBKDF2 hash of the password.
  def self.createHash( password )
    salt = SecureRandom.base64( SALT_BYTE_SIZE )
    pbkdf2 = OpenSSL::PKCS5::pbkdf2_hmac_sha1(
      password,
      salt,
      PBKDF2_ITERATIONS,
      HASH_BYTE_SIZE
    )
    return ["sha1", PBKDF2_ITERATIONS, salt, Base64.encode64( pbkdf2 )].join( SECTION_DELIMITER )
  end

  # Checks if a password is correct given a hash of the correct one.
  # correctHash must be a hash string generated with createHash.
  def self.validatePassword( password, correctHash )
    params = correctHash.split( SECTION_DELIMITER )
    return false if params.length != HASH_SECTIONS

    pbkdf2 = Base64.decode64( params[HASH_INDEX] )
    testHash = OpenSSL::PKCS5::pbkdf2_hmac_sha1(
      password,
      params[SALT_INDEX],
      params[ITERATIONS_INDEX].to_i,
      pbkdf2.length
    )

    return pbkdf2 == testHash
  end

  # Run tests to ensure the module is functioning properly.
  # Returns true if all tests succeed, false if not.
  def self.runSelfTests
    puts "Sample hashes:"
    3.times { puts createHash("password") }

    puts "\nRunning self tests..."
    @@allPass = true

    correctPassword = 'aaaaaaaaaa'
    wrongPassword = 'aaaaaaaaab'
    hash = createHash(correctPassword)

    assert( validatePassword( correctPassword, hash ) == true, "correct password" )
    assert( validatePassword( wrongPassword, hash ) == false, "wrong password" )

    h1 = hash.split( SECTION_DELIMITER )
    h2 = createHash( correctPassword ).split( SECTION_DELIMITER )
    assert( h1[HASH_INDEX] != h2[HASH_INDEX], "different hashes" )
    assert( h1[SALT_INDEX] != h2[SALT_INDEX], "different salt" )

    if @@allPass
      puts "*** ALL TESTS PASS ***"
    else
      puts "*** FAILURES ***"
    end

    return @@allPass
  end

  def self.assert( truth, msg )
    if truth
      puts "PASS [#{msg}]"
    else
      puts "FAIL [#{msg}]"
      @@allPass = false
    end
  end

end

PasswordHash.runSelfTests

from:https://crackstation.net/hashing-security.htm

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章