对称加解密算法中,当前最为安全的是 AES 加密算法(之前应该是是 DES 加密算法),PHP 提供了两个能够用于 AES 加密算法的函数簇:Mcrypt 和 OpenSSL。php
其中 Mcrypt 在 PHP 7.1.0 中被 Deprecated,在 PHP 7.2.0 中被移除,因此便可起你应该使用 OpenSSL 来实现 AES 的数据加解密。java
在一些场景下,咱们不能保证两套通讯系统都使用了相函数簇去实现加密算法,可能 siteA 使用了最新的 OpenSSL 来实现了 AES 加密,但做为第三方服务的 siteB 可能仍在使用 Mcrypt 算法,这就要求咱们必须清楚 Mcrypt 同 OpenSSL 之间的差别,以便保证数据加解密的一致性。算法
下文中咱们将分别使用 Mcrypt 和 OpenSSL 来实现 AES-128/192/256-CBC 加解密,两者同步加解密的要点为:api
一、使用何种填充算法。Mcrypt 自动使用 NUL("\0"),OpenSSL 自动使用 PKCS7。安全
二、否对数据作了base64编码处理。Mcrypt 默认不使用 base64 编码(虽然咱们很建议使用),OpenSSL 默认使用 base64编码。dom
协同好以上两点,就可让 Mcrypt 和 OpenSSL 之间一致性的对数据进行加解密。函数
AES 是当前最为经常使用的安全对称加密算法,关于对称加密这里就不在阐述了。工具
AES 有三种算法,主要是对数据块的大小存在区别:编码
AES-128:须要提供 16bytes 的密钥 key加密
AES-192:须要提供 24 bytes 的密钥 key
AES-256:须要提供 32bytes 的密钥 key
AES 是按数据块大小(128bit/192bit/256bit)对待加密内容进行分块处理的,会常常出现最后一段数据长度不足的场景,这时就须要填充数据长度到加密算法对应的数据块大小。
主要的填充算法有填充 NUL("0") 和 PKCS7,Mcrypt 默认使用的 NUL("0") 填充算法,当前已不被推荐,OpenSSL 则默认模式使用 PKCS7 对数据进行填充并对加密后的数据进行了 base64encode 编码,因此建议开发中使用 PKCS7 对待加密数据进行填充,已保证通用性(alipay sdk 中虽然使用了 Mcrypt 加密簇,但使用 PKCS7 算法对数据进行了填充,这样在必定程度上亲和了 OpenSSL 加密算法)。
Mcrypt 的默认填充算法。NUL 即为 Ascii 表的编号为 0 的元素,即空元素,转移字符是 "\0",PHP 的 pack 打包函数在 'a' 模式下就是以 NUL 字符对内容进行填充的,固然,使用 "\0" 手动拼接也是能够的。
/** * NUL("\0")填充算法 * @param string $source * @return string */ function addZeroPadding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC) { $source = trim($source); // openssl 并无提供加密cipher对应的数据块大小的api这点比较坑 $block = mcrypt_get_block_size($cipher, $mode); $pad = $block - (strlen($source) % $block); if ($pad <= $block) { // $source .= str_repeat("\0", $pad);//KISS写法 // pack 方法 a 模式使用 NUL("\0") 对内容进行填充 A 模式则使用空白字符填充 $source .= pack("a{$pad}", ""); //高端写法 } return $source; } /** * NUL("\0")填充算法移除 * @param string $source * @return string */ function stripZeroPadding($source) { return rtrim($source, "\0"); }
OpenSSL的默认填充算法。下面咱们给出 PKCS7 填充算法 PHP 的实现:
/** * 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; }
默认使用 NUL("\0") 自动对待加密数据进行填充以对齐加密算法数据块长度。
获取 mcrypt 支持的算法,这里咱们只关注 AES 算法。
$cipher_list = mcrypt_list_algorithms(); print_r($cipher_list); // 加密算法名称 对应的 常量标识 // 'rijndael-128' == MCRYPT_RIJNDAEL_128 // 'rijndael-192' == MCRYPT_RIJNDAEL_192 // 'rijndael-256' == MCRYPT_RIJNDAEL_256
注意:mcrypt 虽然支持 AES 三种算法,但除 MCRYPT_RIJNDAEL_128 外,MCRYPT_RIJNDAEL_192/256 并未遵循 AES-192/256 标准进行加解密的算法,即若是你同其余系统通讯(java/.net),使用 MCRYPT_RIJNDAEL_192/256 可能没法被其余严格按照 AES-192/256 标准的系统正确的数据解密。官方文档页面中也有人在 User Contributed Notes 中说起。这里给出如何使用 mcrpyt 作标注的 AES-128/192/256 加解密
即算法统一使用 MCRYPT_RIJNDAEL_128,并经过 key 的位数 来选定是以何种 AES 标准作的加密,iv 是建议添加且建议固定为16位(OpenSSL的 AES加密 iv 始终为 16 位,便于统一对齐),mode 选用的 CBC 模式。
mcrypt 在对数据进行加密处理时,若是发现数据长度与使用的加密算法的数据块长度未对齐,则会自动使用 "\0" 对待加密数据进行填充,但 "\0" 填充模式已再也不被推荐,为了与其余系统有更好的兼容性,建议你们手动对数据进行 PKCS7 填充。
php 文档地址
openssl 簇加密方法更为简单明确,mcrypt 还要将加密算法分为 cipher + mode 去指定,openssl 则只须要直接指定 method 为 AES-128-CBC,AES-192-CBC,AES-256-CBC 便可。且提供了三种数据处理模式,即 默认模式 0 / OPENSSL_RAW_DATA / OPENSSL_ZERO_PADDING。
openssl 默认的数据填充方式是 PKCS7,为兼容 mcrpty 也提供处理 "\0" 填充的数据的模式,具体为下:
//咱们只看部分参数 还有一些默认参数没列出 // 加密 openssl_encrypt(string $data, string $method, string $key, int $options = 0, string $iv = "") // 解密 openssl_decrypt(string $data, string $method, string $key, int $options = 0, string $iv = "")
options 参数即为重要,它是兼容 mcrpty 算法的关键:
options = 0 : 默认模式,自动对明文进行 pkcs7 padding,且数据作 base64 编码处理。
options = 1 : OPENSSL_RAW_DATA,自动对明文进行 pkcs7 padding, 且数据未经 base64 编码处理。
options = 2 : OPENSSL_ZERO_PADDING,要求待加密的数据长度已按 "\0" 填充与加密算法数据块长度对齐,即同 mcrpty 默认填充的方式一致,且对数据作 base64 编码处理。注意,此模式下 openssl 要求待加密数据已按 "\0" 填充好,其并不会自动帮你填充数据,若是未填充对齐,则会报错。
故能够得出 mcrpty簇 与 openssl簇 的兼容条件以下:
一、若是 A 系统使用了 mcrypt 的默认的 "\0" 自动填充算法进行了数据加密,那 B 系统使用 openssl 进行解密时,须要选择 OPENSSL_ZERO_PADDING 模式。这里同时要特别注意,OPENSSL_ZERO_PADDING 模式是认为数据是作了 base64 编码处理的,若是 A 系统 mcrpty 没有对数据作 base64_encode 处理,则 B 解密数据时还须要将数据 base64_encode 后再传递给 openssl_decrypt。
二、若是 A 系统手动使用了 PKCS7 对待加密数据作了处理,那 B 系统只需根据 A 系统是否对数据作了 base64 编码处理选择 0或 OPENSSL_ZERO_PADDING 模式便可。若是对 A 使用了 base64 编码,则 B 使用 0 模式;若是 A 没有使用 base64 编码,则使用 OPENSSL_RAW_DATA 模式。
建议将源码复制到本地运行,根据运行结果更好理解。
<?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 填充, 且返回的加密数据通过 base64 编码 * 1 : OPENSSL_RAW_DATA, 自动对数据作 pkcs7 填充, 且返回的加密数据未通过 base64 编码 * 2 : OPENSSL_ZERO_PADDING, 处理使用 NUL("\0") 的数据,故需手动使用 NUL("\0") 填充好数据再作加密处理,如未作则会报错 * -------------------------------------------------------------------------------------------------------- * 加密工具类 */ // mcrypt AES 固定使用 MCRYPT_RIJNDAEL_128 经过 key 的长度来决定具体使用的具体何种 AES $mcrypt_cipher = MCRYPT_RIJNDAEL_128; $mcrypt_mode = MCRYPT_MODE_CBC; // aes-128=16 aes-192=24 aes-256=32 $key_size = 16; $key = get_random_str($key_size); // openssl AES 向量长度固定 16 位 这里为兼容建议固定长度为 16 位 $iv_size = 16; $iv = get_random_str($iv_size); // 随机字符串 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)); } /** * 加密算法 * @param string $content 待加密数据 * @param string $key 加密key 注意 key 长度要求 * @param string $iv 加密向量 固定为16位能够保证与openssl的兼容性 * @param string $cipher 加密算法 * @param string $mode 加密模式 * @param bool $pkcs7 是否使用pkcs7填充 不然使用 mcrypt 自带的 NUL("\0") 填充 * @param bool $base64 是否对数据作 base64 处理 因加密后数据会有非打印字符 因此推荐作 base64 处理 * @return string 加密后的内容 */ function user_mcrypt_encrypt($content, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC, $pkcs7 = true, $base64 = true) { //AES, 128 模式加密数据 CBC $content = $pkcs7 ? addPKCS7Padding($content) : $content; $content_encrypted = mcrypt_encrypt($cipher, $key, $content, $mode, $iv); return $base64 ? base64_encode($content_encrypted) : $content_encrypted; } /** * 解密算法 * @param [type] $content_encrypted 待解密的内容 * @param [type] $key 加密key 注意 key 长度要求 * @param [type] $iv 加密向量 固定为16位能够保证与openssl的兼容性 * @param [type] $cipher 加密算法 * @param [type] $mode 加密模式 * @param bool $pkcs7 带解密内容是否使用了pkcs7填充 若是没使用则 mcrypt 会自动移除填充的 NUL("\0") * @param bool $base64 是否对数据作 base64 处理 * @return [type] [description] */ function user_mcrypt_decrypt($content_encrypted, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC, $pkcs7 = true, $base64 = true) { //AES, 128 模式加密数据 CBC $content_encrypted = $base64 ? base64_decode($content_encrypted) : $content_encrypted; $content = mcrypt_decrypt($cipher, $key, $content_encrypted, $mode, $iv); // 解密后的内容 要根据填充算法来相应的移除填充数 $content = $pkcs7 ? stripPKSC7Padding($content) : rtrim($content, "\0"); 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; } /** * NUL("\0")填充算法 * @param string $source * @return string */ function addZeroPadding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC) { $source = trim($source); // openssl 并无提供加密cipher对应的数据块大小的api这点比较坑 $block = mcrypt_get_block_size($cipher, $mode); $pad = $block - (strlen($source) % $block); if ($pad <= $block) { // $source .= str_repeat("\0", $pad);//KISS写法 // pack 方法 a 模式使用 NUL("\0") 对内容进行填充 A 模式则使用空白字符填充 $source .= pack("a{$pad}", ""); //高端写法 } return $source; } /** * NUL("\0")填充算法移除 * @param string $source * @return string */ function stripZeroPadding($source) { return rtrim($source, "\0"); } // 待加密内容 $content = "hello world"; echo '使用 NUL("\0") 填充算法 不对结果作 base64 处理:' . PHP_EOL; echo 'mcrypt 加密:' . PHP_EOL; var_dump($data = user_mcrypt_encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode, false, false)); echo 'openssl 解密:' . PHP_EOL; // 需通过 NUL("\0") 填充加密后被 base64_encode 的数据 解密后续手动移除 NUL("\0") var_dump(stripZeroPadding(openssl_decrypt(base64_encode($data), "AES-128-CBC", $key, OPENSSL_ZERO_PADDING, $iv))); echo 'openssl 加密:' . PHP_EOL; // 需对待处理的数据作 NUL("\0") 填充,且返回的数据被 base64_encode 编码了 var_dump($data = base64_decode(openssl_encrypt(addZeroPadding($content), "AES-128-CBC", $key, OPENSSL_ZERO_PADDING, $iv))); echo 'mcrypt 解密:' . PHP_EOL; var_dump(user_mcrypt_decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode, false, false)); echo PHP_EOL; echo '使用 NUL("\0") 填充算法 对结果作 base64 处理:' . PHP_EOL; echo 'mcrypt 加密:' . PHP_EOL; var_dump($data = user_mcrypt_encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode, false, true)); echo 'openssl 解密:' . PHP_EOL; var_dump(stripZeroPadding(openssl_decrypt($data, "AES-128-CBC", $key, OPENSSL_ZERO_PADDING, $iv))); echo 'openssl 加密:' . PHP_EOL; var_dump($data = openssl_encrypt(addZeroPadding($content), "AES-128-CBC", $key, OPENSSL_ZERO_PADDING, $iv)); echo 'mcrypt 解密:' . PHP_EOL; var_dump(user_mcrypt_decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode, false, true)); echo PHP_EOL; echo "使用 pkcs7 填充算法 不对结果作 base64 处理" . PHP_EOL; echo 'mcrypt 加密:' . PHP_EOL; var_dump($data = user_mcrypt_encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode, true, false)); echo 'openssl 解密:' . PHP_EOL; var_dump(openssl_decrypt($data, "AES-128-CBC", $key, OPENSSL_RAW_DATA, $iv)); echo 'openssl 加密:' . PHP_EOL; var_dump($data = openssl_encrypt($content, "AES-128-CBC", $key, OPENSSL_RAW_DATA, $iv)); echo 'mcrypt 解密:' . PHP_EOL; var_dump(user_mcrypt_decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode, true, false)); echo PHP_EOL; echo "使用 pkcs7 填充算法 对结果作 base64 处理(推荐):" . PHP_EOL; echo 'mcrypt 加密:' . PHP_EOL; var_dump($data = user_mcrypt_encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode, true, true)); echo 'openssl 解密:' . PHP_EOL; var_dump(openssl_decrypt($data, "AES-128-CBC", $key, 0, $iv)); echo 'openssl 加密:' . PHP_EOL; var_dump($data = openssl_encrypt($content, "AES-128-CBC", $key, 0, $iv)); echo 'mcrypt 解密:' . PHP_EOL; var_dump(user_mcrypt_decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode, true, true));
一、两者使用的何种填充算法。
二、两者对数据是否有 base64 编码要求。
三、mcrypt 需固定使用 MCRYPT_RIJNDAEL_128,并经过调整 key 的长度 16, 24,32 来实现 ase-128/192/256 加密算法,同时 iv 最好固定为 16 位,由于 openssl 的 aes 的 iv 固定为 16 位。