在PHP中使用crypt()比较密码

17

我需要了解这个函数的基本知识。php.net文档针对blowfish算法指出:

使用以下盐值进行Blowfish散列:"$2a$",两位数字成本参数"$",以及字母表"./0-9A-Za-z"中的22个base64数字。在盐值中使用此范围之外的字符将导致crypt()返回一个零长度的字符串

因此,根据定义,下面的代码不应该工作:

echo crypt('rasmuslerdorf', '$2a$07$usesomadasdsadsadsadasdasdasdsadesillystringforsalt$');

然而,它会输出:

$2a$07$usesomadasdsadsadsadaeMTUHlZEItvtV00u0.kb7qhDlC0Kou9e

似乎是crypt()函数将盐值裁剪至长度为22。请问有人可以解释一下吗?

我还无法理解这个函数的另一个方面,即当使用crypt()来比较密码时。参考http://php.net/manual/en/function.crypt.php(查看示例#1)。这是否意味着,如果我对所有加密密码都使用相同的盐值,我必须先进行crypt()处理吗?例如:

$salt = "usesomadasdsadsadsadae";
$salt_crypt = crypt($salt);

if (crypt($user_input, $salt) == $password) {
   // FAIL WONT WORK
}

if (crypt($user_input, $salt_crypt) == $password) {
   // I HAVE TO DO THIS?
}    

感谢您的时间


我毫不羞耻地顶起了这个帖子。我的剩下的问题是对ZZ Coder的回应,请看本帖子底部。 - soren.qvist
6个回答

22

下面的代码示例可能会回答您的问题。

要使用Blowfish生成散列密码,首先需要生成一个盐值,它以$2a$开头,后跟迭代次数和22个Base64字符串字符。

$salt = '$2a$07$usesomadasdsadsadsadasdasdasdsadesillystringfors';
$digest = crypt('rasmuslerdorf', $salt);

将整个$digest存储在数据库中,它包含盐和摘要。

比较密码时,只需执行以下操作:

  if (crypt($user_input, $digest) == $digest)

你正在将摘要用作盐。crypt 从算法标识符中知道盐的长度。


1
是的,这就是应该做的。而且 $salt 应该在每次设置密码时随机生成。 - caf
1
啊,我明白了,我原以为只需要在数据库中存储哈希值。但是将盐和哈希值一起存储在数据库中似乎有点失去了盐的作用,不是吗? - soren.qvist
2
@soren.qvist:不完全如此。即使攻击者知道盐值,他仍然必须为每个可能的密码重新计算带有该盐值的哈希值;仅计算一次并在结果中查找哈希值是不够的。如果盐值是未知的,那么它甚至更好,因为那样他必须尝试每个可能的盐值下的所有密码,但即使每个密码旁边都存储了每个用户的盐值,它仍然需要重新计算该用户的盐值。另请参阅http://en.wikipedia.org/wiki/Salt_%28cryptography%29。 - Michael Madsen
@Michael Madsen,感谢您的回复,您会建议在数据库中为每个密码条目使用随机生成的盐吗? - soren.qvist
2
@soren.qvist:你应该为每个记录使用不同的盐。无论它是随机的还是不随机的-我不确定有多大区别。即使盐是可预测的,你仍然需要为每个盐运行哈希。当密码更改时创建新盐也是一样的;你仍然需要为那个盐计算所有内容。这可以防止某人通过预先计算哈希值并重复使用来针对特定用户,但仅此而已。然而,话虽如此,在每次更改时拥有一个随机盐肯定不会有坏处-所以你最好这样做。 - Michael Madsen
1
请查看WordPress中的gensalt_blowfish():http://cvsweb.openwall.com/cgi/cvsweb.cgi/projects/phpass/PasswordHash.php?rev=1.7 - ZZ Coder

4

每个密码都使用新的盐值

$password = 'p@ssw0rd';

$salt = uniqid('', true);
$algo = '6'; // CRYPT_SHA512
$rounds = '5042';
$cryptSalt = '$'.$algo.'$rounds='.$rounds.'$'.$salt;

$hashedPassword = crypt($password, $cryptSalt);
// Store complete $hashedPassword in DB

echo "<hr>$password<hr>$algo<hr>$rounds<hr>$cryptSalt<hr>$hashedPassword";

认证

if (crypt($passwordFromPost, $hashedPasswordInDb) == $hashedPasswordInDb) {
    // Authenticated

2
BCrypt使用128位盐,因此是22个字节的Base64编码,只有最后一个字节的两个比特被使用。
哈希是使用盐和密码计算出来的。当你传递加密后的密码时,算法读取强度、盐(忽略其后的所有内容)和你提供的密码,然后计算哈希值并附加它。如果你拥有PostgreSQL和pg_crypto,SELECT gen_salt('bf');将显示正在读取的$salt的内容。
以下是一个生成盐的代码示例,来自我的.NET实现的test-vector-gen.php文件中:
$salt = sprintf('$2a$%02d$%s', [strength goes here],
    strtr(str_replace(
    '=', '', base64_encode(openssl_random_pseudo_bytes(16))
    ),
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
    './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'));

没有理由为所有密码使用相同的盐。无论如何,盐都是输出的一部分,所以在方便性方面你不会得到任何好处...尽管我承认PHP应该有一个内置的gen_salt函数。

2

引用手册:

CRYPT_BLOWFISH - 使用Blowfish哈希算法,盐值格式如下:"$2a$"、两位数字的代价参数、"$"和22个来自字母表的base64数字

注意:22个base64数字。


英语不是我的母语,所以也许我完全误解了这个问题 :) 在我的示例中,我没有使用22个字符,但在PHP中完全没有问题? - soren.qvist
3
PHP对盐值超过22个字符不会有异议;只有盐值少于22个字符时才会出问题。然而,实际上只有前22个字符会被使用:使用相同的盐值对' rasmuslerdorf '进行加密,无论是'$2a $ 07 $usesomadasdsadsadsadasdasdasdsadesillystringforsalt $'还是'$2a$07$usesomadasdsadsadsadasxxxxxxxxxxxxxxxxxxxxxxxxxxxxx$'都将返回相同的结果,因为它们的前22个字符是相同的。 - Mark Baker

0

这个问题与我对ZZ Coder答案的回应有关。基本上我的问题是关于将crypt()结果存储在数据库中。我是否应该将整个输出存储在数据库中,以便我的数据库看起来像这样:

--------------------------------------------------------------------------------
| ID | Username |                          Password                            |
--------------------------------------------------------------------------------
| 32 | testuser | $2a$07$usesomadasdsadsadsadaeMTUHlZEItvtV00u0.kb7qhDlC0Kou9e |
--------------------------------------------------------------------------------

如果是这样,那么这不是有点违背使用盐的初衷吗?如果有人获得了数据库的访问权限,他们可以清楚地看到用于加密的盐吗?

附加问题:对于每个密码使用相同的盐是否安全?


2
首先 - 你正在进行哈希,而不是加密。其次 - 通过为每个用户使用不同的盐值,可以保护自己免受彩虹表攻击。盐值存储在数据库中并不会显著地帮助攻击者 - 你无法从盐值中猜测密码(对于良好的哈希算法)。 - Greg
所以,是的,我应该用这种方式存储密码吗?我以为哈希是加密的结果,但也许我把术语都搞错了。 - soren.qvist

0

第一个问题:

因此,根据定义,这不应该起作用:

echo crypt('rasmuslerdorf', '$2a$07$usesomadasdsadsadsadasdasdasdsadesillystringforsalt$');

在这里,似乎crypt()函数已经将盐值剪切到了22个字符的长度。有人能解释一下吗?

使用太多字符并不会造成问题...短语"在盐中使用范围外的字符将导致crypt()返回长度为零的字符串"是指base 64范围之外的字符,而不是22个字符的范围。试着在盐值字符串中放入非法字符,你会发现得到的输出为空(或者如果你放了小于22个字符,就会得到非法的空字节)。

第二个问题:

你将加密后存储的密码作为盐值传入,因为盐值字符串总是出现在加密字符串中(按设计),这样可以确保你对存储和用户输入的密码进行加密时使用相同的盐值。


那我将我的加密密码存储在数据库中,类似于“MTUHlZEItvtV00u0.kb7qhDlC0Kou9e”,然后将其用作盐?但是我使用另一个盐来创建该加密,不是吗? - soren.qvist
1
好的,你已经使用了另一个盐(或默认生成的盐)来创建第一个加密。由于加密算法的某些(我想象中是精心设计的)特殊性质,你可以将该加密字符串用作另一个加密的盐,从而获得使用相同盐的效果。 - G__

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接