如何在php中加密/解密数据?

119

我目前是一名学生,正在学习PHP,我尝试使用PHP进行简单的数据加密/解密。我进行了一些在线调查,但其中一些对我来说相当令人困惑。

这是我的目标:

我有一个表格,包含这些字段(UserID,Fname,Lname,Email,Password)

我想要的是将所有字段加密,然后再解密(如果不行可以使用sha256进行加密/解密,如果不可以请提供其他加密算法)

我想要学习的另一件事是如何创建一个单向哈希(sha256)结合一个好的“盐”。 (基本上,我只想要一个简单的加密/解密实现,哈希(sha256)+盐

先生/女士,您的答案将会非常有帮助并且非常感激。谢谢++


3
Sha-1加密的恐怖!(文章链接)Linkedin宣布有650万个经过哈希处理的密码被泄露。现在请更改您的密码。 - Naftali
http://php.net/manual/en/faq.passwords.php - Steve Ross
9
SHA是一种哈希算法,而不是加密算法。关键点在于哈希不能轻易地被反向解密为原始数据。如果您需要加密算法,可以使用mcrypt,如果不可用,我会建议使用phpseclib。但是需要注意的是,任何涉及大量低级数学运算的纯PHP实现都会非常慢。这就是为什么我喜欢phpseclib,因为它首先使用mcrypt(如果可用),只有作为最后的选择才会回退到PHP实现。 - DaveRandom
7
通常情况下,您不希望能够解密密码! - Ja͢ck
1
基本上你不应该在这个层面考虑加密,而应该考虑访问控制、机密性、完整性和身份验证。之后再检查如何实现这一点,可能使用加密或安全哈希。你可能想要了解 PBKDF2 和 bcrypt/scrypt 等密码安全哈希的相关知识。 - Maarten Bodewes
你可以使用PHP的password_hashpassword_verify函数来自动加盐和哈希密码。你不想让你的密码可解密。只需将其哈希化并使用即可。有效地销毁原始密码,因此没有人知道它是什么,但仍然可以通过比较输入密码与存储的哈希值来检查密码,使用password_verify。很好。不确定加密...这就是我在这里的原因!:-D - user1300214
6个回答

298

前言

从您的表定义开始:

- UserID
- Fname
- Lname
- Email
- Password
- IV

以下是更改:

  1. 字段 FnameLnameEmail 将使用由OpenSSL提供的对称密码加密算法进行加密。
  2. IV 字段将存储用于加密的初始化向量。存储需求取决于所使用的密码和模式;稍后会详细介绍。
  3. Password字段将使用单向密码哈希进行哈希处理,

加密

密码和模式

选择最佳的加密密码和模式超出了本回答的范围,但最终选择会影响加密密钥和初始化向量的大小;在本文中,我们将使用AES-256-CBC,其固定块大小为16字节,密钥大小为16、24或32字节。

加密密钥

一个好的加密密钥是从可靠的随机数发生器生成的二进制数据块。以下示例建议(≥ 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:

$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)是二进制的;可以使用指定的列类型,如BINARYVARBINARY来将这些值存储在数据库中。

与IV一样,输出值也是二进制的。若要将这些值存储在MySQL中,请考虑使用BINARYVARBINARY列。如果不行,则可以使用base64_encode()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
));

身份验证加密

通过附加一个由秘密密钥(与加密密钥不同)和密码文本生成的签名,可以进一步提高生成的密码文本的完整性。在解密密码文本之前,首先验证签名(最好使用常量时间比较方法)。

示例

// 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()

哈希处理

尽可能避免在数据库中存储可逆的密码;您只需验证密码而不知道其内容。如果用户丢失密码,最好让他们重置密码,而不是发送原始密码(确保密码重置只能在有限时间内进行)。

应用哈希函数是单向操作;之后它可以安全地用于验证而不暴露原始数据。对于密码,由于很多人密码长度较短且选择糟糕,暴力破解方法是可行的。

像MD5或SHA1这样的哈希算法是为了根据已知哈希值验证文件内容而设计的。它们被优化得非常好,以使该验证尽可能快速而准确。考虑到它们相对有限的输出空间,可以轻松构建具有已知密码及其相应哈希输出的数据库,即"彩虹表"。

在对密码进行哈希处理之前添加盐可以使彩虹表毫无用处,但是最近硬件的进步使得暴力查找成为可行的方法。这就是为什么需要一种故意缓慢且无法进行优化的哈希算法。它还应该能够增加更快硬件的负载,而不影响验证现有密码哈希的能力,以使其具备未来性。

目前有两个流行的选择:

  1. PBKDF2(基于密码的密钥派生函数v2)
  2. 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']; // 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引入了密码哈希函数,您可以使用这些函数来简化上述哈希方法:

$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);

并进行验证:

if (password_verify($given_password, $db_hash)) {
    // password valid
}

另请参见:password_hash()password_verify()


@MadMax 这完全由你自己决定;我的回答涵盖了您可以使用的不同编码方式,无论是二进制、十六进制还是Base64,以及每个字符需要多少存储空间。 - Ja͢ck
2
当然可以,但这取决于它的使用方式。如果您发布一个加密库,您不知道开发人员将如何实现它。这就是为什么https://github.com/defuse/php-encryption提供了认证对称密钥加密,并且不允许开发人员在不编辑其代码的情况下削弱它。 - Scott Arciszewski
2
@Scott 很好,我已经添加了一个认证加密的示例;感谢你的推动 :) - Ja͢ck
1
+1 针对认证加密。问题中没有足够的信息表明在此处不需要 AE。当然,SQL 流量通常通过具有未知安全属性的网络进行传输,数据库到存储的流量也是如此。备份和复制也是如此。威胁模型是什么?问题没有说明,并且做出假设可能是危险的。 - Jason Orendorff
1
不要硬编码$iv_size = 16;,我会使用:$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length("AES-256-CBC"))来指示与所使用的密码器一起使用的iv大小之间的链接。您还可以扩展一下对pkcs7_pad()/pkcs7_unpad()的需要(或不需要),或者通过摆脱它们并使用"aes-256-ctr"来简化帖子。好帖子@Ja͢ck - Patrick Allaert
显示剩余10条评论

24

我想这个问题以前已经被回答过了...但无论如何,如果你想加密/解密数据,你不能使用SHA256。

//Key
$key = 'SuperSecretKey';

//To Encrypt:
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, 'I want to encrypt this', MCRYPT_MODE_ECB);

//To Decrypt:
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $encrypted, MCRYPT_MODE_ECB);

7
同样地,您也不应该使用 ECB。 - Maarten Bodewes
7
密钥应该是随机字节,或者您应该使用安全的密钥派生函数。 - Maarten Bodewes
4
MCRYPT_RIJNDAEL_256 不是一个标准化的函数,你应该使用 AES (MCRYPT_RIJNDAEL_128)。 - Maarten Bodewes

14

背景和解释

要理解这个问题,首先必须了解SHA256是什么。SHA256是一种加密哈希函数(Cryptographic Hash Function)。加密哈希函数是一种单向函数,其输出是具有密码学安全性的。这意味着可以轻松地计算哈希值(相当于加密数据),但很难使用哈希值获取原始输入(相当于解密数据)。由于使用加密哈希函数意味着解密在计算上是不可行的,因此您无法使用SHA256进行解密。

您想要使用的是双向函数,更具体地说,是分组密码(Block Cipher)。这是一种允许加密和解密数据的功能。函数mcrypt_encryptmcrypt_decrypt默认使用Blowfish算法。PHP对mcrypt的使用可以在此手册中找到。也存在选择mcrypt使用的密码定义的密码列表。关于Blowfish的维基百科可以在此处找到。块密码将输入以已知大小和位置的块使用已知密钥进行加密,以便稍后可以使用该密钥解密数据。这就是SHA256无法提供的。

代码

$key = 'ThisIsTheCipherKey';

$ciphertext = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, 'This is plaintext.', MCRYPT_MODE_CFB);

$plaintext = mcrypt_decrypt(MCRYPT_BLOWFISH, $key, $encrypted, MCRYPT_MODE_CFB);

在这个问题上,你也不应该使用ECB。 - Maarten Bodewes
密钥应该是随机字节,或者您应该使用安全的密钥派生函数。 - Maarten Bodewes
4
永远不要使用ECB模式。它是不安全的,而且大部分时间实际上并没有帮助加密数据(只是将其编码)。请参阅该主题的优秀维基百科文章获取更多信息。 - Holger Just
1
最好不要使用mcrypt,它已经被废弃,多年没有更新,并且不支持标准的PKCS#7(原名PKCS#5)填充,只支持非标准的空填充,甚至不能用于二进制数据。mcrypt存在许多未解决的错误,追溯到2003年。相反,考虑使用defuse,它正在维护并且是正确的。 - zaph

10
     function my_simple_crypt( $string, $action = 'e' ) {
        // you may change these values to your own
        $secret_key = 'my_simple_secret_key';
        $secret_iv = 'my_simple_secret_iv';

        $output = false;
        $encrypt_method = "AES-256-CBC";
        $key = hash( 'sha256', $secret_key );
        $iv = substr( hash( 'sha256', $secret_iv ), 0, 16 );

        if( $action == 'e' ) {
            $output = base64_encode( openssl_encrypt( $string, $encrypt_method, $key, 0, $iv ) );
        }
        else if( $action == 'd' ){
            $output = openssl_decrypt( base64_decode( $string ), $encrypt_method, $key, 0, $iv );
        }

        return $output;
    }

1
非常简单!我用它来进行URL段加密解密。谢谢。 - Mahbub Tito

9

这里是使用openssl_encrypt函数的示例

//Encryption:
$textToEncrypt = "My Text to Encrypt";
$encryptionMethod = "AES-256-CBC";
$secretHash = "encryptionhash";
$iv = mcrypt_create_iv(16, MCRYPT_RAND);
$encryptedText = openssl_encrypt($textToEncrypt,$encryptionMethod,$secretHash, 0, $iv);

//Decryption:
$decryptedText = openssl_decrypt($encryptedText, $encryptionMethod, $secretHash, 0, $iv);
print "My Decrypted Text: ". $decryptedText;

2
我会使用openssl_random_pseudo_bytes(openssl_cipher_iv_length($encryptionMethod))代替mcrypt_create_iv(),这样的方法适用于任何$encryptionMethod的值,并且只使用openssl扩展。 - Patrick Allaert
上面的代码对于 openssl_decrypt() 返回了 false。请参见 https://dev59.com/0p7ha4cB1Zd3GeqPnLKw 由于块密码(如AES)要求输入数据是块大小的精确倍数(AES为16字节),因此需要填充。 - Avatar

0

我花了相当长的时间才弄清楚如何在使用openssl_decrypt()时避免得到false,并使加密和解密正常工作。

    // cryptographic key of a binary string 16 bytes long (because AES-128 has a key size of 16 bytes)
    $encryption_key = '58adf8c78efef9570c447295008e2e6e'; // example
    $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
    $encrypted = openssl_encrypt($plaintext, 'aes-256-cbc', $encryption_key, OPENSSL_RAW_DATA, $iv);
    $encrypted = $encrypted . ':' . base64_encode($iv);

    // decrypt to get again $plaintext
    $parts = explode(':', $encrypted);
    $decrypted = openssl_decrypt($parts[0], 'aes-256-cbc', $encryption_key, OPENSSL_RAW_DATA, base64_decode($parts[1])); 

如果您想通过URL传递加密字符串,您需要对字符串进行urlencode编码:
    $encrypted = urlencode($encrypted);

为了更好地理解正在发生的事情,请阅读:

要生成16字节长的密钥,您可以使用:

    $bytes = openssl_random_pseudo_bytes(16);
    $hex = bin2hex($bytes);

要查看openssl的错误消息,可以使用以下命令:echo openssl_error_string();

希望对您有所帮助。


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