Foreword
从表定义开始:
- UserID
- Fname
- Lname
- Email
- Password
- IV
以下是变化:
- 田野
Fname
, Lname
and Email
将使用对称密码进行加密,由以下提供OpenSSL http://php.net/openssl,
- The
IV
字段将存储初始化向量 http://en.wikipedia.org/wiki/Initialization_vector用于加密。存储要求取决于所使用的密码和模式;稍后会详细介绍这一点。
- The
Password
字段将使用散列one-way密码哈希,
加密
密码和模式
选择最佳加密密码和模式超出了本答案的范围,但最终选择会影响加密密钥和初始化向量的大小;在这篇文章中,我们将使用 AES-256-CBC,它的固定块大小为 16 字节,密钥大小为 16、24 或 32 字节。
加密密钥
好的加密密钥是由可靠的随机数生成器生成的二进制 blob。推荐使用以下示例(>= 5.3):
$key_size = 32; // 256 bits
$encryption_key = openssl_random_pseudo_bytes($key_size, $strong);
// $strong will be true if the key is crypto safe
这可以完成一次或多次(如果您希望创建加密密钥链)。尽可能将这些内容保密。
IV
初始化向量为加密添加了随机性,并且是 CBC 模式所需的。理想情况下,这些值应仅使用一次(技术上每个加密密钥一次),因此对行的任何部分的更新都应重新生成它。
提供了一个函数来帮助您生成 IV:
$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
Example
让我们使用之前的方法对名称字段进行加密$encryption_key
and $iv
;为此,我们必须将数据填充到块大小:
function pkcs7_pad($data, $size)
{
$length = $size - strlen($data) % $size;
return $data . str_repeat(chr($length), $length);
}
$name = 'Jack';
$enc_name = openssl_encrypt(
pkcs7_pad($name, 16), // padded data
'AES-256-CBC', // cipher and mode
$encryption_key, // secret key
0, // options (not used)
$iv // initialisation vector
);
存储要求
加密输出与 IV 一样,是二进制的;将这些值存储在数据库中可以通过使用指定的列类型来完成,例如BINARY
or VARBINARY
.
输出值与 IV 一样,是二进制的;要将这些值存储在 MySQL 中,请考虑使用BINARY or VARBINARY http://dev.mysql.com/doc/refman/5.0/en/binary-varbinary.html列。如果这不是一个选项,您还可以使用以下命令将二进制数据转换为文本表示形式:base64_encode() http://php.net/base64_encode or bin2hex() http://php.net/bin2hex,这样做需要多出 33% 到 100% 的存储空间。
解密
存储值的解密类似:
function pkcs7_unpad($data)
{
return substr($data, 0, -ord($data[strlen($data) - 1]));
}
$row = $result->fetch(PDO::FETCH_ASSOC); // read from database result
// $enc_name = base64_decode($row['Name']);
// $enc_name = hex2bin($row['Name']);
$enc_name = $row['Name'];
// $iv = base64_decode($row['IV']);
// $iv = hex2bin($row['IV']);
$iv = $row['IV'];
$name = pkcs7_unpad(openssl_decrypt(
$enc_name,
'AES-256-CBC',
$encryption_key,
0,
$iv
));
认证加密
您可以通过附加从密钥(不同于加密密钥)和密文生成的签名来进一步提高生成的密文的完整性。在解密密文之前,首先验证签名(最好采用恒定时间比较方法)。
Example
// generate once, keep safe
$auth_key = openssl_random_pseudo_bytes(32, $strong);
// authentication
$auth = hash_hmac('sha256', $enc_name, $auth_key, true);
$auth_enc_name = $auth . $enc_name;
// verification
$auth = substr($auth_enc_name, 0, 32);
$enc_name = substr($auth_enc_name, 32);
$actual_auth = hash_hmac('sha256', $enc_name, $auth_key, true);
if (hash_equals($auth, $actual_auth)) {
// perform decryption
}
也可以看看:hash_equals() http://php.net/hash_equals
Hashing
必须尽可能避免在数据库中存储可逆密码;您只想验证密码而不是知道其内容。如果用户丢失了密码,最好允许他们重置密码,而不是向他们发送原始密码(确保密码重置只能在有限的时间内完成)。
应用哈希函数是一种单向操作;之后可以安全地用于验证,而不会泄露原始数据;对于密码来说,暴力破解是一种可行的破解方法,因为它的长度相对较短,而且很多人对密码的选择较差。
MD5 或 SHA1 等哈希算法用于根据已知哈希值验证文件内容。它们经过了极大的优化,可以尽可能快地进行验证,同时仍然准确。鉴于其相对有限的输出空间,很容易构建一个包含已知密码及其各自的哈希输出(彩虹表)的数据库。
在散列之前向密码添加盐会使彩虹表变得无用,但最近的硬件进步使暴力查找成为一种可行的方法。这就是为什么您需要一种故意缓慢且根本无法优化的哈希算法。它还应该能够增加更快硬件的负载,而不影响验证现有密码散列以使其面向未来的能力。
目前有两种流行的选择:
- PBKDF2(基于密码的密钥派生函数 v2)
- bcrypt(又名河豚)
这个答案将使用 bcrypt 的示例。
一代
密码哈希可以这样生成:
$password = 'my password';
$random = openssl_random_pseudo_bytes(18);
$salt = sprintf('$2y$%02d$%s',
13, // 2^n cost factor
substr(strtr(base64_encode($random), '+', '.'), 0, 22)
);
$hash = crypt($password, $salt);
盐的生成是openssl_random_pseudo_bytes() http://php.net/openssl_random_pseudo_bytes形成一个随机的数据块,然后运行该数据块base64_encode()
and strtr()
匹配所需的字母表[A-Za-z0-9/.]
.
The crypt() http://php.net/crypt函数根据算法执行散列($2y$
对于 Blowfish)、成本因子(在 3GHz 机器上,因子 13 大约需要 0.40 秒)和 22 个字符的盐。
验证
获取包含用户信息的行后,您可以通过以下方式验证密码:
$given_password = $_POST['password']; // the submitted password
$db_hash = $row['Password']; // field with the password hash
$given_hash = crypt($given_password, $db_hash);
if (isEqual($given_hash, $db_hash)) {
// user password verified
}
// constant time string compare
function isEqual($str1, $str2)
{
$n1 = strlen($str1);
if (strlen($str2) != $n1) {
return false;
}
for ($i = 0, $diff = 0; $i != $n1; ++$i) {
$diff |= ord($str1[$i]) ^ ord($str2[$i]);
}
return !$diff;
}
要验证密码,您可以致电crypt()
再次,但您将之前计算的哈希值作为盐值传递。如果给定的密码与哈希值匹配,则返回值将产生相同的哈希值。为了验证哈希值,通常建议使用恒定时间比较函数以避免定时攻击。
使用 PHP 5.5 进行密码哈希处理
PHP 5.5 引入了密码散列函数 http://www.php.net/manual/en/ref.password.php您可以使用它来简化上述哈希方法:
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);
并验证:
if (password_verify($given_password, $db_hash)) {
// password valid
}
也可以看看:password_hash() http://php.net/password_hash, password_verify() http://php.net/password_verify