PHP 将 mcrypt_encrypt 迁移至 openssl_encrypt 的方法

注:php 的 mcrypt_簇在 7.1.0 版本中开始 deprecated,并在 7.2.0 版本中完全废弃。其实在 2015 就已经开始建议你们使用 openssl_encrypt/openssl_decrypt来代替 mcrypt_encrypt/mcrypt_decrypt,缓冲了 N 久,这一天终于在 7.2.0 版本上到来了。

为何要说起迁移,好比 A & B 两套系统使用AES加密作数据传输,A 做为客户端,B 则是第三方的服务,且 A 已经在使用 7.2.0+ 版本,而 B 做为长期运行的服务仍在使用 7.1.0-,那咱们能作的就是在 A 上使用 openssl_簇 原样的实现mcrypt_簇的加解密功能,以便兼容 B 服务,且 mcrypt_簇 是有一些须要多加注意的地方,不然迁移之路略微坎坷。php

mcrypt_簇 虽然说被遗弃了,但 文档页 上依然有不少值得注意的文档贡献,有助于咱们将 mcrypt_簇 迁移至openssl_簇,你们应该仔细看一下。java

1.If you're writing code to encrypt/encrypt data in 2015, you should use openssl_encrypt() and openssl_decrypt(). The underlying library (libmcrypt) has been abandoned since 2007, and performs far worse than OpenSSL (which leverages AES-NI on modern processors and is cache-timing safe).

2.Also, MCRYPT_RIJNDAEL_256 is not AES-256, it's a different variant of the Rijndael block cipher. If you want AES-256 in mcrypt, you have to use MCRYPT_RIJNDAEL_128 with a 32-byte key. OpenSSL makes it more obvious which mode you are using (i.e. 'aes-128-cbc' vs 'aes-256-ctr').算法

3.OpenSSL also uses PKCS7 padding with CBC mode rather than mcrypt's NULL byte padding. Thus, mcrypt is more likely to make your code vulnerable to padding oracle attacks than OpenSSL.安全

一、即刻起,应尽量的使用openssl_簇代替mcrypt_簇来实现数据的加密功能。oracle

二、MCRYPT_RIJNDAEL_256并非AES-256,若是想使用mcrypt_簇 实现AES-256,则你应该使用 MCRYPT_RIJNDAEL_128 算法 + 32位的 key,openssl_簇 则更为清晰的明确了各类模式。这里我整理了一下对应关系供你们参考:dom

MCRYPT_RIJNDAEL_128 & MCRYPT_MODE_CBC + 16位Key = openssl_encrypt(AES-128-CBC, 16位Key) = AES-128
MCRYPT_RIJNDAEL_128 & MCRYPT_MODE_CBC + 24位Key = openssl_encrypt(AES-192-CBC, 24位Key) = AES-192
MCRYPT_RIJNDAEL_128 & MCRYPT_MODE_CBC + 32位Key = openssl_encrypt(AES-256-CBC, 32位Key) = AES-256

(注:AES-128, 192 and 256 的加密 key 的长度分别为 16, 24 and 32 位)函数

openssl_簇的确更为准确,并且mcrypt_get_key_size获得的key长度都是 32 位,因此不太靠谱。工具

iv到是会根据cipher变动 16 、2四、32,但openssl_簇AES cipheriv长度固定16位。编码

因此,咱们为了最大的适配,即使如今不会再用,也要知道mcrypt_簇实现 AES-128/192/256 的标准方式为:加密

  1. cipher 固定选用 MCRYPT_RIJNDAEL_128
  2. 根据 cipher 和 mode 生成 iv(mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128 , MCRYPT_MODE_CBC))),因 cipher 咱们固定为 MCRYPT_RIJNDAEL_128 因此 iv 固定为 16 位,同 openssl 兼容。
  3. 根据实际业务来肯定 key 长度: AES-128 16位 / AES-192 24位 / AES-256 32位,而不是使用 mcrypt_get_key_size(MCRYPT_RIJNDAEL_128 下使用此方法获取的 key 长度固定为 16 位,不符合要求)

三、这一点其实蛮重要的,涉及加密算法数据块&填充算法PKCS7的概念。我在支付宝alipay SDK中有看到此算法的实现,虽然 sdk 中仍然使用的mcrypt_簇,但已结合了PKCS7填充算法,为何要这样作呢?其一是为了安全&兼容,php mcrypt会默认使用null('\000')对数据块进行填充,java/.net则默认使用PKCS7。其二则是为后期迁移至openssl_簇的准备,openssl的默认填充算法也是PKCS7(固然也能够指定使用null('\000')填充模式,但极力不推荐的)。

mcrypt_encrypt / mcrypt_decrypt

相关的支持函数

// 支持的算法 rijndael-128|rijndael-192|rijndael-256(此算法并不是AES-256,需使用rijndael-128 + key32byte实现)
mcrypt_list_algorithms()
// 支持的模式 cbc ecb 等
mcrypt_list_modes()
// 算法所对应的 key 长度:AES-128, 192 and 256 的加密 key 的长度分别为 16, 24 and 32 位
mcrypt_get_key_size(string $cipher , string $mode)
// 算法所对应的加密向量 iv 的长度
mcrypt_get_iv_size(string $cipher , string $mode)
// 生成 iv
mcrypt_create_iv(mcrypt_get_iv_size(string $cipher , string $mode))
// 加密算法数据块的大小 主要用于填充算法
mcrypt_get_block_size(string $cipher , string $mode)

PKCS7 填充算法的实现

/**
 * 填充算法
 * @param string $source
 * @return string
 */
function addPKCS7Padding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source     = trim($source);
    // 获取加密算法数据块大小 用于计算须要填充多少位
    $block_size = mcrypt_get_block_size($cipher, $mode);
    $pad        = $block_size - (strlen($source) % $block_size);
    if ($pad <= $block_size) {
        $char = chr($pad);
        $source .= str_repeat($char, $pad);
    }
    return $source;
}

/**
 * 移去填充算法
 * @param string $source
 * @return string
 */
function stripPKCS7Padding($source)
{
    $source = trim($source);
    $char   = substr($source, -1);
    $num    = ord($char);
    if ($num == 62) {
        return $source;
    }

    $source = substr($source, 0, -$num);
    return $source;
}

openssl_encrypt/openssl_decrypt

简单讲解一下平常开发中用到的参数

/**
 * $data 待加密内容
 * $method 加密算法
 * $key 加密key
 * $options 数据块填充模式
 * $iv 加密向量
**/
openssl_encrypt(string $data, string $method, string $key[, int $options = 0[, string $iv = ""
[, string &$tag = NULL[, string $aad = ""[, int $tag_length = 16 ]]]]]): string

openssl_decrypt(string $data, string $method, string $key[, int $options = 0[, string $iv = ""
[, string $tag = "" [, string $aad = "" ]]]] ) : string

这里须要特别注意的就是 options 选项,不少人 mcrypt_簇 迁移至 openssl_簇 时两者加密结果内容不一致,大都是此处没有搞清楚的缘由。options 共 3 个值可选

0 默认值 使用 PKCS7 填充算法,不对加密结果进行 base64encode
1 OPENSSL_RAW_DATA 使用 PKCS7 填充算法,且对加密结果进行 base64encode
2 OPENSSL_ZERO_PADDING 使用 null('0') 进行填充,且对加密结果进行 base64encode

因此要注意填充算法及对结果是否进行了 base64encode 编码。

mcrypt_簇 迁移至 openssl_簇

mcrypt_簇

/**
 * 加密算法
 * @param  string $content    待加密数据
 * @param  string $key    加密key
 * @param  string $iv     加密向量
 * @param  string $cipher 加密算法
 * @param  string $mode   加密模式
 * @return string         加密后的内容且base64encode
 */
function encrypt($content, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密数据 CBC
    $content           = addPKCS7Padding($content);
    $content_encrypted = mcrypt_encrypt($cipher, $key, $content, $mode, $iv);
    return base64_encode($content_encrypted);
}

/**
 * 解密算法
 * @param  [type] $content [description]
 * @param  [type] $key     [description]
 * @param  [type] $iv      [description]
 * @param  [type] $cipher  [description]
 * @param  [type] $mode    [description]
 * @return [type]          [description]
 */
function decrypt($content_encrypted, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密数据 CBC
    $content_encrypted = base64_decode($content_encrypted);
    $content           = mcrypt_decrypt($cipher, $key, $content_encrypted, $mode, $iv);
    $content           = stripPKSC7Padding($content);
    return $content;
}

/**
 * PKCS7填充算法
 * @param string $source
 * @return string
 */
function addPKCS7Padding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source = trim($source);
    $block  = mcrypt_get_block_size($cipher, $mode);
    $pad    = $block - (strlen($source) % $block);
    if ($pad <= $block) {
        $char = chr($pad);
        $source .= str_repeat($char, $pad);
    }
    return $source;
}
/**
 * 移去PKCS7填充算法
 * @param string $source
 * @return string
 */
function stripPKSC7Padding($source)
{
    $source = trim($source);
    $char   = substr($source, -1);
    $num    = ord($char);
    if ($num == 62) {
        return $source;
    }

    $source = substr($source, 0, -$num);
    return $source;
}

openssl_簇

转换实例

以 AES-128 为例

// 固定使用此算法 而后经过 key 的长度来决定具体使用的是何种 AES
$cipher = MCRYPT_RIJNDAEL_128;
$mode   = MCRYPT_MODE_CBC;

// openssl_簇 iv 固定为 16 位,mcrypt_簇 MCRYPT_RIJNDAEL_128 是 16位
// 但改成 MCRYPT_RIJNDAEL_192/256 就是 24/32 位了,会不兼容 openssl_簇
// 因此务必注意向量长度统一固定 16 位方便两套算法对齐
// $iv = mcrypt_create_iv(mcrypt_get_iv_size($cipher, $mode), MCRYPT_RAND);

// 根据须要自行定义相应的 key 长度 aes-128=16 aes-192=24 aes-256=32
$key = '0123456789012345';
// 固定为 16 位
$iv  = '0123456789012345';

$content = "hello world";

// mcrypt 加解密
$mcrypt_data = encrypt($content, $key, $iv, $cipher, $mode);
var_dump($mcrypt_data);
$content = decrypt($mcrypt_data, $key, $iv, $cipher, $mode);
var_dump($content);

// mcrypt 时使用了 PKCS7 填充 并对结果 base64encode
// 若是 +PKCS7 +base64encode 则 option = 0
// 若是 +PKCS7 -base64encode 则 option = 1
// 若是 -PKCS7 +base64encode 则 option = 2
$openssl_data = openssl_encrypt($content, "AES-128-CBC", $key, 0, $iv)
var_dump($openssl_data);
$content = openssl_decrypt($openssl_data, "AES-128-CBC", $key, 0, $iv)
var_dump($content);

// 相互转换
$content = openssl_decrypt($mcrypt_data, "AES-128-CBC", $key, 0, $iv)
var_dump($content);
$content = decrypt($openssl_data, $key, $iv, $cipher, $mode);
var_dump($content);

总结

一、PKCS7 填充算法。
二、openssl_encrypt / openssl_decrypt 三种模式所表示的 PKCS7/base64encode。
三、mcrypt_簇 的 cipher/mode 同 openssl_簇 的转换。

<?php
/**
 * MCRYPT_RIJNDAEL_128 & CBC + 16位Key + 16位iv = openssl_encrypt(AES-128-CBC, 16位Key, 16位iv) = AES-128
 * MCRYPT_RIJNDAEL_128 & CBC + 24位Key + 16位iv = openssl_encrypt(AES-192-CBC, 24位Key, 16位iv) = AES-192
 * MCRYPT_RIJNDAEL_128 & CBC + 32位Key + 16位iv = openssl_encrypt(AES-256-CBC, 32位Key, 16位iv) = AES-256
 * ------------------------------------------------------------------------------------------------------
 * openssl_簇 options
 * 0 : 自动对明文进行 pkcs7 padding, 返回的数据通过 base64 编码.
 * 1 : OPENSSL_RAW_DATA, 自动对明文进行 pkcs7 padding, 但返回的结果未通过 base64 编码
 * 2 : OPENSSL_ZERO_PADDING, 自动对明文进行 null('0') 填充, 同 mcrpty 一致,且返回的结果通过 base64 编码, openssl 不推荐 0 填充的方式, 即便选择此项也不会自动进行 padding, 仍需手动 padding
 * --------------------------------------------------------------------------------------------------------
 * mcrypt 默认是用 0 填充,为保持良好的兼容性建议使用 pkcs7 填充数据 openssl 0|1 都使用的 pkcs7
 * pkcs7 填充
 * 加密工具类
 */

// 随机字符串
function get_random_str($length = 16)
{
    $char_set = array_merge(range('a', 'z'), range('A', 'Z'), range('0', '9'));
    shuffle($char_set);
    return implode('', array_slice($char_set, 0, $length));
}

// 固定使用此算法 而后经过 key 的长度来决定具体使用的是何种 AES
$mcrypt_cipher = MCRYPT_RIJNDAEL_128;
$mcrypt_mode   = MCRYPT_MODE_CBC;
// openssl_簇 AES iv 固定为 16 位,mcrypt_簇只有在 MCRYPT_RIJNDAEL_128 为 16 位 需注意保持一致
$iv = mcrypt_create_iv(mcrypt_get_iv_size($mcrypt_cipher, $mcrypt_mode), MCRYPT_RAND);
// aes-128=16 aes-192=24 aes-256=32
$key_size = 16;
$key      = get_random_str($key_size);
// openssl_ AES 向量长度固定 16 位 这里为
$iv = get_random_str(16);

/**
 * 加密算法
 * @param  string $content    待加密数据
 * @param  string $key    加密key
 * @param  string $iv     加密向量
 * @param  string $cipher 加密算法
 * @param  string $mode   加密模式
 * @return string         加密后的内容且base64encode
 */
function encrypt($content, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密数据 CBC
    $content           = addPKCS7Padding($content);
    $content_encrypted = mcrypt_encrypt($cipher, $key, $content, $mode, $iv);
    return base64_encode($content_encrypted);
}

/**
 * 解密算法
 * @param  [type] $content [description]
 * @param  [type] $key     [description]
 * @param  [type] $iv      [description]
 * @param  [type] $cipher  [description]
 * @param  [type] $mode    [description]
 * @return [type]          [description]
 */
function decrypt($content_encrypted, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密数据 CBC
    $content_encrypted = base64_decode($content_encrypted);
    $content           = mcrypt_decrypt($cipher, $key, $content_encrypted, $mode, $iv);
    $content           = stripPKSC7Padding($content);
    return $content;
}

/**
 * PKCS7填充算法
 * @param string $source
 * @return string
 */
function addPKCS7Padding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source = trim($source);
    $block  = mcrypt_get_block_size($cipher, $mode);
    $pad    = $block - (strlen($source) % $block);
    if ($pad <= $block) {
        $char = chr($pad);
        $source .= str_repeat($char, $pad);
    }
    return $source;
}
/**
 * 移去PKCS7填充算法
 * @param string $source
 * @return string
 */
function stripPKSC7Padding($source)
{
    $source = trim($source);
    $char   = substr($source, -1);
    $num    = ord($char);
    if ($num == 62) {
        return $source;
    }

    $source = substr($source, 0, -$num);
    return $source;
}

$content = "hello world";

var_dump($data = encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode));
var_dump(decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode));
var_dump($openssl_data = openssl_encrypt($content, "AES-128-CBC", $key, 0, $iv));
var_dump(openssl_decrypt($openssl_data, "AES-128-CBC", $key, 0, $iv));

// var_dump(openssl_cipher_iv_length('AES-256-CBC'));
// var_dump(mcrypt_get_key_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC));
相关文章
相关标签/搜索