本篇經(jīng)與 @Gnnng, @pollow 討論、查資料化漆,在 MSTC 群中請教討論總結(jié)而成估脆。

我們都知道,數(shù)據(jù)庫里不能儲(chǔ)存用戶密碼的明文座云,那怎樣存儲(chǔ)才是最科學(xué)的呢疙赠?
為什么不能存明文?
仔細(xì)想想朦拖,即使使用明文放在數(shù)據(jù)庫里圃阳,用戶還是看不到別人的密碼,那為什么不能使用明文存儲(chǔ)呢璧帝?這是因?yàn)槟悴荒鼙WC自己數(shù)據(jù)庫的安全捍岳。之前 CSDN 因?yàn)橐粋€(gè)漏洞整站數(shù)據(jù)庫被拖下來,用戶的帳戶名和密碼就全部暴露在網(wǎng)站上了睬隶。這帶來一個(gè)更嚴(yán)重的問題锣夹,很多人在不同的站點(diǎn)上使用的都是同一個(gè)用戶名和密碼,別人拿著這個(gè)被暴露出來的密碼到其他各大網(wǎng)站苏潜,比如淘寶银萍,支付寶,網(wǎng)銀等等試一下恤左,用戶的損失就嚴(yán)重了贴唇。
加密 Hash
加密 Hash 是對數(shù)據(jù)的單向映射贰锁。單向映射就是指拿到 hash 后的值,無法得到原來的數(shù)據(jù)滤蝠。上面那個(gè)問題,如果我們把用戶的密碼 hash 一下再放到數(shù)據(jù)庫里授嘀,然后每次用戶登入的時(shí)候我們拿到用戶的真實(shí)密碼都先 hash 物咳,再與數(shù)據(jù)庫中的值進(jìn)行比對,這樣即使數(shù)據(jù)庫被人拖下來蹄皱,他也無法通過那個(gè) hash 后的值得到用戶的真實(shí)密碼了览闰。
Rainbow Table
可是真的是這樣嗎?一定程度上說是的巷折,別人得到 hash 之后压鉴,的確幾乎不可能逆向算出原始密碼。但換個(gè)思路想一想锻拘,用戶的密碼通常是簡單并且容易構(gòu)造的油吭,只要把這些常見的密碼 hash 一下得到這個(gè)對應(yīng)關(guān)系表,就很容易得到原始密碼了署拟。這樣的對應(yīng)關(guān)系表通常被稱為彩虹表[1](Rainbow Table)婉宰。
現(xiàn)在黑客們已經(jīng)構(gòu)造出 TB 級別的彩虹表[2],所以很多直接 hash 得來的密碼都會(huì)被直接查詢到推穷。
Salt
那用什么辦法可以防止彩虹表的直接查詢呢心包,有人提出了一個(gè)解決方案——加鹽(salt)。具體來說就是得到用戶的真實(shí)密碼后馒铃,再隨機(jī)生成一個(gè)字符串(即鹽)蟹腾,把密碼和鹽用某種方式組合起來(即加鹽)然后再 hash 后放后數(shù)據(jù)庫中,同時(shí)把鹽也放入數(shù)據(jù)庫中区宇。用戶登入的時(shí)候娃殖,從數(shù)據(jù)庫中取出對應(yīng)的鹽,再和得到的用戶密碼組合一下進(jìn)行 hash 萧锉,與數(shù)據(jù)庫中的 hash 值比對即可珊随。
那為什么這樣就能避免彩虹表碰撞呢?如果數(shù)據(jù)庫被人拖下來柿隙,他獲得了 hash 叶洞,同時(shí)也獲得了鹽,如果碰撞成功禀崖,他應(yīng)該很容易通過拿到的鹽得出用戶真實(shí)密碼榜帽佟?
是波附,也不是艺晴。
其實(shí)加鹽的真實(shí)目的昼钻,是對用戶的密碼進(jìn)行強(qiáng)化。用戶常常會(huì)使用弱的密碼封寞,比如幾位純數(shù)字之類然评,這太好構(gòu)造,很容易碰撞出來狈究。但是如果是幾十位碗淌,甚至幾百位的大小寫字母加數(shù)字還有特殊字符,這就很難構(gòu)造了抖锥,因?yàn)檫@樣的組合數(shù)目實(shí)在太多了亿眠。我們可以看到在 Rainbow Crack 這個(gè)網(wǎng)站上面,1 - 9 位數(shù)字和字母的組合就達(dá)到了 13,759,005,997,841,642 種之多磅废,數(shù)據(jù)量達(dá)到了 864 GB纳像,在這個(gè)基礎(chǔ)上每增加一位或者增加一種字符帶來的都是指數(shù)級別的數(shù)據(jù)量增長。因此拯勉,只要我們的鹽足夠強(qiáng)竟趾,很難用彩虹表碰撞成功。而換一種策略谜喊,即使他使用拿到的每個(gè)鹽去構(gòu)造新的一些彩虹表潭兽,代價(jià)也是巨大的。
多 hash 幾次斗遏?
有些人可能會(huì)想到山卦,拿到用戶的密碼后,多 hash 幾次會(huì)不會(huì)從一定程度上避免彩虹表碰撞呢诵次?答案是否定的账蓉。事實(shí)上,和大多數(shù)人想象得正好相反逾一,這樣做反而更加不安全铸本。
前面說,hash 函數(shù)是不可逆的遵堵,這是因?yàn)樵?hash 的過程中丟掉了原始數(shù)據(jù)的部分信息箱玷。也就是說,hash 是減熵的陌宿。當(dāng)進(jìn)行多重 hash 的時(shí)候锡足,整體的安全性其實(shí)取決于最弱的那個(gè) hash 函數(shù),因?yàn)槿绻麊我?hash 碰撞壳坪,多重 hash 一定碰撞舶得。
把上面那句話用表達(dá)式寫出來:
比如原先有函數(shù) hash1,并且
數(shù)據(jù)庫里面存儲(chǔ)了 y1 作為校驗(yàn)爽蝴,原始密碼是 x1沐批。如果有人碰撞出了 x2 纫骑,在我們的數(shù)據(jù)庫中就驗(yàn)證成功了。
而現(xiàn)在又有函數(shù) hash2九孩,并且
并且有
同時(shí)又有
如果用戶使用 x11 作為密碼先馆,那么使用 x12, x21, x22 都可以得到相同的 z1,所以更加不安全了躺彬。
換種思路竊取密碼
如果我們按照上面的策略存儲(chǔ)密碼了磨隘,可以暫時(shí)認(rèn)為數(shù)據(jù)庫方面是安全的了。如果要想竊取用戶的密碼顾患,就應(yīng)該從更薄弱的環(huán)節(jié)入手,比如網(wǎng)絡(luò)傳輸「鲞螅現(xiàn)在仍然有大量的網(wǎng)站沒有使用 HTTPS 傳輸數(shù)據(jù)江解,這意味著用戶發(fā)送的數(shù)據(jù)可能在經(jīng)過的每一個(gè)路由節(jié)點(diǎn)上被監(jiān)聽到。所以還沒等服務(wù)器拿到用戶的密碼原文徙歼,中間人已經(jīng)獲取到所有想要的信息了犁河。
這時(shí)候怎么辦呢?最好的解決辦法就是換成 HTTPS魄梯,從根本上避免這種監(jiān)聽桨螺。但如果做不到,我們可以退而求其次想一些折衷的辦法[3]酿秸。
策略一灭翔,前端直接 hash 密碼送到后端進(jìn)行加鹽 hash。不可行辣苏。因?yàn)槿绻腥藦膭e處已經(jīng)得到了一些 hash 后的值肝箱,那么他就不需要猜測用戶原來的密碼,直接把 hash 送到后端進(jìn)行驗(yàn)證就行了稀蟋,反而降低了難度煌张。
策略二,在前端加鹽 hash 退客,再傳到后端直接進(jìn)行比對骏融。不可行。這給黑客提供了一個(gè)方便——他不需要知道密碼就可以方便地在你這里驗(yàn)證某些用戶名或郵箱是否是有效的萌狂。
策略三档玻,既然不能從后端獲取 salt ,那簡便的方法就是使用前后端約定好的一個(gè)固定的 salt 進(jìn)行 hash粥脚,比如用戶的用戶名窃肠,或者郵箱。這樣就保證了中間人監(jiān)聽不到真實(shí)的密碼刷允,同時(shí)又因?yàn)樵诤蠖擞诌M(jìn)行了一次安全的加鹽 hash 冤留,保證了數(shù)據(jù)庫的安全性碧囊。
用什么 hash 函數(shù)?
- 經(jīng)過安全測試的加密 hash 函數(shù)纤怒,如: SHA256, SHA512, RipeMD, WHIRLPOOL, SHA3 等等
- Key Stretching 算法糯而,如: PBKDF2, bcrypt, scrypt 等
- 安全版本的 Unix crypt,如: $2y$, $5$, $6$
總結(jié)
- 前端使用固定 salt 加密后送給后端
- 后端生成強(qiáng)大的 salt 將前端送來的值加密儲(chǔ)存
- 使用安全的 hash 函數(shù)
- 如果可能泊窘,使用 HTTPS