1. 程式人生 > >即使被拖庫,也可以保證密碼不泄露-加鹽

即使被拖庫,也可以保證密碼不泄露-加鹽

常用 隨著 src 意義 解決方案 為什麽 算法 簡單的 hash

在前一篇文章《設計安全的賬號系統的正確姿勢》中,主要提出了一些設計的方法和思路,並沒有給出一個更加具體的,可以實施的安全加密方案。經過我仔細的思考並了解了目前一些方案後,我設計了一個自認為還比較安全的安全加密方案。本文主要就是講述這個方案,非常歡迎和期待有讀者一起來討論。

首先,我們明確一下安全加密方案的終極目標:

即使在數據被拖庫,代碼被泄露,請求被劫持的情況下,也能保障用戶的密碼不被泄露。

說具體一些,我們理想中的絕對安全的系統大概是這樣的:

  1. 首先保障數據很難被拖庫。
  2. 即使數據被拖庫,攻擊者也無法從中破解出用戶的密碼。
  3. 即使數據被拖庫,攻擊者也無法偽造登錄請求通過驗證。
  4. 即使數據被拖庫,攻擊者劫持了用戶的請求數據,也無法破解出用戶的密碼。

如何保障數據不被拖庫,這裏就不展開講了。首先我們來說說密碼加密。現在應該很少系統會直接保存用戶的密碼了吧,至少也是會計算密碼的 md5 後保存。md5 這種不可逆的加密方法理論上已經很安全了,但是隨著彩虹表的出現,使得大量長度不夠的密碼可以直接從彩虹表裏反推出來。

所以,只對密碼進行 md5 加密是肯定不夠的。聰明的程序員想出了個辦法,即使用戶的密碼很短,只要我在他的短密碼後面加上一段很長的字符,再計算 md5 ,那反推出原始密碼就變得非常困難了。加上的這段長字符,我們稱為鹽(Salt),通過這種方式加密的結果,我們稱為 加鹽 Hash 。比如:

技術分享圖片

上一篇我們講過,常用的哈希函數中,SHA-256、SHA-512 會比 md5 更安全,更難破解,出於更高安全性的考慮,我的這個方案中,會使用 SHA-512 代替 md5 。

技術分享圖片

通過上面的加鹽哈希運算,即使攻擊者拿到了最終結果,也很難反推出原始的密碼。不能反推,但可以正著推,假設攻擊者將 salt 值也拿到了,那麽他可以枚舉遍歷所有 6 位數的簡單密碼,加鹽哈希,計算出一個結果對照表,從而破解出簡單的密碼。這就是通常所說的暴力破解。

為了應對暴力破解,我使用了加鹽的慢哈希。慢哈希是指執行這個哈希函數非常慢,這樣暴力破解需要枚舉遍歷所有可能結果時,就需要花上非常非常長的時間。比如:bcrypt 就是這樣一個慢哈希函數:

技術分享圖片

通過調整 cost 參數,可以調整該函數慢到什麽程度。假設讓 bcrypt 計算一次需要 0.5 秒,遍歷 6 位的簡單密碼,需要的時間為:((26 * 2 + 10)^6) / 2 秒,約 900 年。

好了,有了上面的基礎,來看看我的最終解決方案:

技術分享圖片

上圖裏有很多細節,我分階段來講:

1. 協商密鑰

基於非對稱加密的密鑰協商算法,可以在通信內容完全被公開的情況下,雙方協商出一個只有雙方才知道的密鑰,然後使用該密鑰進行對稱加密傳輸數據。比如圖中所用的 ECDH 密鑰協商。

2. 請求 Salt

雙方協商出一個密鑰 SharedKey 之後,就可以使用 SharedKey 作為 AES 對稱加密的密鑰進行通信,客戶端傳給服務端自己的公鑰 A ,以及加密了的用戶ID(uid)。服務端從數據庫中查找到該 uid 對於的 Salt1 和 Salt2 ,然後再加密返回給客戶端。

註意,服務端保存的 Salt1 和 Salt2 最好和用戶數據分開存儲,存到其他服務器的數據庫裏,這樣即使被 SQL 註入,想要獲得 Salt1 和 Salt2 也會非常困難。

3. 驗證密碼

這是最重要的一步了。客戶端拿到 Salt1 和 Salt2 之後,可以計算出兩個加鹽哈希:

SaltHash1 = bcrypt(SHA512(password), uid + salt1, 10)
SaltHash2 = SHA512(SaltHash1 + uid + salt2)

使用 SaltHash2 做為 AES 密鑰,加密包括 uid,time,SaltHash1,RandKey 等內容傳輸給服務端:

Ticket = AES(SaltHash2, uid + time + SaltHash1 + RandKey)
AES(SharedKey, Ticket)

服務端使用 SharedKey 解密出 Ticket 之後,再從數據庫中找到該 uid 對應的 SaltHash2 ,解密 Ticket ,得到 SaltHash1 ,使用 SaltHash1 重新計算 SaltHash2 看是否和數據庫中的 SaltHash2 一致,從而驗證密碼是否正確。

校驗兩個哈希值是否相等時,使用時間恒定的比較函數,防止試探性攻擊。

time 用於記錄數據包發送的時間,用來防止錄制回放攻擊。

4. 加密傳輸

密碼驗證通過後,服務端生成一個隨機的臨時密鑰 TempKey(使用安全的隨機函數),並使用 RandKey 做為密鑰,傳輸給客戶端。之後雙方的數據交互都通過 TempKey 作為 AES 密鑰進行加密。

假設被拖庫了

以上就是整個加密傳輸、存儲的全過程。我們來假設幾種攻擊場景:

  1. 假設數據被拖庫了,密碼會泄露嗎?

    數據庫中的 Salt1 ,Salt2 , SaltHash2 暴露了,想從 SaltHash2 直接反解出原始密碼幾乎是不可能的事情。

  2. 假設數據被拖庫了,攻擊者能不能偽造登錄請求通過驗證?

    攻擊者在生成 Ticket 時,需要 SaltHash1 ,但由於並不知道密碼,所以無法計算出 SaltHash1 ,又無法從 SaltHash2 反推 SaltHash1 ,所以無法偽造登錄請求通過驗證。

  3. 假設數據被拖庫了,攻擊者使用中間人攻擊,劫持了用戶的請求,密碼會被泄露嗎?

    中間人擁有真實服務器所有的數據,仿冒了真實的 Server ,因此,他可以解密出 Ticket 中的 SaltHash1 ,但是 SaltHash1 是無法解密出原始密碼的。所以,密碼也不會被泄露。

    但是,中間人攻擊可以獲取到最後的 TempKey ,從而能監聽後續的所有通信過程。這是很難解決的問題,因為在服務端所有東西都暴露的情況下,中間人假設可以劫持用戶數據,仿冒真實 Server , 是很難和真實的 Server 區分開的。解決的方法也許只有防止被中間人攻擊,保證 Server 的公鑰在客戶端不被篡改。

    假設攻擊已經進展到了這樣的程度,還有辦法補救嗎?有。由於攻擊者只能監聽用戶的登錄過程,並不知道真實的密碼。所以,只需要在服務端對 Salt2 進行升級,即可生成新的 SaltHash2 ,從而讓攻擊者所有攻擊失效。

    具體是這樣的:用戶正常的登錄,服務端驗證通過後,生成新的 Salt2 ,然後根據傳過來的 SaltHash1 重新計算了 SaltHash2 存入數據庫。下次用戶再次登錄時,獲取到的是新的 Salt2 ,密碼沒有變,同樣能登錄,攻擊者之前拖庫的那份數據也失效了。

Q & A

  1. 使用 bcrypt 慢哈希函數,服務端應對大量的用戶登錄請求,性能承受的了嗎?

    該方案中,細心一點會註意到, bcrypt 只是在客戶端進行運算的,服務端是直接拿到客戶端運算好的結果( SaltHash1 )後 SHA-512 計算結果進行驗證的。所以,把性能壓力分攤到了各個客戶端。

  2. 為什麽要使用兩個 Salt 值?

    使用兩個 Salt 值,是為了防止拖庫後,劫持了用戶請求後將密碼破解出來。只有擁有密碼的用戶,才能用第一個 Salt 值計算出 SaltHash1 ,並且不能反推回原始密碼。第二個 Salt 值可以加大被拖庫後直接解密出 SaltHash1 的難度。

  3. 為什麽要動態請求 Salt1 和 Salt2 ?

    Salt 值直接寫在客戶端肯定不好,而且寫死了要修改還得升級客戶端。動態請求 Salt 值,還可以實現不升級客戶端的情況下,對密碼進行動態升級:服務端可定期更換 Salt2 ,重新計算 SaltHash2 ,讓攻擊者即使拖了一次數據也很快處於失效狀態。

  4. 數據庫都已經全被拖走了,密碼不泄露還有什麽意義呢?

    其實是有意義的,正如剛剛提到的升級 Salt2 的補救方案,用戶可以在完全不知情的情況下,不需要修改密碼就升級了賬號體系。同時,保護好用戶的密碼,不被攻擊者拿去撞別家網站的庫,也是一份責任。

歡迎大家針對本文的方案進行討論,如有不實或者考慮不周的地方,請盡情指出。或者有更好的建議或意見,歡迎交流!

更新:反饋匯總

  1. “應該從源頭上禁止用戶使用簡單密碼”

    回復:非常同意!

  2. “獲取 salt 並不需要啥驗證,那麽還有必要分開存儲麽,脫褲者直接根據uid調一遍接口不就拿到了?相當於就是公開的吧?”

    回復:確實是這樣。salt 相當於公開的了,沒有必要分開存儲了。如果你有更好的方法,請告訴我。

  3. “使用 HTTPS(SSL/TLS) 來保障傳輸的安全是不是就可以了?”

    回復:理論上是足夠了,而且推薦使用。 如果你的項目安全級別非常高,本著不信任絕對安全的角度可考慮再一層加固。

  4. “salt 使用密碼學安全的隨機數生成就夠了,不需要使用 uid 。”

    回復:同意,確實不是很必要。

  5. “服務器性能夠強勁,bcrypt 放在服務端執行也沒什麽問題。”

    回復:通過調整 bcrypt 參數讓服務端執行在可接受的時間範圍內確實可以。但是把這種耗時的操作放到客戶端去做不是更好嗎?

  6. “不知攻焉知防,還是使用現有的算法和協議比較好,不要自己發明。即使自己發明,也需要經過實踐的檢驗不斷叠代才行。”

    回復:首先,文中用到的都是現有的成熟算法,如 bcrypt,SHA-512, AES ,包括 ECDH,並沒有重新發明什麽。文章重點是對密碼的兩次加鹽哈希以及密碼的驗證方式。當然,方案需要在實踐中不斷叠代優化,我也是不能同意再多。

有一位朋友說的非常好,很多互聯網公司對安全不重視,近年來密碼安全事故頻繁發生,導致密碼泄露後被拿去撞庫,用戶利益受損。應該去推動一下密碼安全的業界標準,避免企業犯錯用戶買單。同時,互聯網沒有絕對的安全,強烈建議用戶不要用同一個密碼,密碼定期改!

https://blog.coderzh.com/2016/01/10/a-password-security-design-example/

即使被拖庫,也可以保證密碼不泄露-加鹽