学习PHP扩展库OpenSSL的对称加密相关知识

作者: 乘风御上者 分类: PHP 发布时间: 2020-08-08 12:45

进入PHP7之后Mcrypt扩展逐步被弃用,学习替代者OpenSSL扩展就显得很有必要了

学习该扩展之前,先温习一下和加解密相关的关键词:

对称加密

加密与解密使用同一个密码。

非对称加密

加密与解密使用不同的密码(一个公钥,一个私钥)。

公钥/私钥

字面意思: 公钥表示能公开的密钥,私钥表示不能公开的私密的密钥。
公钥与密钥是通过算法生成的一个密钥对,具有唯一性并且可以互相解密: 使用公钥加密的数据能用私钥解密,使用私钥加密的数据能用公钥解密。

签名

使用私钥对数据加密,这个过程就是签名。

验证签名

对使用私钥加密的数据通过公钥解密,这个过程就是验证签名。


介绍下对称加密所用到的几个相关函数:

// 获取到OpenSSL扩展所支持的所有加密算法
openssl_get_cipher_methods()


// 获取指定加密算法的初始化向量(iv)所需要长度(长度单位Byte)
// 因每种算法在加解密时需要的iv值长度不同
openssl_cipher_iv_length($method)


// 随机生成指定长度的随机字节串(不是字符串)
// 字节串直接打印是乱码哦
openssl_random_pseudo_bytes($length)

// 作用同上,不同点是该函数属于PHP7内置
random_bytes($length)


// 加密与解密,除参一外,其他参数完全相同
// 加密时$data是需要加密的数据,解密时$data是需要解密的数据
openssl_encrypt($data, $method, $key, $options = 0, $iv = '')
openssl_decrypt($data, $method, $key, $options = 0, $iv = '')

加密函数与解密函数需要详细的记录一下:

  • data: 明文(对应需要加密和解密的数据)
  • method: 加密的算法(openssl_get_cipher_methods已列出所有算法)
  • key: 密钥(不同的算法有效长度不同)
  • options: 作用是修饰明文,控制函数返回数据格式。值有0、OPENSSL_RAW_DATA对应1、OPENSSL_ZERO_PADDING对应2
    • 0: 自动对明文填充,返回数据经base64编码处理
    • 1: 自动对明文填充,返回原始字节串数据
    • 2: 官方不推荐,忽略即可。
  • iv: 初始化向量,不同算法对此长度要求不同。不使用该参数会抛出警告


以上参数中关于密钥的长度、初始化向量的长度问题需要再次细说下:

主流的对称加密方式有DES、AES(算法名称以此开头),这两种加密方式都属于密码分组加密。

DES加密方式的密钥长度为64bit(默认UTF-8编码),也就是8个字节(1字节=8bit),密钥不足8字节将使用0补充。加密过程大体是将明文按照64bit长度进行分组,最后一组不足64位时需要填充数据。分组后明文组与密钥按位替代或交换的方法形成密文组。

AES加密方式的分组长度是128bit。密钥的长度根据不同的加密算法不同,有128bit(AES-128=16字节)、192bit(AES-192)、256bit(AES-256)。与DES方式相同,密钥长度不足时同样使用0补充,超过该长度的部分无效。(目的取代DES)

在很多加密算法名字中带有“CBC”字样(算法名称以此结尾)。它们都使用同一种加密模式:密码分组链接模式(Cipher Block Chaining)。

使用CBC模式加密时,明文会被分成若干个组,以组为单位加密。每个组的加密过程,依赖他前一个组的数据,因为要跟前一组的数据进行异或操作后生成本组的密文。那么最开头的那个组又要依赖谁呢?依赖的就是IV,这就是名称叫初始化向量(initialization vector)的原因。

所以CBC加密模式必须使用指定长度的IV,而像ECB模式则不需要IV。关于加密方式与加密模式等详细准确的内容,则需要感兴趣的道友自己查阅。

结合以上内容大致能明白各种算法在加密与解密时key与iv的取值了。


// 加密数据
$data = '我的要加密数据';

// 加密算法
$method = 'AES-128-CBC';

// KEY的长度不能通过任何函数得到,AES-128-CBC有效密钥长度为16位
$key = random_bytes(16);

// 生成IV
$len = openssl_cipher_iv_length($method);
$iv = $len ? random_bytes($len) : '';

// 加密
$encrypted = openssl_encrypt($data, $method, $key, 0, $iv);

// 解密
$decrypted = openssl_decrypt($encrypted, $method, $key, 0, $iv);

// 不固定FzqBms+WFJREL4sSVEWJh5IQfYyTf709u/jundwn2as=
var_dump($encrypted);

// 我的要加密数据
var_dump($decrypted);

以上示例有个严重缺陷:数据在加密和解密时,key与iv必须使用同一个。所以单纯的作为栗子可以,实际在加密后,加密数据、key、iv三者都要存储起来,在下次解密数据时,必须使用加密时的key与iv才能正确解密数据。

通过查看Laravel加密相关源码,我发现它将iv巧妙地加入到加密数据中,解密时先从加密字符中拿到iv,再进行正式解密。这样只需要将加密数据与key存储,解密也只需要加密数据和key即可完成解密工作。

/**
 * 加密解密工具类
 * 密钥通过generateKey静态方法生成
 * 如需存储密钥则需要先将其bin2hex转成十六进制
 * 存储的密钥在实例化该类前将其hex2bin转成字节
 */
class Encrypter
{
    // 加密密钥
    protected $key;
    // 用于加密的算法
    protected $cipher;


    /**
     * 创建加密器实例
     * @param string $key 密钥-根据算法不同决定位数[AES-128-CBC=16位,AES-256-CBC=32位]
     * @param string $cipher 算法-[AES-128-CBC | AES-256-CBC]
     * @throws Exception
     */
    public function __construct($key, $cipher = 'AES-128-CBC')
    {
        $key = (string)$key;

        if (static::supported($key, $cipher)) {
            $this->key = $key;
            $this->cipher = $cipher;
        } else {
            throw new Exception('密钥错误,算法AES-128-CBC与AES-256-CBC分别需要16位或32位密钥');
        }
    }


    /**
     * 根据指定算法生成随机密钥
     * @param string $cipher
     * @return string
     * @throws Exception
     */
    public static function generateKey($cipher = 'AES-128-CBC')
    {
        return random_bytes($cipher === 'AES-128-CBC' ? 16 : 32);
    }


    /**
     * 加密给定字符串
     * @param $string
     * @return string
     * @throws Exception
     */
    public function encrypt($string)
    {
        $iv = random_bytes(openssl_cipher_iv_length($this->cipher));
        $value = \openssl_encrypt($string, $this->cipher, $this->key, 0, $iv);
        if ($value === false) {
            throw new Exception('数据加密失败[ENCRYPT]');
        }

        $mac = $this->hash($iv = base64_encode($iv), $value);
        $json = json_encode(compact('iv', 'value', 'mac'), JSON_UNESCAPED_SLASHES);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new Exception('数据加密失败[HASH]');
        }

        return base64_encode($json);
    }


    /**
     * 解密给定字符串
     * @param $payload
     * @return string
     * @throws Exception
     */
    public function decrypt($payload)
    {
        $payload = $this->getJsonPayload($payload);
        $iv = base64_decode($payload['iv']);
        $decrypted = \openssl_decrypt($payload['value'], $this->cipher, $this->key, 0, $iv);
        if ($decrypted === false) {
            throw new Exception('数据解密失败[DECRYPT]');
        }

        return $decrypted;
    }


    /**
     * 验证密钥是否与给定的算法相匹配
     * @param string $key 密钥
     * @param string $cipher 算法
     * @return bool
     */
    protected static function supported($key, $cipher)
    {
        $length = mb_strlen($key, '8bit');

        return ($cipher === 'AES-128-CBC' && $length === 16) ||
            ($cipher === 'AES-256-CBC' && $length === 32);
    }

    /**
     * HASH加密
     * @param string $iv
     * @param mixed $value
     * @return string
     */
    protected function hash($iv, $value)
    {
        return hash_hmac('sha256', $iv . $value, $this->key);
    }

    /**
     * 解密JSON数据
     * @param $payload
     * @return mixed
     * @throws Exception
     */
    protected function getJsonPayload($payload)
    {
        $payload = json_decode(base64_decode($payload), true);
        if (!$this->validPayload($payload)) {
            throw new Exception('The payload is invalid.');
        }

        if (!$this->validMac($payload)) {
            throw new Exception('The MAC is invalid.');
        }

        return $payload;
    }

    /**
     * 验证加密数据是否有效
     * @param mixed $payload
     * @return bool
     */
    protected function validPayload($payload)
    {
        return is_array($payload) && isset($payload['iv'], $payload['value'], $payload['mac']) &&
            strlen(base64_decode($payload['iv'], true)) === openssl_cipher_iv_length($this->cipher);
    }

    /**
     * 验证HASH加密数据是否有效
     * @param array $payload
     * @return bool
     * @throws Exception
     */
    protected function validMac(array $payload)
    {
        $calculated = $this->calculateMac($payload, $bytes = random_bytes(16));

        return hash_equals(hash_hmac('sha256', $payload['mac'], $bytes, true), $calculated);
    }

    /**
     * 计算给定负载的散列
     * @param array $payload
     * @param string $bytes
     * @return string
     */
    protected function calculateMac($payload, $bytes)
    {
        return hash_hmac('sha256', $this->hash($payload['iv'], $payload['value']), $bytes, true);
    }
}

记得要分清bit与byte的关系哦~

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复