為什麼這麼設計(Why's THE Design)是一系列關於計算機領域中程序設計決策的文章,我們在這個系列的每一篇文章中都會提出一個具體的問題並從不同的角度討論這種設計的優缺點、對具體實現造成的影響。如果你有想要了解的問題,可以在文章下面留言。
很多軟體工程師都認為 MD5 是一種加密算法,然而這種觀點其實是大錯特錯並且十分危險的,作為一個 1992 年第一次被公開的算法,到今天為止已經被發現了一些致命的漏洞,我們在生產環境的任何場景都不應該繼續使用 MD5 算法,無論是對數據或者文件的內容進行校驗還是用於所謂的『加密』。
這篇文章的主要目的是幫助讀者理解 MD5 到底是什麼,為什麼我們不應該繼續使用它,尤其是不應該使用它在資料庫中存儲密碼,作者也希望使用過 MD5 或者明文存儲密碼的開發者們能夠找到更加合理和安全的方式對用戶的這些機密信息進行存儲(這樣也可以間接提高我在各類網站中存儲密碼的安全性)。
概述與『為什麼我們不能使用 MD5 來存儲密碼?』這一問題相似的其實還有『為什麼我們不能使用明文來存儲密碼?』,使用明文來存儲密碼是一種看起來就不可行的方案,除非我們能夠 100% 保證資料庫中的密碼欄位不會被任何人訪問到,不僅包括潛在的攻擊者,還包括系統的開發者和管理員。
不過這是一個非常理想的情況,在實際的生產環境中,我們不能抵禦來自黑客的所有攻擊,甚至也不能完全阻擋開發者和管理員的訪問,因為我們總需要信任並授權一些人或者程序具有當前資料庫的所有訪問權限,這也就給攻擊者留下了可以利用的漏洞,在抵禦外部攻擊時我們沒有辦法做到全面,只能儘可能提高攻擊者的成本,這也就是使用 MD5 或者其他方式存儲密碼的原因了。
很多開發者對於 MD5 的作用和定義都有著非常大的誤解,MD5 並不是一種加密算法,而是一種摘要算法,我們也可以叫它哈希函數,哈希函數可以將無限鍵值空間中的所有鍵都均勻地映射到一個指定大小的鍵值空間中;一個好的摘要算法能夠幫助我們保證文件的完整性,避免攻擊者的惡意篡改,但是加密算法或者加密的功能是 —— 通過某種特定的方式來編碼消息或者信息,只有授權方可以訪問原始數據,而沒有被授權的人無法從密文中獲取原文。
由於加密需要同時保證消息的秘密性和完整性,所以加密的過程使用一系列的算法,MD5 確實可以在加密的過程中作為哈希函數使用來保證消息的完整性,但是我們還需要另一個算法來保證消息的秘密性,所以由於 MD5 哈希的信息無法被還原,只依靠 MD5 是無法完成加密的。
在任何場景下,我們都應該避免 MD5 的使用,可以選擇更好的摘要算法替代 MD5,例如 SHA256、SHA512。
聊了這麼多對於 MD5 的誤解,我們重新回到今天最開始的題目,『為什麼 MD5 不能用於存儲密碼』,對於這個問題有一個最簡單的答案,也就是 MD5 不夠安全。當整個系統中的資料庫被攻擊者入侵之後,存儲密碼的摘要而不是明文是我們能夠對所有用戶的最大保護。需要知道的是,不夠安全的不只是 MD5,任何摘要算法在存儲密碼這一場景下都不夠安全,我們在這篇文章中就會哈希函數『為什麼哈希函數不能用於存儲密碼』以及其他相關機制的安全性。
設計既然我們已經對哈希函數和加密算法有了一些簡單的了解,接下來的這一節中分析使用以下幾種不同方式存儲密碼的安全性:
使用哈希存儲密碼;
使用哈希加鹽存儲密碼;
使用加密算法存儲密碼;
使用 bcrypt 存儲密碼;
在分析的過程中可能會涉及到一些簡單的密碼學知識,也會談到一些密碼學歷史上的一些事件,不過這對於理解不同方式的安全性不會造成太大的障礙。
哈希在今天,如果我們直接使用哈希來存儲密碼,那其實跟存儲明文沒有太多的區別,所有的攻擊者在今天都已經掌握了彩虹表這個工具,我們可以將彩虹表理解成一張預計算的大表,其中存儲著一些常見密碼的哈希,當攻擊者通過入侵拿到某些網站的資料庫之後就可以通過預計算表中存儲的映射來查找原始密碼。
攻擊者只需要將一些常見密碼提前計算一些哈希就可以找到資料庫中很多用於存儲的密碼,Wikipedia 上有一份關於最常見密碼的 列表,在 2016 年的統計中發現使用情況最多的前 25 個密碼佔了調查總數的 10%,雖然這不能排除統計本身的不準確因素,但是也足以說明僅僅使用哈希的方式存儲密碼是不夠安全的。
哈希加鹽僅僅使用哈希來存儲密碼無法抵禦來自彩虹表的攻擊,在上世紀 70 到 80 年代,早期版本的 Unix 系統就在 /etc/passwrd 中存儲加鹽的哈希密碼,密碼加鹽後的哈希與鹽會被一起存儲在 /etc/passwd 文件中,今天哈希加鹽的策略與幾十年前的也沒有太多的不同,差異可能在於鹽的生成和選擇:
md5(salt, password), salt
加鹽的方式主要還是為了增加攻擊者的計算成本,當攻擊者順利拿到資料庫中的數據時,由於每個密碼都使用了隨機的鹽進行哈希,所以預先計算的彩虹表就沒有辦法立刻破譯出哈希之前的原始數據,攻擊者對每一個哈希都需要單獨進行計算,這樣能夠增加了攻擊者的成本,減少原始密碼被大範圍破譯的可能性。
在這種情況下,攻擊者破解一個用戶密碼的成本其實就等於發現哈希碰撞的概率,因為攻擊者其實不需要知道用戶的密碼是什麼,他只需要找到一個值 value,這個值加鹽後的哈希與密碼加鹽後的哈希完全一致就能登錄用戶的帳號:
hash(salt, value) = hash(salt, password)
這種情況在密碼學中叫做哈希碰撞,也就是兩個不同值對應哈希相同,一個哈希函數或者摘要算法被找到哈希碰撞的概率決定了該算法的安全性,早在幾十年前,我們就在 MD5 的設計中發現了缺陷並且在隨後的發展中找到了低成本快速製造哈希碰撞的方法。
1996 年 The Status of MD5 After a Recent Attack —— 發現了 MD5 設計中的缺陷,但是並沒有被認為是致命的缺點,密碼學專家開始推薦使用其他的摘要算法;
2004 年 How to Break MD5 and Other Hash Functions —— 發現了 MD5 摘要算法不能抵抗哈希碰撞,我們不能在數字安全領域使用 MD5 算法;
2006 年 A Study of the MD5 Attacks: Insights and Improvements —— 創建一組具有相同 MD5 摘要的文件;
2008 年 MD5 considered harmful today —— 創建偽造的 SSL 證書;
2010 年 MD5 vulnerable to collision attacks —— CMU 軟體工程機構認為 MD5 摘要算法已經在密碼學上被破譯並且不適合使用;
2012 年 Flame —— 惡意軟體利用了 MD5 的漏洞並偽造了微軟的數字籤名;
從過往的歷史來看,為了保證用戶敏感信息的安全,我們不應該使用 MD5 加鹽的方式來存儲用戶的密碼,那麼我們是否可以使用更加安全的摘要算法呢?不可以,哈希函數並不是專門用來設計存儲用戶密碼的,所以它的計算可能相對來說還是比較快,攻擊者今天可以通過 GPU 每秒執行上億次的計算來破解用戶的密碼,所以不能使用這種方式存儲用戶的密碼,感興趣的讀者可以了解一下用於恢復密碼的工具 Hashcat。
加密既然今天的硬體已經能夠很快地幫助攻擊者破解用戶的密碼,那麼我們能否通過其他的方式來取代哈希函數來存儲密碼呢?有些工程師想到使用加密算法來替代哈希函數,這樣能夠從源頭上避免哈希碰撞的的發生,這種方式看起來非常美好,但是有一個致命的缺點,就是我們如何存儲用於加密密碼的秘鑰。
既然存儲密碼的倉庫能被洩露,那麼用於存儲秘鑰的服務也可能會被攻擊,我們永遠都沒有辦法保證我們的資料庫和伺服器是安全的,一旦秘鑰被攻擊者獲取,他們就可以輕而易舉地恢復用戶的密碼,因為核對用戶密碼的過程需要在內存對密碼進行解密,這時明文的密碼就可能暴露在內存中,依然有導致用戶密碼洩露的風險。
使用加密的方式存儲密碼相比於哈希加鹽的方式,在一些安全意識和能力較差的公司和網站反而更容易導緻密碼的洩露和安全事故。
bcrypt哈希加鹽的方式確實能夠增加攻擊者的成本,但是今天來看還遠遠不夠,我們需要一種更加安全的方式來存儲用戶的密碼,這也就是今天被廣泛使用的 bcrypt,使用 bcrypt 相比於直接使用哈希加鹽是一種更加安全的方式,也是我們目前推薦使用的方法,為了增加攻擊者的成本,bcrypt 引入了計算成本這一可以調節的參數,能夠調節執行 bcrypt 函數的成本。
當我們將驗證用戶密碼的成本提高几個數量級時,攻擊者的成本其實也相應的提升了幾個數量級,只要我們讓攻擊者的攻擊成本大於硬體的限制,同時保證正常請求的耗時在合理範圍內,我們就能夠保證用戶密碼的相對安全。
"bcrypt was designed for password hashing hence it is a slow algorithm. This is good for password hashing as it reduces the number of passwords by second an attacker could hash when crafting a dictionary attack. "
bcrypt 這一算法就是為哈希密碼而專門設計的,所以它是一個執行相對較慢的算法,這也就能夠減少攻擊者每秒能夠處理的密碼數量,從而避免攻擊者的字典攻擊。
func main() {
for cost := 10; cost <= 15; cost++ {
startedAt := time.Now()
bcrypt.GenerateFromPassword([]byte("password"), cost)
duration := time.Since(startedAt)
fmt.Printf("cost: %d, duration: %v\n", cost, duration)
}
}
$ go run bcrypt.go
cost: 10, duration: 51.483401ms
cost: 11, duration: 100.639251ms
cost: 12, duration: 202.788492ms
cost: 13, duration: 399.552731ms
cost: 14, duration: 801.041128ms
cost: 15, duration: 1.579692689s
運行上述 代碼片段 時就能發現 cost 和運行時間的關係,算法運行的成本每 +1,當前算法最終的耗時就會翻一倍,這與 bcrypt 算法的實現原理有關,你可以在 Wikipedia 上找到算法執行過程的偽代碼,這可以幫助我們快速理解算法背後的設計。
如果硬體的發展使攻擊者能夠對使用 bcrypt 存儲的密碼進行攻擊時,我們就可以直接提升 bcrypt 算法的 cost 參數以增加攻擊者的成本,這也是 bcrypt 設計上的精妙之處,所以使用 bcrypt 是一種在存儲用戶密碼時比較安全的方式。
總結這篇文章分析的問題其實是 —— 當資料庫被攻擊者獲取時,我們怎麼能夠保證用戶的密碼很難被攻擊者『破譯』,作為保護用戶機密信息的最後手段,選擇安全並且合適的方法至關重要。攻擊者能否破解用戶的密碼一般取決於兩個條件:
抵禦攻擊者的攻擊的方式其實就是提高單次算法運行的成本,當我們將用戶的驗證耗時從 0.1ms 提升到了 500ms,攻擊者的計算成本也就提升了 5000 倍,這種結果就是之前需要幾小時破解的密碼現在需要幾年的時間。
不論如何,使用 MD5、MD5 加鹽或者其他哈希的方式來存儲密碼都是不安全的,希望各位工程師能夠避免在這樣的場景下使用 MD5,在其他必須使用哈希函數的場景下也建議使用其他算法代替,例如 SHA-512 等。
當然,如何保證用戶機密信息的安全不只是一個密碼學問題,它還是一個工程問題,任何工程開發商的疏漏都可能導致安全事故,所以我們作為開發者在與用於敏感信息打交道時也應該小心謹慎、懷有敬畏之心。到最後,我們還是來看一些比較開放的相關問題,有興趣的讀者可以仔細思考一下下面的問題:
使用 GPU 每秒可以計算多少 MD5 哈希(數量級)?能夠在多長時間破解使用 MD5 加鹽存儲的密碼?
假設計算一次哈希耗時 500ms,破解 bcrypt 算法生成的哈希需要多長時間?
MD5 哈希 23cdc18507b52418db7740cbb5543e54 對應的原文可能是?談談你使用的工具和破譯的過程。
如果對文章中的內容有疑問或者想要了解更多軟體工程上一些設計決策背後的原因,可以在博客下面留言,作者會及時回複本文相關的疑問並選擇其中合適的主題作為後續的內容。
ReferenceIs salted MD5 or salted SHA considered secure?
How to securely hash passwords?
Rainbow table
The MD5 Message-Digest Algorithm · RFC1321
Collision (computer science)
Why You Should Use Bcrypt to Hash Stored Passwords
How can bcrypt have built-in salts?
bcrypt
系列文章長按二維碼↑
關注『真沒什麼邏輯』公眾號