如果你是Web開發者,你很可能需要開發一個用戶賬戶系統。這個系統最重要的方面,就是怎樣保護用戶的密碼。存放帳號的數據庫經常成為入侵的目標,所以你必須做點什么來保護密碼,以防網站被攻破時發生危險。最好的辦法就是對密碼進行加鹽哈希,這篇文章將介紹它是如何做到這點。
在對密碼進行哈希加密的問題上,人們有許多爭論和誤解,這大概是由於網絡上廣泛的誤傳吧。密碼哈希是一件非常簡單的事情,但是依然有很多人理解錯誤了。本文闡述的並不是進行密碼哈希唯一正確的方法,但是會告訴你為什么這樣是正確的。
鄭重警告:如果你在試圖編寫自己的密碼哈希代碼,趕緊停下來!那太容易搞砸了。即使你受過密碼學的高等教育,也應該聽從這個警告。這是對所有人說的:不要自己寫加密函數!安全存儲密碼的難題現在已經被解決了,請使用phpass或者本文給出的一些源代碼。
如果因為某些原因你忽視了上面那個紅色警告,請翻回去好好讀一遍,我是認真的。這篇文章的目的不是教你研究出自己的安全算法,而是講解為什么密碼應該被這樣儲存。
為什么密碼需要進行哈希?
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直譯是古怪的哈希函數,大概是由於作者不認可這種組合多種哈希函數的做法,為了便於理解,本文還是翻譯為組合哈希函數)
這節講述了另一種對密碼哈希的誤解:使用組合哈希函數。人們經常不由自主地認為將不同的哈希函數組合起來,結果會更加安全。實際上這樣做幾乎沒有好處,僅僅造成了函數之間互相影響的問題,甚至有時候會變得更加不安全。永遠不要嘗試發明自己的加密方法,只需只用已經被設計好的標准算法。有的人會說使用多種哈希函數會使計算更慢,從而破解也更慢,但是還有其他的辦法能更好地減緩破解速度,后面會提到的。
這里有些低端的組合哈希函數,我在網上某些論壇看到它們被推薦使用:
- md5(sha1(password))
- md5(md5(salt) + md5(password))
- sha1(sha1(password))
- sha1(str_rot13(password + salt))
- md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))
不要使用其中任何一種。
注意:這節內容是有爭議的。我已經收到的大量的郵件,為組合哈希函數而辯護。他們的理由是如果攻擊者不知道系統使用的哪種哈希函數,那么也就很難預先為這種組合構造出彩虹表,於是破解起來會花費更多的時間。
誠然,攻擊者在不知道加密算法的時候是無法發動攻擊的,但是不要忘了Kerckhoffs’s principle,攻擊者通常很容易就能拿到源碼(尤其是那些免費或開源的軟件)。通過系統中取出的一些密碼-哈希值對應關系,很容易反向推導出加密算法。破解組合哈希函數確實需要更多時間,但也只是受了一點可以確知的因素影響。更好的辦法是使用一個很難被並行計算出結果的迭代算法,然后增加適當的鹽值防止彩虹表攻擊。
當然你實在想用“標准的”組合哈希函數,比如HMAC,也是可以的。但如果只是為了使破解起來更慢,那么先讀讀下面講到的密鑰擴展。
創造新的哈希函數可能帶來安全問題,構造哈希函數的組合又可能帶來函數間互相影響的問題,它們帶來的一丁點好處和這些比起來真是微不足道。顯然最好的做法是使用標准的、經過完整測試的算法。
哈希碰撞
哈希函數將任意大小的數據轉化為定長的字符串,因此其中一定有些輸入經過哈希計算之后得到了相同的結果。加密哈希函數的設計就是為了使這樣的碰撞盡可能難以被發現。隨着時間流逝,密碼學家發現攻擊者越來越容易找到碰撞了,最近的例子就是MD5算法的碰撞已經確定被發現了。
碰撞攻擊的出現表明很可能有一個和用戶密碼不同的字符串卻和它有着相同的哈希值。然而,即使在MD5這樣脆弱的哈希函數中找到碰撞也需要耗費大量的計算,因此這樣的碰撞“意外地”在實際中出現的可能性是很低的。於是站在實用性的角度上可以這么說,加鹽MD5和加鹽SHA256的安全性是一樣的。不過可能的話,使用本身更安全的哈希函數總是好的,比如SHA256、SHA512、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 |
對於每個用戶的每個密碼,鹽值都應該是獨一無二的。每當有新用戶注冊或者修改密碼,都應該使用新的鹽值進行加密。並且這個鹽值也應該足夠長,使得有足夠多的鹽值以供加密。一個好的標准的是:鹽值至少和哈希函數的輸出一樣長;鹽值應該被儲存和密碼哈希一起儲存在賬戶數據表中。
存儲密碼的步驟
- 使用CSPRNG生成一個長度足夠的鹽值
- 將鹽值混入密碼,並使用標准的加密哈希函數進行加密,如SHA256
- 把哈希值和鹽值一起存入數據庫中對應此用戶的那條記錄
校驗密碼的步驟
- 從數據庫取出用戶的密碼哈希值和對應鹽值
- 將鹽值混入用戶輸入的密碼,並且使用同樣的哈希函數進行加密
- 比較上一步的結果和數據庫儲存的哈希值是否相同,如果相同那么密碼正確,反之密碼錯誤
文章最后有幾個加鹽密碼哈希的代碼實現,分別使用了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程序。雇主也有責任保證他所有的開發人員都有資質編寫安全的程序。
對你的程序進行第三方“滲透測試”是一個不錯的選擇。最好的程序員也可能犯錯,因此有一個安全專家審查你的代碼尋找潛在的漏洞是有意義的。找尋值得信賴的機構(或招聘人員)來對你的代碼進行審查。安全審查應該從編碼的初期就着手進行,一直貫穿整個開發過程。
監控你的網站來發現入侵行為也是很重要的,我推薦至少雇佣一個人全職負責監測和處理安全隱患。如果有個漏洞沒被發現,攻擊者可能通過網站利用惡意軟件感染訪問者,因此檢測漏洞並且及時應對是十分重要的。
常見問題
我應該使用什么哈希算法?
應該使用:
- 本文末尾的PHP source code, Java source code, C# source code or the Ruby source code
- OpenWall的Portable PHP password hashing framework
- 任何先進的、被良好測試過的哈希加密算法,比如SHA256,SHA512,RipeMD,WHIRLPOOL,SHA3等等
- 設計良好的密鑰擴展算法,如PBKDF2,bcrypt,scrypt
- 安全的crypt()版本($2y$,$5$,$6$)
不要使用:
- 過時的函數,比如MD5或SHA1
- 不安全的crypt()版本($1$,$2$,$2x$,$3$)
- 任何你自己設計的加密算法。只應該使用那些在公開領域中的,並且被密碼學家完整測試過的技術
盡管還沒有一種針對MD5或SHA1非常效率的攻擊手段,但是它們太古老也被廣泛地認為不足以勝任存儲密碼的工作(某種程度上甚至是錯誤的),因此我也不推薦使用它們。但是有個例外,PBKDF2中頻繁地使用了SHA1作為它底層的哈希函數。
當用戶忘記密碼的時候,怎樣進行重置?
我個人的觀點是,當前所有廣泛使用的密碼重置機制都是不安全的。如果你對安全性有極高的要求,比如一個加密服務,那么不要允許用戶重置密碼。
大多數網站向那些忘記密碼的用戶發送電子郵件來進行身份認證。首先,需要隨機生成一個一次性的令牌,它直接關聯到用戶的賬戶。然后將這個令牌混入一個重置密碼的鏈接中,發送到用戶的電子郵箱。最后當用戶點擊這個包含有效令牌的鏈接時,提示他們可以設置新的密碼。要確保這個令牌只對一個賬戶有效,以防攻擊者從郵箱獲取到令牌后,用來重置其他用戶的密碼。
令牌必須在15分鍾內使用,並且一旦被使用就立即失效。當用戶重新請求令牌時,或用戶登錄成功時(說明他還記得密碼),使原令牌失效也是一個好做法。如果一個令牌始終不過期,那么它一直可以用於入侵用戶的帳號。電子郵件(SMTP)是一個純文本協議,並且網絡上有很多惡意路由在截取郵件信息。在用戶修改密碼后,那些包含重置密碼鏈接的郵件在很長一段時間內依然缺乏保護。因此應該盡早使令牌過期,降低把用戶信息暴露給攻擊者的可能。
攻擊者是可以篡改令牌的,所以不要把賬戶信息和失效時間存儲在里面。這些信息應該以不可猜解的二進制形式存在,並且只用來識別數據庫中某條用戶的記錄。
永遠不要通過電子郵件向用戶發送新密碼,同時也記得在用戶重置密碼的時候隨機生成一個新的鹽值用於加密,不要重復使用之前密碼的那個鹽值。
當賬戶數據庫被泄漏或入侵時,應該怎么做?
你首先需要做的,是查看系統被暴露到什么程度了,然后修復這個攻擊者利用的漏洞。如果你沒有應對入侵的經驗,我強烈推薦雇一個第三方安全機構來做這件事。
將一個漏洞精心掩蓋期待沒有人能注意到,是否聽起來很省事而又誘人呢?但是這樣只會讓你顯得更糟糕,因為你在用戶不知情的情況下,將他們的密碼和個人信息暴露在危險之中。即使用戶還無法理解到底發生了什么,你也應該盡快履行告知的義務。比如在首頁放置一個鏈接,指向對此問題更詳細的說明,可能的話還可以通過電子郵件告知用戶目前的情況。
向你的用戶說明你是如何保護他們的密碼的——最好是使用了加鹽哈希——即便如此惡意黑客也能使用字典攻擊和暴力攻擊。設想用戶可能在很多服務中使用相同的密碼,攻擊者會用找到的密碼去嘗試登錄其他網站。提示你的用戶應該修改所有相似的密碼,不論它們被使用在哪個服務上,並且強制用戶下次登錄你的網站時修改密碼。大部分用戶會嘗試將密碼“修改”為和之前相同的以便記憶,你應該使用老密碼的哈希值來確保用戶無法這么做。
即使有加鹽哈希的保護,攻擊者也很可能快速破解其中一些脆弱的密碼。為了減少攻擊者使用的它們機會,你應該對這些密碼的帳號發送認證電子郵件,直到用戶修改了密碼。可以參考上一個問題,其中有一些實現電子郵件認證的要點。
另外也要告訴你的用戶,網站到底儲存了哪些個人信息。如果你的數據庫中有用戶的信用卡號,你應該指導用戶檢查自己近期的賬單,並且注銷掉這張信用卡。
我應該使用什么樣的密碼規則?是否應該強制用戶使用復雜的密碼?
如果你的服務對安全性沒有嚴格的要求,那么不要對用戶進行限制。我推薦在用戶輸入密碼的時候,頁面上顯示出密碼強度,由用戶自己決定需要多安全的密碼。如果你的服務對安全有特殊的需求,那就應該強制用戶輸入長度至少為12個字符的密碼,並且其中至少包括兩個字母、兩個數字和兩個符號。
不要過於頻繁地強制你的用戶修改密碼,最多6個月1次,因為那樣做會使用戶疲於選擇一個強度足夠好的密碼。更好的做法是指導用戶在他們感覺密碼可能泄漏的時候去主動修改,並且提示用戶不要把密碼告訴任何人。如果這是在商業環境中,鼓勵你的員工利用工作時間熟記並使用他們的密碼。
如果攻擊者入侵了我的數據庫,他們難道不能把其中的密碼哈希替換為自己的值,然后登錄系統么?
當然可以,但是如果他已經入侵了你的數據庫,那么很可能已經有權限訪問你服務器上任何東西了,因此完全沒必要登錄賬戶去獲取他想要的。對密碼進行哈希加密的手段,(對網站而言)不是保護網站免受入侵,而是在入侵已經發生時保護數據庫中的密碼。
通過為數據庫連接設置兩種權限,可以防止密碼哈希在遭遇注入攻擊時被篡改。一種權限用於創建用戶:它對用戶表可讀可寫;另一種用於用戶登錄,它只能讀用戶表而不能寫。
為什么我非得用像HMAC那種特殊的算法?為什么不能簡單地把密鑰混入密碼?
像MD5、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個字符串,它們的哈希值的第一字節包含了所有可能的情況。然后用它們去系統中嘗試登錄,並記錄系統返回結果所消耗的時間,耗時最長的那個就是第一字節猜對的那個。接下來用同樣的方式猜測第二字節、第三字節等等。直到攻擊者獲取了最夠長的哈希值片段,最后只需在自己的機器上破解即可,完全不受在線系統的限制。
乍看之下在網絡上進行計時攻擊是不可能做到的,然而有人已經實現了,並運用到實際中了。因此本文提供的代碼才使用固定的時間去比較字符串,不論它們有多相似。
“慢比較”的代碼是如何工作的?
上一個問題解釋了為什么“慢比較”是有必要的,現在來講解一下代碼具體是怎么實現的。
1 private static boolean slowEquals(byte[] a, byte[] b) 2 { 3 int diff = a.length ^ b.length; 4 for(int i = 0; i < a.length && i < b.length; i++) 5 diff |= a[i] ^ b[i]; 6 return diff == 0; 7 }
代碼中使用了異或運算符“^”(XOR)來比較兩個整數是否相等,而不是“==”。當且僅當兩位相等時,異或的結果才是0。因為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機器成被編譯為如下匯編語言:
1 MOV EAX, [A] 2 CMP [B], EAX 3 JZ equal 4 JMP done 5 equal: 6 AND [VALID], 1 7 done: 8 AND [VALID], 0
其中的分支導致代碼運行的時間不固定,決定於兩個整數相等的程度和CPU內部的跳轉預測機制(branch prediction)。
而C語言代碼“diff |=a ^ b”會被編譯為下面的樣子,它執行的時間和兩個整數是什么樣的情況無關。
1 MOV EAX, [A] 2 XOR EAX, [B] 3 OR [DIFF], EAX
弄這么麻煩干嘛?
用戶在你的網站上輸入密碼,說明他們相信你會保障密碼的安全。如果你的數據庫被黑了,又沒有對用戶密碼加以保護,惡意黑客就可以使用這些密碼去入侵用戶在其他網站或服務的賬戶(大部分人會在各處使用相同的密碼)。這不僅僅關乎你網站的安全,更關系到用戶的。你需要對用戶的安全負責。
PHP PBKDF2 密碼哈希代碼
下面是PBKDF2在PHP中一種安全的實現,你也可以在這個頁面找到測試用例和基准測試的代碼。
如果你需要兼容的PHP和C#代碼,點擊這里。
1 <?php 2 /* 3 * Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm). 4 * Copyright (c) 2013, Taylor Hornby 5 * All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions are met: 9 * 10 * 1. Redistributions of source code must retain the above copyright notice, 11 * this list of conditions and the following disclaimer. 12 * 13 * 2. Redistributions in binary form must reproduce the above copyright notice, 14 * this list of conditions and the following disclaimer in the documentation 15 * and/or other materials provided with the distribution. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 * POSSIBILITY OF SUCH DAMAGE. 28 */ 29 30 // These constants may be changed without breaking existing hashes. 31 define("PBKDF2_HASH_ALGORITHM", "sha256"); 32 define("PBKDF2_ITERATIONS", 1000); 33 define("PBKDF2_SALT_BYTE_SIZE", 24); 34 define("PBKDF2_HASH_BYTE_SIZE", 24); 35 36 define("HASH_SECTIONS", 4); 37 define("HASH_ALGORITHM_INDEX", 0); 38 define("HASH_ITERATION_INDEX", 1); 39 define("HASH_SALT_INDEX", 2); 40 define("HASH_PBKDF2_INDEX", 3); 41 42 function create_hash($password) 43 { 44 // format: algorithm:iterations:salt:hash 45 $salt = base64_encode(mcrypt_create_iv(PBKDF2_SALT_BYTE_SIZE, MCRYPT_DEV_URANDOM)); 46 return PBKDF2_HASH_ALGORITHM . ":" . PBKDF2_ITERATIONS . ":" . $salt . ":" . 47 base64_encode(pbkdf2( 48 PBKDF2_HASH_ALGORITHM, 49 $password, 50 $salt, 51 PBKDF2_ITERATIONS, 52 PBKDF2_HASH_BYTE_SIZE, 53 true 54 )); 55 } 56 57 function validate_password($password, $correct_hash) 58 { 59 $params = explode(":", $correct_hash); 60 if(count($params) < HASH_SECTIONS) 61 return false; 62 $pbkdf2 = base64_decode($params[HASH_PBKDF2_INDEX]); 63 return slow_equals( 64 $pbkdf2, 65 pbkdf2( 66 $params[HASH_ALGORITHM_INDEX], 67 $password, 68 $params[HASH_SALT_INDEX], 69 (int)$params[HASH_ITERATION_INDEX], 70 strlen($pbkdf2), 71 true 72 ) 73 ); 74 } 75 76 // Compares two strings $a and $b in length-constant time. 77 function slow_equals($a, $b) 78 { 79 $diff = strlen($a) ^ strlen($b); 80 for($i = 0; $i < strlen($a) && $i < strlen($b); $i++) 81 { 82 $diff |= ord($a[$i]) ^ ord($b[$i]); 83 } 84 return $diff === 0; 85 } 86 87 /* 88 * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt 89 * $algorithm - The hash algorithm to use. Recommended: SHA256 90 * $password - The password. 91 * $salt - A salt that is unique to the password. 92 * $count - Iteration count. Higher is better, but slower. Recommended: At least 1000. 93 * $key_length - The length of the derived key in bytes. 94 * $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise. 95 * Returns: A $key_length-byte key derived from the password and salt. 96 * 97 * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt 98 * 99 * This implementation of PBKDF2 was originally created by https://defuse.ca 100 * With improvements by http://www.variations-of-shadow.com 101 */ 102 function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false) 103 { 104 $algorithm = strtolower($algorithm); 105 if(!in_array($algorithm, hash_algos(), true)) 106 trigger_error('PBKDF2 ERROR: Invalid hash algorithm.', E_USER_ERROR); 107 if($count <= 0 || $key_length <= 0) 108 trigger_error('PBKDF2 ERROR: Invalid parameters.', E_USER_ERROR); 109 110 if (function_exists("hash_pbkdf2")) { 111 // The output length is in NIBBLES (4-bits) if $raw_output is false! 112 if (!$raw_output) { 113 $key_length = $key_length * 2; 114 } 115 return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output); 116 } 117 118 $hash_length = strlen(hash($algorithm, "", true)); 119 $block_count = ceil($key_length / $hash_length); 120 121 $output = ""; 122 for($i = 1; $i <= $block_count; $i++) { 123 // $i encoded as 4 bytes, big endian. 124 $last = $salt . pack("N", $i); 125 // first iteration 126 $last = $xorsum = hash_hmac($algorithm, $last, $password, true); 127 // perform the other $count - 1 iterations 128 for ($j = 1; $j < $count; $j++) { 129 $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true)); 130 } 131 $output .= $xorsum; 132 } 133 134 if($raw_output) 135 return substr($output, 0, $key_length); 136 else 137 return bin2hex(substr($output, 0, $key_length)); 138 } 139 ?>
Java PBKDF2 密碼哈希代碼
下面是PBKDF2在Java中一種安全的實現。
1 /* 2 * Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm). 3 * Copyright (c) 2013, Taylor Hornby 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * 2. Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 * POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29 import java.security.SecureRandom; 30 import javax.crypto.spec.PBEKeySpec; 31 import javax.crypto.SecretKeyFactory; 32 import java.math.BigInteger; 33 import java.security.NoSuchAlgorithmException; 34 import java.security.spec.InvalidKeySpecException; 35 36 /* 37 * PBKDF2 salted password hashing. 38 * Author: havoc AT defuse.ca 39 * www: http://crackstation.net/hashing-security.htm 40 */ 41 public class PasswordHash 42 { 43 public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1"; 44 45 // The following constants may be changed without breaking existing hashes. 46 public static final int SALT_BYTE_SIZE = 24; 47 public static final int HASH_BYTE_SIZE = 24; 48 public static final int PBKDF2_ITERATIONS = 1000; 49 50 public static final int ITERATION_INDEX = 0; 51 public static final int SALT_INDEX = 1; 52 public static final int PBKDF2_INDEX = 2; 53 54 /** 55 * Returns a salted PBKDF2 hash of the password. 56 * 57 * @param password the password to hash 58 * @return a salted PBKDF2 hash of the password 59 */ 60 public static String createHash(String password) 61 throws NoSuchAlgorithmException, InvalidKeySpecException 62 { 63 return createHash(password.toCharArray()); 64 } 65 66 /** 67 * Returns a salted PBKDF2 hash of the password. 68 * 69 * @param password the password to hash 70 * @return a salted PBKDF2 hash of the password 71 */ 72 public static String createHash(char[] password) 73 throws NoSuchAlgorithmException, InvalidKeySpecException 74 { 75 // Generate a random salt 76 SecureRandom random = new SecureRandom(); 77 byte[] salt = new byte[SALT_BYTE_SIZE]; 78 random.nextBytes(salt); 79 80 // Hash the password 81 byte[] hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE); 82 // format iterations:salt:hash 83 return PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" + toHex(hash); 84 } 85 86 /** 87 * Validates a password using a hash. 88 * 89 * @param password the password to check 90 * @param correctHash the hash of the valid password 91 * @return true if the password is correct, false if not 92 */ 93 public static boolean validatePassword(String password, String correctHash) 94 throws NoSuchAlgorithmException, InvalidKeySpecException 95 { 96 return validatePassword(password.toCharArray(), correctHash); 97 } 98 99 /** 100 * Validates a password using a hash. 101 * 102 * @param password the password to check 103 * @param correctHash the hash of the valid password 104 * @return true if the password is correct, false if not 105 */ 106 public static boolean validatePassword(char[] password, String correctHash) 107 throws NoSuchAlgorithmException, InvalidKeySpecException 108 { 109 // Decode the hash into its parameters 110 String[] params = correctHash.split(":"); 111 int iterations = Integer.parseInt(params[ITERATION_INDEX]); 112 byte[] salt = fromHex(params[SALT_INDEX]); 113 byte[] hash = fromHex(params[PBKDF2_INDEX]); 114 // Compute the hash of the provided password, using the same salt, 115 // iteration count, and hash length 116 byte[] testHash = pbkdf2(password, salt, iterations, hash.length); 117 // Compare the hashes in constant time. The password is correct if 118 // both hashes match. 119 return slowEquals(hash, testHash); 120 } 121 122 /** 123 * Compares two byte arrays in length-constant time. This comparison method 124 * is used so that password hashes cannot be extracted from an on-line 125 * system using a timing attack and then attacked off-line. 126 * 127 * @param a the first byte array 128 * @param b the second byte array 129 * @return true if both byte arrays are the same, false if not 130 */ 131 private static boolean slowEquals(byte[] a, byte[] b) 132 { 133 int diff = a.length ^ b.length; 134 for(int i = 0; i < a.length && i < b.length; i++) 135 diff |= a[i] ^ b[i]; 136 return diff == 0; 137 } 138 139 /** 140 * Computes the PBKDF2 hash of a password. 141 * 142 * @param password the password to hash. 143 * @param salt the salt 144 * @param iterations the iteration count (slowness factor) 145 * @param bytes the length of the hash to compute in bytes 146 * @return the PBDKF2 hash of the password 147 */ 148 private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes) 149 throws NoSuchAlgorithmException, InvalidKeySpecException 150 { 151 PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8); 152 SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); 153 return skf.generateSecret(spec).getEncoded(); 154 } 155 156 /** 157 * Converts a string of hexadecimal characters into a byte array. 158 * 159 * @param hex the hex string 160 * @return the hex string decoded into a byte array 161 */ 162 private static byte[] fromHex(String hex) 163 { 164 byte[] binary = new byte[hex.length() / 2]; 165 for(int i = 0; i < binary.length; i++) 166 { 167 binary[i] = (byte)Integer.parseInt(hex.substring(2*i, 2*i+2), 16); 168 } 169 return binary; 170 } 171 172 /** 173 * Converts a byte array into a hexadecimal string. 174 * 175 * @param array the byte array to convert 176 * @return a length*2 character string encoding the byte array 177 */ 178 private static String toHex(byte[] array) 179 { 180 BigInteger bi = new BigInteger(1, array); 181 String hex = bi.toString(16); 182 int paddingLength = (array.length * 2) - hex.length(); 183 if(paddingLength > 0) 184 return String.format("%0" + paddingLength + "d", 0) + hex; 185 else 186 return hex; 187 } 188 189 /** 190 * Tests the basic functionality of the PasswordHash class 191 * 192 * @param args ignored 193 */ 194 public static void main(String[] args) 195 { 196 try 197 { 198 // Print out 10 hashes 199 for(int i = 0; i < 10; i++) 200 System.out.println(PasswordHash.createHash("p\r\nassw0Rd!")); 201 202 // Test password validation 203 boolean failure = false; 204 System.out.println("Running tests..."); 205 for(int i = 0; i < 100; i++) 206 { 207 String password = ""+i; 208 String hash = createHash(password); 209 String secondHash = createHash(password); 210 if(hash.equals(secondHash)) { 211 System.out.println("FAILURE: TWO HASHES ARE EQUAL!"); 212 failure = true; 213 } 214 String wrongPassword = ""+(i+1); 215 if(validatePassword(wrongPassword, hash)) { 216 System.out.println("FAILURE: WRONG PASSWORD ACCEPTED!"); 217 failure = true; 218 } 219 if(!validatePassword(password, hash)) { 220 System.out.println("FAILURE: GOOD PASSWORD NOT ACCEPTED!"); 221 failure = true; 222 } 223 } 224 if(failure) 225 System.out.println("TESTS FAILED!"); 226 else 227 System.out.println("TESTS PASSED!"); 228 } 229 catch(Exception ex) 230 { 231 System.out.println("ERROR: " + ex); 232 } 233 } 234 235 }
ASP.NET(C#) PBKDF2 密碼哈希代碼
下面是PBKDF2在ASP.NET(C#)中一種安全的實現。
1 /* 2 * Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm). 3 * Copyright (c) 2013, Taylor Hornby 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * 2. Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 * POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29 using System; 30 using System.Text; 31 using System.Security.Cryptography; 32 33 namespace PasswordHash 34 { 35 /// <summary> 36 /// Salted password hashing with PBKDF2-SHA1. 37 /// Author: havoc AT defuse.ca 38 /// www: http://crackstation.net/hashing-security.htm 39 /// Compatibility: .NET 3.0 and later. 40 /// </summary> 41 public class PasswordHash 42 { 43 // The following constants may be changed without breaking existing hashes. 44 public const int SALT_BYTE_SIZE = 24; 45 public const int HASH_BYTE_SIZE = 24; 46 public const int PBKDF2_ITERATIONS = 1000; 47 48 public const int ITERATION_INDEX = 0; 49 public const int SALT_INDEX = 1; 50 public const int PBKDF2_INDEX = 2; 51 52 /// <summary> 53 /// Creates a salted PBKDF2 hash of the password. 54 /// </summary> 55 /// <param name="password">The password to hash.</param> 56 /// <returns>The hash of the password.</returns> 57 public static string CreateHash(string password) 58 { 59 // Generate a random salt 60 RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider(); 61 byte[] salt = new byte[SALT_BYTE_SIZE]; 62 csprng.GetBytes(salt); 63 64 // Hash the password and encode the parameters 65 byte[] hash = PBKDF2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE); 66 return PBKDF2_ITERATIONS + ":" + 67 Convert.ToBase64String(salt) + ":" + 68 Convert.ToBase64String(hash); 69 } 70 71 /// <summary> 72 /// Validates a password given a hash of the correct one. 73 /// </summary> 74 /// <param name="password">The password to check.</param> 75 /// <param name="correctHash">A hash of the correct password.</param> 76 /// <returns>True if the password is correct. False otherwise.</returns> 77 public static bool ValidatePassword(string password, string correctHash) 78 { 79 // Extract the parameters from the hash 80 char[] delimiter = { ':' }; 81 string[] split = correctHash.Split(delimiter); 82 int iterations = Int32.Parse(split[ITERATION_INDEX]); 83 byte[] salt = Convert.FromBase64String(split[SALT_INDEX]); 84 byte[] hash = Convert.FromBase64String(split[PBKDF2_INDEX]); 85 86 byte[] testHash = PBKDF2(password, salt, iterations, hash.Length); 87 return SlowEquals(hash, testHash); 88 } 89 90 /// <summary> 91 /// Compares two byte arrays in length-constant time. This comparison 92 /// method is used so that password hashes cannot be extracted from 93 /// on-line systems using a timing attack and then attacked off-line. 94 /// </summary> 95 /// <param name="a">The first byte array.</param> 96 /// <param name="b">The second byte array.</param> 97 /// <returns>True if both byte arrays are equal. False otherwise.</returns> 98 private static bool SlowEquals(byte[] a, byte[] b) 99 { 100 uint diff = (uint)a.Length ^ (uint)b.Length; 101 for (int i = 0; i < a.Length && i < b.Length; i++) 102 diff |= (uint)(a[i] ^ b[i]); 103 return diff == 0; 104 } 105 106 /// <summary> 107 /// Computes the PBKDF2-SHA1 hash of a password. 108 /// </summary> 109 /// <param name="password">The password to hash.</param> 110 /// <param name="salt">The salt.</param> 111 /// <param name="iterations">The PBKDF2 iteration count.</param> 112 /// <param name="outputBytes">The length of the hash to generate, in bytes.</param> 113 /// <returns>A hash of the password.</returns> 114 private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes) 115 { 116 Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt); 117 pbkdf2.IterationCount = iterations; 118 return pbkdf2.GetBytes(outputBytes); 119 } 120 } 121 }
Ruby(on Rails) PBKDF2 密碼哈希代碼
下面是PBKDF2在Ruby(on Rails)中一種安全的實現。
# 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編寫。
轉載自:加鹽密碼哈希:如何正確使用