PHP 将 mcrypt_encrypt 迁移至 openssl_encrypt 的方法
为什么要提及迁移,比如 A & B 两套系统使用 AES 加密做数据传输,且 A 作为客户端,B 则是第三方的服务,且 A 已经在使用 7.2.0+ 版本,而 B 作为长期运行的服务仍在使用 7.1.0-,那我们能做的就是在 A 上使用 openssl_簇 原样的实现 mcrypt_簇 的加解密功能,以便兼容 B 服务,且 mcrypt_簇 是有一些需要多加注意的地方,否则迁移之路略微坎坷。
mcrypt_簇 虽说被遗弃了,但 文档页 上依然有很多值得注意的文档贡献,有助于我们将 mcrypt_簇 迁移至 openssl_簇,大家应该仔细看一下。
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.
1、即刻起,应尽可能的使用 openssl_簇 代替 mcrypt_簇 来实现数据的加密功能。
2、MCRYPT_RIJNDAEL_256 并不是 AES-256,如果想使用 mcrypt_簇 实现 AES-256,则你应该使用 MCRYPT_RIJNDAEL_128 算法 + 32位的 key,openssl_簇 则更为清晰的明确了各种模式。这里我整理了一下对应关系供大家参考:
MCRYPT_RIJNDAEL_128 & CBC + 16位Key = openssl_encrypt(AES-128-CBC, 16位Key) = AES-128 MCRYPT_RIJNDAEL_128 & CBC + 24位Key = openssl_encrypt(AES-192-CBC, 24位Key) = AES-192 MCRYPT_RIJNDAEL_128 & 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 、24、32,但 openssl_簇的 AES cipher 的 iv 长度适中要求为 16 位。
所以,我们为了最大的适配,即便现在不会再用,也要知道 mcrypt_簇 实现 AES-128/192/256 的标准方式为:
- cipher 选 MCRYPT_RIJNDAEL_128
- 根据 cipher 和 mode 获取 iv 长度并生成 iv
- 根据实际业务来确定 key 长度: AES-128 16位 / AES-192 24位 / AES-256 32位,而不是使用 mcrypt_get_key_size
3、这一点其实蛮重要的,涉及 加密算法数据块 & 填充算法PKCS7 的概念。我在支付宝 alipay SDK 中有看到此算法的实现,虽然 sdk 中仍然使用的 mcrypt_簇,但已结合了 PKCS7 填充算法,为什么要这样做呢?其一是为了安全&兼容,php mcrypt 会使用 null('0') 对数据块进行填充,java/.net 则是使用 PKCS7。其二则是为后期迁移至 openssl_簇 的准备,openssl 的填充模式默认是使用 PKCS7 填充的(当然也可以指定使用 null('0') 填充模式,但极力不推荐的)。
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 填充算法,不对加密结果进行 base64encode1 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);
总结
1、PKCS7 填充算法。
2、openssl_encrypt / openssl_decrypt 三种模式所表示的 PKCS7/base64encode。
3、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));