密码散列[翻译]
密码散列
作者:James McGlinn
翻译:mikespook
来源:http://phpsec.org/
在本文中我将论述一个初学者经常理解不好的问题——密码散列。最近我已经被邀请参观了许多有着同样安全隐患的web项目,这些项目将密码直接存储于数据库中。密码散列是一种将密码存储于数据库之前,对其进行加密的方法。这样当有人意外获得了你的数据库的时候也不会带来更大的损失。散列已经不是什么新技术了。具我所知,它已经在Unix系统的密码上使用了很久。并且极有可能在更早以前就被使用在其他系统上。本文中,我将解释什么是散列,为什么你应该使用它代替真实的密码存储在你的系统中,并且给你一些例子来说明如何在PHP和MySQL中使用密码散列。
前言
当你阅读本文的时候你会看到我建议使用的散列算法叫做Secure Hashing Algorithm 1(SHA-1)。当我写这篇文章的时候,一个研究团队——王小云、尹艺群和余洪波(译注:音译)已经证明了SHA-1比一直以来认为的要更加脆弱。这就意味着在如数字签名这样的用途上建议使用如SHA-256和SHA-512这些更加强大的算法。而在一般的密码散列中,SHA-1依然可以为大多数应用提供比直接存储密码更高的安全级别。你应该知道这个事实,并且开始思考在保证易用性的同时,在你的代码中使用更加强大的算法。
更多信息请看Bruce Schneier发布在这里的分析:
http://www.schneier.com/blog/archives/2005/02/cryptanalysis_o.html
什么是散列?
散列(也被称作散列编码、摘要、信息摘要)可以认为是从数据中采集部分数据作为起指纹一样作用的摘要。你可以很容易的通过单向数学算法从字符串中生成固定长度的散列值。这意味着不可能(有效的)从散列中还原原始的字符串。同时也意味着不同的字符串生成相同的散列值,也就是“散列碰撞”,的情况非常微小。这就使得在你的应用中生成散列用来存储密码理论上非常完美。为什么?因为当一个攻击者入侵内到你的一部分系统并且获得了密码散列值的时候,他们无法仅仅依靠散列值来得到你真实的密码。
那么我如何识别用户?
我们已经确信从散列值中还原原始的密码几乎是不可能的,那么我们的应用如何判断一个用户输入的是正确的密码还是错误的?非常简单——生成一个用户输入的密码的散列值,并且将这个“指纹”同系统中用户信息存储的散列值进行比较,你就能知道密码是否相同了。让我们来看一个例子:用户注册与密码验证
在注册过程中我们的新用户将提供他们期望使用的密码(当然是已经通过了验证并且满足安全需要的)。使用像下面这样的代码,我们将存储用户名和密码散列值到数据库中:
图解1 用户输入他们的帐户信息
<?php
/* Store user details */
$passwordHash = sha1($_POST['password']);
$sql = 'INSERT INTO user (username,passwordHash) VALUES (?,?)';
$result = $db->query($sql, array($_POST['username'], $passwordHash));
?>
下一次用户登陆时,我们使用像下面这样的代码来检查他们的帐号:
图解2再次登陆
<?php
/* 检查用户信息 */
$passwordHash = sha1($_POST['password']);
$sql = 'SELECT username FROM user WHERE username = ? AND passwordHash = ?';
$result = $db->query($sql, array($_POST['username'], $passwordHash));
if ($result->numRows() < 1)
{
/* 拒绝登陆 */
echo 'Sorry, your username or password was incorrect!';
}
else
{
/* 用户登陆 */
printf('Welcome back %s!', $_POST['username']);
}
?>
散列的类型
有很多的散列算法可以使用,一般常用的是MD5和SHA-1。在一些旧的系统中(包括很多Linux的变体)使用标准数据加密(DES)散列。由于长度只有56位,这种算法已经不再被认为是一种足够强壮的散列并且应该避免使用。
例子
在PHP中你可以使用函数md5和sha1生成散列值。md5返回一个128位的散列值(32个16进制字符),而sha1返回160位的散列值(40个16进制字符)。例如:
<?php
$string = 'PHP & Information Security';
printf("Original string: %s\n", $string);
printf("MD5 hash: %s\n", md5($string));
printf("SHA-1 hash: %s\n", sha1($string));
?>
这段代码将输出下面的内容:
Original string: PHP & Information Security
MD5 hash: 88dd8f 282721af2c 704e238e7f 338c 41
SHA-1 hash: b47210605096b9aa0129f 88695e229ce309dd362
你还可以使用MySQL的内置函数password(),md5或者sha1生成散列值。password() 函数使用在MySQL自身的用户验证系统中。在MySQL4.1版本以前它返回一个16字节的字符串,4.1以及之后的版本它返回一个41字节的字符串(基于SHA-1两次散列)。MySQL版本
mysql> select PASSWORD( 'PHP & Information Security' );
+------------------------------------------+
| PASSWORD( 'PHP & Information Security' ) |
+------------------------------------------+
| 379693e271cd3bd6 |
+------------------------------------------+
1 row in set (0.00 sec)
mysql> select MD5( 'PHP & Information Security' );
+-------------------------------------+
| MD5( 'PHP & Information Security' ) |
+-------------------------------------+
| 88dd8f 282721af2c 704e238e7f 338c 41 |
+-------------------------------------+
1 row in set (0.01 sec)
注意:不建议在你自己的应用中使用MySQL的password()函数——这个函数的算法已经改变了许多次,而且在4.1之前的版本中它相当的脆弱。
你可以使用MySQL来计算你的散列值而不是PHP。这个存储我们的用户从表单中填写的注册信息的例子将变成这样:
<?php
/* 存储用户信息 */
$sql = 'INSERT INTO user (username, passwordHash) VALUES (?, SHA1(?))';
$result = $db->query($sql, array($_POST['username'], $_POST['password']));
?>
(未完待续...)