对称加解密算法中,当前最为安全的是 AES 加密算法(以前应该是是 DES 加密算法),PHP 提供了两个可以用于 AES 加密算法的函数簇:Mcrypt 和 OpenSSL。
其中 Mcrypt 在 PHP 7.1.0 中被 Deprecated,在 PHP 7.2.0 中被移除,所以即可起你应该使用 OpenSSL 来实现 AES 的数据加解密。
在一些场景下,我们不能保证两套通信系统都使用了相函数簇去实现加密算法,可能 siteA 使用了最新的 OpenSSL 来实现了 AES 加密,但作为第三方服务的 siteB 可能仍在使用 Mcrypt 算法,这就要求我们必须清楚 Mcrypt 同 OpenSSL 之间的差异,以便保证数据加解密的一致性。
下文中我们将分别使用 Mcrypt 和 OpenSSL 来实现 AES-128/192/256-CBC 加解密,二者同步加解密的要点为:
1、使用何种填充算法。Mcrypt 自动使用 NUL("\0"),OpenSSL 自动使用 PKCS7。
2、否对数据做了base64编码处理。Mcrypt 默认不使用 base64 编码(虽然我们很建议使用),OpenSSL 默认使用 base64编码。
协同好以上两点,就可以让 Mcrypt 和 OpenSSL 之间一致性的对数据进行加解密。
AES 概述
AES 是当前最为常用的安全对称加密算法,关于对称加密这里就不在阐述了。
AES 有三种算法,主要是对数据块的大小存在区别:AES-128:需要提供 16 位的密钥 key
AES-192:需要提供 24 位的密钥 key
AES-256:需要提供 32 位的密钥 key
填充算法
AES 是按数据块大小(128/192/256)对待加密内容进行分块处理的,会经常出现最后一段数据长度不足的场景,这时就需要填充数据长度到加密算法对应的数据块大小。
主要的填充算法有填充 NUL("0") 和 PKCS7,Mcrypt 默认使用的 NUL("0") 填充算法,当前已不被推荐,OpenSSL 则默认模式使用 PKCS7 对数据进行填充并对加密后的数据进行了 base64encode 编码,所以建议开发中使用 PKCS7 对待加密数据进行填充,已保证通用性(alipay sdk 中虽然使用了 Mcrypt 加密簇,但使用 PKCS7 算法对数据进行了填充,这样在一定程度上亲和了 OpenSSL 加密算法)。
NUL("\0") 填充算法
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");
}
PKCS7 填充算法
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;
}
Mcrypt
默认使用 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
加解密
AES-128-CBC = mcrpyt(cipher MCRYPT_RIJNDAEL_128 + key 16位 + iv 16 位 + mode MCRYPT_MODE_CBC)
AES-192-CBC = mcrpyt(cipher MCRYPT_RIJNDAEL_128 + key 24位 + iv 16 位 + mode MCRYPT_MODE_CBC)
AES-256-CBC = mcrpyt(cipher MCRYPT_RIJNDAEL_128 + key 32位 + iv 16 位 + mode MCRYPT_MODE_CBC)
即算法统一使用 MCRYPT_RIJNDAEL_128
,并通过 key 的位数 来选定是以何种 AES 标准做的加密,iv 是建议添加且建议固定为16位(OpenSSL的 AES加密 iv 始终为 16 位,便于统一对齐),mode 选用的 CBC 模式。
mcrypt 在对数据进行加密处理时,如果发现数据长度与使用的加密算法的数据块长度未对齐,则会自动使用 "\0" 对待加密数据进行填充,但 "\0" 填充模式已不再被推荐,为了与其他系统有更好的兼容性,建议大家手动对数据进行 PKCS7 填充。
OpenSSL
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簇 的兼容条件如下:
1、如果 A 系统使用了 mcrypt 的默认的 "\0" 自动填充算法进行了数据加密,那 B 系统使用 openssl 进行解密时,需要选择OPENSSL_ZERO_PADDING
模式。这里同时要特别注意,OPENSSL_ZERO_PADDING
模式是认为数据是做了 base64 编码处理的,如果 A 系统 mcrpty 没有对数据做 base64_encode 处理,则 B 解密数据时还需要将数据 base64_encode 后再传递给 openssl_decrypt。
2、如果 A 系统手动使用了 PKCS7 对待加密数据做了处理,那 B 系统只需根据 A 系统是否对数据做了 base64 编码处理选择 0或OPENSSL_ZERO_PADDING
模式即可。如果对 A 使用了 base64 编码,则 B 使用 0 模式;如果 A 没有使用 base64 编码,则使用OPENSSL_RAW_DATA
模式。
加解密实例
建议将源码复制到本地运行,根据运行结果更好理解。
总结要点
1、二者使用的何种填充算法。
2、二者对数据是否有 base64 编码要求。
3、mcrypt 需固定使用 MCRYPT_RIJNDAEL_128,并通过调整 key 的长度 16, 24,32 来实现 ase-128/192/256 加密算法。同时二者 IV 长度都应该是。