page contents

openssl 下的对称加密和非对称加密

对称加密: 在加密和解密过程中使用相同的密钥, 或是两个可以简单地相互推算的密钥的加密算法. 非对称加密: 也称为公开加密, 它需要一个密钥对, 一个是公钥, 一个是私钥, 一个负责加密, 一个负责解密.

attachments-2020-04-VxcaPoKk5e8e88ac12712.jpg

对称加密: 在加密和解密过程中使用相同的密钥, 或是两个可以简单地相互推算的密钥的加密算法.

非对称加密: 也称为公开加密, 它需要一个密钥对, 一个是公钥, 一个是私钥, 一个负责加密, 一个负责解密.

对称加密在性能上要优于非对称加密, 但是安全性低于非对称加密.

PHP 7.1 之后的对称加密和非对称加密都需要借助 openssl 扩展实现. mcrypt 库已经被移除.


对称加密函数


openssl_get_cipher_methods() : 返回 openssl 支持的所有加密方式.
openssl_encrypt($data, $method, $key, $options = 0, $iv = '') : 以指定方式 method 和密钥 key 加密 data, 返回 false 或加密后的数据.

  • 1.data : 明文
  • 2.method : 加密算法
  • 3.key : 密钥
  • 4.options :

0 : 自动对明文进行 padding, 返回的数据经过 base64 编码.

1 : OPENSSL_RAW_DATA, 自动对明文进行 padding, 但返回的结果未经过 base64 编码.

2 : OPENSSL_ZERO_PADDING, 自动对明文进行 0 填充, 返回的结果经过 base64 编码. 但是, openssl 不推荐 0 填充的方式, 即使选择此项也不会自动进行 padding, 仍需手动 padding.


5.iv : 非空的初始化向量, 不使用此项会抛出一个警告. 如果未进行手动填充, 则返回加密失败.

  • openssl_decrypt($data, $method, $key, $options = 0, $iv = '') : 解密数据.
    openssl_cipher_iv_length($method) : 获取 method 要求的初始化向量的长度.
    openssl_random_pseudo_bytes($length) : 生成指定长度的伪随机字符串.
    hash_mac($method, $data, $key, $raw_out) : 生成带有密钥的哈希值.
  • method : 加密算法
  • data : 明文
  • key : 密钥
  • raw_output :

  • TRUE : 输出原始二进制数据
  • FALSE : 输出长度固定的小写 16 进制字符串

主流的对称加密方式有 DES, AES. 这两种加密方式都属于分组加密, 先将明文分成多个等长的模块 ( block ), 然后进行加密.


DES 加密


DES 加密的密钥长度为 64 bit, 实际应用中有效使用的是 56 位, 剩余 8 位作为奇偶校验位. 如果密钥长度不足 8 个字节, 将会使用 \0 补充到 8 个字节. 如密钥为 "12345", 其加密后的密文与密钥 "12345\0\0\0" 加密后的密文相同. 明文按 64 bit ( UTF-8 下为 8 个字节长度 ) 进行分组, 每 64 位分成一组 ( 最后一组不足 64 位的需要填充数据 ), 分组后的明文组和密钥按位替代或交换的方法形成密文组.

class DES
{
    private $method = 'DES-CBC';
    private $key;

    public function __construct($key)
    {
        // 密钥长度不能超过64bit(UTF-8下为8个字符长度),超过64bit不会影响程序运行,但有效使用的部分只有64bit,多余部分无效,可通过openssl_error_string()查看错误提示
        $this->key = $key;
    }

    public function encrypt($plaintext)
    {
        // 生成加密所需的初始化向量, 加密时缺失iv会抛出一个警告
        $ivlen = openssl_cipher_iv_length($this->method);
        $iv = openssl_random_pseudo_bytes($ivlen);

        // 按64bit一组填充明文
        //$plaintext = $this->padding($plaintext);
        // 加密数据. 如果options参数为0, 则不再需要上述的填充操作. 如果options参数为1, 也不需要上述的填充操作, 但是返回的密文未经过base64编码. 如果options参数为2, 虽然PHP说明是自动0填充, 但实际未进行填充, 必须需要上述的填充操作进行手动填充. 上述手动填充的结果和options为0和1是自动填充的结果相同.
        $ciphertext = openssl_encrypt($plaintext, $this->method, $this->key, 1, $iv);
        // 生成hash
        $hash = hash_hmac('sha256', $ciphertext, $this->key, false);

        return base64_encode($iv . $hash . $ciphertext);

    }

    public function decrypt($ciphertext)
    {
        $ciphertext = base64_decode($ciphertext);
        // 从密文中获取iv
        $ivlen = openssl_cipher_iv_length($this->method);
        $iv = substr($ciphertext, 0, $ivlen);
        // 从密文中获取hash
        $hash = substr($ciphertext, $ivlen, 64);
        // 获取原始密文
        $ciphertext = substr($ciphertext, $ivlen + 64);
        // hash校验
        if(hash_equals($hash, hash_hmac('sha256', $ciphertext, $this->key, false)))
        {
            // 解密数据
            $plaintext = openssl_decrypt($ciphertext, $this->method, $this->key, 1, $iv) ?? false;
            // 去除填充数据. 加密时进行了填充才需要去填充
            $plaintext = $plaintext? $this->unpadding($plaintext) : false;

            return $plaintext;
        }

        return '解密失败';
    }

    // 按64bit一组填充数据
    private function padding($plaintext)
    {
        $padding = 8 - (strlen($plaintext)%8);
        $chr = chr($padding);

        return $plaintext . str_repeat($chr, $padding);
    }

    private function unpadding($ciphertext)
    {
        $chr = substr($ciphertext, -1);
        $padding = ord($chr);

        if($padding > strlen($ciphertext))
        {
            return false;
        }
        if(strspn($ciphertext, $chr, -1 * $padding, $padding) !== $padding)
        {
            return false;
        }

        return substr($ciphertext, 0, -1 * $padding);
    }
}


AES 加密


AES 加密的分组长度是 128 位, 即每个分组为 16 个字节 ( 每个字节 8 位 ). 密钥的长度根据加密方式的不同可以是 128 位, 192 位, 256 位. 与 DES 加密一样. 密钥长度超过指定长度时, 超出部分无效. 密钥长度不足时, 会自动以`\0`补充到指定长度.

v2-4cfe2449768691a313a0c08a19aa4c5d_720w.jpg

class AES
{
    private $key;
    private $method = 'aes-128-cbc';

    public function __construct($key)
    {
        // 是否启用了openssl扩展
        extension_loaded('openssl') or die('未启用 OPENSSL 扩展');
        $this->key = $key;
    }

    public function encrypt($plaintext)
    {
        if(!in_array($this->method, openssl_get_cipher_methods()))
        {
            die('不支持该加密算法!');
        }
       // options为1, 不需要手动填充
        //$plaintext = $this->padding($plaintext);
        // 获取加密算法要求的初始化向量的长度
        $ivlen = openssl_cipher_iv_length($this->method);
        // 生成对应长度的初始化向量. aes-128模式下iv长度是16个字节, 也可以自由指定.
        $iv = openssl_random_pseudo_bytes($ivlen);
        // 加密数据
        $ciphertext = openssl_encrypt($plaintext, $this->method, $this->key, 1, $iv);
        $hmac = hash_hmac('sha256', $ciphertext, $this->key, false);

        return base64_encode($iv . $hmac . $ciphertext);
    }

    public function decrypt($ciphertext)
    {
        $ciphertext = base64_decode($ciphertext);
        $ivlen = openssl_cipher_iv_length($this->method);
        $iv = substr($ciphertext, 0, $ivlen);
        $hmac = substr($ciphertext, $ivlen, 64);
        $ciphertext = substr($ciphertext, $ivlen + 64);
        $verifyHmac = hash_hmac('sha256', $ciphertext, $this->key, false);
        if(hash_equals($hmac, $verifyHmac))
        {
            $plaintext = openssl_decrypt($ciphertext, $this->method, $this->key, 1, $iv)??false;
         // 加密时未手动填充, 不需要去填充
            //if($plaintext)
            //{
            //    $plaintext = $this->unpadding($plaintext);
            //    echo $plaintext;
            //}

            return $plaintext;
        }else
        {
            die('数据被修改!');
        }
    }

    private function padding(string $data) : string
    {
        $padding = 16 - (strlen($data) % 16);
        $chr = chr($padding);
        return $data . str_repeat($chr, $padding);
    }

    private function unpadding($ciphertext)
    {
        $chr = substr($ciphertext, -1);
        $padding = ord($chr);

        if($padding > strlen($ciphertext))
        {
            return false;
        }

        if(strspn($ciphertext, $chr, -1 * $padding, $padding) !== $padding)
        {
            return false;
        }

        return substr($ciphertext, 0, -1 * $padding);
    }
}


非对称加密函数


$res = openssl_pkey_new([array $config]) : 生成一个新的私钥和公钥对. 通过配置数组, 可以微调密钥的生成.

  • digest_alg : 摘要或签名哈希算法.
  • private_key_bits : 指定生成的私钥的长度.
  • private_key_type : 指定生成私钥的算法. 默认 OPENSSL_KEYTYPE_RSA, 可指定 OPENSSL_KEYTYPE_DSA, OPENSSL_KEYTYPE_DH, OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_EC.
  • config : 自定义 openssl.conf 文件的路径.

openssl_pkey_free($res) : 释放有 openssl_pkey_new() 创建的私钥.
openssl_get_md_methods() : 获取可用的摘要算法.
openssl_pkey_export_to_file($res, $outfilename) : 将 ASCII 格式 ( PEM 编码 ) 的密钥导出到文件中. 使用相对路径时, 是相对服务器目录, 而非当前所在目录.
openssl_pkey_export($res, &$out) : 提取 PEM 格式私钥字符串.
openssl_pkey_get_details($res) : 返回包含密钥详情的数组.
openssl_get_privatekey($key) : 获取私钥. key 是一个 PEM 格式的文件或一个 PEM 格式的私钥.
openssl_get_publickey($certificate) : 获取公钥. certificate 是一个 X.509 证书资源或一个 PEM 格式的文件或一个 PEM 格式的公钥.
openssl_private_encrypt($data, &$crypted, $privKey [, $padding = OPENSSL_PKCS1_PADDING]) : 使用私钥加密数据, 并保存到 crypted . 其中填充模式为 OPENSSL_PKCS1_PADDING 时, 如果明文长度不够, 加密时会在明文中随机填充数据. 为 OPENSSL_NO_PADDING 时, 如果明文长度不够, 会在明文的头部填充 0 .
openssl_public_decrypt($crypted, &$decrypted, $pubKey [, $padding]) : 使用公钥解密数据, 并保存到 decrypted .
openssl_public_encrypt($data, &$crypted, $pubKey [, $padding]) : 使用公钥加密数据, 并保存到 crypted .
openssl_private_decrypt($crypted, &$decrypted, $privKey [, $padding]) : 使用私钥解密数据, 并保存到 decrypted .

非对称加密


RSA 也是一种分组加密方式, 但明文的分组长度根据选择的填充方式的不同而不同.

class RSA
{
    private $private_key; // 私钥
    private $public_key; // 公钥
    private $private_res; // 私钥资源
    private $public_res; // 公钥资源

    public function __construct()
    {
        extension_loaded('openssl') or die('未加载 openssl');
        // 生成新的公钥和私钥对资源
        $config = [
            'digest_alg' => 'sha256',
            'private_key_bits' => 1204,
            'private_key_type' => OPENSSL_KEYTYPE_RSA
        ];
        $res = openssl_pkey_new($config);
        if(!$res)
        {
            die('生成密钥对失败');
        }

        // 获取公钥, 生成公钥资源
        $this->public_key = openssl_pkey_get_details($res)['key'];
        $this->public_res = openssl_pkey_get_public($this->public_key);

        // 获取私钥, 生成私钥资源
        openssl_pkey_export($res, $this->private_key);
        $this->private_res = openssl_pkey_get_private($this->private_key);

        openssl_free_key($res);
    }

    // 加密
    public function encrypt($plaintext)
    {
        $ciphertext = null;
        openssl_public_encrypt($plaintext, $ciphertext, $this->public_res);
        return $ciphertext;
    }

    // 解密
    public function decrypt($ciphertext)
    {
        $plaintext = null;
        openssl_private_decrypt($ciphertext, $plaintext, $this->private_res);
        return $plaintext;
    }
}

在传输重要信息时, 一般会采用对称加密和非对称加密相结合的方式, 而非使用单一加密方式. 一般先通过 AES 加密数据, 然后通过 RSA 加密 AES 密钥, 然后将加密后的密钥和数据一起发送. 接收方接收到数据后, 先解密 AES 密钥, 然后使用解密后的密钥解密数据。


attachments-2020-04-g6LUVqvX5e8e885b1818a.jpg

  • 发表于 2020-04-09 10:32
  • 阅读 ( 363 )

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1474 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章