我的意思是:

Original String + Salt or Key --> Encrypted String
Encrypted String + Salt or Key --> Decrypted (Original String)

也许是这样的:

"hello world!" + "ABCD1234" --> Encrypt --> "2a2ffa8f13220befbe30819047e23b2c" (may be, for e.g)
"2a2ffa8f13220befbe30819047e23b2c" --> Decrypt with "ABCD1234" --> "hello world!"

在PHP中,如何做到这一点?

尝试使用Crypt_Blowfish,但它不适合我。


当前回答

更新

PHP 7就绪版本。它使用了PHP OpenSSL库中的openssl_encrypt函数。

class Openssl_EncryptDecrypt {
    function encrypt ($pure_string, $encryption_key) {
        $cipher     = 'AES-256-CBC';
        $options    = OPENSSL_RAW_DATA;
        $hash_algo  = 'sha256';
        $sha2len    = 32;
        $ivlen = openssl_cipher_iv_length($cipher);
        $iv = openssl_random_pseudo_bytes($ivlen);
        $ciphertext_raw = openssl_encrypt($pure_string, $cipher, $encryption_key, $options, $iv);
        $hmac = hash_hmac($hash_algo, $ciphertext_raw, $encryption_key, true);
        return $iv.$hmac.$ciphertext_raw;
    }
    function decrypt ($encrypted_string, $encryption_key) {
        $cipher     = 'AES-256-CBC';
        $options    = OPENSSL_RAW_DATA;
        $hash_algo  = 'sha256';
        $sha2len    = 32;
        $ivlen = openssl_cipher_iv_length($cipher);
        $iv = substr($encrypted_string, 0, $ivlen);
        $hmac = substr($encrypted_string, $ivlen, $sha2len);
        $ciphertext_raw = substr($encrypted_string, $ivlen+$sha2len);
        $original_plaintext = openssl_decrypt($ciphertext_raw, $cipher, $encryption_key, $options, $iv);
        $calcmac = hash_hmac($hash_algo, $ciphertext_raw, $encryption_key, true);
        if(function_exists('hash_equals')) {
            if (hash_equals($hmac, $calcmac)) return $original_plaintext;
        } else {
            if ($this->hash_equals_custom($hmac, $calcmac)) return $original_plaintext;
        }
    }
    /**
     * (Optional)
     * hash_equals() function polyfilling.
     * PHP 5.6+ timing attack safe comparison
     */
    function hash_equals_custom($knownString, $userString) {
        if (function_exists('mb_strlen')) {
            $kLen = mb_strlen($knownString, '8bit');
            $uLen = mb_strlen($userString, '8bit');
        } else {
            $kLen = strlen($knownString);
            $uLen = strlen($userString);
        }
        if ($kLen !== $uLen) {
            return false;
        }
        $result = 0;
        for ($i = 0; $i < $kLen; $i++) {
            $result |= (ord($knownString[$i]) ^ ord($userString[$i]));
        }
        return 0 === $result;
    }
}

define('ENCRYPTION_KEY', '__^%&Q@$&*!@#$%^&*^__');
$string = "This is the original string!";

$OpensslEncryption = new Openssl_EncryptDecrypt;
$encrypted = $OpensslEncryption->encrypt($string, ENCRYPTION_KEY);
$decrypted = $OpensslEncryption->decrypt($encrypted, ENCRYPTION_KEY);

其他回答

在进一步操作之前,请先了解加密和身份验证之间的区别,以及为什么可能需要经过身份验证的加密,而不仅仅是加密。

为了实现认证加密,你需要加密然后MAC。加密和认证的顺序非常重要!这个问题的一个现有答案犯了这个错误;就像许多用PHP编写的密码库一样。

您应该避免实现自己的密码学,而是使用由密码学专家编写和审查的安全库。

更新:PHP 7.2现在提供libsodium!为了获得最好的安全性,请将您的系统更新为使用PHP 7.2或更高版本,并且只遵循本文中的libsodium建议。

如果您有PECL访问权限,则使用libsodium(如果您想要没有PECL的libsodium,则使用sodium_compat);否则…… 使用缓和/ php-encryption;不要滚你自己的密码!

上面链接的两个库使您可以轻松地在自己的库中实现经过身份验证的加密。

如果您仍然想编写和部署您自己的密码学库,与互联网上每个密码学专家的传统智慧相反,那么您必须采取以下步骤。

加密:

CTR模式下使用AES加密。您也可以使用GCM(它消除了对单独MAC的需要)。此外,ChaCha20和Salsa20(由libsodium提供)是流密码,不需要特殊模式。 除非您选择了上面的GCM,否则您应该使用HMAC-SHA-256(或者,对于流密码,使用Poly1305—大多数libsodium api为您做这件事)来验证密文。MAC应该能覆盖IV和密文!

解密:

除非使用Poly1305或GCM,否则请重新计算密文的MAC,并将其与使用hash_equals()发送的MAC进行比较。如果失败,中止。 解密消息。

其他设计考虑因素:

Do not compress anything ever. Ciphertext is not compressible; compressing plaintext before encryption can lead to information leaks (e.g. CRIME and BREACH on TLS). Make sure you use mb_strlen() and mb_substr(), using the '8bit' character set mode to prevent mbstring.func_overload issues. IVs should be generating using a CSPRNG; If you're using mcrypt_create_iv(), DO NOT USE MCRYPT_RAND! Also check out random_compat. Unless you're using an AEAD construct, ALWAYS encrypt then MAC! bin2hex(), base64_encode(), etc. may leak information about your encryption keys via cache timing. Avoid them if possible.

即使您遵循这里给出的建议,密码学也会出现很多问题。始终让密码学专家检查您的实现。如果你没有足够的幸运与当地大学的密码学学生成为朋友,你可以尝试密码学堆栈交换论坛寻求建议。

如果您需要对您的实现进行专业分析,您总是可以雇佣一个有信誉的安全顾问团队来检查您的PHP加密代码(披露:我的雇主)。

重要提示:何时不使用加密

不要加密密码。相反,你想使用以下密码哈希算法之一来哈希它们:

Argon2 scrypt bcrypt PBKDF2-SHA256, 86,000次迭代

永远不要使用通用哈希函数(MD5, SHA256)来存储密码。

不要加密URL参数。这不是做这项工作的合适工具。

使用Libsodium的PHP字符串加密示例

如果您使用的是PHP < 7.2或没有安装libsodium,则可以使用sodium compat来实现相同的结果(尽管速度较慢)。

<?php
declare(strict_types=1);

/**
 * Encrypt a message
 * 
 * @param string $message - message to encrypt
 * @param string $key - encryption key
 * @return string
 * @throws RangeException
 */
function safeEncrypt(string $message, string $key): string
{
    if (mb_strlen($key, '8bit') !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
        throw new RangeException('Key is not the correct size (must be 32 bytes).');
    }
    $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
    
    $cipher = base64_encode(
        $nonce.
        sodium_crypto_secretbox(
            $message,
            $nonce,
            $key
        )
    );
    sodium_memzero($message);
    sodium_memzero($key);
    return $cipher;
}

/**
 * Decrypt a message
 * 
 * @param string $encrypted - message encrypted with safeEncrypt()
 * @param string $key - encryption key
 * @return string
 * @throws Exception
 */
function safeDecrypt(string $encrypted, string $key): string
{   
    $decoded = base64_decode($encrypted);
    $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
    $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
    
    $plain = sodium_crypto_secretbox_open(
        $ciphertext,
        $nonce,
        $key
    );
    if (!is_string($plain)) {
        throw new Exception('Invalid MAC');
    }
    sodium_memzero($ciphertext);
    sodium_memzero($key);
    return $plain;
}

然后测试一下:

<?php
// This refers to the previous code block.
require "safeCrypto.php"; 

// Do this once then store it somehow:
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
$message = 'We are all living in a yellow submarine';

$ciphertext = safeEncrypt($message, $key);
$plaintext = safeDecrypt($ciphertext, $key);

var_dump($ciphertext);
var_dump($plaintext);

Halite - Libsodium更容易

我一直在做的一个项目是一个名为Halite的加密库,它旨在使libsodium更简单、更直观。

<?php
use \ParagonIE\Halite\KeyFactory;
use \ParagonIE\Halite\Symmetric\Crypto as SymmetricCrypto;

// Generate a new random symmetric-key encryption key. You're going to want to store this:
$key = new KeyFactory::generateEncryptionKey();
// To save your encryption key:
KeyFactory::save($key, '/path/to/secret.key');
// To load it again:
$loadedkey = KeyFactory::loadEncryptionKey('/path/to/secret.key');

$message = 'We are all living in a yellow submarine';
$ciphertext = SymmetricCrypto::encrypt($message, $key);
$plaintext = SymmetricCrypto::decrypt($ciphertext, $key);

var_dump($ciphertext);
var_dump($plaintext);

所有底层密码学都由libsodium处理。

使用化解/php加密的示例

<?php
/**
 * This requires https://github.com/defuse/php-encryption
 * php composer.phar require defuse/php-encryption
 */

use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;

require "vendor/autoload.php";

// Do this once then store it somehow:
$key = Key::createNewRandomKey();

$message = 'We are all living in a yellow submarine';

$ciphertext = Crypto::encrypt($message, $key);
$plaintext = Crypto::decrypt($ciphertext, $key);

var_dump($ciphertext);
var_dump($plaintext);

注意:Crypto::encrypt()返回十六进制编码的输出。

加密密钥管理

如果你想要设置一个“密码”,现在就停止。你需要一个随机的128位加密密钥,而不是一个人类可记忆的密码。

你可以像这样存储一个长期使用的加密密钥:

$storeMe = bin2hex($key);

并且,根据需要,你可以像这样检索它:

$key = hex2bin($storeMe);

我强烈建议只存储一个随机生成的密钥以供长期使用,而不是将任何类型的密码作为密钥(或派生密钥)。

如果你正在使用化解的库:

$string = $keyObject->saveToAsciiSafeString() $loaded = Key::loadFromAsciiSafeString($string);

“但我真的很想用密码。”

这是个坏主意,但好吧,下面是安全的方法。

首先,生成一个随机键并将其存储在一个常量中。

/**
 * Replace this with your own salt! 
 * Use bin2hex() then add \x before every 2 hex characters, like so:
 */
define('MY_PBKDF2_SALT', "\x2d\xb7\x68\x1a\x28\x15\xbe\x06\x33\xa0\x7e\x0e\x8f\x79\xd5\xdf");

请注意,您正在增加额外的工作,可以使用这个常量作为关键,从而为自己省去很多麻烦!

然后使用PBKDF2(像这样)从您的密码派生一个合适的加密密钥,而不是直接使用您的密码进行加密。

/**
 * Get an AES key from a static password and a secret salt
 * 
 * @param string $password Your weak password here
 * @param int $keysize Number of bytes in encryption key
 */
function getKeyFromPassword($password, $keysize = 16)
{
    return hash_pbkdf2(
        'sha256',
        $password,
        MY_PBKDF2_SALT,
        100000, // Number of iterations
        $keysize,
        true
    );
}

不要只使用16个字符的密码。你的加密密钥会被滑稽地破解。

不要做什么

警告: 这个答案使用了欧洲央行。ECB不是一个加密模式,它只是一个构建模块。使用这个答案中演示的ECB实际上并不能安全地加密字符串。不要在代码中使用ECB。请参阅Scott的回答以获得一个好的解决方案。

我自己弄到的。实际上我在谷歌上找到了一些答案,只是修改了一些东西。然而,结果是完全不安全的。

<?php
define("ENCRYPTION_KEY", "!@#$%^&*");
$string = "This is the original data string!";

echo $encrypted = encrypt($string, ENCRYPTION_KEY);
echo "<br />";
echo $decrypted = decrypt($encrypted, ENCRYPTION_KEY);

/**
 * Returns an encrypted & utf8-encoded
 */
function encrypt($pure_string, $encryption_key) {
    $iv_size = mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB);
    $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
    $encrypted_string = mcrypt_encrypt(MCRYPT_BLOWFISH, $encryption_key, utf8_encode($pure_string), MCRYPT_MODE_ECB, $iv);
    return $encrypted_string;
}

/**
 * Returns decrypted original string
 */
function decrypt($encrypted_string, $encryption_key) {
    $iv_size = mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB);
    $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
    $decrypted_string = mcrypt_decrypt(MCRYPT_BLOWFISH, $encryption_key, $encrypted_string, MCRYPT_MODE_ECB, $iv);
    return $decrypted_string;
}
?>

如果你不想使用库(你应该),那么使用这样的东西(PHP 7):

function sign($message, $key) {
    return hash_hmac('sha256', $message, $key) . $message;
}

function verify($bundle, $key) {
    return hash_equals(
      hash_hmac('sha256', mb_substr($bundle, 64, null, '8bit'), $key),
      mb_substr($bundle, 0, 64, '8bit')
    );
}

function getKey($password, $keysize = 16) {
    return hash_pbkdf2('sha256',$password,'some_token',100000,$keysize,true);
}

function encrypt($message, $password) {
    $iv = random_bytes(16);
    $key = getKey($password);
    $result = sign(openssl_encrypt($message,'aes-256-ctr',$key,OPENSSL_RAW_DATA,$iv), $key);
    return bin2hex($iv).bin2hex($result);
}

function decrypt($hash, $password) {
    $iv = hex2bin(substr($hash, 0, 32));
    $data = hex2bin(substr($hash, 32));
    $key = getKey($password);
    if (!verify($data, $key)) {
      return null;
    }
    return openssl_decrypt(mb_substr($data, 64, null, '8bit'),'aes-256-ctr',$key,OPENSSL_RAW_DATA,$iv);
}

$string_to_encrypt='John Smith';
$password='password';
$encrypted_string=encrypt($string_to_encrypt, $password);
$decrypted_string=decrypt($encrypted_string, $password);

下面的代码适用于php中所有具有特殊字符的字符串

   // Encrypt text --

    $token = "9611222007552";

      $cipher_method = 'aes-128-ctr';
      $enc_key = openssl_digest(php_uname(), 'SHA256', TRUE);  
      $enc_iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher_method));  
      $crypted_token = openssl_encrypt($token, $cipher_method, $enc_key, 0, $enc_iv) . "::" . bin2hex($enc_iv);
    echo    $crypted_token;
    //unset($token, $cipher_method, $enc_key, $enc_iv);

    // Decrypt text  -- 

    list($crypted_token, $enc_iv) = explode("::", $crypted_token);  
      $cipher_method = 'aes-128-ctr';
      $enc_key = openssl_digest(php_uname(), 'SHA256', TRUE);
      $token = openssl_decrypt($crypted_token, $cipher_method, $enc_key, 0, hex2bin($enc_iv));
    echo   $token;
    //unset($crypted_token, $cipher_method, $enc_key, $enc_iv);

对于Laravel框架

如果您正在使用Laravel框架,那么使用内部函数加密和解密将更加容易。

$string = 'Some text to be encrypted';
$encrypted = \Illuminate\Support\Facades\Crypt::encrypt($string);
$decrypted_string = \Illuminate\Support\Facades\Crypt::decrypt($encrypted);

var_dump($string);
var_dump($encrypted);
var_dump($decrypted_string);

注意:请确保在中设置16,24或32个字符的随机字符串 config/app.php文件的关键选项。否则,加密值 不会有安全感。