PHP AES算法使用细节

简单的实现

PHP的实现相当简单,两行代码就行,结果就是偶尔与其他平台不一致,原因就是屏蔽了很多细节。这只是一篇很无聊的笔记,略过。

如下面两行代码,虽然实现了目的,但效果并不理想

$iv = @mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), MCRYPT_RAND);
echo @base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, md5("loveyu.org"), "loveyu.org", MCRYPT_MODE_CBC, $iv));

秘钥长度与拓展选择

PHP支持N多的加密算法,而这些算法则来源于Openssl,如果使用函数openssl_get_cipher_methods()会得到一份列表,过滤后AES相关的如下:

aes-128-cbc
aes-128-cbc-hmac-sha1
aes-128-cbc-hmac-sha256
aes-128-ccm
aes-128-cfb
aes-128-cfb1
aes-128-cfb8
aes-128-ctr
aes-128-ecb
aes-128-gcm
aes-128-ofb
aes-128-xts
aes-192-cbc
aes-192-ccm
aes-192-cfb
aes-192-cfb1
aes-192-cfb8
aes-192-ctr
aes-192-ecb
aes-192-gcm
aes-192-ofb
aes-256-cbc
aes-256-cbc-hmac-sha1
aes-256-cbc-hmac-sha256
aes-256-ccm
aes-256-cfb
aes-256-cfb1
aes-256-cfb8
aes-256-ctr
aes-256-ecb
aes-256-gcm
aes-256-ofb
aes-256-xts
id-aes128-CCM
id-aes128-GCM
id-aes128-wrap
id-aes192-CCM
id-aes192-GCM
id-aes192-wrap
id-aes256-CCM
id-aes256-GCM
id-aes256-wrap

也就是说,它该有的都有了,问题来了这里使用的是Openssl拓展,而不是mcrypt拓展,原因只有一点,官方对mcrypt_encrypt函数做了如下标记:

Warning: This function has been DEPRECATED as of PHP 7.1.0. Relying on this function is highly discouraged. @see

向量的作用

看看具体的例子:mcrypt_create_iv 为什么要做这样的事情,简单点说就是安全考虑与保证加密结果的一致性,复杂点说详见Wiki初始向量,那么这个向量需要保密么,当然是可选的,毕竟不是秘钥。一个更通俗的解释就是,在加密之前对区块进行初始化(如随机数算法给你一个种子每次就一样了)

那么如果向量不一致能解密么?
可以的,只是会导致解密的数据结果顺序异常,重新组合下就好了,所以保密的作用并不是特别大,实际呢,你试试就知道了。

秘钥长度的理解

AES 128指什么 AES的区块长度固定为128比特(16字节)
AES192 和 AES256类似,区块大了24字节和32字节
怎么加密的呢,一块一块加密,如果一段文本很长很长,就截断为对应长度再加密
单个区块如何加密?这个问题可以看看这里:Link
这样就很好理解秘钥长度是多少了,对应的

  • AES 128 => 16个字符
  • AES 192 => 24个字符
  • AES 256 => 32个字符

如果要说,还有没有其他长度呢,当然有,不细说,没啥作用。

加密模式有哪些呢

CBC、ECB、CTR、OCF、CFB 安全性不一样,工作模式不一样,所需要的参数不一样,具体场景具体选择

CBC 优点:

  1. 不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL、IPSec的标准

CBC缺点:

  1. 不利于并行计算
  2. 误差传递
  3. 需要初始化向量IV

具体内容可以看看: http://www.cnblogs.com/happyhippy/archive/2006/12/23/601353.html

填充算法

为什么会有填充算法的存在呢?假设我们执行如下AES加密,得出结果:

1) aes("loveyu")       = C1955B/EdtfgKc0dWAwGFA==
2) aes("loveyu123456") = q+VvUfq79k0He8Rf9dEOrA==

解密的结果是怎么样的呢?如下

string(16) "loveyu          "   >>>>>>  HEX  6c6f76657975000000000000000000
string(16) "loveyu123456    "

结果不算太出人意料,但得到了Mcrypt的默认填充算法,零填充,也能发现这样的算法有很大的局限性。

填充算法的种类

ANSI X.923
… | DD DD DD DD DD DD DD DD | DD DD DD DD 00 00 00 04 |

ISO 10126 ????
… | DD DD DD DD DD DD DD DD | DD DD DD DD 81 A6 23 04 |

PKCS7 与 PKCS5(两者一致除非有64的填充)
… | DD DD DD DD DD DD DD DD | DD DD DD DD 04 04 04 04 |

Zeropadding
… | DD DD DD DD DD DD DD DD | DD DD DD DD 00 00 00 00 |

大多数时候,各个不同的平台使用的默认填充算法略有差异,使用起来差别不大,了解了细节就没啥难度了。对于PKCS7和PKCS5,主要在于长度不一样,而AES貌似没有64位的填充。

差异来自于哪里

$iv = @mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), MCRYPT_RAND);
echo @base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, md5("loveyu.org"), "loveyu.org", MCRYPT_MODE_CBC, $iv));
  1. 使用了随机向量,PHP官方例子就这样写的
  2. 使用了AES128的算法
  3. 使用了32个长度的秘钥
  4. 使用了默认填充算法
会有哪些问题?
  1. 你怎么告诉别人你的向量是什么,MCRYPT_RAND 生成的么,还是Base64一下
  2. 你怎么解释你的AES128使用了32位的秘钥,大家都是PHP无所谓,如果对方用openssl解密怎么办
  3. 加密解密前后的结果不一致,非必现,偶尔有一样

日常使用会有哪些参数是必要?

算法类型 AES 256
加密模式 aes-cbc-256
填充算法 PKCS7_TEXT
秘钥 MD5(“loveyu”)
IV 1234567890123456

为什么这里的向量长度是16而不是32?

echo openssl_cipher_iv_length("aes-256-cbc");
echo @mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);

结果为:16和32,差异来自这里
由于AES-CBC 总会输出16的整数倍数据,所以需要的向量大小也是16个字节

算法实现的不同会导致所需的条件不一致,而大量客户端均采用openssl的实现,所以使用16长度的向量

算法实现

Aes-128-cbc zeroPadding

$mode = "aes-128-cbc";
$key = "1234567890123456";
$iv = "1234567890123456";
$padding = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
$data = "1234567890\0\0\0\0\0\0";
echo base64_encode(openssl_encrypt($data, $mode, $key, $padding, $iv)), "\n";
echo @base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, $iv));

输出结果:

yOW03u6FmhY2rHTmiy7Ttg==
yOW03u6FmhY2rHTmiy7Ttg==

要通过openssl_encrypt实现zeroPadding并不容易,而且还不好转换

算法实现 nodejs

const crypto = require('crypto');
let mode = "aes-128-cbc";
let key = "1234567890123456";
let iv = "1234567890123456";
let data = "1234567890\0\0\0\0\0\0";
let cipher = crypto.createCipheriv(mode, key, iv);
cipher.setAutoPadding(false);
let crypt = cipher.update(data, 'utf8', 'base64');
crypt += cipher.final("base64");
console.log(crypt);

Output:yOW03u6FmhY2rHTmiy7Ttg==

当我们使用相同的参数配置时,得到的结果总是可靠的

Nodejs和PHP的mcrypt一样,会默认对数据进行填充,只是算法略有不同,为PKCS

Aes-256-cbc加密的实现

算法类型 AES 256
加密模式 aes-256-cbc
填充算法 PKCS7_TEXT
秘钥 a4055e3e9879951930961af352066068
IV 1234567890123456
加密字符 ABCDEF0123456789 size:16
ABCDEF012345678 size:15

PHP openSSL加密结果:

8xpgNuUhQNdQLsuq5DqjAvws3S9hWNKV24kZij3+aSA= size:32
L5zn/8HqNmengnanZ9qQ4w== size:16
const crypto = require('crypto');
let mode = "aes-256-cbc";
let key = "a4055e3e9879951930961af352066068";
let iv = "1234567890123456";
// let data = "ABCDEF0123456789";
let data = "ABCDEF012345678";
let cipher = crypto.createCipheriv(mode, key, iv);
let crypt = cipher.update(data, 'utf8', 'base64');
crypt += cipher.final("base64");
console.log(crypt);

NodeJS 加密结果:

8xpgNuUhQNdQLsuq5DqjAvws3S9hWNKV24kZij3+aSA= size:32
L5zn/8HqNmengnanZ9qQ4w== size:16

Mcrypt此时无结果,并扔给你一个警告:
mcrypt_encrypt(): Received initialization vector of size 16, but size 32 is required for this encryption mode.

Aes-256-cbc解密的实现

PHP版本

$mode = "aes-256-cbc";
$key = "a4055e3e9879951930961af352066068";
$iv = "1234567890123456";
$padding = PKCS7_TEXT;
$data = "8xpgNuUhQNdQLsuq5DqjAvws3S9hWNKV24kZij3+aSA=";
var_dump(openssl_decrypt(base64_decode($data), $mode, $key, $padding, $iv));

输出结果:string(16) “ABCDEF0123456789”

NodeJS版本

const crypto = require('crypto');
let mode = "aes-256-cbc";
let key = "a4055e3e9879951930961af352066068";
let iv = "1234567890123456";
let data = "L5zn/8HqNmengnanZ9qQ4w==";
let decipher = crypto.createDecipheriv(mode, key, iv);
let decode = decipher.update(data, 'base64', 'utf8');
decode += decipher.final("utf8");
console.log(decode, "size:" + decode.length);

输出结果:ABCDEF012345678 size:15

到这里,大部分实现都处理了,有很多细枝末节不兼容也正常,如果使用相同的实现和相同的算法能解决很多问题,而大部分时候标准的Openssl兼容性是极好的。

2条评论在“PHP AES算法使用细节”

写下你最简单的想法