大多數的web開發者都會遇到設計用戶帳號系統的需求。帳號系統最重要的一個方面就是如何保護用戶的密碼。一些大公司的用戶數據庫泄露事件也時有發生,因此咱們必須採起一些措施來保護用戶的密碼,即便網站被攻破的狀況下也不會形成較大的危害。若是你還在存儲用戶密碼的MD5,那可真的有點弱了。趕忙來看看這篇文章吧。javascript
保護密碼最好的的方式就是使用帶鹽的密碼hash(salted password hashing).對密碼進行hash操做是一件很簡單的事情,可是不少人都犯了錯。接下來我但願能夠詳細的闡述如何恰當的對密碼進行hash,以及爲何要這樣作。php
若是你打算本身寫一段代碼來進行密碼hash,那麼趕忙停下吧。這樣太容易犯錯了。這個提醒適用於每個人,不要本身寫密碼的hash算法 !關於保存密碼的問題已經有了成熟的方案,那就是使用phpass或者本文提供的源碼。html
什麼是hashjava
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69afe7dd238f3364946366 hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542
Hash算法是一種單向的函數。它能夠把任意數量的數據轉換成固定長度的「指紋」,這個過程是不可逆的。並且只要輸入發生改變,哪怕只有一個bit,輸出的hash值也會有很大不一樣。這種特性剛好合適用來用來保存密碼。由於咱們但願使用一種不可逆的算法來加密保存的密碼,同時又須要在用戶登錄的時候驗證密碼是否正確。python
在一個使用hash的帳號系統中,用戶註冊和認證的大體流程以下:程序員
1, 用戶建立本身的帳號
2, 用戶密碼通過hash操做以後存儲在數據庫中。沒有任何明文的密碼存儲在服務器的硬盤上。
3, 用戶登錄的時候,將用戶輸入的密碼進行hash操做後與數據庫裏保存的密碼hash值進行對比。
4, 若是hash值徹底同樣,則認爲用戶輸入的密碼是正確的。不然就認爲用戶輸入了無效的密碼。
5, 每次用戶嘗試登錄的時候就重複步驟3和步驟4。
在步驟4的時候不要告訴用戶是帳號仍是密碼錯了。只須要顯示一個通用的提示,好比帳號或密碼不正確就能夠了。這樣能夠防止攻擊者枚舉有效的用戶名。web
還須要注意的是用來保護密碼的hash函數跟數據結構課上見過的hash函數不徹底同樣。好比實現hash表的hash函數設計的目的是快速,可是不夠安全。只有加密hash函數(cryptographic hash functions)能夠用來進行密碼的hash。這樣的函數有SHA256, SHA512, RipeMD, WHIRLPOOL等。算法
一個常見的觀念就是密碼通過hash以後存儲就安全了。這顯然是不正確的。有不少方式能夠快速的從hash恢復明文的密碼。還記得那些md5破解網站吧,只須要提交一個hash,不到一秒鐘就能知道結果。顯然,單純的對密碼進行hash仍是遠遠達不到咱們的安全需求。下一部分先討論一下破解密碼hash,獲取明文常見的手段。sql
最多見的破解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惟一的方式就是進行字典或者暴力攻擊了。
對於特定的hash類型,若是須要破解大量hash的話,查表是一種很是有效並且快速的方式。它的理念就是預先計算(pre-compute)出密碼字典中每個密碼的hash。而後把hash和對應的密碼保存在一個表裏。一個設計良好的查詢表結構,即便存儲了數十億個hash,每秒鐘仍然能夠查詢成百上千個hash。
若是你想感覺下查表破解hash的話能夠嘗試一下在CraskStation上破解下下面的sha256 hash。
c11083b4b0a7743af748c85d343dfee9fbb8b2576c05f3a7f0d632b0926aadfc 08eac03b80adc33dc7d8fbe44b7c7b05d3a2c511166bdb43fcb710b03ba919e7 e4ba5cbd251c98e6cd1c23f126a3b81d8d8328abc95387229850952b3ef9f904 5206b8b8a996cf5320cb12ca91c7b790fba9f030408efe83ebb83548dc3007bd
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(z@29hjja) in users' hash list... : No users used this password
這種方式可讓攻擊者不預先計算一個查詢表的狀況下同時對大量hash進行字典和暴力破解攻擊。
首先,攻擊者會根據獲取到的數據庫數據製做一個用戶名和對應的hash表。而後將常見的字典密碼進行hash以後,跟這個表的hash進行對比,就能夠知道用哪些用戶使用了這個密碼。這種攻擊方式頗有效果,由於一般狀況下不少用戶都會有使用相同的密碼。
彩虹表是一種使用空間換取時間的技術。跟查表破解很類似。只是它犧牲了一些破解時間來達到更小的存儲空間的目的。由於彩虹表使用的存儲空間更小,因此單位空間就能夠存儲更多的hash。彩虹表已經可以破解8位長度的任意md5hash。彩虹表具體的原理能夠參考http://www.project-rainbowcrack.com/
下一章節咱們會討論一種叫作「鹽」(salting)的技術。經過這種技術可讓查表和彩虹表的方式沒法破解hash。
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1 hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007
查表和彩虹表的方式之因此有效是由於每個密碼的都是經過一樣的方式來進行hash的。若是兩個用戶使用了一樣的密碼,那麼必定他們的密碼hash也必定相同。咱們能夠經過讓每個hash隨機化,同一個密碼hash兩次,獲得的不一樣的hash來避免這種攻擊。
具體的操做就是給密碼加一個隨即的前綴或者後綴,而後再進行hash。這個隨即的後綴或者前綴成爲「鹽」。正如上面給出的例子同樣,經過加鹽,相同的密碼每次hash都是徹底不同的字符串了。檢查用戶輸入的密碼是否正確的時候,咱們也還須要這個鹽,因此鹽通常都是跟hash一塊兒保存在數據庫裏,或者做爲hash字符串的一部分。
鹽不須要保密,只要鹽是隨機的話,查表,彩虹表都會失效。由於攻擊者沒法事先知道鹽是什麼,也就沒有辦法預先計算出查詢表和彩虹表。若是每一個用戶都是使用了不一樣的鹽,那麼反向查表攻擊也無法成功。
下一節,咱們會介紹一些鹽的常見的錯誤實現。
最多見的錯誤實現就是一個鹽在多個hash中使用或者使用的鹽很短。
無論是將鹽硬編碼在程序裏仍是隨機一次生成的,在每個密碼hash裏使用相同的鹽會使這種防護方法失效。由於相同的密碼hash兩次獲得的結果仍是相同的。攻擊者就可使用反向查表的方式進行字典和暴力攻擊。只要在對字典中每個密碼進行hash以前加上這個固定的鹽就能夠了。若是是流行的程序的使用了硬編碼的鹽,那麼也可能出現針對這種程序的這個鹽的查詢表和彩虹表,從而實現快速破解hash。
用戶每次建立或者修改密碼必定要使用一個新的隨機的鹽
若是鹽的位數過短的話,攻擊者也能夠預先製做針對全部可能的鹽的查詢表。好比,3位ASCII字符的鹽,一共有95x95x95 = 857,375種可能性。看起來好像不少。假如每個鹽製做一個1MB的包含常見密碼的查詢表,857,375個鹽纔是837GB。如今買個1TB的硬盤都只要幾百塊而已。
基於一樣的理由,千萬不要用用戶名作爲鹽。雖然對於每個用戶來講用戶名多是不一樣的,可是用戶名是可預測的,並非徹底隨機的。攻擊者徹底能夠用常見的用戶名做爲鹽來製做查詢表和彩虹表破解hash。
根據一些經驗得出來的規則就是鹽的大小要跟hash函數的輸出一致。好比,SHA256的輸出是256bits(32bytes),鹽的長度也應該是32個字節的隨機數據。
這一節討論另一個常見的hash密碼的誤解:古怪的hash算法組合。人們可能解決的將不一樣的hash函數組合在一塊兒用可讓數據更安全。但實際上,這種方式帶來的效果很微小。反而可能帶來一些互通性的問題,甚至有時候會讓hash更加的不安全。本文一開始就提到過,永遠不要嘗試本身寫hash算法,要使用專家們設計的標準算法。有些人會以爲經過使用多個hash函數能夠下降計算hash的速度,從而增長破解的難度。經過減慢hash計算速度來防護攻擊有更好的方法,這個下文會詳細介紹。
下面是一些網上找到的古怪的hash函數組合的樣例。
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算法。
由於hash函數是將任意數量的數據映射成一個固定長度的字符串,因此必定存在不一樣的輸入通過hash以後變成相同的字符串的狀況。加密hash函數(Cryptographic hash function)在設計的時候但願使這種碰撞攻擊實現起來成本難以置信的高。但時不時的就有密碼學家發現快速實現hash碰撞的方法。最近的一個例子就是MD5,它的碰撞攻擊已經實現了。
碰撞攻擊是找到另一個跟原密碼不同,可是具備相同hash的字符串。可是,即便在相對弱的hash算法,好比MD5,要實現碰撞攻擊也須要大量的算力(computing power),因此在實際使用中偶然出現hash碰撞的狀況幾乎不太可能。一個使用加鹽MD5的密碼hash在實際使用中跟使用其餘算法好比SHA256同樣安全。不過若是能夠的話,使用更安全的hash函數,好比SHA256, SHA512, RipeMD, WHIRLPOOL等是更好的選擇。
這部分會詳細討論如何恰當的進行密碼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擴展的保護下很是重要。
密碼hash僅僅是在發生安全事故的時候保護密碼。它並不能讓應用程序更加安全。對於保護用戶密碼hash更多的是須要保護密碼hash不被偷走。
即便經驗豐富的程序也須要通過安全培訓才能寫出安全的應用。一個不錯的學習web應用漏洞的資源是OWASP。除非你理解了OWASP Top Ten Vulnerability List,不然不要去寫關係到敏感數據的程序。公司有責任確保全部的開發者都通過了足夠的安全開發的培訓。
經過第三方的滲透測試也是不錯的方式。即便最好的程序員也會犯錯,因此讓安全專家來審計代碼老是有意義的。尋找一個可信賴的第三方或者本身招聘一個安全人員來機型按期的代碼審計。安全評審要在應用生命週期的早期就開始而且貫穿整個開發過程。
對網站進行入侵監控也十分重要。我建議至少招聘一名全職的安全人員進行入侵檢測和安全事件響應。若是入侵沒有檢測到,攻擊者可能讓在你的網站上掛馬影響你的用戶。因此迅速的入侵檢測和響應也很重要。
我應該使用什麼hash算法
可使用
1, 本文最後介紹的代碼
2, OpenWall的Portable PHP password hashing framework
3, 通過充分測試的加密hash函數,好比SHA256, SHA512, RipeMD, WHIRLPOOL, SHA3等
4, 設計良好的key擴展hash算法,好比PBKDF2,bcrypt,scrypt
5, crypt#Library_Function_crypt.283.29)的安全版本。($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 函數是怎麼工做的
上一回答講到了咱們須要比較時間固定的函數,這部分詳細講一下代碼的實現。
1. private static boolean slowEquals(byte[] a, byte[] b) 2. { 3. int diff = a.length ^ b.length; 4. for(int i = 0; i < a.length && i < b.length; i++) 5. diff |= a[i] ^ b[i]; 6. return diff == 0; 7. }
這段代碼使用了異或(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的東西
用戶在你的網站上輸入密碼,是相信你的安全性。若是你的數據庫被黑了。而用戶密碼又沒有恰當的保護,那麼惡意的攻擊者就能夠利用這些密碼嘗試登錄其餘的網站和服務。進行撞庫攻擊。(不少用戶在全部的地方都是使用相同的密碼)這不只僅是你的網站安全,是你的全部用戶的安全。你要對你用戶的安全負責。
<?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));}?>
/* * 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); } } }
/* * 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); } } }
# 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