标签 crypto 下的文章

吐槽微信的AES加密设计

微信公众号平台的消息可以使用AES加密,他们在技术文档中说明,他们使用了256位AES加密算法,并按pkcs#7标准对加密数据进行补位,也提供了官方的PHP版加解密库程序。

我浏览了一下他们提供的PHP程序,不得不吐槽一下这个DEMO的开发者。看代码的感觉完全是一个非PHP程序员一边查着手册一边写出来的。代码里有不少低效率的写法和实现,所以我决定要重写一遍他们的加解密库。

考虑到OpenSSL库实现的AES性能是mcrypt库的好几倍,所以我优先考虑基于OpenSSL函数来开发。结果问题就出来了,AES加密算法规定的block-size是128位,不是整数倍block-size的数据需要先补位才能进行AES加密。腾讯选择pkcs#7规范进行补位我没意见,可是你定义的最大补位长度为什么是32字节?按理说最大补位长度应该和AES的block-size一样,128位应该是16字节才对呀。腾讯的这个奇怪规定直接导致无法使用OpenSSL库内置的补位支持。PHP的OpenSSL函数库到PHP5.4版以上才加入了0补位的选项参数,这才能实现自定义补位,这直接导致我的代码无法兼容PHP5.3以前的版本。

我把微信公众号开发用到的库整理出来,放到github上,目前先有这个加解密库,后续准备把微信的Web Service API接口也封装一下,逐渐发布。项目地址: https://github.com/SyuTingSong/php-wechat

理解PHP的AES加密

这是一篇翻译的文章,原文在这里。为排版效果,有改动。

理解PHP的AES加密

这一段PHP代码给出了PHP的AES加密的一个基本轮廓

首先要了解的东西是下面几个常量:

MCRYPT_RIJNDAEL_128
MCRYPT_RIJNDAEL_192
MCRYPT_RIJNDAEL_256

你可能认为MCRYPT_RIJNDAEL_256意味着256位加密技术,但其实不是的。这三个选项定义的是Rijndael加密过程中所使用的块尺寸(block-size),而与密钥长度(即所谓的加密强度)完全无关。(后面会解释AES加密中的加密强度如何设置。)

Rijndael是一种块加密算法,它在一系列互不相关的数据块上分别进行操作。所以必须对数据进行补位操作,以保证被加密的数据长度正好是块尺寸的整数倍。(PHP使用NULL字节进行补位) 因此,如果你指定了MCRYPT_RIJNDAEL_256,你加密输出的数据长度一定是32字节(256位)的整数倍;如果你指定了MCRYPT_RIJNDAEL_128,你的加密输出则一定是16字节的整数倍。

注意:严格地说,AES和Rijndael并不完全等价(尽管实践中它们经常互换使用),因为Rijndael支持更大范围的块尺寸和密钥长度;AES使用固定的128位块尺寸,而密钥长度可以是128位、192位或者256位。Rijndael的块尺寸和密钥长度只要求使用32位的整数倍,最小128位,最大256位。

简而言之:如果你想和AES兼容,请永远使用MCRYPT_RIJNDAEL_128。

那么,第一步自然是初始化加密器对象

$cipher = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');

我们使用CBC模式(cipher-block chaining)进行加密。块加密模式详见这里。要使用CBC模式,需要提供一个初始向量(IV)。初始向量的长度必须和块尺寸相当(不一定等于密钥长度)。既然我们的块尺寸是128位,所以IV也应该是128位(16字节)。也就是说,对于AES加密方法,初始向量总是16字节,不论加密强度如何。

这里是验证初始向量长度的代码:

$iv_size = mcrypt_enc_get_iv_size($cipher);
printf("iv_size = %d\n",$iv_size);

那么在PHP中,如何进行256位而不是128位的AES加密?答案是使用32字节长的密钥。

譬如:

$key256 = '12345678901234561234567890123456';
$key128 = '1234567890123456';

我们的128位初始向量既可以用于128位加密,又可以用于256位加密。

$iv =  '1234567890123456';

printf("iv: %s\n",bin2hex($iv));
printf("key256: %s\n",bin2hex($key256));
printf("key128: %s\n",bin2hex($key128));

这是要加密的原文:

$cleartext = 'The quick brown fox jumped over the lazy dog';
printf("plainText: %s\n\n",$cleartext);

mcrypt_generic_init函数初始化的时候,需要同时提供密钥和初始向量。密钥的长度决定了我们是在进行128位、192位还是256位加密。这里我们先进行256位加密:

if (mcrypt_generic_init($cipher, $key256, $iv) != -1)
{
    // 如果$cleartext的长度并非恰好是块尺寸的整数倍,PHP会用NULL字符进行补位
    $cipherText = mcrypt_generic($cipher,$cleartext );
    mcrypt_generic_deinit($cipher);

    // 以十六进制显示最终结果:
    printf("256-bit encrypted result:\n%s\n\n",bin2hex($cipherText));
}

这次我们进行128位加密:

if (mcrypt_generic_init($cipher, $key128, $iv) != -1)
{
    // 如果$cleartext的长度并非恰好是块尺寸的整数倍,PHP会用NULL字符进行补位
    $cipherText = mcrypt_generic($cipher,$cleartext );
    mcrypt_generic_deinit($cipher);

    // 以十六进制显示最终结果:
    printf("128-bit encrypted result:\n%s\n\n",bin2hex($cipherText));
}

结果

你可以用下列测试来指导针对你自己的AES实现进行的测试

256位密钥CBC模式

IV = '1234567890123456'
 (hex: 31323334353637383930313233343536)
Key = '12345678901234561234567890123456'
 (hex: 3132333435363738393031323334353631323334353637383930313233343536)
PlainText:
 'The quick brown fox jumped over the lazy dog'
CipherText(hex):
 2fddc3abec692e1572d9b7d629172a05caf230bc7c8fd2d26ccfd65f9c54526984f7cb1c4326ef058cd7bee3967299e3

128位密钥CBC模式

IV = '1234567890123456'
 (hex: 31323334353637383930313233343536)
Key = '1234567890123456'
 (hex: 31323334353637383930313233343536)
PlainText:
 'The quick brown fox jumped over the lazy dog'
CipherText(hex):
 f78176ae8dfe84578529208d30f446bbb29a64dc388b5c0b63140a4f316b3f341fe7d3b1a3cc5113c81ef8dd714a1c99