目的
发布版本时,使用openssl加密版本,放到服务器上
产品升级版本时,下载版本包后,先使用openssl进行解密,然后升级
折腾了两天终于搞定了,把一些东西记录下
使用openssl源码
真正需要用到的只有一个结构体和三个函数,注释如下:
unsigned char key[32];//密钥字符串,最长32位 unsigned char iv[16];//向量字符串,最长16位 AES_KEY aesKey;//aes格式密钥 //加密时,先将加密密钥字符串转化为AES专用格式 AES_set_encrypt_key( const unsigned char *userKey, //输入的密钥 const int bits,//aes常用128 | 192 | 256三种加密安全级别 &aesKey)//生成AES格式密钥 //解密时,先将解密密钥字符串转换为AES专用格式密钥 AES_set_decrypt_key( const unsigned char *userKey, const int bits, AES_KEY *key); //aes cbc加解密API,enc为1代表加密,0代表解密 AES_cbc_encrypt( in, //输入需要加解密的字符串 out, //输出已经加解密完成的字符串 inlen,//字符串长度 &aesKey,// AES格式密钥 iv,//向量,可以不使用 enc)//加密还是解密
由于产品的运行环境是极度裁剪过的linux,没有openssl库以及相关依赖库。所以考虑将openssl的aes加解密源码部分直接提取出来编译使用。如果只需要使用openssl库,那可以略过这段。
用了两三个小时,把需要的文件提取出来,删除用不到的宏/代码/头文件,
一共留下6个文件:
aes.h
aes_cbc.c
aes_core.c (x86使用aes_x86core.c)
cbc128.c
modes.h
还有把一些用到的宏组装到一个文件aes_local.h中:
# include <stdio.h> # include <stdlib.h> # include <string.h> #define STRICT_ALIGNMENT 1 #undef PEDANTIC #undef FULL_UNROLL #undef OPENSSL_SMALL_FOOTPRINT # define GETU32(pt) (((u32)(pt)[0] << 24) ^ ((u32)(pt)[1] << 16) ^ ((u32)(pt)[2] << 8) ^ ((u32)(pt)[3])) # define PUTU32(ct, st) { (ct)[0] = (u8)((st) >> 24); (ct)[1] = (u8)((st) >> 16); (ct)[2] = (u8)((st) >> 8); (ct)[3] = (u8)(st); } typedef long long i64; typedef unsigned long long u64; typedef unsigned int u32; typedef unsigned short u16; typedef unsigned char u8; # define MAXKC (256/32) # define MAXKB (256/8) # define MAXNR 14
让我们来试试吧:
int main() { unsigned char key[32] = "1234567890"; unsigned char iv[16] = "123456"; unsigned char iv_copy[16]; unsigned char buf_normal[64] = "加解密测试明文字符串"; unsigned char buf_encrypt[64] = ""; AES_KEY aesKey; //加密 memcpy(iv_copy, iv, 16);//向量在运算过程中会被改变,为了之后可以正常解密,拷贝一份副本使用 AES_set_encrypt_key(key, 256, &aesKey); AES_cbc_encrypt(buf_normal, buf_encrypt, sizeof(buf_normal), &aesKey, iv_copy, 1); //解密 memcpy(iv_copy, iv, 16); AES_set_decrypt_key(key, 256, &aesKey); AES_cbc_encrypt(buf_encrypt, buf_normal, sizeof(buf_encrypt), &aesKey, iv_copy, 0); }
至此大功告成。然而,苦逼的旅程才刚刚开始
openssl命令
#命令行加密 openssl enc -aes-256-cbc -K 1234567890 -iv 123456 -in 明文文件 -out 加密文件 #命令行解密 openssl enc -aes-256-cbc -d -K 1234567890 -iv 123456 -in 加密文件 -out 明文文件
问题出现了,在命令行使用openssl命令可以正常进行aes256加解密,在代码里使用aes256的API也可以正常加解密,但是两者不能互相加解密。
兼容
原因在哪儿呢?
其实此时疑惑有两处,
第一,为什么两者无法通用
第二,在提取代码的时候顺便看了看aes的实现,知道aes是每次16个字节运算一次,所以得出来的加密数据必然是16的整数倍。如果明文有9个字节,加密之后是16个字节这我知道。那解密的时候怎么知道原始数据的大小呢。毕竟版本包是用gzip压缩过的,差一个字节都没法正常解压缩出来。
让我们看看源码里面怎么用这几个api的。
将openssl源码加-g选项编译,使用gdb调试
gdb启动openssl
在AES_cbc_encrypt处下断点,然后填入参数enc -aes-256-cbc -K 1234567890 -iv 123456 -in ./123.txt -out ./456.txt
好,run一下,让我们看看究竟怎么一回事。
然而,根本没有断住,程序正常运行结束了,加密成功。这就让人头大了。难道openssl命令根本没有用这些API??
在程序开始直接下断点断住,一步步跟一下。跟了一个多小时。终于绝望了。源码使用了引擎来实现,阅读起来比较费劲,而且调用的貌似是早已经编译好的汇编模块,而不是C代码API。
无语了,不过也不是完全没有收获,我们本来就不是要找这几个API在哪里被用的,而且加解密怎么实现的我们都知道了。真正想要知道的是API调用前后都做过哪些处理。
果然找到了,原来在进行加解密之前,openssl先对key和iv做过setHex处理。
简单来说,key如果是长度为10的字符串“1234567890”,将会被转化为长度为5的char数组 char key[5] = [0x12, 0x34, 0x56, 0x78, 0x90]
很好,把setHex加入到我们代码的加解密之前试试
果然,用openssl命令加密的文件,我们的代码可以完美解密出来了
第一个问题顺利解决。
其实并没有顺利解决。因为现在可以解密openssl命令加密的文件,但是用自己的代码加密文件,openssl命令还无法解密出来。不过,猜测明显和第二个问题有关。那我们继续研究第二个问题:怎么知道解密后数据原始长度是多少。
openssl源码暂时就不跟了,之前跟了一圈根本没发现什么东西。既然已经到了现在的进度,再解决这个问题就已经很简单了。毕竟aes是16个字节为一组进行加密的,相互没啥关联。所以信息肯定隐藏在最后16个字节里面
准备一个文件test.log. 里面内容写1,这是文件长度为1 。用openssl命令加密,然后用自己的代码解密后,得出来的16个字节如下:
1, 15, 15, 15……(共重复15次)
将test.log内容改为字符串123,这时长度变成3,再试一下
1,2,3,13,13,13,13(共重复13次)
原来如此,规律找到了:
加密之前,如果不满16个字节,先按照这种格式填充一下,补满16个字节,再进行加密
解密之后,按照上述格式剔除填充字节,得到真正原始数据。
网上搜索了一下,这种填充方式叫做pkcs5填充,是对称加密算法里面经常用到的填充方式,特别的,当明文文件为16字节的整数倍时,也需要填充一个16字节,也就是说,使用pkcs5方式填充,没有任何一种情况会不进行填充,这样会方便代码进行处理。
好,终于搞定了。openssl命令和我们自己的代码可以相互加解密,完美兼容。
步骤如下:
将key和iv进行setHex
加密时,使用pkcs5先填充一下,然后调用API,得到加密数据
解密时,先调用API进行解密,然后用pkcs5剔除填充字节。得到原始数据