|
密碼學(xué)是一個(gè)復(fù)雜的話題,我也不是這方面的專家。許多高校和研究機(jī)構(gòu)在這方面都有長(zhǎng)期的研究。在這篇文章里,我希望盡量使用簡(jiǎn)單易懂的方式向你展示一種安全存儲(chǔ)Web程序密碼的方法。
2.“Hash”是做什么的?
“Hash將一段數(shù)據(jù)(小數(shù)據(jù)或大數(shù)據(jù))轉(zhuǎn)換成一段相對(duì)短小的數(shù)據(jù),如字符串或整數(shù)。”
這是依靠單向hash函數(shù)來完成的。所謂單向是指很難(或者是實(shí)際上不可能)將其反轉(zhuǎn)回來。一個(gè)常見的hash函數(shù)的例子是md5(),它流行于各種計(jì)算機(jī)語(yǔ)言和系統(tǒng)。
復(fù)制代碼 代碼如下:
$data = "Hello World";
$hash = md5($data);
echo $hash; // b10a8db164e0754105b7a99be72e3fe5
使用md5()運(yùn)算出來的結(jié)果總是32個(gè)字符的字符串,不過它只包含16進(jìn)制的字符,從技術(shù)上來說它也可以用128位(16字節(jié))的整形數(shù)來表示。你可以使用md5()來處理很長(zhǎng)的字符串和數(shù)據(jù),但是你始終得到的是一個(gè)固定長(zhǎng)度的hash值,這也可能可以幫助你理解為什么這個(gè)函數(shù)是“單向”的。
3.使用Hash函數(shù)來存儲(chǔ)密碼
典型的用戶注冊(cè)過程:
用戶填寫注冊(cè)表單,其中包含密碼字段;
程序?qū)⑺杏脩籼顚懙男畔?a href=/pingce/cunchu/ target=_blank class=infotextkey>存儲(chǔ)到數(shù)據(jù)庫(kù)中;
然而密碼在存儲(chǔ)到數(shù)據(jù)庫(kù)前通過hash函數(shù)加密處理;
原始的密碼不再存儲(chǔ)在任何地方,或者說它被丟棄了。
用戶登錄過程:
用戶輸入用戶名和密碼;
程序?qū)⒚艽a通過以注冊(cè)相同的hash函數(shù)進(jìn)行加密;
程序從數(shù)據(jù)庫(kù)查到用戶,并讀取hash后的密碼;
程序比較用戶名和密碼,如果匹配則給用戶授權(quán)。
如何選擇合適的方法來加密密碼,我們將在文章的后面討論這個(gè)問題。
4.問題1:hash碰撞
hash碰撞是指對(duì)兩個(gè)不同的內(nèi)容進(jìn)行hash得到了相同的hash值。發(fā)生hash碰撞的可能性取決于所用的hash算法。
如何產(chǎn)生?
舉個(gè)例子,一些老式程序使用crc32()來hash密碼,這種算法產(chǎn)生一個(gè)32位的整數(shù)作為hash結(jié)果,這意味著只有2^32 (即4,294,967,296) 種可能的輸出結(jié)果。
讓我們來hash一個(gè)密碼:
復(fù)制代碼 代碼如下:
echo crc32('supersecretpassword');
// outputs: 323322056
現(xiàn)在我們假設(shè)一個(gè)人竊取了數(shù)據(jù)庫(kù),得到了hash過的密碼。他可能不能將323322056還原為‘supersecretpassword',然而他可以找到另一個(gè)密碼,也能被hash出同樣的值。這只需要一個(gè)很簡(jiǎn)單的程序:
復(fù)制代碼 代碼如下:
set_time_limit(0);
$i = 0;
while (true) {
if (crc32(base64_encode($i)) == 323322056) {
echo base64_encode($i);
exit;
}
$i++;
}
這個(gè)程序可能需要運(yùn)行一段時(shí)間,但是最終它能返回一個(gè)字符串。我們可以使用這個(gè)字符串來代替‘supersecretpassword',并使用它成功的登錄使用該密碼的用戶帳戶。
比如在我的電腦上運(yùn)行上面的程序幾個(gè)月后,我得到了一個(gè)字符串:‘MTIxMjY5MTAwNg=='。我們來測(cè)試一下:
復(fù)制代碼 代碼如下:
echo crc32('supersecretpassword');
// outputs: 323322056
echo crc32('MTIxMjY5MTAwNg==');
// outputs: 323322056
如何解決?
現(xiàn)在一個(gè)稍強(qiáng)一點(diǎn)的家用PC機(jī)就可以一秒鐘運(yùn)行十億次hash函數(shù),所以我們需要一個(gè)能產(chǎn)生更大范圍的結(jié)果的hash函數(shù)。比如md5()就更合適一些,它可以產(chǎn)生128位的hash值,也就是有340,282,366,920,938,463,463,374,607,431,768,211,456種可能的 輸出。所以人們一般不可能做那么多次循環(huán)來找到hash碰撞。然而仍然有人找到方法來做這件事情,詳細(xì)可以查看例子。
sha1()是一個(gè)更好的替代方案,因?yàn)樗a(chǎn)生長(zhǎng)達(dá)160位的hash值。
5.問題2:彩虹表
即使我們解決了碰撞問題,還是不夠安全。
“彩虹表通過計(jì)算常用的詞及它們的組合的hash值建立起來的表?!?
這個(gè)表可能存儲(chǔ)了幾百萬(wàn)甚至十億條數(shù)據(jù)?,F(xiàn)在存儲(chǔ)已經(jīng)非常的便宜,所以可以建立非常大的彩虹表。
現(xiàn)在我們假設(shè)一個(gè)人竊取了數(shù)據(jù)庫(kù),得到了幾百萬(wàn)個(gè)hash過的密碼。竊取者可以很容易地一個(gè)一個(gè)地在彩虹表中查找這些hash值,并得到原始密碼。雖然不是所有的hash值都能在彩虹表中找到,但是肯定會(huì)有能找到的。
如何解決?
我們可以嘗試給密碼加點(diǎn)干擾,比如下面的例子:
復(fù)制代碼 代碼如下:
$password = "easypassword";
// this may be found in a rainbow table
// because the password contains 2 common words
echo sha1($password); // 6c94d3b42518febd4ad747801d50a8972022f956
// use bunch of random characters, and it can be longer than this
$salt = "f#@V)Hu^%Hgfds";
// this will NOT be found in any pre-built rainbow table
echo sha1($salt . $password); // cd56a16759623378628c0d9336af69b74d9d71a5
在這里我們所做的只是在每個(gè)密碼前附加上一個(gè)干擾字符串后進(jìn)行hash,只要附加的字符串足夠復(fù)雜,hash后的值肯定是在預(yù)建的彩虹表中找不到的。不過現(xiàn)在還是不夠安全。
6.問題3:還是彩虹表
注意,彩虹表可能在竊取到干攏字符串后重頭開始建立。干擾字符串一樣也可能被和數(shù)據(jù)庫(kù)一起被竊取,然后他們可以利用這個(gè)干擾字符串從頭開始創(chuàng)建彩虹表,如“easypassword”的hash值可能在普通的彩虹表中存在,但是在新建的彩虹表里,“f#@V)Hu^%Hgfdseasypassword”的hash值也會(huì)存在。
如何解決?
我們可以對(duì)每個(gè)用戶使用唯一的干擾字符串。一個(gè)可用的方案就是使用用戶在數(shù)據(jù)庫(kù)中的id:
復(fù)制代碼 代碼如下:
$hash = sha1($user_id . $password);
這種方法的前提是用戶的id是一個(gè)不變的值(一般應(yīng)用都是這樣的)
我們也可以為每個(gè)用戶隨機(jī)生成一串唯一的干擾字符串,不過我們也需要將這個(gè)串存儲(chǔ)起來:
復(fù)制代碼 代碼如下:
// generates a 22 character long random string
function unique_salt() {
return substr(sha1(mt_rand()),0,22);
}
$unique_salt = unique_salt();
$hash = sha1($unique_salt . $password);
// and save the $unique_salt with the user record
// ...
這種方法就防止了我們受到彩虹表的危害,因?yàn)槊恳粋€(gè)密碼都使用一個(gè)不同的字符串進(jìn)行了干擾。攻擊者需要?jiǎng)?chuàng)建和密碼數(shù)量一樣的彩虹表,這是很不切實(shí)際的。
7.問題4:hash速度
大部分hash算法在設(shè)計(jì)時(shí)就考慮了速度問題,因?yàn)樗话阌脕碛?jì)算大數(shù)據(jù)或文件的hash值,以驗(yàn)證數(shù)據(jù)的正確性和完整性。
如何產(chǎn)生?
如前所述,現(xiàn)在一臺(tái)強(qiáng)勁的PC機(jī)可以一秒運(yùn)算數(shù)十億次,很容易用暴力破解法去嘗試每個(gè)密碼。你可能會(huì)以為8個(gè)以上字符的密碼就可以避免被暴力破解了,但是讓我們來看看是否真是這樣:
如果密碼可以包含小寫字母,大寫字母和數(shù)字,那就有62(26+26+10)個(gè)字符可選;
一個(gè)8位的密碼有62^8種可能組合,這個(gè)數(shù)字略大于218萬(wàn)億。
以一秒鐘運(yùn)算10億次hash值的速度計(jì)算,這只需要60小時(shí)就可以解決。
對(duì)于一個(gè)6位的密碼,也是很常用的密碼,只需要1分鐘就可以破解。要求9到10位的密碼可能會(huì)比較安全了,不過這樣有的用戶可能會(huì)覺得很麻煩。
如何解決?
使用慢一點(diǎn)的hash函數(shù)。
“假設(shè)你使用一個(gè)在相同硬件條件下一秒鐘只能運(yùn)行100萬(wàn)次的算法來代替一秒10億次的算法,那么攻擊者可能需要要花1000倍的時(shí)間來做暴力破解,60小只將會(huì)變成7年!”
你可以自己實(shí)現(xiàn)這種方法:
復(fù)制代碼 代碼如下:
function myhash($password, $unique_salt) {
$salt = "f#@V)Hu^%Hgfds";
$hash = sha1($unique_salt . $password);
// make it take 1000 times longer
for ($i = 0; $i < 1000; $i++) {
$hash = sha1($hash);
}
return $hash;
}
你也可以使用一個(gè)支持“成本參數(shù)”的算法,比如 BLOWFISH。在php中可以用crypt()函數(shù)實(shí)現(xiàn):
復(fù)制代碼 代碼如下:
function myhash($password, $unique_salt) {
// the salt for blowfish should be 22 characters long
return crypt($password, '$2a$10.$unique_salt');
}
這個(gè)函數(shù)的第二個(gè)參數(shù)包含了由”$”符號(hào)分隔的幾個(gè)值。第一個(gè)值是“$2a”,指明應(yīng)該使用BLOWFISH算法。第二個(gè)參數(shù)“$10”在這里就是成本參數(shù),這是以2為底的對(duì)數(shù),指示計(jì)算循環(huán)迭代的次數(shù)(10 => 2^10 = 1024),取值可以從04到31。
舉個(gè)例子:
復(fù)制代碼 代碼如下:
function myhash($password, $unique_salt) {
return crypt($password, '$2a$10.$unique_salt');
}
function unique_salt() {
return substr(sha1(mt_rand()),0,22);
}
$password = "verysecret";
echo myhash($password, unique_salt());
// result: $2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC
結(jié)果的hash值包含$2a算法,成本參數(shù)$10,以及一個(gè)我們使用的22位干擾字符串。剩下的就是計(jì)算出來的hash值,我們來運(yùn)行一個(gè)測(cè)試程序:
復(fù)制代碼 代碼如下:
// assume this was pulled from the database
$hash = '$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC';
// assume this is the password the user entered to log back in
$password = "verysecret";
if (check_password($hash, $password)) {
echo "Access Granted!";
} else {
echo "Access Denied!";
}
function check_password($hash, $password) {
// first 29 characters include algorithm, cost and salt
// let's call it $full_salt
$full_salt = substr($hash, 0, 29);
// run the hash function on $password
$new_hash = crypt($password, $full_salt);
// returns true or false
return ($hash == $new_hash);
}
運(yùn)行它,我們會(huì)看到”Access Granted!”
8.整合起來
根據(jù)以上的幾點(diǎn)討論,我們寫了一個(gè)工具類:
復(fù)制代碼 代碼如下:
class PassHash {
// blowfish
private static $algo = '$2a';
// cost parameter
private static $cost = '$10';
// mainly for internal use
public static function unique_salt() {
return substr(sha1(mt_rand()),0,22);
}
// this will be used to generate a hash
public static function hash($password) {
return crypt($password,
self::$algo .
self::$cost .
'$'. self::unique_salt());
}
// this will be used to compare a password against a hash
public static function check_password($hash, $password) {
$full_salt = substr($hash, 0, 29);
$new_hash = crypt($password, $full_salt);
return ($hash == $new_hash);
}
}
以下是注冊(cè)時(shí)的用法:
復(fù)制代碼 代碼如下:
// include the class
require ("PassHash.php");
// read all form input from $_POST
// ...
// do your regular form validation stuff
// ...
// hash the password
$pass_hash = PassHash::hash($_POST['password']);
// store all user info in the DB, excluding $_POST['password']
// store $pass_hash instead
// ...
以下是登錄時(shí)的用法:
復(fù)制代碼 代碼如下:
// include the class
require ("PassHash.php");
// read all form input from $_POST
// ...
// fetch the user record based on $_POST['username'] or similar
// ...
// check the password the user tried to login with
if (PassHash::check_password($user['pass_hash'], $_POST['password']) {
// grant access
// ...
} else {
// deny access
// ...
}
9.加密是否可用
并不是所有系統(tǒng)都支持Blowfish加密算法,雖然它現(xiàn)在已經(jīng)很普遍了,你可以用以下代碼來檢查你的系統(tǒng)是否支持:
復(fù)制代碼 代碼如下:
if (CRYPT_BLOWFISH == 1) {
echo "Yes";
} else {
echo "No";
}
不過對(duì)于php5.3,你就不必?fù)?dān)心這點(diǎn)了,因?yàn)樗鼉?nèi)置了這個(gè)算法的實(shí)現(xiàn)。
結(jié)論
通過這種方法加密的密碼對(duì)于絕大多數(shù)Web應(yīng)用程序來說已經(jīng)足夠安全了。不過不要忘記你還是可以讓用戶使用安全強(qiáng)度更高的密碼,比如要求最少位數(shù),使用字母,數(shù)字和特殊字符混合密碼等。
php技術(shù):理解php Hash函數(shù),增強(qiáng)密碼安全,轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。