php - Hash函數理解,增強密碼安全


◆聲明

本文只是簡易的討論,向你展示一種安全存儲Web程序密碼的方法

 

◆基本知識

Hash,一般翻譯做“散列”,也有直接音譯為“哈希”的,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。

目前一個很通常的例子是,我們通常用md5去做密碼的加密

1 $data = "Hello World"; 
2 $hash = md5($data); 
3 echo $hash; // b10a8db164e0754105b7a99be72e3fe5 

 使用md5()運算出來的結果總是32個字符的字符串(md5() 第二個參數設置true,將得到16個字符的散列值),不過它只包含16進制的字符。你可以使用md5()來處理很長的字符串和數據,但是你始終得到的是一個固定長度的hash值,這也可能可以幫助你理解為什么這個函數是“單向”的。

 

◆使用Hash函數來存儲密碼的流程


用戶注冊流程:
1. 用戶填寫注冊表單,其中包含密碼字段
2. 程序將用戶填寫信息存儲數據庫(密碼存儲到數據庫前通過hash函數加密處理)

用戶登錄流程:
1. 用戶輸入用戶名和密碼
2. 程序將密碼通過以注冊相同的hash函數進行加密
3. 程序從數據庫查到用戶,並讀取hash后的密碼
4. 程序比較用戶名和密碼是否匹配,如果匹配則給用戶授權

 

◆涉及到的問題:

1. hash碰撞
hash碰撞是指對兩個不同的內容進行hash得到了相同的hash值。發生hash碰撞取決所用的hash算法。
一些老式程序使用crc32()來hash密碼,這種算法產生一個32位的整數作為hash結果,這意味着只有2^32 (即4,294,967,296) 種可能的輸出結果。

1 echo crc32('supersecretpassword'); 
2 // outputs: 323322056 

 現在我們假設一個人竊取了數據庫,得到了hash過的密碼。他可能不能將323322056還原為‘supersecretpassword',然而他可以找到另一個密碼,也能被hash出同樣的值。這只需要一個很簡單的程序:

1 set_time_limit(0); 
2 $i = 0; 
3 while (true) { 
4 if (crc32(base64_encode($i)) == 323322056) { 
5 echo base64_encode($i); 
6 exit; 
7 } 
8 $i++; 
9 } 

 這個程序可能需要運行一段時間,但是最終它能返回一個字符串。我們可以使用這個字符串來代替‘supersecretpassword',並使用它成功的登錄使用該密碼的用戶帳戶。
比如在我的電腦上運行上面的程序幾個月后,我得到了一個字符串:‘MTIxMjY5MTAwNg=='。我們來測試一下:

1 echo crc32('supersecretpassword'); 
2 // outputs: 323322056 
3 echo crc32('MTIxMjY5MTAwNg=='); 
4 // outputs: 323322056 

 

如何解決?
現在一個稍強一點的家用PC機就可以一秒鍾運行十億次hash函數,所以我們需要一個能產生更大范圍的結果的hash函數。比如md5()就更合適一些,它可以產生128位的hash值,也就是有340,282,366,920,938,463,463,374,607,431,768,211,456種可能的 輸出。所以人們一般不可能做那么多次循環來找到hash碰撞。然而仍然有人找到方法來做這件事情,詳細可以查看例子。
sha1()是一個更好的替代方案,因為它產生長達160位的hash值。


2.彩虹表
即使我們解決了碰撞問題,還是不夠安全。
“彩虹表通過計算常用的詞及它們的組合的hash值建立起來的表。”
這個表可能存儲了幾百萬甚至十億條數據。現在存儲已經非常的便宜,所以可以建立非常大的彩虹表。
現在我們假設一個人竊取了數據庫,得到了幾百萬個hash過的密碼。竊取者可以很容易地一個一個地在彩虹表中查找這些hash值,並得到原始密碼。雖然不是所有的hash值都能在彩虹表中找到,但是肯定會有能找到的。
如何解決?
我們可以嘗試給密碼加點干擾,比如下面的例子:

1 $password = "easypassword"; 
2 // this may be found in a rainbow table 
3 // because the password contains 2 common words 
4 echo sha1($password); // 6c94d3b42518febd4ad747801d50a8972022f956 
5 // use bunch of random characters, and it can be longer than this 
6 $salt = "f#@V)Hu^%Hgfds"; 
7 // this will NOT be found in any pre-built rainbow table 
8 echo sha1($salt . $password); // cd56a16759623378628c0d9336af69b74d9d71a5 

在這里我們所做的只是在每個密碼前附加上一個干擾字符串后進行hash,只要附加的字符串足夠復雜,hash后的值肯定是在預建的彩虹表中找不到的。不過現在還是不夠安全。

3:還是彩虹表
注意,彩虹表可能在竊取到干攏字符串后重頭開始建立。干擾字符串一樣也可能被和數據庫一起被竊取,然后他們可以利用這個干擾字符串從頭開始創建彩虹表,如“easypassword”的hash值可能在普通的彩虹表中存在,但是在新建的彩虹表里,“f#@V)Hu^%Hgfdseasypassword”的hash值也會存在。
如何解決?
我們可以對每個用戶使用唯一的干擾字符串。一個可用的方案就是使用用戶在數據庫中的id:

1 $hash = sha1($user_id . $password); 

這種方法的前提是用戶的id是一個不變的值(一般應用都是這樣的)
我們也可以為每個用戶隨機生成一串唯一的干擾字符串,不過我們也需要將這個串存儲起來:

1 // generates a 22 character long random string 
2 function unique_salt() { 
3 return substr(sha1(mt_rand()),0,22); 
4 } 
5 $unique_salt = unique_salt(); 
6 $hash = sha1($unique_salt . $password); 
7 // and save the $unique_salt with the user record 
8 // ... 

這種方法就防止了我們受到彩虹表的危害,因為每一個密碼都使用一個不同的字符串進行了干擾。攻擊者需要創建和密碼數量一樣的彩虹表,這是很不切實際的。


4:hash速度
大部分hash算法在設計時就考慮了速度問題,因為它一般用來計算大數據或文件的hash值,以驗證數據的正確性和完整性。
如何產生?
如前所述,現在一台強勁的PC機可以一秒運算數十億次,很容易用暴力破解法去嘗試每個密碼。你可能會以為8個以上字符的密碼就可以避免被暴力破解了,但是讓我們來看看是否真是這樣:
如果密碼可以包含小寫字母,大寫字母和數字,那就有62(26+26+10)個字符可選;
一個8位的密碼有62^8種可能組合,這個數字略大於218萬億。
以一秒鍾運算10億次hash值的速度計算,這只需要60小時就可以解決。
對於一個6位的密碼,也是很常用的密碼,只需要1分鍾就可以破解。要求9到10位的密碼可能會比較安全了,不過這樣有的用戶可能會覺得很麻煩。
如何解決?
使用慢一點的hash函數。
“假設你使用一個在相同硬件條件下一秒鍾只能運行100萬次的算法來代替一秒10億次的算法,那么攻擊者可能需要要花1000倍的時間來做暴力破解,60小只將會變成7年!”
你可以自己實現這種方法:

1 function myhash($password, $unique_salt) { 
2 $salt = "f#@V)Hu^%Hgfds"; 
3 $hash = sha1($unique_salt . $password); 
4 // make it take 1000 times longer 
5 for ($i = 0; $i < 1000; $i++) { 
6 $hash = sha1($hash); 
7 } 
8 return $hash; 
9 } 

你也可以使用一個支持“成本參數”的算法,比如 BLOWFISH。在php中可以用crypt()函數實現:

1 function myhash($password, $unique_salt) { 
2 // the salt for blowfish should be 22 characters long 
3 return crypt($password, '$2a$10.$unique_salt'); 
4 } 

這個函數的第二個參數包含了由”$”符號分隔的幾個值。第一個值是“$2a”,指明應該使用BLOWFISH算法。第二個參數“$10”在這里就是成本參數,這是以2為底的對數,指示計算循環迭代的次數(10 => 2^10 = 1024),取值可以從04到31。
舉個例子:

1 function myhash($password, $unique_salt) { 
2 return crypt($password, '$2a$10.$unique_salt'); 
3 } 
4 function unique_salt() { 
5 return substr(sha1(mt_rand()),0,22); 
6 } 
7 $password = "verysecret"; 
8 echo myhash($password, unique_salt()); 
9 // result: $2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC 

結果的hash值包含$2a算法,成本參數$10,以及一個我們使用的22位干擾字符串。剩下的就是計算出來的hash值,我們來運行一個測試程序:

 1 // assume this was pulled from the database 
 2 $hash = '$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC'; 
 3 // assume this is the password the user entered to log back in 
 4 $password = "verysecret"; 
 5 if (check_password($hash, $password)) { 
 6 echo "Access Granted!"; 
 7 } else { 
 8 echo "Access Denied!"; 
 9 } 
10 function check_password($hash, $password) { 
11 // first 29 characters include algorithm, cost and salt 
12 // let's call it $full_salt 
13 $full_salt = substr($hash, 0, 29); 
14 // run the hash function on $password 
15 $new_hash = crypt($password, $full_salt); 
16 // returns true or false 
17 return ($hash == $new_hash); 
18 } 

運行它,我們會看到”Access Granted!”

◆整合起來


根據以上的幾點討論,我們寫了一個工具類:

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); 
} 
} 

 

以下是注冊時的用法:

// 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 
// ... 

 

以下是登錄時的用法:

// 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 
// ... 
} 

 
◆加密是否可用
並不是所有系統都支持Blowfish加密算法,雖然它現在已經很普遍了,你可以用以下代碼來檢查你的系統是否支持:

if (CRYPT_BLOWFISH == 1) { 
echo "Yes"; 
} else { 
echo "No"; 
} 

不過對於php5.3,你就不必擔心這點了,因為它內置了這個算法的實現。

 

◆結論
通過這種方法加密的密碼對於絕大多數Web應用程序來說已經足夠安全了。不過不要忘記你還是可以讓用戶使用安全強度更高的密碼,比如要求最少位數,使用字母,數字和特殊字符混合密碼等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM