关于方法
crypt是一个单向的字符串哈希方法。
string crypt ( string $str [, string $salt ] )
$str是需要进行哈希的字符串。
$salt是进行哈希时使用盐值,是个可选项。
例如以下代码:
echo crypt('mypassword');
会输出类似的结果:
$1$1Y.rRSxY$htFPrMkCbV.Av.yh2lTJd.
注意:从PHP 5.6.0开始,如果没有传$salt参数,会报E_NOTICE错误。
关于盐值
我们既没有指定使用的算法,也没有指定盐值,crypt是怎么知道使用什么算法和盐值的呢?其实crypt是根据$salt参数来判断使用哪种哈希算法。也就是说,$salt本身就包含了算法的类型以及哈希时实际用到盐值。
我们先来看看,如果没有传$salt的话,crypt会如何处理。 在5.3版本前,PHP在安装的时候会根据系统的crypt()方法检测可用的算法。如果没有提供$salt参数,PHP会使用自动产生一个标准的两个字符(DES)的盐值或者一个12个字符(MD5)的盐值,视乎MD5算法是否可用。
从PHP 5.3版本开始,PHP包含了自身实现的crypt算法,包括MD5、标准DES、扩展DES和Blowfish算法。如果系统不支持这些算法,就会使用PHP自己实现的。例如上面那个例子,最终使用的是MD5算法的盐值。
PHP设置了一个常量CRYPT_SALT_LENGTH,用来表示盐值最大允许的长度。
关于算法
那crypt是怎么根据盐值判断不同的算法呢?其实它是按照一定规则去匹配盐值,如果符合某个算法的规则,就表示使用哪种算法。我们逐一看下目前支持的几种算法。
标准DES(Standard DES)
在没有匹配到其他算法的情况下,则使用标准DES算法,此时取前两个字符为盐值(不足两个字符则返回*0):
echo crypt('rasmuslerdorf', 'rl');
// 输出结果如下:
// rl.3StKT.4T8M
盐值的字符必须是./0-9A-Za-z里的字符,否则会引起 crypt()失败(个人测了下,好像字符的范围要比文档里说的大)。
使用这个算法时,$str只取前面8个字符,所以无论字符串有多长,如果前8个字符一样,在盐值一样的情况下,返回的哈希值也是一样的。
扩展DES(Extended DES)
以下划线_开头,后面紧接着4字节的迭代次数和4字节的盐值:
echo crypt('rasmuslerdorf', '_J9..rasm');
// 输出结果如下:
// _J9..rasmBYk8r9AiWNc
同样的,那8个字节的字符必须是./0-9A-Za-z里的字符。
MD5
以$1$开头,然后是12个字符以内的盐值:
echo crypt('rasmuslerdorf', '$1$rasmusle$');
// 输出结果如下:
// $1$rasmusle$rISCgZzpwk3UhDidwXvin0
其实例子里的$1$rasmusle$最后一位改成任何字符都不会影响结果,例如$1$rasmuslea和$1$rasmusle1得到的结果跟$1$rasmusle$是一样的,因为最后一位一定是$,没有的话会自动补上。例如$1$rasm会变成$1$rasm$,$1$会变成$1$$。
Blowfish
以$2a$、$2x$或者$2y$开头,然后是用于cost参数的两位数字,紧接着一个$字符,最后是22位./0-9A-Za-z范围里的字符:
echo crypt('rasmuslerdorf', '$2a$07$usesomesillystringforsalt$');
// 输出结果如下:
// $2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi
两位数字用于表示Blowfish算法的迭代次数,是以2位底的对数,范围为04-31。在PHP 5.3.7版本之前,只支持$2a$作为前缀,5.3.7之后,增加了$2x$和$2y$两种前缀,用于解决一些安全性问题。如果使用5.3.7版本或以上,建议使用$2y$。
使用这个算法的时候,$str最长支持72个字符,超过会被截掉。
SHA256
以$5$开头,然后是16个字符的盐值,盐值之前还可以使用rounds=$的格式表明哈希的循环次数(N):
echo crypt('rasmuslerdorf', '$5$rounds=5000$usesomesillystringforsalt$');
// 输出结果如下:
// $5$rounds=5000$usesomesillystri$KqJWpanXZHKq2BOB43TSaYhEWsQ1Lr5QNyPCDH/Tp.6
rounds的默认值为5000,范围是1000到999,999,999,如果N不在这个范围里,会被截取到最接近的范围里。
PHP 5.3.2增加的算法,算法的实现基于Ulrich Drepper的实现。
SHA512
以$6$开头,然后是16个字符的盐值,盐值之前还可以使用rounds=$的格式表明哈希的循环次数(N):
echo crypt('rasmuslerdorf', '$6$rounds=5000$usesomesillystringforsalt$');
// 输出结果如下:
// $6$rounds=5000$usesomesillystri$D4IrlXatmP7rx3P3InaxBeoomnAihCKRVQP22JZ6EY47Wc6BkroIuUUBOov1i.S5KPgErtP/EN5mcO.ChWQW21
rounds的默认值为5000,范围是1000到999,999,999,如果N不在这个范围里,会被截取到最接近的范围里。
PHP 5.3.2增加的算法,算法的实现基于Ulrich Drepper的实现。
如何判断支持哪种算法
crypt提供了以下的常量来标识是否支持某个算法(1表示支持,0表示不支持):
CRYPT_STD_DES
CRYPT_EXT_DES
CRYPT_MD5
CRYPT_BLOWFISH
CRYPT_SHA256
CRYPT_SHA512
关于输出结果
从上面的那些例子的输出可以看到,crypt方法输出的结果其实包含了盐值本身。所以我们能从输出结果的本身知道这个结果是使用哪个算法和什么盐值进行运算的。
另外,如果盐值包含了非法的字符,例如通常盐值都要求是./0-9A-Za-z范围里的字符,如果不是的话,crypt会返回结果*0。另外,在5.5.21之前的5.5分支版本以及5.6.5之前的5.6分支版本,如果$salt的值为*0,会返回一个使用DES算法的哈希值,而之后的版本则返回*1。
关于验证
要验证一字符串的哈希值,我们一般是用相同的盐值相同的算法进行一次运算,然后跟之前的值进行比较。而使用crypt方法,由于它的输出结果本身就带有算法和盐值的信息,我们只需要把输出结果当做$salt参数即可:
$hashed_password = crypt('mypassword');
if ($hashed_password === crypt($user_input, $hashed_password)) {
echo 'Password verified!';
}
为了防止基于时间的攻击,PHP 5.6提供了一个更为安全的字符串比较方法hash_equals(),建议使用:
$hashed_password = crypt('mypassword');
if (hash_equals($hashed_password, crypt($user_input, $hashed_password))) {
echo 'Password verified!';
}
参考