如何存儲用戶的密碼才能算安全?
緣起
前段時間將一個集成了 spring-security-oauth2 的舊項目改造了一番,將 springboot 升級成了 springboot 2.0,眾所周知 springboot 2.0 依賴的是 spring5,並且許多相關的依賴都發生了較大的改動,與本文相關的改動羅列如下,有興趣的同學可以參考:Spring Security 5.0 New Features 。其中一個比較有意思的改動---- Spring Security 5.0 重構了密碼編碼器的實現(Password Encoding,由於大多數 PasswordEncoder 相關的演算法是 hash 演算法,所以本文將 PasswordEncoder 翻譯成『密碼編碼器』和並非『密碼加密器』),引起了我興趣官方稱之為
Modernized Password Encoding — 現代化的密碼編碼方式
一開始,我僅僅修改了依賴,將
升級成了
不出意料出現了兼容性的問題,我在嘗試登陸時,出現了如下的報錯
原因也很明顯,正如 spring security 的更新文檔中描述的那樣,spring security 5 對 PasswordEncoder 做了相關的重構,原先默認配置的 PlainTextPasswordEncoder(明文密碼)被移除了。隨即我產生了疑問,spring security 在新版本中對於 passwordEncoder 進行了哪些改造,這些改造背後又是出於什麼樣的目的呢?賣個關子,先從遠古時期的案例來一步步演化出所謂的「現代化密碼編碼方式」。
密碼存儲演進史
自從互聯網有了用戶的那一刻起,存儲用戶密碼這件事便成為了一個健全的系統不得不面對的一件事。遠古時期,明文存儲密碼可能還不被認為是一個很大的系統缺陷(事實上這是一件很恐怖的事)。提及明文存儲密碼,我立刻聯想到的是 CSDN 社區在 2011 年末發生的 600 萬用戶密碼泄露的事件,誰也不會想到這個和程序員密切相關的網站會犯如此低級的錯誤。明文存儲密碼使得惡意用戶可以通過 sql 注入等攻擊方式來獲取用戶名和密碼,雖然安全框架和良好的編碼規範可以規避很多類似的攻擊,但依舊避免不了系統管理員,DBA 有途徑獲取用戶密碼這一事實。事實上,不用明文存儲存儲密碼,程序員們早在 n 多年前就已經達成了共識。
不能明文存儲,一些 hash 演算法便被廣泛用做密碼的編碼器,對密碼進行單向 hash 處理後存儲資料庫,當用戶登錄時,計算用戶輸入的密碼的 hash 值,將兩者進行比對。單向 hash 演算法,顧名思義,它無法(或者用不能輕易更為合適)被反向解析還原出原密碼。這杜絕了管理員直接獲取密碼的途徑,可僅僅依賴於普通的 hash 演算法(如 md5,sha256)是不合適的,他主要有 3 個特點:
同一密碼生成的 hash 值一定相同
不同密碼的生成的 hash 值可能相同(md5 的碰撞問題相比 sha256 還要嚴重)
計算速度快。
以上三點結合在一起,破解此類演算法成了不是那麼困難的一件事,尤其是第三點,會在下文中再次提到,多快才算非常快?按照相關資料的說法:
modern hardware perform billions of hash calculations a second.
考慮到大多數用戶使用的密碼多為數字+字母+特殊符號的組合,攻擊者將常用的密碼進行枚舉,甚至通過排列組合來暴力破解,這被稱為 rainbow table。演算法愛好者能夠立刻看懂到上述的方案,這被親切地稱之為—打表,一種暴力美學,這張表是可以被複用的。
雖然僅僅依賴於傳統 hash 演算法的思路被否決了,但這種 hash 後比對的思路,幾乎被後續所有的優化方案繼承。
hash 方案迎來的第一個改造是對引入一個「隨機的因子」來摻雜進明文中進行 hash 計算,這樣的隨機因子通常被稱之為鹽 (salt)。salt 一般是用戶相關的,每個用戶持有各自的 salt。此時狗蛋和二丫的密碼即使相同,由於 salt 的影響,存儲在資料庫中的密碼也是不同的,除非…為每個用戶單獨建議一張 rainbow table。很明顯 salted hash 相比普通的單向 hash 方案加大了 hacker 攻擊的難度。但了解過 GPU 並行計算能力之強大的童鞋,都能夠意識到,雖然破解 salted hash 比較麻煩,卻並非不可行,勤勞勇敢的安全專家似乎也對這個方案不夠滿意。
為解決上述 salted hash 仍然存在的問題,一些新型的單向 hash 演算法被研究了出來。其中就包括:Bcrypt,PBKDF2,Scrypt,Argon2。為什麼這些 hash 演算法能保證密碼存儲的安全性?因為他們足夠慢,恰到好處的慢。這麼說不嚴謹,只是為了給大家留個深刻的映像:慢。這類演算法有一個特點,存在一個影響因子,可以用來控制計算強度,這直接決定了破解密碼所需要的資源和時間,直觀的體會可以見下圖,在一年內破解如下演算法所需要的硬體資源花費(折算成美元)
一年內破解如下演算法所需要的硬體資源花費
這使得破解成了一件極其困難的事,並且,其中的計算強度因子是可控的,這樣,即使未來量子計算機的計算能力爆表,也可以通過其控制計算強度以防破解。注意,普通的驗證過程只需要計算一次 hash 計算,使用此類 hash 演算法並不會影響到用戶體驗。
慢 hash 演算法真的安全嗎?
Bcrypt,Scrypt,PBKDF2 這些慢 hash 演算法是目前最為推崇的 password encoding 方式,好奇心驅使我思考了這樣一個問題:慢 hash 演算法真的安全嗎?
我暫時還沒有精力仔細去研究他們中每一個演算法的具體實現,只能通過一些文章來拾人牙慧,簡單看看這幾個演算法的原理和安全性。
PBKDF2 被設計的很簡單,它的基本原理是通過一個偽隨機函數(例如 HMAC 函數),把明文和一個鹽值作為輸入參數,然後按照設置的計算強度因子重複進行運算,並最終產生密鑰。這樣的重複 hash 已經被認為足夠安全,但也有人提出了不同意見,此類演算法對於傳統的 CPU 來說的確是足夠安全,但 GPU 被搬了出來,前文提到過 GPU 的並行計算能力非常強大。
Bcrypt 強大的一點在於,其不僅僅是 CPU 密集型,還是 RAM 密集型!雙重的限制因素,導致 GPU,ASIC(專用集成電路)無法應對 Bcrypt 帶來的破解困境。
然後…看了 Scrypt 的相關資料之後我才意識到這個坑有多深。一個熟悉又陌生的詞出現在了我面前:FPGA(現場可編程邏輯門陣列),這貨就比較厲害了。現成的晶元指令結構如傳統的 CPU,GPU,ASIC 都無法破解 Bcrypt,但是 FPGA 支持燒錄邏輯門(如AND、OR、XOR、NOT),通過編程的方式燒錄指令集的這一特性使得可以定製硬體來破解 Bcrypt。儘管我不認為懂這個技術的人會去想辦法破解真正的系統,但,只要這是一個可能性,就總有方法會被發明出來與之對抗。Scrypt 比 Bcrypt 額外考慮到的就是大規模的自定義硬體攻擊 ,從而刻意設計需要大量內存運算。
理論終歸是理論,實際上 Bcrypt 演算法被發明至今 18 年,使用範圍廣,且從未因為安全問題而被修改,其有限性是已經被驗證過的,相比之下 Scrypt 據我看到的文章顯示是 9 年的歷史,沒有 Bcrypt 使用的廣泛。從破解成本和權威性的角度來看,Bcrypt 用作密碼編碼器是不錯的選擇。
spring security 廢棄的介面
回到文檔中,spring security 5 對 PasswordEncoder 做了相關的重構,原先默認配置的 PlainTextPasswordEncoder(明文密碼)被移除了,想要做到明文存儲密碼,只能使用一個過期的類來過渡
實際上,spring security 提供了 BCryptPasswordEncoder 來進行密碼編碼,並作為了相關配置的默認配置,只不過沒有暴露為全局的 Bean。使用明文存儲的風險在文章一開始就已經強調過,NoOpPasswordEncoder 只能存在於 demo 中。
別忘了對你資料庫中的密碼進行同樣的編碼,否則無法對應。
更深層的思考
實際上,spring security 5 的另一個設計是促使我寫成本文的初衷。
不知道有沒有讀者產生跟我相同的困擾:
如果我要設計一個 QPS 很高的登錄系統,使用 spring security 推薦的 BCrypt 會不會存在性能問題?
spring security 怎麼這麼坑,原來的密碼編碼器都給改了,我需要怎麼遷移舊密碼編碼的應用程序?
萬一以後出了更高效的加密演算法,這種笨重的硬編碼方式配置密碼編碼器是不是不夠靈活?
在 spring security 5 提供了這樣一個思路,應該將密碼編碼之後的 hash 值和加密方式一起存儲,並提供了一個 DelegatingPasswordEncoder 來作為眾多密碼密碼編碼方式的集合。
負責生產 DelegatingPasswordEncoder 的工廠方法:
如此注入 PasswordEncoder 之後,我們在資料庫中需要這麼存儲數據:
還記得文章開始的報錯嗎?
這個 id 就是因為我們沒有為資料庫中的密碼添加 此類的前綴導致的。
你會不會擔心密碼泄露後,,,, 此類前綴會直接暴露密碼的編碼方式?其實這個考慮是多餘的,因為密碼存儲的依賴演算法並不是一個秘密。大多數能搞到你密碼的 hacker 都可以輕鬆的知道你用的是什麼演算法,例如,bcrypt 演算法通常以
2a
開頭
稍微思考下,前面的三個疑問就可以迎刃而解,這就是文檔中所謂的:能夠自適應伺服器性能的現代化密碼編碼方案。
參考
Password Hashing: PBKDF2, Scrypt, Bcrypt
core-services-password-encoding
show me the code
spring security oauth2 的 github 代碼示例,體會下 spring security 4 -> spring security 5 的相關變化。
https://github.com/lexburner/oauth2-demo
TAG:Kirito的技術分享 |