在前一篇文章《設(shè)計(jì)安全的賬號(hào)系統(tǒng)的正確姿勢(shì)》中,主要提出了一些設(shè)計(jì)的方法和思路,并沒(méi)有給出一個(gè)更加具體的,可以實(shí)施的安全加密方案。經(jīng)過(guò)我仔細(xì)的思考并了解了目前一些方案后,我設(shè)計(jì)了一個(gè)自認(rèn)為還比較安全的安全加密方案。本文主要就是講述這個(gè)方案,非常歡迎和期待有讀者一起來(lái)討論。
首先,我們明確一下安全加密方案的終極目標(biāo):
即使在數(shù)據(jù)被拖庫(kù),代碼被泄露,請(qǐng)求被劫持的情況下,也能保障用戶的密碼不被泄露。
說(shuō)具體一些,我們理想中的絕對(duì)安全的系統(tǒng)大概是這樣的:
- 首先保障數(shù)據(jù)很難被拖庫(kù)。
- 即使數(shù)據(jù)被拖庫(kù),攻擊者也無(wú)法從中破解出用戶的密碼。
- 即使數(shù)據(jù)被拖庫(kù),攻擊者也無(wú)法偽造登錄請(qǐng)求通過(guò)驗(yàn)證。
- 即使數(shù)據(jù)被拖庫(kù),攻擊者劫持了用戶的請(qǐng)求數(shù)據(jù),也無(wú)法破解出用戶的密碼。
如何保障數(shù)據(jù)不被拖庫(kù),這里就不展開(kāi)講了。首先我們來(lái)說(shuō)說(shuō)密碼加密?,F(xiàn)在應(yīng)該很少系統(tǒng)會(huì)直接保存用戶的密碼了吧,至少也是會(huì)計(jì)算密碼的 md5 后保存。md5 這種不可逆的加密方法理論上已經(jīng)很安全了,但是隨著彩虹表的出現(xiàn),使得大量長(zhǎng)度不夠的密碼可以直接從彩虹表里反推出來(lái)。
所以,只對(duì)密碼進(jìn)行 md5 加密是肯定不夠的。聰明的程序員想出了個(gè)辦法,即使用戶的密碼很短,只要我在他的短密碼后面加上一段很長(zhǎng)的字符,再計(jì)算 md5 ,那反推出原始密碼就變得非常困難了。加上的這段長(zhǎng)字符,我們稱為鹽(Salt),通過(guò)這種方式加密的結(jié)果,我們稱為 加鹽 Hash 。比如:
md5(md5(password) + salt)
上一篇我們講過(guò),常用的哈希函數(shù)中,SHA-256、SHA-512 會(huì)比 md5 更安全,更難破解,出于更高安全性的考慮,我的這個(gè)方案中,會(huì)使用 SHA-512 代替 md5 。
SHA512(SHA512(password) + salt)
通過(guò)上面的加鹽哈希運(yùn)算,即使攻擊者拿到了最終結(jié)果,也很難反推出原始的密碼。不能反推,但可以正著推,假設(shè)攻擊者將 salt 值也拿到了,那么他可以枚舉遍歷所有 6 位數(shù)的簡(jiǎn)單密碼,加鹽哈希,計(jì)算出一個(gè)結(jié)果對(duì)照表,從而破解出簡(jiǎn)單的密碼。這就是通常所說(shuō)的暴力破解。
為了應(yīng)對(duì)暴力破解,我使用了加鹽的慢哈希。慢哈希是指執(zhí)行這個(gè)哈希函數(shù)非常慢,這樣暴力破解需要枚舉遍歷所有可能結(jié)果時(shí),就需要花上非常非常長(zhǎng)的時(shí)間。比如:bcrypt 就是這樣一個(gè)慢哈希函數(shù):
bcrypt(SHA512(password), salt, cost)
通過(guò)調(diào)整 cost 參數(shù),可以調(diào)整該函數(shù)慢到什么程度。假設(shè)讓 bcrypt 計(jì)算一次需要 0.5 秒,遍歷 6 位的簡(jiǎn)單密碼,需要的時(shí)間為:((26 * 2 + 10)^6) / 2 秒,約 900 年。
好了,有了上面的基礎(chǔ),來(lái)看看我的最終解決方案:

上圖里有很多細(xì)節(jié),我分階段來(lái)講:
1. 協(xié)商密鑰
基于非對(duì)稱加密的密鑰協(xié)商算法,可以在通信內(nèi)容完全被公開(kāi)的情況下,雙方協(xié)商出一個(gè)只有雙方才知道的密鑰,然后使用該密鑰進(jìn)行對(duì)稱加密傳輸數(shù)據(jù)。比如圖中所用的 ECDH 密鑰協(xié)商。
2. 請(qǐng)求 Salt
雙方協(xié)商出一個(gè)密鑰 SharedKey 之后,就可以使用 SharedKey 作為 AES 對(duì)稱加密的密鑰進(jìn)行通信,客戶端傳給服務(wù)端自己的公鑰 A ,以及加密了的用戶ID(uid)。服務(wù)端從數(shù)據(jù)庫(kù)中查找到該 uid 對(duì)于的 Salt1 和 Salt2 ,然后再加密返回給客戶端。
注意,服務(wù)端保存的 Salt1 和 Salt2 最好和用戶數(shù)據(jù)分開(kāi)存儲(chǔ),存到其他服務(wù)器的數(shù)據(jù)庫(kù)里,這樣即使被 SQL 注入,想要獲得 Salt1 和 Salt2 也會(huì)非常困難。
3. 驗(yàn)證密碼
這是最重要的一步了??蛻舳四玫?Salt1 和 Salt2 之后,可以計(jì)算出兩個(gè)加鹽哈希:
SaltHash1 = bcrypt(SHA512(password), uid + salt1, 10)
SaltHash2 = SHA512(SaltHash1 + uid + salt2)
使用 SaltHash2 做為 AES 密鑰,加密包括 uid,time,SaltHash1,RandKey 等內(nèi)容傳輸給服務(wù)端:
Ticket = AES(SaltHash2, uid + time + SaltHash1 + RandKey)
AES(SharedKey, Ticket)
服務(wù)端使用 SharedKey 解密出 Ticket 之后,再?gòu)臄?shù)據(jù)庫(kù)中找到該 uid 對(duì)應(yīng)的 SaltHash2 ,解密 Ticket ,得到 SaltHash1 ,使用 SaltHash1 重新計(jì)算 SaltHash2 看是否和數(shù)據(jù)庫(kù)中的 SaltHash2 一致,從而驗(yàn)證密碼是否正確。
校驗(yàn)兩個(gè)哈希值是否相等時(shí),使用時(shí)間恒定的比較函數(shù),防止試探性攻擊。
time 用于記錄數(shù)據(jù)包發(fā)送的時(shí)間,用來(lái)防止錄制回放攻擊。
4. 加密傳輸
密碼驗(yàn)證通過(guò)后,服務(wù)端生成一個(gè)隨機(jī)的臨時(shí)密鑰 TempKey(使用安全的隨機(jī)函數(shù)),并使用 RandKey 做為密鑰,傳輸給客戶端。之后雙方的數(shù)據(jù)交互都通過(guò) TempKey 作為 AES 密鑰進(jìn)行加密。
假設(shè)被拖庫(kù)了
以上就是整個(gè)加密傳輸、存儲(chǔ)的全過(guò)程。我們來(lái)假設(shè)幾種攻擊場(chǎng)景:
- 假設(shè)數(shù)據(jù)被拖庫(kù)了,密碼會(huì)泄露嗎?數(shù)據(jù)庫(kù)中的 Salt1 ,Salt2 , SaltHash2 暴露了,想從 SaltHash2 直接反解出原始密碼幾乎是不可能的事情。
- 假設(shè)數(shù)據(jù)被拖庫(kù)了,攻擊者能不能偽造登錄請(qǐng)求通過(guò)驗(yàn)證?攻擊者在生成 Ticket 時(shí),需要 SaltHash1 ,但由于并不知道密碼,所以無(wú)法計(jì)算出 SaltHash1 ,又無(wú)法從 SaltHash2 反推 SaltHash1 ,所以無(wú)法偽造登錄請(qǐng)求通過(guò)驗(yàn)證。
- 假設(shè)數(shù)據(jù)被拖庫(kù)了,攻擊者使用中間人攻擊,劫持了用戶的請(qǐng)求,密碼會(huì)被泄露嗎?中間人擁有真實(shí)服務(wù)器所有的數(shù)據(jù),仿冒了真實(shí)的 Server ,因此,他可以解密出 Ticket 中的 SaltHash1 ,但是 SaltHash1 是無(wú)法解密出原始密碼的。所以,密碼也不會(huì)被泄露。
但是,中間人攻擊可以獲取到最后的 TempKey ,從而能監(jiān)聽(tīng)后續(xù)的所有通信過(guò)程。這是很難解決的問(wèn)題,因?yàn)樵诜?wù)端所有東西都暴露的情況下,中間人假設(shè)可以劫持用戶數(shù)據(jù),仿冒真實(shí) Server , 是很難和真實(shí)的 Server 區(qū)分開(kāi)的。解決的方法也許只有防止被中間人攻擊,保證 Server 的公鑰在客戶端不被篡改。
假設(shè)攻擊已經(jīng)進(jìn)展到了這樣的程度,還有辦法補(bǔ)救嗎?有。由于攻擊者只能監(jiān)聽(tīng)用戶的登錄過(guò)程,并不知道真實(shí)的密碼。所以,只需要在服務(wù)端對(duì) Salt2 進(jìn)行升級(jí),即可生成新的 SaltHash2 ,從而讓攻擊者所有攻擊失效。
具體是這樣的:用戶正常的登錄,服務(wù)端驗(yàn)證通過(guò)后,生成新的 Salt2 ,然后根據(jù)傳過(guò)來(lái)的 SaltHash1 重新計(jì)算了 SaltHash2 存入數(shù)據(jù)庫(kù)。下次用戶再次登錄時(shí),獲取到的是新的 Salt2 ,密碼沒(méi)有變,同樣能登錄,攻擊者之前拖庫(kù)的那份數(shù)據(jù)也失效了。
Q & A
- 使用 bcrypt 慢哈希函數(shù),服務(wù)端應(yīng)對(duì)大量的用戶登錄請(qǐng)求,性能承受的了嗎?該方案中,細(xì)心一點(diǎn)會(huì)注意到, bcrypt 只是在客戶端進(jìn)行運(yùn)算的,服務(wù)端是直接拿到客戶端運(yùn)算好的結(jié)果( SaltHash1 )后 SHA-512 計(jì)算結(jié)果進(jìn)行驗(yàn)證的。所以,把性能壓力分?jǐn)偟搅烁鱾€(gè)客戶端。
- 為什么要使用兩個(gè) Salt 值?使用兩個(gè) Salt 值,是為了防止拖庫(kù)后,劫持了用戶請(qǐng)求后將密碼破解出來(lái)。只有擁有密碼的用戶,才能用第一個(gè) Salt 值計(jì)算出 SaltHash1 ,并且不能反推回原始密碼。第二個(gè) Salt 值可以加大被拖庫(kù)后直接解密出 SaltHash1 的難度。
- 為什么要?jiǎng)討B(tài)請(qǐng)求 Salt1 和 Salt2 ?Salt 值直接寫(xiě)在客戶端肯定不好,而且寫(xiě)死了要修改還得升級(jí)客戶端。動(dòng)態(tài)請(qǐng)求 Salt 值,還可以實(shí)現(xiàn)不升級(jí)客戶端的情況下,對(duì)密碼進(jìn)行動(dòng)態(tài)升級(jí):服務(wù)端可定期更換 Salt2 ,重新計(jì)算 SaltHash2 ,讓攻擊者即使拖了一次數(shù)據(jù)也很快處于失效狀態(tài)。
- 數(shù)據(jù)庫(kù)都已經(jīng)全被拖走了,密碼不泄露還有什么意義呢?其實(shí)是有意義的,正如剛剛提到的升級(jí) Salt2 的補(bǔ)救方案,用戶可以在完全不知情的情況下,不需要修改密碼就升級(jí)了賬號(hào)體系。同時(shí),保護(hù)好用戶的密碼,不被攻擊者拿去撞別家網(wǎng)站的庫(kù),也是一份責(zé)任。
歡迎大家針對(duì)本文的方案進(jìn)行討論,如有不實(shí)或者考慮不周的地方,請(qǐng)盡情指出?;蛘哂懈玫慕ㄗh或意見(jiàn),歡迎交流!
via:coderzh.com
哈爾濱品用軟件有限公司致力于為哈爾濱的中小企業(yè)制作大氣、美觀的優(yōu)秀網(wǎng)站,并且能夠搭建符合百度排名規(guī)范的網(wǎng)站基底,使您的網(wǎng)站無(wú)需額外費(fèi)用,即可穩(wěn)步提升排名至首頁(yè)。歡迎體驗(yàn)最佳的哈爾濱網(wǎng)站建設(shè)。
