简单的实现
PHP的实现相当简单,两行代码就行,结果就是偶尔与其他平台不一致,原因就是屏蔽了很多细节。这只是一篇很无聊的笔记,略过。
如下面两行代码,虽然实现了目的,但效果并不理想
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 优点:
- 不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL、IPSec的标准
CBC缺点:
- 不利于并行计算
- 误差传递
- 需要初始化向量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位的填充。
差异来自于哪里
echo @base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, md5("loveyu.org"), "loveyu.org", MCRYPT_MODE_CBC, $iv));
- 使用了随机向量,PHP官方例子就这样写的
- 使用了AES128的算法
- 使用了32个长度的秘钥
- 使用了默认填充算法
会有哪些问题?
- 你怎么告诉别人你的向量是什么,MCRYPT_RAND 生成的么,还是Base64一下
- 你怎么解释你的AES128使用了32位的秘钥,大家都是PHP无所谓,如果对方用openssl解密怎么办
- 加密解密前后的结果不一致,非必现,偶尔有一样
日常使用会有哪些参数是必要?
算法类型 | AES 256 |
---|---|
加密模式 | aes-cbc-256 |
填充算法 | PKCS7_TEXT |
秘钥 | MD5(“loveyu”) |
IV | 1234567890123456 |
为什么这里的向量长度是16而不是32?
echo @mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
结果为:16和32,差异来自这里
由于AES-CBC 总会输出16的整数倍数据,所以需要的向量大小也是16个字节
算法实现的不同会导致所需的条件不一致,而大量客户端均采用openssl的实现,所以使用16长度的向量
算法实现
Aes-128-cbc zeroPadding
$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
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
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版本
$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版本
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兼容性是极好的。
完全看不懂。。。
瞅瞅离开了