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应用程序来说已经足够安全了。不过不要忘记你还是可以让用户使用安全强度更高的密码,比如要求最少位数,使用字母,数字和特殊字符混合密码等。

posted on 2013-05-15 16:39  jlokys  阅读(4282)  评论(1编辑  收藏  举报