https://crackstation.net/hashing-security.htmphp
http://blog.jobbole.com/61872/html
若是你是Web開發者,你極可能須要開發一個用戶帳戶系統。這個系統最重要的方面,就是怎樣保護用戶的密碼。存放賬號的數據庫常常成爲入侵的目標,因此你必須作點什麼來保護密碼,以防網站被攻破時發生危險。最好的辦法就是對密碼進行加鹽哈希,這篇文章將介紹它是如何作到這點。java
在對密碼進行哈希加密的問題上,人們有許多爭論和誤解,這大概是因爲網絡上普遍的誤傳吧。密碼哈希是一件很是簡單的事情,可是依然有不少人理解錯誤了。本文闡述的並非進行密碼哈希惟一正確的方法,可是會告訴你爲何這樣是正確的。python
鄭重警告:若是你在試圖編寫本身的密碼哈希代碼,趕忙停下來!那太容易搞砸了。即便你受過密碼學的高等教育,也應該遵從這個警告。這是對全部人說的:不要本身寫加密函數!安全存儲密碼的難題如今已經被解決了,請使用phpass或者本文給出的一些源代碼。git
若是由於某些緣由你忽視了上面那個紅色警告,請翻回去好好讀一遍,我是認真的。這篇文章的目的不是教你研究出本身的安全算法,而是講解爲何密碼應該被這樣儲存。程序員
下面一些連接能夠用來快速跳轉到本文的各章節。github
這裏也給出了一些基於BSD許可的哈希函數源代碼:web
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69afe7dd238f3364946366
hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542
哈希算法是一個單向函數。它能夠將任何大小的數據轉化爲定長的「指紋」,而且沒法被反向計算。另外,即便數據源只改動了一丁點,哈希的結果也會徹底 不一樣(參考上面的例子)。這樣的特性使得它很是適合用於保存密碼,由於咱們須要加密後的密碼沒法被解密,同時也能保證正確校驗每一個用戶的密碼。算法
在基於哈希加密的帳戶系統中,一般用戶註冊和認證的流程是這樣的:數據庫
在第4步中,永遠不要告訴用戶究竟是用戶名錯了,仍是密碼錯了。只須要給出一個大概的提示,好比「無效的用戶名或密碼」。這能夠防止攻擊者在不知道密碼的狀況下,枚舉出有效的用戶名。
須要提到的是,用於保護密碼的哈希函數和你在數據結構中學到的哈希函數是不一樣的。好比用於實現哈希表這之類數據結構的哈希函數,它們的目標是快速查找,而不是高安全性。只有加密哈希函數才能用於保護密碼,例如SHA256,SHA512,RipeMD和WHIRLPOOL。
也許你很容易就認爲只須要簡單地執行一遍加密哈希函數,密碼就能安全,那麼你大錯特錯了。有太多的辦法能夠快速地把密碼從簡單哈希值中恢復出來,但 也有不少比較容易實現的技術能使攻擊者的效率大大下降。黑客的進步也在激勵着這些技術的進步,好比這樣一個網站:你能夠提交一系列待破解的哈希值,而且在 不到1秒的時間內獲得告終果。顯然,簡單哈希加密並不能知足咱們對安全性的需求。
那麼下一節會講到幾種經常使用的破解簡單哈希加密的辦法。
字典攻擊和暴力攻擊
Dictionary Attack
Trying apple : failed
Trying blueberry : failed
Trying justinbeiber : failed
...
Trying letmein : failed
Trying s3cr3t : success!
Brute Force Attack
Trying aaaa : failed
Trying aaab : failed
Trying aaac : failed
...
Trying acdb : failed
Trying acdc : success!
• 破解哈希加密最簡單的辦法,就是去猜,將每一個猜想值哈希以後的結果和目標值比對,若是相同則破解成功。兩種最多見的猜密碼的辦法是字典攻擊和暴力攻擊。
• 字典攻擊須要使用一個字典文件,它包含單詞、短語、經常使用密碼以及其餘可能用做密碼的字符串。其中每一個詞都是進過哈希後儲存的,用它們和密碼哈希比對,若是 相同,這個詞就是密碼。字典文件的構成是從大段文本中分解出的單詞,甚至還包括一些數據庫中真實的密碼。而後還能夠對字典文件進行更進一步的處理使它更有 效,好比把單詞中的字母替換爲它們的「形近字」(hello變爲h3110)。
• 暴力攻擊會嘗試每個在給定長度下各類字符的組合。這種攻擊會消耗大量的計算,也一般是破解哈希加密中效率最低的辦法,可是它最終會找到正確的密碼。所以密碼須要足夠長,以致於遍歷全部可能的字符串組合將耗費太長時間,從而不值得去破解它。
• 咱們沒有辦法阻止字典攻擊和暴擊攻擊,儘管能夠下降它們的效率,但那也不是徹底阻止。若是你的密碼哈希系統足夠安全,惟一的破解辦法就是進行字典攻擊或者暴力遍歷每個哈希值。
查表法
Searching: 5f4dcc3b5aa765d61d8327deb882cf99: FOUND: password5
Searching: 6cbe615c106f422d23669b610b564800: not in database
Searching: 630bf032efe4507f2c57b280995925a9: FOUND: letMEin12
Searching: 386f43fab5d096a7a66d67c8f213e5ec: FOUND: mcd0nalds
Searching: d5ec75d5fe70d428685510fae36492d9: FOUND: p@ssw0rd!
查表法對於破解一系列算法相同的哈希值有着無與倫比的效率。主要的思想就是預計算密碼字典中的每一個密碼,而後把哈希值和對應的密碼儲存到一個用於快速查詢的數據結構中。一個良好的查表實現能夠每秒進行數百次哈希查詢,即便表中儲存了幾十億個哈希值。
若是你想更好地體驗查表法的速度,嘗試使用CrackStation的free hash cracker來破解下圖中四個SHA256加密的哈希值吧。
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
這種方法可使攻擊者同時對多個哈希值發起字典攻擊或暴力攻擊,而不須要預先計算出一個查詢表。
首先攻擊者構造一個基於密碼-用戶名的一對多的表,固然數據須要從某個已經被入侵的數據庫得到,而後猜想一系列哈希值而且從表中查找擁有此密碼的用戶。一般許多用戶可能有着相同的密碼,所以這種攻擊方式也顯得尤其有效。
彩虹表
彩虹表是一種在時間和空間的消耗上找尋平衡的破解技術。它和查表法很相似,可是爲了使查詢表佔用的空間更小而犧牲了破解速度。由於它更小,因而咱們能夠在必定的空間內存儲更多的哈希值,從而使攻擊更加有效。可以破解任何8位及如下長度MD5值的彩虹表已經出現了。
下面咱們會講到一種讓查表法和彩虹表都失去做用的技術,叫作加鹽。
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007
查表法和彩虹表只有在全部密碼都以相同方式進行哈希加密時纔有效。若是兩個用戶密碼相同,那麼他們密碼的哈希值也是相同的。咱們能夠經過「隨機化」哈希來阻止這類攻擊,因而當相同的密碼被哈希兩次以後,獲得的值就不相同了。
好比能夠在密碼中混入一段「隨機」的字符串再進行哈希加密,這個被字符串被稱做鹽值。如同上面例子所展現的,這使得同一個密碼每次都被加密爲徹底不 同的字符串。爲了校驗密碼是否正確,咱們須要儲存鹽值。一般和密碼哈希值一塊兒存放在帳戶數據庫中,或者直接存爲哈希字符串的一部分。
鹽值並不須要保密,因爲隨機化了哈希值,查表法、反向查表法和彩虹表都再也不有效。攻擊者沒法確知鹽值,因而就不能預先計算出一個查詢表或者彩虹表。這樣每一個用戶的密碼都混入不一樣的鹽值後再進行哈希,所以反向查表法也變得難以實施。
錯誤一:短鹽值和鹽值重複
最多見的錯誤就是在屢次哈希加密中使用相同的鹽值或者過短的鹽值。
鹽值重複
每次哈希加密都使用相同的鹽值是很容易犯的一個錯誤,這個鹽值要麼被硬編碼到程序裏,要麼只在第一次使用時隨機得到。這樣加鹽的方式是作無用功,因 爲兩個相同的密碼依然會獲得相同的哈希值。攻擊者仍然可使用反向查表法對每一個值進行字典攻擊,只須要把鹽值應用到每一個猜想的密碼上再進行哈希便可。若是 鹽值被硬編碼到某個流行的軟件裏,能夠專門爲這個軟件製做查詢表和彩虹表,那麼破解它生成的哈希值就變得很簡單了。
用戶建立帳戶或每次修改密碼時,都應該從新生成新的鹽值進行加密。
短鹽值
若是鹽值過短,攻擊者能夠構造一個查詢表包含全部可能的鹽值。以只有3個ASCII字符的鹽值爲例,一共有95x95x95=857,375種可 能。這看起來不少,可是若是對於每一個鹽值查詢表只包含1MB最多見的密碼,那麼總共只須要837GB的儲存空間。一個不到100美圓的1000GB硬盤就 能解決問題。
一樣地,用戶名也不該該被用做鹽值。儘管在一個網站中用戶名是惟一的,可是它們是可預測的,而且常常重複用於其餘服務中。攻擊者能夠針對常見用戶名構建查詢表,而後對用戶名鹽值哈希發起進攻。
爲了使攻擊者沒法構造包含全部可能鹽值的查詢表,鹽值必須足夠長。一個好的作法是使用和哈希函數輸出的字符串等長的鹽值,好比SHA256算法的輸出是256bits(32 bytes),那麼鹽值也至少應該是32個隨機字節。
錯誤二:兩次哈希和組合哈希函數
(譯註:此節標題原文中的Wacky Hash Functions直譯是古怪的哈希函數,大概是因爲做者不承認這種組合多種哈希函數的作法,爲了便於理解,本文仍是翻譯爲組合哈希函數)
這節講述了另外一種對密碼哈希的誤解:使用組合哈希函數。人們常常情不自禁地認爲將不一樣的哈希函數組合起來,結果會更加安全。實際上這樣作幾乎沒有好 處,僅僅形成了函數之間互相影響的問題,甚至有時候會變得更加不安全。永遠不要嘗試發明本身的加密方法,只需只用已經被設計好的標準算法。有的人會說使用 多種哈希函數會使計算更慢,從而破解也更慢,可是還有其餘的辦法能更好地減緩破解速度,後面會提到的。
這裏有些低端的組合哈希函數,我在網上某些論壇看到它們被推薦使用:
不要使用其中任何一種。
注意:這節內容是有爭議的。我已經收到的大量的郵件,爲組合哈希函數而辯護。他們的理由是若是攻擊者不知道系統使用的哪一種哈希函數,那麼也就很難預先爲這種組合構造出彩虹表,因而破解起來會花費更多的時間。
誠然,攻擊者在不知道加密算法的時候是沒法發動攻擊的,可是不要忘了Kerckhoffs’s principle, 攻擊者一般很容易就能拿到源碼(尤爲是那些免費或開源的軟件)。經過系統中取出的一些密碼-哈希值對應關係,很容易反向推導出加密算法。破解組合哈希函數 確實須要更多時間,但也只是受了一點能夠確知的因素影響。更好的辦法是使用一個很難被並行計算出結果的迭代算法,而後增長適當的鹽值防止彩虹表攻擊。
固然你實在想用「標準的」組合哈希函數,好比HMAC,也是能夠的。但若是隻是爲了使破解起來更慢,那麼先讀讀下面講到的密鑰擴展。
創造新的哈希函數可能帶來安全問題,構造哈希函數的組合又可能帶來函數間互相影響的問題,它們帶來的一丁點好處和這些比起來真是微不足道。顯然最好的作法是使用標準的、通過完整測試的算法。
哈希碰撞
哈希函數將任意大小的數據轉化爲定長的字符串,所以其中必定有些輸入通過哈希計算以後獲得了相同的結果。加密哈希函數的設計就是爲了使這樣的碰撞儘量難以被發現。隨着時間流逝,密碼學家發現攻擊者愈來愈容易找到碰撞了,最近的例子就是MD5算法的碰撞已經肯定被發現了。
碰撞攻擊的出現代表極可能有一個和用戶密碼不一樣的字符串卻和它有着相同的哈希值。然而,即便在MD5這樣脆弱的哈希函數中找到碰撞也須要耗費大量的 計算,所以這樣的碰撞「意外地」在實際中出現的可能性是很低的。因而站在實用性的角度上能夠這麼說,加鹽MD5和加鹽SHA256的安全性是同樣的。不過 可能的話,使用自己更安全的哈希函數老是好的,好比SHA25六、SHA5十二、RipeMD或者WHIRLPOOL。
正確的作法:恰當使用哈希加密
本節會準確講述應該如何對密碼進行哈希加密。其中第一部分介紹最基本的要素,也是在哈希加密中必定要作到的;後面講解怎樣在這個基礎上進行擴展,使得加密更難被破解。
基本要素:加鹽哈希
忠告:你不只僅要用眼睛看文章,更要本身動手去實現後面講到的「讓密碼更難破解:慢哈希函數」。
在前文中咱們已經看到,利用查表法和彩虹表,普通哈希加密是多麼容易被惡意攻擊者破解,也知道了能夠經過隨機加鹽的辦法也解決這個問題。那麼到底應該使用怎樣的鹽值呢,又如何把它混入密碼?
鹽值應該使用基於加密的僞隨機數生成器(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 |
對於每一個用戶的每一個密碼,鹽值都應該是獨一無二的。每當有新用戶註冊或者修改密碼,都應該使用新的鹽值進行加密。而且這個鹽值也應該足夠長,使得有 足夠多的鹽值以供加密。一個好的標準的是:鹽值至少和哈希函數的輸出同樣長;鹽值應該被儲存和密碼哈希一塊兒儲存在帳戶數據表中。
存儲密碼的步驟
校驗密碼的步驟
文章最後有幾個加鹽密碼哈希的代碼實現,分別使用了PHP、C#、Java和Ruby。
在Web程序中,永遠在服務器端進行哈希加密
若是你正在開發一個Web程序,你可能會疑惑到底在哪進行加密。是使用JavaScript在用戶的瀏覽器上操做呢,仍是將密碼「裸體」傳送到服務器再進行加密?
即便瀏覽器端用JavaScript加密了,你仍然須要在服務端再次進行加密。試想有個網站在瀏覽器將密碼通過哈希後傳送到服務器,那麼在認證用戶 的時候,網站收到哈希值和數據庫中的值進行比對就能夠了。這看起來比只在服務器端加密安全得多,由於至始至終沒有將用戶的密碼明文傳輸,但實際上不是這 樣。
問題在於,從客戶端來看,通過哈希的密碼邏輯上成爲用戶真正的密碼。爲了經過服務器認證,用戶只須要發送密碼的哈希值便可。若是有壞小子獲取了這個 哈希值,他甚至能夠在不知道用戶密碼的狀況經過認證。更進一步,若是他用某種手段入侵了網站的數據庫,那麼不須要去猜解任何人的密碼,就能夠隨意使用每一個 人的賬號登陸。
這並非說你不該該在瀏覽器端進行加密,可是若是你這麼作了,必定要在服務端再次加密。在瀏覽器中進行哈希加密是個好想法,不過實現的時候注意下面幾點:
• 客戶端密碼哈希並不能代替HTTPS(SSL/TLS)。若是瀏覽器和服務器之間的鏈接是不安全的,那麼中間人攻擊能夠修改JavaScript代碼,刪除加密函數,從而獲取用戶密碼。
• 有些瀏覽器不支持JavaScript,也有的用戶禁用了瀏覽器的JavaScript功能。爲了最好的兼容性,你的程序應該檢測JavaScript是否可用,若是答案爲否,須要在服務端模擬客戶端的加密。
• 客戶端哈希一樣須要加鹽,很顯然的辦法就是向服務器請求用戶的鹽值,可是不要這麼作。由於這給了壞蛋一個機會,可以在不知道密碼的狀況下檢測用戶名是否有 效。既然你已經在服務端對密碼進行了加鹽哈希,那麼在客戶端把用戶名(或郵箱)加上網站特有的字符串(如域名)做爲鹽值是可行的。
讓密碼更難破解:慢哈希函數
加鹽使攻擊者沒法採用特定的查詢表和彩虹錶快速破解大量哈希值,可是卻不能阻止他們使用字典攻擊或暴力攻擊。高端的顯卡(GPU)和定製的硬件能夠每秒進行數十億次哈希計算,所以這類攻擊依然能夠很高效。爲了下降攻擊者的效率,咱們可使用一種叫作密鑰擴展的技術。
這種技術的思想就是把哈希函數變得很慢,因而即便有着超高性能的GPU或定製硬件,字典攻擊和暴力攻擊也會慢得讓攻擊者沒法接受。最終的目標是把哈希函數的速度降到足以讓攻擊者望而卻步,但形成的延遲又不至於引發用戶的注意。
密鑰擴展的實現是依靠一種CPU密集型哈希函數。不要嘗試本身發明簡單的迭代哈希加密,若是迭代不夠多,是能夠被高效的硬件快速並行計算出來的,就和普通哈希同樣。應該使用標準的算法,好比PBKDF2或者bcrypt。這裏能夠找到PBKDF2在PHP上的一種實現。
這類算法使用一個安全因子或迭代次數做爲參數,這個值決定了哈希函數會有多慢。對於桌面軟件或者手機軟件,獲取參數最好的辦法就是執行一個簡短的性能基準測試,找到使哈希函數大約耗費0.5秒的值。這樣,你的程序就能夠儘量保證安全,而又不影響到用戶體驗。
若是你在一個Web程序中使用密鑰擴展,記得你須要額外的資源處理大量認證請求,而且密鑰擴展也使得網站更容易遭受拒絕服務攻擊(DoS)。但我依 然推薦使用密鑰擴展,不過把迭代次數設定得低一點,你應該基於認證請求最高峯時的剩餘硬件資源來計算迭代次數。要求用戶每次登陸時輸入驗證碼能夠消除拒絕 服務的威脅。另外,必定要把你的系統設計爲迭代次數可隨時調整的。
若是你擔憂計算量帶來的負載,但又想在Web程序中使用密鑰擴展,能夠考慮在瀏覽器中用JavaScript完成。Stanford JavaScript Crypto Library裏 包含了PBKDF2的實現。迭代次數應該被設置到足夠低,以適應速度較慢的客戶端,好比移動設備。同時當客戶端不支持JavaScript的時候,服務端 應該接手計算。客戶端的密鑰擴展並不能免除服務端進行哈希加密的職責,你必須對客戶端傳來的哈希值再次進行哈希加密,就像對付一個普通密碼同樣。
沒法破解的哈希加密:密鑰哈希和密碼哈希設備
只要攻擊者能夠檢測對一個密碼的猜想是否正確,那麼他們就能夠進行字典攻擊或暴力攻擊。所以下一步就是向哈希計算中增長一個密鑰,只有知道這個密鑰的人才能校驗密碼。有兩種辦法能夠實現:將哈希值加密,好比使用AES算法;將密鑰包含到哈希字符串中,好比使用密鑰哈希算法HMAC。
聽起來很簡單,作起來就不同了。這個密鑰須要在任何狀況下都不被攻擊者獲取,即便系統由於漏洞被攻破了。若是攻擊者獲取了進入系統的最高權限,那 麼不論密鑰被儲存在哪,他們均可以竊取到。所以密鑰須要儲存在外部系統中,好比另外一個用於密碼校驗的物理服務器,或者一個關聯到服務器的特製硬件,如YubiHSM。
我強烈推薦大型服務(10萬用戶以上)使用這類辦法,由於我認爲面對如此多的用戶是有必要的。
若是你難以負擔多個服務器或專用的硬件,仍然有辦法在一個普通Web服務器上利用密鑰哈希技術。大部分針對數據庫的入侵都是因爲SQL注入攻擊, 所以不要給攻擊者進入本地文件系統的權限(禁止數據庫服務訪問本地文件系統,若是它有這個功能的話)。這樣一來,當你隨機生成一個密鑰存到經過Web程序 沒法訪問的文件中,而後混入加鹽哈希,獲得的哈希值就再也不那麼脆弱了,即使這時數據庫遭受了注入攻擊。不要把將密鑰硬編碼到代碼裏,應該在安裝時隨機生 成。這固然不如獨立的硬件系統安全,由於若是Web程序存在SQL注入點,那麼可能還存在其餘一些問題,好比本地文件包含漏洞(Local File Inclusion),攻擊者能夠利用它讀取本地密鑰文件。不管如何,這個措施比沒有好。
請注意密鑰哈希不表明無需進行加鹽。高明的攻擊者早晚會找到辦法竊取密鑰,所以依然對密碼哈希進行加鹽和密鑰擴展很重要。
其餘安全措施
哈希加密能夠在系統發生入侵時保護密碼,但這並不能使整個程序更加安全。首先還有不少事情須要作,來保證密碼哈希(和其餘用戶數據)不被竊取。
即便經驗豐富的開發者也須要額外學習安全知識,才能寫出安全的程序。這裏有個關於Web程序漏洞的資源:The Open Web Application Security Project (OWASP),還有一個很好的介紹:OWASP Top Ten Vulnerability List。除非你瞭解列表中全部的漏洞,才能嘗試編寫一個處理敏感數據的Web程序。僱主也有責任保證他全部的開發人員都有資質編寫安全的程序。
對你的程序進行第三方「滲透測試」是一個不錯的選擇。最好的程序員也可能犯錯,所以有一個安全專家審查你的代碼尋找潛在的漏洞是有意義的。找尋值得信賴的機構(或招聘人員)來對你的代碼進行審查。安全審查應該從編碼的初期就着手進行,一直貫穿整個開發過程。
監控你的網站來發現入侵行爲也是很重要的,我推薦至少僱傭一我的全職負責監測和處理安全隱患。若是有個漏洞沒被發現,攻擊者可能經過網站利用惡意軟件感染訪問者,所以檢測漏洞而且及時應對是十分重要的。
我應該使用什麼哈希算法?
應該使用:
不要使用:
儘管尚未一種針對MD5或SHA1很是效率的攻擊手段,可是它們太古老也被普遍地認爲不足以勝任存儲密碼的工做(某種程度上甚至是錯誤的),所以我也不推薦使用它們。可是有個例外,PBKDF2中頻繁地使用了SHA1做爲它底層的哈希函數。
當用戶忘記密碼的時候,怎樣進行重置?
我我的的觀點是,當前全部普遍使用的密碼重置機制都是不安全的。若是你對安全性有極高的要求,好比一個加密服務,那麼不要容許用戶重置密碼。
大多數網站向那些忘記密碼的用戶發送電子郵件來進行身份認證。首先,須要隨機生成一個一次性的令牌,它直接關聯到用戶的帳戶。而後將這個令牌混入一個重置密碼的連接中,發送到用戶的電子郵箱。最後當用戶點擊這個包含有效令牌的連接時,提示他們能夠設置新的密碼。要確保這個令牌只對一個帳戶有效,以防攻擊者從郵箱獲取到令牌後,用來重置其餘用戶的密碼。
令牌必須在15分鐘內使用,而且一旦被使用就當即失效。當用戶從新請求令牌時,或用戶登陸成功時(說明他還記得密碼),使原令牌失效也是一個好作 法。若是一個令牌始終不過時,那麼它一直能夠用於入侵用戶的賬號。電子郵件(SMTP)是一個純文本協議,而且網絡上有不少惡意路由在截取郵件信息。在用 戶修改密碼後,那些包含重置密碼連接的郵件在很長一段時間內依然缺少保護。所以應該儘早使令牌過時,下降把用戶信息暴露給攻擊者的可能。
攻擊者是能夠篡改令牌的,因此不要把帳戶信息和失效時間存儲在裏面。這些信息應該以不可猜解的二進制形式存在,而且只用來識別數據庫中某條用戶的記錄。
永遠不要經過電子郵件向用戶發送新密碼,同時也記得在用戶重置密碼的時候隨機生成一個新的鹽值用於加密,不要重複使用以前密碼的那個鹽值。
當帳戶數據庫被泄漏或入侵時,應該怎麼作?
你首先須要作的,是查看系統被暴露到什麼程度了,而後修復這個攻擊者利用的漏洞。若是你沒有應對入侵的經驗,我強烈推薦僱一個第三方安全機構來作這件事。
將一個漏洞精心掩蓋期待沒有人能注意到,是否聽起來很省事而又誘人呢?可是這樣只會讓你顯得更糟糕,由於你在用戶不知情的狀況下,將他們的密碼和個 人信息暴露在危險之中。即便用戶還沒法理解到底發生了什麼,你也應該儘快履行告知的義務。好比在首頁放置一個連接,指向對此問題更詳細的說明,可能的話還 能夠經過電子郵件告知用戶目前的狀況。
向你的用戶說明你是如何保護他們的密碼的——最好是使用了加鹽哈希——即使如此惡意黑客也能使用字典攻擊和暴力攻擊。設想用戶可能在不少服務中使用 相同的密碼,攻擊者會用找到的密碼去嘗試登陸其餘網站。提示你的用戶應該修改全部類似的密碼,不論它們被使用在哪一個服務上,而且強制用戶下次登陸你的網站 時修改密碼。大部分用戶會嘗試將密碼「修改」爲和以前相同的以便記憶,你應該使用老密碼的哈希值來確保用戶沒法這麼作。
即便有加鹽哈希的保護,攻擊者也極可能快速破解其中一些脆弱的密碼。爲了減小攻擊者使用的它們機會,你應該對這些密碼的賬號發送認證電子郵件,直到用戶修改了密碼。能夠參考上一個問題,其中有一些實現電子郵件認證的要點。
另外也要告訴你的用戶,網站到底儲存了哪些我的信息。若是你的數據庫中有用戶的信用卡號,你應該指導用戶檢查本身近期的帳單,而且註銷掉這張信用卡。
我應該使用什麼樣的密碼規則?是否應該強制用戶使用複雜的密碼?
若是你的服務對安全性沒有嚴格的要求,那麼不要對用戶進行限制。我推薦在用戶輸入密碼的時候,頁面上顯示出密碼強度,由用戶本身決定須要多安全的密 碼。若是你的服務對安全有特殊的需求,那就應該強制用戶輸入長度至少爲12個字符的密碼,而且其中至少包括兩個字母、兩個數字和兩個符號。
不要過於頻繁地強制你的用戶修改密碼,最多6個月1次,由於那樣作會使用戶疲於選擇一個強度足夠好的密碼。更好的作法是指導用戶在他們感受密碼可能 泄漏的時候去主動修改,而且提示用戶不要把密碼告訴任何人。若是這是在商業環境中,鼓勵你的員工利用工做時間熟記並使用他們的密碼。
若是攻擊者入侵了個人數據庫,他們難道不能把其中的密碼哈希替換爲本身的值,而後登陸系統麼?
固然能夠,可是若是他已經入侵了你的數據庫,那麼極可能已經有權限訪問你服務器上任何東西了,所以徹底不必登陸帳戶去獲取他想要的。對密碼進行哈希加密的手段,(對網站而言)不是保護網站免受入侵,而是在入侵已經發生時保護數據庫中的密碼。
經過爲數據庫鏈接設置兩種權限,能夠防止密碼哈希在遭遇注入攻擊時被篡改。一種權限用於建立用戶:它對用戶表可讀可寫;另外一種用於用戶登陸,它只能讀用戶表而不能寫。
爲何我非得用像HMAC那種特殊的算法?爲何不能簡單地把密鑰混入密碼?
像MD五、SHA1和SHA2這類哈希函數是基於Merkle–Damgård構 造的,所以在長度擴展攻擊面前很是脆弱。就是說若是已經知道一個哈希值H(X),對於任意的字符串Y,攻擊者能夠計算出H(pad(X) + Y)的值,而不須要知道X是多少,其中pad(X)是哈希函數的填充函數(padding function,好比MD5將數據每512bit分爲一組,最後不足的將填充字節)。
在攻擊者不知道密鑰(key)的狀況下,他仍然能夠根據哈希值H(key + message)計算出H(pad(key + message) + extension)。若是這個哈希值用於身份認證,而且依靠其中的密鑰來防止攻擊者篡改消息,這個辦法已經行不通了。由於攻擊者無需知道密鑰,也能構造 出包含message + extension的一個有效的哈希值。
目前還不清楚攻擊者可否用這個辦法更快破解密碼,可是因爲這種攻擊的出現,在密鑰哈希中使用上述哈希函數已經被認爲是差勁的實踐了。也許某天高明的密碼學家會發現一個利用長度擴展攻擊的新思路,從而更快地破解密碼,因此仍是使用HMAC吧。
鹽值應該加到密碼前面仍是後面?
都行,可是在一個程序中應該保持一致,以避免出現互操做方面的問題。目前看來加到密碼以前是比較經常使用的作法。
爲何本文中的代碼在比較哈希值的時候,都是通過固定的時間才返回結果?
讓比較過程耗費固定的時間能夠保證攻擊者沒法對一個在線系統使用計時攻擊,以此獲取密碼的哈希值,而後進行本地破解工做。
比較兩個字節序列(字符串)的標準作法是,從第一字節開始,每一個字節逐一順序比較。只要發現某字節不相同了,就能夠當即返回「假」的結果。若是遍歷 整個字符串也沒有找到不一樣的字節,那麼兩個字符串就是相同的,而且返回「真」。這意味着比較字符串的耗時決定於兩個字符串到底有多大的不一樣。
舉個例子,使用標準的方法比較「xyzabc」和「abcxyz」,因爲第一個字符就不一樣,不須要檢查後面的內容就能夠立刻返回結果。相反,若是比 較「aaaaaaaaaaB」和「aaaaaaaaaaZ」,比較算法就須要遍歷最後一位前全部的「a」,而後才能知道它們是不相同的。
假設攻擊者妄圖入侵一個在線系統,而且此係統限制了每秒只能嘗試一次用戶認證。還假設他已經知道了密碼哈希全部的參數(鹽值、哈希函數的類型等 等),除了密碼的哈希值和密碼自己(顯然啊,不然還破解個什麼)。若是攻擊者能精確測量在線系統耗時多久去比較他猜想的密碼和真實密碼,那麼他就能使用計 時攻擊獲取密碼的哈希值,而後進行離線破解,從而繞過系統對認證頻率的限制。
首先攻擊者準備256個字符串,它們的哈希值的第一字節包含了全部可能的狀況。而後用它們去系統中嘗試登陸,並記錄系統返回結果所消耗的時間,耗時 最長的那個就是第一字節猜對的那個。接下來用一樣的方式猜想第二字節、第三字節等等。直到攻擊者獲取了最夠長的哈希值片斷,最後只需在本身的機器上破解即 可,徹底不受在線系統的限制。
乍看之下在網絡上進行計時攻擊是不可能作到的,然而有人已經實現了,並運用到實際中了。所以本文提供的代碼才使用固定的時間去比較字符串,不論它們有多類似。
「慢比較」的代碼是如何工做的?
上一個問題解釋了爲何「慢比較」是有必要的,如今來說解一下代碼具體是怎麼實現的。
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。由於0 XOR 0 = 0, 1 XOR 1 = 0, 0 XOR 1 = 1, 1 XOR 0 = 1。應用到整數中每一位就是說,當且僅當字節兩個整數各位都相等,結果纔是0。
代碼中的第一行,比較a.length和b.length,相同的話diff是0,不然diff非0。而後使用異或比較數組中各字節,而且將結果和 diff求或。若是有任何一個字節不相同,diff就會變成非0的值。由於或運算沒有「置0」的功能,因此循環結束後diff是0的話只有一種可能,那就 是循環前兩個數組長度相等(a.length == b.length),而且數組中每個字節都相同(每次異或的結果都非0)。
咱們使用XOR而不是「==」來比較整數的緣由是:「==」一般被翻譯/編譯/解釋爲帶有分支的語句。例如C語言中的「diff &= a == b」可能在x86機器成被編譯爲以下彙編語言:
MOV EAX, [A]
CMP [B], EAX
JZ equal
JMP done
equal:
AND [VALID], 1
done:
AND [VALID], 0
其中的分支致使代碼運行的時間不固定,決定於兩個整數相等的程度和CPU內部的跳轉預測機制(branch prediction)。
而C語言代碼「diff |=a ^ b」會被編譯爲下面的樣子,它執行的時間和兩個整數是什麼樣的狀況無關。
MOV EAX, [A]
XOR EAX, [B]
OR [DIFF], EAX
弄這麼麻煩幹嗎?
用戶在你的網站上輸入密碼,說明他們相信你會保障密碼的安全。若是你的數據庫被黑了,又沒有對用戶密碼加以保護,惡意黑客就可使用這些密碼去入侵 用戶在其餘網站或服務的帳戶(大部分人會在各處使用相同的密碼)。這不只僅關乎你網站的安全,更關係到用戶的。你須要對用戶的安全負責。
下面是PBKDF2在PHP中一種安全的實現,你也能夠在這個頁面找到測試用例和基準測試的代碼。
若是你須要兼容的PHP和C#代碼,點擊這裏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
<?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
));
}
?>
|
下面是PBKDF2在Java中一種安全的實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
|
/**
* 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);
}
}
}
|
下面是PBKDF2在ASP.NET(C#)中一種安全的實現。
若是你須要兼容的PHP和C#代碼,點擊這裏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
/*
* 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);
}
}
}
|
下面是PBKDF2在Ruby(on Rails)中一種安全的實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
# 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
|
文章和代碼由Defuse Security編寫。
If you're a web developer, you've probably had to make a user account system. The most important aspect of a user account system is how user passwords are protected. User account databases are hacked frequently, so you absolutely must do something to protect your users' passwords if your website is ever breached. The best way to protect passwords is to employ salted password hashing. This page will explain why it's done the way it is.
There are a lot of conflicting ideas and misconceptions on how to do password hashing properly, probably due to the abundance of misinformation on the web. Password hashing is one of those things that's so simple, but yet so many people get wrong. With this page, I hope to explain not only the correct way to do it, but why it should be done that way.
If for some reason you missed that big red warning note, please go read it now. Really, this guide is not meant to walk you through the process of writing your own storage system, it's to explain the reasons why passwords should be stored a certain way.
You may use the following links to jump to the different sections of this page.
1. What is password hashing? | 2. How Hashes are Cracked | 3. Adding Salt |
4. Ineffective Hashing Methods | 5. How to hash properly | 6. Frequently Asked Questions |
There is BSD-licensed password hashing source code at the bottom of this page:
PHP Source Code | Java Source Code | ASP.NET (C#) Source Code | Ruby (on Rails) Source Code |
Hash algorithms are one way functions. They turn any amount of data into a fixed-length "fingerprint" that cannot be reversed. They also have the property that if the input changes by even a tiny bit, the resulting hash is completely different (see the example above). This is great for protecting passwords, because we want to store passwords in a form that protects them even if the password file itself is compromised, but at the same time, we need to be able to verify that a user's password is correct.
The general workflow for account registration and authentication in a hash-based account system is as follows:
In step 4, never tell the user if it was the username or password they got wrong. Always display a generic message like "Invalid username or password." This prevents attackers from enumerating valid usernames without knowing their passwords.
It should be noted that the hash functions used to protect passwords are not the same as the hash functions you may have seen in a data structures course. The hash functions used to implement data structures such as hash tables are designed to be fast, not secure. Only cryptographic hash functions may be used to implement password hashing. Hash functions like SHA256, SHA512, RipeMD, and WHIRLPOOL are cryptographic hash functions.
It is easy to think that all you have to do is run the password through a cryptographic hash function and your users' passwords will be secure. This is far from the truth. There are many ways to recover passwords from plain hashes very quickly. There are several easy-to-implement techniques that make these "attacks" much less effective. To motivate the need for these techniques, consider this very website. On the front page, you can submit a list of hashes to be cracked, and receive results in less than a second. Clearly, simply hashing the password does not meet our needs for security.
The next section will discuss some of the common attacks used to crack plain password hashes.
Trying apple : failed Trying blueberry : failed Trying justinbeiber : failed Trying s3cr3t : success! |
Trying aaaa : failed Trying aaab : failed Trying aaac : failed Trying acdc : success! |
The simplest way to crack a hash is to try to guess the password, hashing each guess, and checking if the guess's hash equals the hash being cracked. If the hashes are equal, the guess is the password. The two most common ways of guessing passwords are dictionary attacks and brute-force attacks.
A dictionary attack uses a file containing words, phrases, common passwords, and other strings that are likely to be used as a password. Each word in the file is hashed, and its hash is compared to the password hash. If they match, that word is the password. These dictionary files are constructed by extracting words from large bodies of text, and even from real databases of passwords. Further processing is often applied to dictionary files, such as replacing words with their "leet speak" equivalents ("hello" becomes "h3110"), to make them more effective.
A brute-force attack tries every possible combination of characters up to a given length. These attacks are very computationally expensive, and are usually the least efficient in terms of hashes cracked per processor time, but they will always eventually find the password. Passwords should be long enough that searching through all possible character strings to find it will take too long to be worthwhile.
There is no way to prevent dictionary attacks or brute force attacks. They can be made less effective, but there isn't a way to prevent them altogether. If your password hashing system is secure, the only way to crack the hashes will be to run a dictionary or brute-force attack on each hash.
Lookup tables are an extremely effective method for cracking many hashes of the same type very quickly. The general idea is to pre-compute the hashes of the passwords in a password dictionary and store them, and their corresponding password, in a lookup table data structure. A good implementation of a lookup table can process hundreds of hash lookups per second, even when they contain many billions of hashes.
If you want a better idea of how fast lookup tables can be, try cracking the following sha256 hashes with CrackStation's free hash cracker.
This attack allows an attacker to apply a dictionary or brute-force attack to many hashes at the same time, without having to pre-compute a lookup table.
First, the attacker creates a lookup table that maps each password hash from the compromised user account database to a list of users who had that hash. The attacker then hashes each password guess and uses the lookup table to get a list of users whose password was the attacker's guess. This attack is especially effective because it is common for many users to have the same password.
Rainbow tables are a time-memory trade-off technique. They are like lookup tables, except that they sacrifice hash cracking speed to make the lookup tables smaller. Because they are smaller, the solutions to more hashes can be stored in the same amount of space, making them more effective. Rainbow tables that can crack any md5 hash of a password up to 8 characters long exist.
Next, we'll look at a technique called salting, which makes it impossible to use lookup tables and rainbow tables to crack a hash.
Lookup tables and rainbow tables only work because each password is hashed the exact same way. If two users have the same password, they'll have the same password hashes. We can prevent these attacks by randomizing each hash, so that when the same password is hashed twice, the hashes are not the same.
We can randomize the hashes by appending or prepending a random string, called a salt, to the password before hashing. As shown in the example above, this makes the same password hash into a completely different string every time. To check if a password is correct, we need the salt, so it is usually stored in the user account database along with the hash, or as part of the hash string itself.
The salt does not need to be secret. Just by randomizing the hashes, lookup tables, reverse lookup tables, and rainbow tables become ineffective. An attacker won't know in advance what the salt will be, so they can't pre-compute a lookup table or rainbow table. If each user's password is hashed with a different salt, the reverse lookup table attack won't work either.
In the next section, we'll look at how salt is commonly implemented incorrectly.
The most common salt implementation errors are reusing the same salt in multiple hashes, or using a salt that is too short.
A common mistake is to use the same salt in each hash. Either the salt is hard-coded into the program, or is generated randomly once. This is ineffective because if two users have the same password, they'll still have the same hash. An attacker can still use a reverse lookup table attack to run a dictionary attack on every hash at the same time. They just have to apply the salt to each password guess before they hash it. If the salt is hard-coded into a popular product, lookup tables and rainbow tables can be built for that salt, to make it easier to crack hashes generated by the product.
A new random salt must be generated each time a user creates an account or changes their password.
If the salt is too short, an attacker can build a lookup table for every possible salt. For example, if the salt is only three ASCII characters, there are only 95x95x95 = 857,375 possible salts. That may seem like a lot, but if each lookup table contains only 1MB of the most common passwords, collectively they will be only 837GB, which is not a lot considering 1000GB hard drives can be bought for under $100 today.
For the same reason, the username shouldn't be used as a salt. Usernames may be unique to a single service, but they are predictable and often reused for accounts on other services. An attacker can build lookup tables for common usernames and use them to crack username-salted hashes.
To make it impossible for an attacker to create a lookup table for every possible salt, the salt must be long. A good rule of thumb is to use a salt that is the same size as the output of the hash function. For example, the output of SHA256 is 256 bits (32 bytes), so the salt should be at least 32 random bytes.
This section covers another common password hashing misconception: wacky combinations of hash algorithms. It's easy to get carried away and try to combine different hash functions, hoping that the result will be more secure. In practice, though, there is very little benefit to doing it. All it does is create interoperability problems, and can sometimes even make the hashes less secure. Never try to invent your own crypto, always use a standard that has been designed by experts. Some will argue that using multiple hash functions makes the process of computing the hash slower, so cracking is slower, but there's a better way to make the cracking process slower as we'll see later.
Here are some examples of poor wacky hash functions I've seen suggested in forums on the internet.
Do not use any of these.
Note: This section has proven to be controversial. I've received a number of emails arguing that wacky hash functions are a good thing, because it's better if the attacker doesn't know which hash function is in use, it's less likely for an attacker to have pre-computed a rainbow table for the wacky hash function, and it takes longer to compute the hash function.
An attacker cannot attack a hash when he doesn't know the algorithm, but note Kerckhoffs's principle, that the attacker will usually have access to the source code (especially if it's free or open source software), and that given a few password-hash pairs from the target system, it is not difficult to reverse engineer the algorithm. It does take longer to compute wacky hash functions, but only by a small constant factor. It's better to use an iterated algorithm that's designed to be extremely hard to parallelize (these are discussed below). And, properly salting the hash solves the rainbow table problem.
If you really want to use a standardized "wacky" hash function like HMAC, then it's OK. But if your reason for doing so is to make the hash computation slower, read the section below about key stretching first.
Compare these minor benefits to the risks of accidentally implementing a completely insecure hash function and the interoperability problems wacky hashes create. It's clearly best to use a standard and well-tested algorithm.
Because hash functions map arbitrary amounts of data to fixed-length strings, there must be some inputs that hash into the same string. Cryptographic hash functions are designed to make these collisions incredibly difficult to find. From time to time, cryptographers find "attacks" on hash functions that make finding collisions easier. A recent example is the MD5 hash function, for which collisions have actually been found.
Collision attacks are a sign that it may be more likely for a string other than the user's password to have the same hash. However, finding collisions in even a weak hash function like MD5 requires a lot of dedicated computing power, so it is very unlikely that these collisions will happen "by accident" in practice. A password hashed using MD5 and salt is, for all practical purposes, just as secure as if it were hashed with SHA256 and salt. Nevertheless, it is a good idea to use a more secure hash function like SHA256, SHA512, RipeMD, or WHIRLPOOL if possible.
This section describes exactly how passwords should be hashed. The first subsection covers the basics—everything that is absolutely necessary. The following subsections explain how the basics can be augmented to make the hashes even harder to crack.
Warning: Do not just read this section. You absolutely must implement the stuff in the next section: "Making Password Cracking Harder: Slow Hash Functions".
We've seen how malicious hackers can crack plain hashes very quickly using lookup tables and rainbow tables. We've learned that randomizing the hashing using salt is the solution to the problem. But how do we generate the salt, and how do we apply it to the password?
Salt should be generated using a Cryptographically Secure Pseudo-Random Number Generator (CSPRNG). CSPRNGs are very different than ordinary pseudo-random number generators, like the "C" language's rand() function. As the name suggests, CSPRNGs are designed to be cryptographically secure, meaning they provide a high level of randomness and are completely unpredictable. We don't want our salts to be predictable, so we must use a CSPRNG. The following table lists some CSPRNGs that exist for some popular programming platforms.
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 |
The salt needs to be unique per-user per-password. Every time a user creates an account or changes their password, the password should be hashed using a new random salt. Never reuse a salt. The salt also needs to be long, so that there are many possible salts. As a rule of thumb, make your salt is at least as long as the hash function's output. The salt should be stored in the user account table alongside the hash.
At the bottom of this page, there are implementations of salted password hashing in PHP, C#, Java, and Ruby.
If you are writing a web application, you might wonder where to hash. Should the password be hashed in the user's browser with JavaScript, or should it be sent to the server "in the clear" and hashed there?
Even if you are hashing the user's passwords in JavaScript, you still have to hash the hashes on the server. Consider a website that hashes users' passwords in the user's browser without hashing the hashes on the server. To authenticate a user, this website will accept a hash from the browser and check if that hash exactly matches the one in the database. This seems more secure than just hashing on the server, since the users' passwords are never sent to the server, but it's not.
The problem is that the client-side hash logically becomes the user's password. All the user needs to do to authenticate is tell the server the hash of their password. If a bad guy got a user's hash they could use it to authenticate to the server, without knowing the user's password! So, if the bad guy somehow steals the database of hashes from this hypothetical website, they'll have immediate access to everyone's accounts without having to guess any passwords.
This isn't to say that you shouldn't hash in the browser, but if you do, you absolutely have to hash on the server too. Hashing in the browser is certainly a good idea, but consider the following points for your implementation:
Client-side password hashing is not a substitute for HTTPS (SSL/TLS). If the connection between the browser and the server is insecure, a man-in-the-middle can modify the JavaScript code as it is downloaded to remove the hashing functionality and get the user's password.
Some web browsers don't support JavaScript, and some users disable JavaScript in their browser. So for maximum compatibility, your app should detect whether or not the browser supports JavaScript and emulate the client-side hash on the server if it doesn't.
You need to salt the client-side hashes too. The obvious solution is to make the client-side script ask the server for the user's salt. Don't do that, because it lets the bad guys check if a username is valid without knowing the password. Since you're hashing and salting (with a good salt) on the server too, it's OK to use the username (or email) concatenated with a site-specific string (e.g. domain name) as the client-side salt.
Salt ensures that attackers can't use specialized attacks like lookup tables and rainbow tables to crack large collections of hashes quickly, but it doesn't prevent them from running dictionary or brute-force attacks on each hash individually. High-end graphics cards (GPUs) and custom hardware can compute billions of hashes per second, so these attacks are still very effective. To make these attacks less effective, we can use a technique known as key stretching.
The idea is to make the hash function very slow, so that even with a fast GPU or custom hardware, dictionary and brute-force attacks are too slow to be worthwhile. The goal is to make the hash function slow enough to impede attacks, but still fast enough to not cause a noticeable delay for the user.
Key stretching is implemented using a special type of CPU-intensive hash function. Don't try to invent your own–simply iteratively hashing the hash of the password isn't enough as it can be parallelized in hardware and executed as fast as a normal hash. Use a standard algorithm like PBKDF2 or bcrypt. You can find a PHP implementation of PBKDF2 here.
These algorithms take a security factor or iteration count as an argument. This value determines how slow the hash function will be. For desktop software or smartphone apps, the best way to choose this parameter is to run a short benchmark on the device to find the value that makes the hash take about half a second. This way, your program can be as secure as possible without affecting the user experience.
If you use a key stretching hash in a web application, be aware that you will need extra computational resources to process large volumes of authentication requests, and that key stretching may make it easier to run a Denial of Service (DoS) attack on your website. I still recommend using key stretching, but with a lower iteration count. You should calculate the iteration count based on your computational resources and the expected maximum authentication request rate. The denial of service threat can be eliminated by making the user solve a CAPTCHA every time they log in. Always design your system so that the iteration count can be increased or decreased in the future.
If you are worried about the computational burden, but still want to use key stretching in a web application, consider running the key stretching algorithm in the user's browser with JavaScript. The Stanford JavaScript Crypto Library includes PBKDF2. The iteration count should be set low enough that the system is usable with slower clients like mobile devices, and the system should fall back to server-side computation if the user's browser doesn't support JavaScript. Client-side key stretching does not remove the need for server-side hashing. You must hash the hash generated by the client the same way you would hash a normal password.
As long as an attacker can use a hash to check whether a password guess is right or wrong, they can run a dictionary or brute-force attack on the hash. The next step is to add a secret key to the hash so that only someone who knows the key can use the hash to validate a password. This can be accomplished two ways. Either the hash can be encrypted using a cipher like AES, or the secret key can be included in the hash using a keyed hash algorithm like HMAC.
This is not as easy as it sounds. The key has to be kept secret from an attacker even in the event of a breach. If an attacker gains full access to the system, they'll be able to steal the key no matter where it is stored. The key must be stored in an external system, such as a physically separate server dedicated to password validation, or a special hardware device attached to the server such as the YubiHSM.
I highly recommend this approach for any large scale (more than 100,000 users) service. I consider it necessary for any service hosting more than 1,000,000 user accounts.
If you can't afford multiple dedicated servers or special hardware devices, you can still get some of the benefits of keyed hashes on a standard web server. Most databases are breached using SQL Injection Attacks, which, in most cases, don't give attackers access to the local filesystem (disable local filesystem access in your SQL server if it has this feature). If you generate a random key and store it in a file that isn't accessible from the web, and include it into the salted hashes, then the hashes won't be vulnerable if your database is breached using a simple SQL injection attack. Don't hard-code a key into the source code, generate it randomly when the application is installed. This isn't as secure as using a separate system to do the password hashing, because if there are SQL injection vulnerabilities in a web application, there are probably other types, such as Local File Inclusion, that an attacker could use to read the secret key file. But, it's better than nothing.
Please note that keyed hashes do not remove the need for salt. Clever attackers will eventually find ways to compromise the keys, so it is important that hashes are still protected by salt and key stretching.
Password hashing protects passwords in the event of a security breach. It does not make the application as a whole more secure. Much more must be done to prevent the password hashes (and other user data) from being stolen in the first place.
Even experienced developers must be educated in security in order to write secure applications. A great resource for learning about web application vulnerabilities is The Open Web Application Security Project (OWASP). A good introduction is the OWASP Top Ten Vulnerability List. Unless you understand all the vulnerabilities on the list, do not attempt to write a web application that deals with sensitive data. It is the employer's responsibility to ensure all developers are adequately trained in secure application development.
Having a third party "penetration test" your application is a good idea. Even the best programmers make mistakes, so it always makes sense to have a security expert review the code for potential vulnerabilities. Find a trustworthy organization (or hire staff) to review your code on a regular basis. The security review process should begin early in an application's life and continue throughout its development.
It is also important to monitor your website to detect a breach if one does occur. I recommend hiring at least one person whose full time job is detecting and responding to security breaches. If a breach goes undetected, the attacker can make your website infect visitors with malware, so it is extremely important that breaches are detected and responded to promptly.
DO use:
DO NOT use:
Even though there are no cryptographic attacks on MD5 or SHA1 that make their hashes easier to crack, they are old and are widely considered (somewhat incorrectly) to be inadequate for password storage. So I don't recommend using them. An exception to this rule is PBKDF2, which is frequently implemented using SHA1 as the underlying hash function.
It is my personal opinion that all password reset mechanisms in widespread use today are insecure. If you have high security requirements, such as an encryption service would, do not let the user reset their password.
Most websites use an email loop to authenticate users who have forgotten their password. To do this, generate a random single-use token that is strongly tied to the account. Include it in a password reset link sent to the user's email address. When the user clicks a password reset link containing a valid token, prompt them for a new password. Be sure that the token is strongly tied to the user account so that an attacker can't use a token sent to his own email address to reset a different user's password.
The token must be set to expire in 15 minutes or after it is used, whichever comes first. It is also a good idea to expire any existing password tokens when the user logs in (they remembered their password) or requests another reset token. If a token doesn't expire, it can be forever used to break into the user's account. Email (SMTP) is a plain-text protocol, and there may be malicious routers on the internet recording email traffic. And, a user's email account (including the reset link) may be compromised long after their password has been changed. Making the token expire as soon as possible reduces the user's exposure to these attacks.
Attackers will be able to modify the tokens, so don't store the user account information or timeout information in them. They should be an unpredictable random binary blob used only to identify a record in a database table.
Never send the user a new password over email. Remember to pick a new random salt when the user resets their password. Don't re-use the one that was used to hash their old password.
Your first priority is to determine how the system was compromised and patch the vulnerability the attacker used to get in. If you do not have experience responding to breaches, I highly recommend hiring a third-party security firm.
It may be tempting to cover up the breach and hope nobody notices. However, trying to cover up a breach makes you look worse, because you're putting your users at further risk by not informing them that their passwords and other personal information may be compromised. You must inform your users as soon as possible—even if you don't yet fully understand what happened. Put a notice on the front page of your website that links to a page with more detailed information, and send a notice to each user by email if possible.
Explain to your users exactly how their passwords were protected—hopefully hashed with salt—and that even though they were protected with a salted hash, a malicious hacker can still run dictionary and brute force attacks on the hashes. Malicious hackers will use any passwords they find to try to login to a user's account on a different website, hoping they used the same password on both websites. Inform your users of this risk and recommend that they change their password on any website or service where they used a similar password. Force them to change their password for your service the next time they log in. Most users will try to "change" their password to the original password to get around the forced change quickly. Use the current password hash to ensure that they cannot do this.
It is likely, even with salted slow hashes, that an attacker will be able to crack some of the weak passwords very quickly. To reduce the attacker's window of opportunity to use these passwords, you should require, in addition to the current password, an email loop for authentication until the user has changed their password. See the previous question, "How should I allow users to reset their password when they forget it?" for tips on implementing email loop authentication.
Also tell your users what kind of personal information was stored on the website. If your database includes credit card numbers, you should instruct your users to look over their recent and future bills closely and cancel their credit card.
If your service doesn't have strict security requirements, then don't limit your users. I recommend showing users information about the strength of their password as they type it, letting them decide how secure they want their password to be. If you have special security needs, enforce a minimum length of 12 characters and require at least two letters, two digits, and two symbols.
Do not force your users to change their password more often than once every six months, as doing so creates "user fatigue" and makes users less likely to choose good passwords. Instead, train users to change their password whenever they feel it has been compromised, and to never tell their password to anyone. If it is a business setting, encourage employees to use paid time to memorize and practice their password.
Yes, but if someone has accesss to your database, they probably already have access to everything on your server, so they wouldn't need to login to your account to get what they want. The purpose of password hashing (in the context of a website) is not to protect the website from being breached, but to protect the passwords if a breach does occur.
You can prevent hashes from being replaced during a SQL injection attack by connecting to the database with two users with different permissions. One for the 'create account' code and one for the 'login' code. The 'create account' code should be able to read and write to the user table, but the 'login' code should only be able to read.
Hash functions like MD5, SHA1, and SHA2 use the Merkle–Damgård construction, which makes them vulnerable to what are known as length extension attacks. This means that given a hash H(X), an attacker can find the value of H(pad(X) + Y), for any other string Y, without knowing X. pad(X) is the padding function used by the hash.
This means that given a hash H(key + message), an attacker can compute H(pad(key + message) + extension), without knowing the key. If the hash was being used as a message authentication code, using the key to prevent an attacker from being able to modify the message and replace it with a different valid hash, the system has failed, since the attacker now has a valid hash of message + extension.
It is not clear how an attacker could use this attack to crack a password hash quicker. However, because of the attack, it is considered bad practice to use a plain hash function for keyed hashing. A clever cryptographer may one day come up with a clever way to use these attacks to make cracking faster, so use HMAC.
It doesn't matter, but pick one and stick with it for interoperability's sake. Having the salt come before the password seems to be more common.
Comparing the hashes in "length-constant" time ensures that an attacker cannot extract the hash of a password in an on-line system using a timing attack, then crack it off-line.
The standard way to check if two sequences of bytes (strings) are the same is to compare the first byte, then the second, then the third, and so on. As soon as you find a byte that isn't the same for both strings, you know they are different and can return a negative response immediately. If you make it through both strings without finding any bytes that differ, you know the strings are the same and can return a positive result. This means that comparing two strings can take a different amount of time depending on how much of the strings match.
For example, a standard comparison of the strings "xyzabc" and "abcxyz" would immediately see that the first character is different and wouldn't bother to check the rest of the string. On the other hand, when the strings "aaaaaaaaaaB" and "aaaaaaaaaaZ" are compared, the comparison algorithm scans through the block of "a" before it determins the strings are unequal.
Suppose an attacker wants to break into an on-line system that rate limits authentication attempts to one attempt per second. Also suppose the attacker knows all of the parameters to the password hash (salt, hash type, etc), except for the hash and (obviously) the password. If the attacker can get a precisise measurement of how long it takes the on-line system to compare the hash of the real password with the hash of a password the attacker provides, he can use the timing attack to extract part of the hash and crack it using an offline attack, bypassing the system's rate limiting.
First, the attacker finds 256 strings whose hashes begin with every possible byte. He sends each string to the on-line system, recording the amount of time it takes the system to respond. The string that takes the longest will be the one whose hash's first byte matches the real hash's first byte. The attacker now knows the first byte, and can continue the attack in a similar manner on the second byte, then the third, and so on. Once the attacker knows enough of the hash, he can use his own hardware to crack it, without being rate limited by the system.
It might seem like it would be impossible to run a timing attack over a network. However, it has been done, and has been shown to be practical. That's why the code on this page compares strings in a way that takes the same amount of time no matter how much of the strings match.
The previous question explains why SlowEquals is necessary, this one explains how the code actually works.
The code uses the XOR "^" operator to compare integers for equality, instead of the "==" operator. The reason why is explained below. The result of XORing two integers will be zero if and only if they are exactly the same. This is because 0 XOR 0 = 0, 1 XOR 1 = 0, 0 XOR 1 = 1, 1 XOR 0 = 1. If we apply that to all the bits in both integers, the result will be zero only if all the bits matched.
So, in the first line, if a.length
is equal to b.length
, the diff variable will get a zero value, but if not, it will get some non-zero value. Next, we compare the bytes using XOR, and OR the result into diff. This will set diff to a non-zero value if the bytes differ. Because ORing never un-sets bits, the only way diff will be zero at the end of the loop is if it was zero before the loop began (a.length == b.length) and all of the bytes in the two arrays match (none of the XORs resulted in a non-zero value).
The reason we need to use XOR instead of the "==" operator to compare integers is that "==" is usually translated/compiled/interpreted as a branch. For example, the C code "diff &= a == b
" might compile to the following x86 assembly:
The branching makes the code execute in a different amount of time depending on the equality of the integers and the CPU's internal branch prediction state.
The C code "diff |= a ^ b
" should compile to something like the following, whose execution time does not depend on the equality of the integers:
Your users are entering their password into your website. They are trusting you with their security. If your database gets hacked, and your users' passwords are unprotected, then malicious hackers can use those passwords to compromise your users' accounts on other websites and services (most people use the same password everywhere). It's not just your security that's at risk, it's your users'. You are responsible for your users' security.
PHP Source Code | Java Source Code | ASP.NET (C#) Source Code | Ruby (on Rails) Source Code |
The following code is a secure implementation of PBKDF2 hashing in PHP. You can find a test suite and benchmark code for it on Defuse Security's PBKDF2 for PHP page.
If you need compatible PHP and C# implementations, see here.
<?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)); } ?>
PHP Source Code | Java Source Code | ASP.NET (C#) Source Code | Ruby (on Rails) Source Code |
The following code is a secure implementation of PBKDF2 hashing in 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); } } }
PHP Source Code | Java Source Code | ASP.NET (C#) Source Code | Ruby (on Rails) Source Code |
The following code is a secure implementation of salted hashing in C# for ASP.NET. It is in the
If you need compatible PHP and C# implementations, see here.
/*
* 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); } } }
PHP Source Code | Java Source Code | ASP.NET (C#) Source Code | Ruby (on Rails) Source Code |
The following is a secure implementation of salted PBKDF2 password hashing in Ruby. The code is
# 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