使用 openssl 进行 AES 加解密

在我之前的一篇博客里介绍了 对称加密的模式 .

这里主要聊一聊如何使用 openssl 来进行 AES 加密 .

一. OPENSSL crypto API

openssl 加密 API 分两个部分: High Level and Low Level . 对于大部分人来说,使用 High Level 就够用了, 这些 API 被冠以 EVP (Envelope) ,表示对 Low Level 的封装。High Level API 提供了包括 对称/非对称加解密签名, 验证, 哈希MAC 等一系列组件,屏蔽了 Low Level API 的复杂逻辑,使用起来安全高效。对于除非有需要进行加密算法级别的改进,否则不建议使用 Low Level API.
大部分 EVP API 有一个 int 型返回值 ,用来表示操作是否成功:1 表示成功, 0 表示失败。但有些时候也会返回 -1 ,表达如内存分配或者其他什么错误。官方指导代码如下:

1
2
3
4
5
6
7
    if(1 != EVP_xxx()) goto err;
if(1 != EVP_yyy()) goto err;

/* ... do some stuff ... */

err:
ERR_print_errors_fp(stderr);

二. 使用 EVP API 进行 AES 加解密

对于 AES 加解密,EVP API 分为两种,EVP_Encrypt / EVP_Decryp 系列 和 EVP_Cipher 系列。后者是对前者进一步的封装。它们具体使用的套路都是一样的:

  1. 创建加解密上下文 EVP_CIPHER_CTX

  2. 调用 xxxInit() 函数,使用 key(密钥)、iv(前置向量)cipher(算法) 对上下文初始化

  3. 调用 xxxUpdate() 函数进行加解密。该函数支持流式操作。即对于一段明文来说,分成多组按顺序进行加密,和一次性全部加密,不影响其生成的密文的正确性。

  4. 调用 xxxFinal() 函数获取上下文中遗留的信息

  5. 释放上下文, 完成加解密。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include <iostream>
#include <cassert>
#include <cmath>
#include <openssl/aes.h>
#include <openssl/evp.h>
#include <openssl/kdf.h>

const char *key = "wandoer.com_wandoer.com_wandoer.";
char iv[16] = { '_','i','v','_','m','u','s','t','_','b','e','_','1','6','B','_' };
const char *plain_text = "The EVP interface supports the ability to perform authenticated encryption and decryption, as well as the option to attach unencrypted,"
" associated data to the message. Such Authenticated-Encryption with Associated-Data (AEAD) schemes provide confidentiality by encrypting the data, and also"
" provide authenticity assurances by creating a MAC tag over the encrypted data. The MAC tag will ensure the data is not accidentally altered or maliciously"
" tampered during transmission and storage.";

template<typename CIPHER>
bool encrypt(CIPHER cipher,const char* key, const char* iv, const unsigned char *in, int in_len, unsigned char *out, int *out_len) {
bool ret = false;

//创建加密上下文
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
int tmp = 0;

//初始化加密上下文
int rst = EVP_EncryptInit(ctx, cipher, reinterpret_cast<const unsigned char *>(key),
reinterpret_cast<const unsigned char *>(iv));
if (rst != 1) goto RET;

//加密操作
rst = EVP_EncryptUpdate(ctx, out, &tmp, in, in_len);
if (rst != 1) goto RET;
*out_len = tmp;

//获取加密上下文中遗留的信息
rst = EVP_EncryptFinal(ctx, out + tmp, &tmp);
if (rst != 1) goto RET;
if (tmp > 0) {
*out_len += tmp;
}

ret = true;

RET:
//释放加密上下文
EVP_CIPHER_CTX_free(ctx);
ctx = NULL;
return ret;
}

template<typename CIPHER>
bool decrypt(CIPHER cipher, const char* key, const char* iv, const unsigned char *in, int in_len, unsigned char *out, int *out_len) {
bool ret = false;
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
int tmp = 0;
int rst = EVP_DecryptInit(ctx, cipher, reinterpret_cast<const unsigned char *>(key),
reinterpret_cast<const unsigned char *>(iv));
if (rst != 1) goto RET;

rst = EVP_DecryptUpdate(ctx, out, &tmp, in, in_len);
if (rst != 1) goto RET;
*out_len = tmp;

rst = EVP_DecryptFinal(ctx, out + tmp, &tmp);
if (rst != 1) goto RET;
if (tmp > 0) {
*out_len += tmp;
}

ret = true;

RET:
EVP_CIPHER_CTX_free(ctx);
ctx = NULL;
return ret;
}

template<typename CIPHER>
bool do_crypto(CIPHER cipher, const char* key, const char* iv, const unsigned char* in, int in_len, unsigned char* out, int* out_len, bool encrypt) {
bool ret = false;
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
int tmp = 0;
int rst = EVP_CipherInit(ctx, cipher, reinterpret_cast<const unsigned char*>(key), reinterpret_cast<const unsigned char*>(iv), encrypt ? 1 : 0);
if (rst != 1) goto RET;
rst = EVP_CipherUpdate(ctx, out, &tmp, in, in_len);
if (rst != 1) goto RET;
*out_len = tmp;

rst = EVP_CipherFinal(ctx, out + tmp, &tmp);
if (rst != 1) goto RET;
if (tmp > 0) {
*out_len += tmp;
}

ret = true;

RET:
EVP_CIPHER_CTX_free(ctx);
ctx = NULL;
return ret;
}

int main(void) {
//TODO init OPENSSL

// EVP_CIPHER 是 OPENSSL 内置的一系列对称加密算法。可以使用 EVP_xxx_() 来获取,使用后无需释放。
// OPENSSL 具体提供哪些内置算法可以在 evp.h 里查看
const EVP_CIPHER *cipher = EVP_aes_256_cfb();

// 计算加解密结果 buffer 的大小。AES 对称加密后数据的大小与加密前相同。
// 考虑到可能有 padding 存在,大小为大于输入的,最小的 AES_BLOCK_SIZE 倍整数。
int out_len = ceil((float)strlen(plain_text) / AES_BLOCK_SIZE) * AES_BLOCK_SIZE;
unsigned char *cipher_text = (unsigned char *)malloc(out_len);
unsigned char *re_plain_text = (unsigned char *)malloc(out_len);

//实际加解密输出的 buffer 大小
int enc_len = 0, dec_len = 0;

/** Method 1
* 使用 EVP_Encrypt / EVP_Decrypt 系列函数
*/
memset(cipher_text, 0, out_len);
memset(re_plain_text, 0, out_len);
bool encryped = encrypt(cipher,key,iv, reinterpret_cast<const unsigned char *>(plain_text),
strlen(plain_text), cipher_text, &enc_len);
assert(encryped);

bool decrypted = decrypt(cipher,key, iv, cipher_text, enc_len, re_plain_text, &dec_len);
assert(decrypted);
if (dec_len < out_len)
re_plain_text[dec_len] = '\0';

int cmp = strcmp(plain_text, reinterpret_cast<char *>(re_plain_text));
assert(cmp == 0);

/** Method 2
* 使用 EVP_Cipher 系列函数
*/
memset(cipher_text, 0, out_len);
memset(re_plain_text, 0, out_len);
encryped = do_crypto(cipher, key, iv, reinterpret_cast<const unsigned char*>(plain_text), strlen(plain_text), cipher_text, &enc_len, true);
assert(encryped);
decrypted = do_crypto(cipher, key, iv, cipher_text, enc_len, re_plain_text, &dec_len, false);
assert(decrypted);
cmp = strcmp(plain_text, reinterpret_cast<char*>(re_plain_text));
assert(cmp == 0);

free(cipher_text);
free(re_plain_text);

//TODO free OPENSSL

return 0;
}

需要注意的地方有:

  1. key 和 iv 的长度。我们知道 AES 一个组的大小是 16Byte, 所以 iv 的长度为 16Byte 。key 的长度随着选择的 cipher 的不同而不同,如 AES_128_CFB 表达 key 长度为 128 位, AES_256_CFB 表示 key 长度为 256 位。如果 key 或 iv 的长度过长,超出的部分不会被使用,反之如果过短,则结果不可预测。 如果不清楚 key 和 iv 到底需要多长,可以使用 EVP_CIPHER_iv_length() / EVP_CIPHER_key_length() 来获取。

  2. 加解密上下文 EVP_CIPHER_CTX 要记得 释放,否则遗留在内存中的上下文信息可以会泄漏机密。

到这里,使用 OPENSSL 进行 AES 加解密已经基本讲完了。没错就是这么简单。如果想做更深一个层次的了解,可以看看以下部分。

三. 使用 Low Level API 进行 AES 加解密

我们知道,一个完整的 AES 分组加解密方法分为两个部分,一是 AES 部分, 二是分组模式部分。

  • 在 OPENSSL 中,AES 算法部分在 aes.h 中,为 AES_encrypt / AES_decrypt 。它是 AES 加解密的核心部分,每次处理 16 个字节(128位) 的数据加解密。

  • 分组与模式算法在 modes.h 中(具体实现在 openssl/crypto/modes 路径下) ,如 CRYPTO_cfb128_encrypt,它将数据分成每 16 字节一组,再将每一组/组与组之间进行不同的模式的计算

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <cassert>
#include <openssl/aes.h>

const char *key = "wandoer.com_wandoer.com_wandoer.";
char iv[16] = { '_','i','v','_','m','u','s','t','_','b','e','_','1','6','B','_' };
const char *plain_text = "The EVP interface supports the ability to perform authenticated encryption and decryption, as well as the option to attach unencrypted,"
" associated data to the message. Such Authenticated-Encryption with Associated-Data (AEAD) schemes provide confidentiality by encrypting the data, and also"
" provide authenticity assurances by creating a MAC tag over the encrypted data. The MAC tag will ensure the data is not accidentally altered or maliciously"
" tampered during transmission and storage.";

bool aes_256_crypto(const char* key, const char* iv, const unsigned char* in, unsigned char* out, int len, bool encrypt) {
bool ret = false;
AES_KEY aes_key;
int num = 0;
unsigned char* uiv = (unsigned char*)malloc(strlen(iv));
memcpy(uiv, iv, strlen(iv));

int rst = AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(key), 256, &aes_key);
if (rst != 0) goto ERR;

AES_cfb128_encrypt(reinterpret_cast<const unsigned char*>(in), out, len, &aes_key, uiv, &num, encrypt ? 1 : 0);

ret = true;

ERR:
free(uiv);
return ret;
}

int main() {
int len = strlen(plain_text);
unsigned char* cipher_text = (unsigned char*)malloc(len);
unsigned char* re_plain_text = (unsigned char*)malloc(len);
memset(cipher_text, 0, len);
memset(re_plain_text, 0, len);

aes_256_crypto(key, iv, reinterpret_cast<const unsigned char*>(plain_text), cipher_text, len, true);

aes_256_crypto(key, iv, cipher_text, re_plain_text, len, false);

int cmp = memcmp(plain_text, re_plain_text, len);
assert(cmp == 0);

free(cipher_text);
free(re_plain_text);

return 0;
}

需要注意,在 AES_cfb128_encrypt() 函数中的参数 iv 是一个 non const 指针。这表示 iv 是会发生变化的。AES_cfb128_encrypt 同样支持流操作,在第二次及以后的操作中需要注意传入前一次操作之后的 iv(如果了解对称加密分组的原理,则不难理解这何要这样做)。