关于php:Crypt函数在比较字符串到hash时不起作用

Crypt function doesn't work when comparing string to hash

我使用的是一种非常标准的cookie登录方式——我给用户两个cookie,一个使用用户名,另一个使用随机生成的字符串和用户特定的salt。

登录时会发生以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
$_SESSION['username']=$row[username];
$_SESSION['user_id']=$row['id'];
$loginhash=generateRandomBase64String()."_".$row['salt'];
$number_of_days = 14;
$date_of_expiry = time() + 60 * 60 * 24 * $number_of_days ;
setcookie("userlogin", $row['username'], $date_of_expiry,"/" ) ;
setcookie("loginhash", $loginhash, $date_of_expiry,"/" ) ;
$cryptedhash=crypt($loginhash);

$today=date("Y-m-d");

mysql_query("update members set last_login='$today',loginhash='$cryptedhash' where id='$row[id]'") or die(mysql_error());

因此,$loginhash值类似于Pe0vFou8qe++CqhcJgFtRmoAldpuIs+d_g5oijF76,其加密版本存储在数据库中。salt已经在数据库中,因为它是在每个用户注册时为他们生成的。

我使用会话变量($_SESSION[username]来保持用户登录。然后,当用户访问站点时,我检查两件事情:如果没有设置$_SESSION[username],但$_COOKIE[userlogin],我检查哈希是否正确,以便登录用户。问题是,哈希值永远不正确。

1
2
3
4
5
6
7
8
9
10
11
12
if($_COOKIE['userlogin'] && !isset($_SESSION[user_id])){
    $username=mysql_real_escape_string($_COOKIE['userlogin']);
    $loginhash=mysql_real_escape_string($_COOKIE['loginhash']);
    $salt=substr($loginhash,-8);
    $result=mysql_query("select * from members where (username='$username' || email='$username') && salt='$salt' limit 1") or die (mysql_error());
    $row=mysql_fetch_assoc($result);
    $cryptedhash=$row['loginhash'];
    if (crypt($loginhash, $cryptedhash) == $cryptedhash){
        $_SESSION['username']=$row[username];
        $_SESSION['user_id']=$row['id'];
    }
}

$_COOKIE[userlogin]是正确的值。当我检查数据库中的用户名/salt组合时,会发现正确的结果(echo $row[username]给出了正确的值)。但是,下面的if条件从未满足。我会觉得我的PHP配置有点奇怪,但我使用相同的加密机制来存储密码,在那里它可以正常工作。

有人能看到这里出了什么问题吗?

ps我不想在这里开始讨论cookie安全性或各种可用的散列函数。


问题是:

在对crypt()的第一个调用中,不指定salt。在对crypt()的第二个调用中,您将$cryptedHash作为salt传递。

如果不提供随机salt,则记录crypt()以生成随机salt,然后将该salt预先发送到返回的哈希。这样做的副作用是,如果将返回的salt+哈希作为后续调用的哈希传递,crypt()仍将从中提取正确的salt。

不幸的是,使用的算法和salt+哈希的长度/格式是基于操作系统、PHP版本以及是否指定salt参数的组合。当您以前使用代码时,您遇到了一个愉快的意外,即在对crypt()的两个调用中都选择了des。现在,您的环境对crypt()的两个调用使用了不同的算法,因为您只在其中一个调用中提供了散列。

解决方案是向crypt()的两个调用传递一致的salt。您可以停止将salt附加到要散列的字符串中,并实际将用户salt作为salt参数传递,一切都会好起来的。


如果您使用的是5.5.0或更高版本的PHP,那么您应该看看phpDoc-密码哈希!我知道你说过你不想讨论不同的散列函数,但我认为这一个让你更容易,因为它应该摆脱你的问题,而且(在我看来)更容易使用!

下面是您的新函数代码:(未测试,如果中断请评论)

1
2
3
4
5
6
7
8
9
10
11
12
13
$_SESSION['username']=$row[username];
$_SESSION['user_id']=$row['id'];
$loginhash=generateRandomBase64String()."_".$row['salt'];
$number_of_days = 14;
$date_of_expiry = time() + 60 * 60 * 24 * $number_of_days ;
setcookie("userlogin", $row['username'], $date_of_expiry,"/" ) ;
setcookie("loginhash", $loginhash, $date_of_expiry,"/" ) ;
$cryptedhash=password_hash($loginhash, PASSWORD_DEFAULT, array("cost" => 10));
// a cost of 10 is standard, you may want to adjust it according to your hardware (lower/higher cost means faster/slower)

$today=date("Y-m-d");

mysql_query("update members set last_login='$today',loginhash='$cryptedhash' where id='$row[id]'") or die(mysql_error());

使用PASSWORD_DEFAULT将使舒尔即使在未来的版本中也始终使用最强大的算法。

1
2
3
4
5
6
7
8
9
10
11
12
if($_COOKIE['userlogin'] && !isset($_SESSION[user_id])){
    $username=mysql_real_escape_string($_COOKIE['userlogin']);
    $loginhash=$_COOKIE['loginhash']; // i guess you should not use mysql_real_escape_string
    $salt=mysql_real_escape_string(substr($loginhash,-8)); // here would be the place to use it
    $result=mysql_query("select * from members where (username='$username' || email='$username') && salt='$salt' limit 1") or die (mysql_error());
    $row=mysql_fetch_assoc($result);
    $cryptedhash=$row['loginhash'];
    if (password_verify($loginhash, $cryptedhash)){
        $_SESSION['username']=$row[username];
        $_SESSION['user_id']=$row['id'];
    }
}


此行错误$loginhash=mysql_real_escape_string($_COOKIE['loginhash']);您不需要转义它(您不在数据库中使用它),在传递给crypt()时必须使用它而不改变,这样该行就可以写为$loginhash=$_COOKIE['loginhash'];(至少对于这部分代码而言)。