使用openssl的aes256加解密算法(带例程 兼容openssl命令)

时间:2022-07-05 18:30:18

目的

发布版本时,使用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剔除填充字节。得到原始数据