前言
从您的表定义开始:
- UserID
- Fname
- Lname
- Email
- Password
- IV
以下是更改:
- 字段
Fname
、Lname
和 Email
将使用由OpenSSL提供的对称密码加密算法进行加密。
IV
字段将存储用于加密的初始化向量。存储需求取决于所使用的密码和模式;稍后会详细介绍。
Password
字段将使用单向密码哈希进行哈希处理,
加密
密码和模式
选择最佳的加密密码和模式超出了本回答的范围,但最终选择会影响加密密钥和初始化向量的大小;在本文中,我们将使用AES-256-CBC,其固定块大小为16字节,密钥大小为16、24或32字节。
加密密钥
一个好的加密密钥是从可靠的随机数发生器生成的二进制数据块。以下示例建议(≥ 5.3):
$key_size = 32
$encryption_key = openssl_random_pseudo_bytes($key_size, $strong)
// $strong will be true if the key is crypto safe
这可以只做一次或多次(如果您希望创建加密密钥链)。尽可能保持私密。
IV
初始化向量为加密添加随机性并且在CBC模式下是必需的。这些值理想情况下应该仅被使用一次(从技术上讲,每个加密密钥使用一次),因此对行的任何部分进行更新都应重新生成IV。
提供了一个函数来帮助您生成IV:
$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
示例
使用之前的$encryption_key
和$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
或VARBINARY
来将这些值存储在数据库中。
与IV一样,输出值也是二进制的。若要将这些值存储在MySQL中,请考虑使用BINARY
或VARBINARY
列。如果不行,则可以使用base64_encode()
或bin2hex()
将二进制数据转换为文本表示形式,但这样做需要增加33%到100%的存储空间。
解密
存储值的解密方式类似:
function pkcs7_unpad($data)
{
return substr($data, 0, -ord($data[strlen($data) - 1]));
}
$row = $result->fetch(PDO::FETCH_ASSOC);
$enc_name = $row['Name'];
$iv = $row['IV'];
$name = pkcs7_unpad(openssl_decrypt(
$enc_name,
'AES-256-CBC',
$encryption_key,
0,
$iv
));
身份验证加密
通过附加一个由秘密密钥(与加密密钥不同)和密码文本生成的签名,可以进一步提高生成的密码文本的完整性。在解密密码文本之前,首先验证签名(最好使用常量时间比较方法)。
示例
$auth_key = openssl_random_pseudo_bytes(32, $strong);
$auth = hash_hmac('sha256', $enc_name, $auth_key, true);
$auth_enc_name = $auth . $enc_name;
$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)) {
}
另请参阅:hash_equals()
哈希处理
尽可能避免在数据库中存储可逆的密码;您只需验证密码而不知道其内容。如果用户丢失密码,最好让他们重置密码,而不是发送原始密码(确保密码重置只能在有限时间内进行)。
应用哈希函数是单向操作;之后它可以安全地用于验证而不暴露原始数据。对于密码,由于很多人密码长度较短且选择糟糕,暴力破解方法是可行的。
像MD5或SHA1这样的哈希算法是为了根据已知哈希值验证文件内容而设计的。它们被优化得非常好,以使该验证尽可能快速而准确。考虑到它们相对有限的输出空间,可以轻松构建具有已知密码及其相应哈希输出的数据库,即"彩虹表"。
在对密码进行哈希处理之前添加盐可以使彩虹表毫无用处,但是最近硬件的进步使得暴力查找成为可行的方法。这就是为什么需要一种故意缓慢且无法进行优化的哈希算法。它还应该能够增加更快硬件的负载,而不影响验证现有密码哈希的能力,以使其具备未来性。
目前有两个流行的选择:
- PBKDF2(基于密码的密钥派生函数v2)
- bcrypt(又名Blowfish)
此答案将使用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()
生成盐值(salt),形成一个随机数据块,然后通过 base64_encode()
和 strtr()
进行处理以匹配所需的字符集 [A-Za-z0-9/.]
。
crypt()
函数根据算法(Blowfish 为 $2y$
)、代价因子(13 的因子大约需要在 3GHz 机器上花费 0.40 秒)和长度为 22 个字符的盐值执行哈希计算。
验证
一旦取回包含用户信息的行,就可以按照以下方式验证密码:
$given_password = $_POST['password'];
$db_hash = $row['Password'];
$given_hash = crypt($given_password, $db_hash);
if (isEqual($given_hash, $db_hash)) {
}
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引入了密码哈希函数,您可以使用这些函数来简化上述哈希方法:
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);
并进行验证:
if (password_verify($given_password, $db_hash)) {
// password valid
}
另请参见:password_hash()
,password_verify()
password_hash
和password_verify
函数来自动加盐和哈希密码。你不想让你的密码可解密。只需将其哈希化并使用即可。有效地销毁原始密码,因此没有人知道它是什么,但仍然可以通过比较输入密码与存储的哈希值来检查密码,使用password_verify
。很好。不确定加密...这就是我在这里的原因!:-D - user1300214