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 // 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 // ...
这种方法就防止了我们受到彩虹表的危害,因为每一个密码都使用一个不同的字符串进行了干扰。攻击者需要创建和密码数量一样的彩虹表,这是很不切实际的。
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 // 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
// ...
}