密碼存儲中MD5的安全問題與替代方案
md5安全嗎?有多么地不安全?如何才能安全地存儲密碼?…
md5安全嗎?
經(jīng)過各種安全事件后,很多系統(tǒng)在存放密碼的時候不會直接存放明文密碼了,大都改成了存放了 md5 加密(hash)后的密碼,可是這樣真的安全嗎?
這兒有個腳本來測試下MD5的速度, 測試結(jié)果:
- [root@f4d5945f1d7c tools]# php speed-of-md5.php
- Array
- (
- [rounds] => 100
- [times of a round] => 1000000
- [avg] => 0.23415904045105
- [max] => 0.28906106948853
- [min] => 0.21188998222351
- )
有沒有發(fā)現(xiàn)一個問題:MD5速度太快了,導(dǎo)致很容易進(jìn)行暴力破解.
簡單計算一下:
- > Math.pow(10, 6) / 1000000 * 0.234
- 0.234
- > Math.pow(36, 6) / 1000000 * 0.234 / 60
- 8.489451110400001
- > Math.pow(62, 6) / 1000000 * 0.234 / 60 / 60
- 3.69201531296
- 使用6位純數(shù)字密碼,破解只要0.234秒!
- 使用6位數(shù)字+小寫字母密碼,破解只要8.49分鐘!
- 使用6位數(shù)字+大小寫混合字母密碼,破解只要3.69個小時!
當(dāng)然,使用長一點的密碼會顯著提高破解難度:
- > Math.pow(10, 8) / 1000000 * 0.234
- 23.400000000000002
- > Math.pow(36, 8) / 1000000 * 0.234 / 60 / 60 / 24
- 7.640505999359999
- > Math.pow(62, 8) / 1000000 * 0.234 / 60 / 60 / 24 / 365
- 1.6201035231755982
- 使用8位純數(shù)字密碼,破解要23.4秒!
- 使用8位數(shù)字+小寫字母密碼,破解要7.64小時!
- 使用8位數(shù)字+大小寫混合字母密碼,破解要1.62年!
但是,別忘了,這個速度只是用PHP這個解釋型語言在筆者的弱雞個人電腦(i5-4460 CPU 3.20GHz)上跑出來的,還只是利用了一個線程一個CPU核心。若是放到最新的 Xeon E7 v4系列CPU的服務(wù)器上跑,充分利用其48個線程,并使用C語言來重寫下測試代碼,很容易就能提升個幾百上千倍速度。那么即使用8位數(shù)字+大小寫混合字母密碼,破解也只要14小時!
更何況,很多人的密碼都是采用比較有規(guī)律的字母或數(shù)字,更能降低暴力破解的難度… 如果沒有加鹽或加固定的鹽,那么彩虹表破解就更easy了…
那么如何提升密碼存儲的安全性呢?bcrypt!
提升安全性就是提升密碼的破解難度,至少讓暴力破解難度提升到攻擊者無法負(fù)擔(dān)的地步。(當(dāng)然用戶密碼的長度當(dāng)然也很重要,建議至少8位,越長越安全)
這里不得不插播一句:PHP果然是世界上最好的語言 — 標(biāo)準(zhǔn)庫里面已經(jīng)給出了解決方案。
PHP 5.5 的版本中加入了 password_xxx 系列函數(shù), 而對之前的版本,也有兼容庫可以用:password_compat.
在這個名叫“密碼散列算法”的核心擴(kuò)展中提供了一系列簡潔明了的對密碼存儲封裝的函數(shù)。簡單介紹下:
- password_hash 是對密碼進(jìn)行加密(hash),目前默認(rèn)用(也只能用)bcrypt算法,相當(dāng)于一個加強(qiáng)版的md5函數(shù)
- password_verify 是一個驗證密碼的函數(shù),內(nèi)部采用的安全的字符串比較算法,可以預(yù)防基于時間的攻擊, 相當(dāng)于 $hashedPassword === md5($inputPassword)
- password_needs_rehash 是判斷是否需要升級的一個函數(shù),這個函數(shù)厲害了,下面再來詳細(xì)講
password_hash 需要傳入一個算法,現(xiàn)在默認(rèn)和可以使用的都只有bcrypt算法,這個算法是怎么樣的一個算法呢?為什么PHP標(biāo)準(zhǔn)庫里面會選擇bcrypt呢?
bcrypt是基于 Blowfish 算法的一種專門用于密碼哈希的算法,由 Niels Provos 和 David Mazieres 設(shè)計的。這個算法的特別之處在于,別的算法都是追求快,這個算法中有一個至關(guān)重要的參數(shù):cost. 正如其名,這個值越大,耗費的時間越長,而且是指數(shù)級增長 — 其加密流程中有一部分是這樣的:
- EksBlowfishSetup(cost, salt, key)
- state <- InitState()
- state <- ExpandKey(state, salt, key)
- repeat (2^cost) // "^"表示指數(shù)關(guān)系
- state <- ExpandKey(state, 0, key)
- state <- ExpandKey(state, 0, salt)
- return state
比如下面是筆者的一次測試結(jié)果(個人弱機(jī)PC, i5-4460 CPU 3.20GHz) :
- cost time
- 8 0.021307
- 9 0.037150
- 10 0.079283
- 11 0.175612
- 12 0.317375
- 13 0.663080
- 14 1.330451
- 15 2.245152
- 16 4.291169
- 17 8.318790
- 18 16.472902
- 19 35.146999
附:測試代碼
這個速度與md5相比簡直是蝸牛與獵豹的差別 — 即使按照cost=8, 一個8位的大小寫字母+數(shù)字的密碼也要14萬年才能暴力破解掉,更何況一般服務(wù)器都會至少設(shè)置為10或更大的值(那就需要54萬年或更久了)。
顯然,cost不是越大越好,越大的話會越占用服務(wù)器的CPU,反而容易引起DOS攻擊。建議根據(jù)服務(wù)器的配置和業(yè)務(wù)的需求設(shè)置為10~12即可。最好同時對同一IP同一用戶的登錄嘗試次數(shù)做限制,預(yù)防DOS攻擊。
一個安全地存儲密碼的方案
總上所述,一個安全地存儲密碼的方案應(yīng)該是這樣子的:(直接放代碼吧)
- class User extends BaseModel
- {
- const PASSWORD_COST = 11; // 這里配置bcrypt算法的代價,根據(jù)需要來隨時升級
- const PASSWORD_ALGO = PASSWORD_BCRYPT; // 默認(rèn)使用(現(xiàn)在也只能用)bcrypt
- /**
- * 驗證密碼是否正確
- *
- * @param string $plainPassword 用戶密碼的明文
- * @param bool $autoRehash 是否自動重新計算下密碼的hash值(如果有必要的話)
- * @return bool
- */
- public function verifyPassword($plainPassword, $autoRehash = true)
- {
- if (password_verify($plainPassword, $this->password)) {
- if ($autoRehash && password_needs_rehash($this->password, self::PASSWORD_ALGO, ['cost' => self::PASSWORD_COST])) {
- $this->updatePassword($plainPassword);
- }
- return true;
- }
- return false;
- }
- /**
- * 更新密碼
- *
- * @param string $newPlainPassword
- */
- public function updatePassword($newPlainPassword)
- {
- $this->password = password_hash($newPlainPassword, self::PASSWORD_ALGO, ['cost' => self::PASSWORD_COST]);
- $this->save();
- }
- }
這樣子,在用戶注冊或修改密碼的時候就調(diào)用 $user->updatePassword() 來設(shè)置密碼,而登錄的時候就調(diào)用 $user->verifyPassword() 來驗證下密碼是否正確。
當(dāng)硬件性能提升到一定程度,而cost=11無法滿足安全需求的時候,則修改下 PASSWORD_COST 的值即可無縫升級,讓存放的密碼更安全。