上一部分介绍了openssl的部分命令行用法,但很多时候我么还需要在程序中使用openssl,这里主要介绍了使用openssl的密码库进行对称密钥加密的相关知识。
约定
在没有特殊说明的情况下,本文提到的长度指的是字节数目
1. 数据输出
头文件
#include <openssl/bio.h>
函数
int BIO_dump_fp(FILE *fp, const char *s, int len);
该函数以16进制+字符形式(如下图所示)打印数据到fp
中, s
为数据所在地址,len
为长度。将fp
指定为stdio
,就可以将数据打印到屏幕上。
stdio
是定义在头文件stdio.h
中的一个FILE*变量,表示标准输出
2. 错误处理
头文件
#include <openssl/err.h>
函数
void ERR_print_errors_fp(FILE *fp);
该函数将openssl的上一条错误信息打印到fp
中,将fp
指定为stderr
,就可以将数据打印到屏幕上。
stderr
是定义在头文件stdio.h
中的一个FILE*变量,表示标准错误输出
3. 对称加密
虽然c语言没有对象,为了方便描述, 我这里把struct结构体称为对象
对称加密和解密的流程类似,一般有以下几个步骤:
- 生成一个记录加密(解密)上下文信息的
EVP_CIPHER_CTX
对象 - 初始化加密(解密)算法,在这一步指定算法和密钥
- 加密(解密)数据
- 处理尾部数据,结束加密(解密)
- 清空并释放加密(解密)上下文对象,清空其他敏感信息
其中使用的函数以及其他一些相关函数如下:
头文件
#include <openssl/evp.h>
上下文处理
EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void);
创建新加密上下文EVP_CIPHER_CTX
对象, 并将其作为返回值返回
void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx);
清除并释放加密上下文对象(防止数据泄露),参数为需要释放的EVP_CIPHER_CTX
对象,在所有加密操作结束后调用该函数
int EVP_CIPHER_CTX_reset(EVP_CIPHER_CTX *ctx);
目前不是很清楚具体作用,可能是重置一个EVP_CIPHER_CTX
对象从而可以循环利用避免不必要的内存释放和分配吧
加解密初始化
加密
int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type,
ENGINE *impl, const unsigned char *key, const unsigned char *iv);
该函数对加密操作进行初始化,参数描述如下:
参数 | 描述 |
---|---|
ctx | 加密上下文对象 |
type | 加密算法类型,在openssl/evp.h 中定义了许多以算法命名的函数 这些函数的返回值作为此参数使用,比如 EVP_aes_256_cbc() |
impl | 利用硬件加密的接口,本文不讨论,设置为NULL |
key | 用于加密的密钥 |
iv | 某些加密模式如cbc 需要使用的初始化向量,如果加密模式不需要可以设置为NULL |
返回值为1
表示成功,0
表示失败,可以使用上述错误处理中的函数打印错误信息
解密
int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type,
ENGINE *impl, const unsigned char *key, const unsigned char *iv);
该函数对解密操作进行初始化,参数与返回值上述加密初始化函数描述相同
执行加解密操作
注意, 输出缓冲区的长度需要比输入缓冲区大一个加密块,否则会出现错误。
注意,如果出现overlap错误,请检查输入和输出缓冲区是否分离,以及是否其长度是否满足第一个注意事项
加密
int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
int *outl, const unsigned char *in, int inl);
执行加密的函数,参数描述如下:
参数 | 描述 |
---|---|
ctx | 加密上下文对象 |
out | 保存输出结果(密文)的缓冲区 |
outl | 接收输出结果长度的指针 |
in | 包含输入数据(明文)的缓冲区 |
inl | 输入数据的长度 |
返回值为1
表示成功,返回值为0
表示失败
解密
int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
int *outl, const unsigned char *in, int inl);
执行解密的函数,参数和返回值和上述加密函数类似,只需要注意输入和输出不要混淆
加解密尾部数据处理
加密
int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out,
int *outl);
该函数处理加密结果的尾部数据(比如填充段块),还可能输出一些密文数据,参数描述如下:
参数 | 描述 |
---|---|
ctx | 加密上下文对象 |
out | 保存输出结果(密文)的缓冲区 (注意这个指针要指向之前已经保存的加密数据的尾部) |
outl | 接收输出结果长度的指针 |
返回值为1表示成功,0表示失败。
解密
int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm,
int *outl);
该函数处理解密结果的尾部数据,还可能输出一些明文数据,参数和返回值同上述加密尾部数据处理的函数类似,注意这个函数输出的是明文即可
资源释放
在加解密操作完成后,对可能的密码缓冲区的清空,以及释放上下文对象,一般使用上下文处理中的
void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx);
释放上下文对象即可
4. 口令生成密钥(key derivation)
有时候我们需要使用口令来生成加密密钥,openssl推荐使用PBKDF2算法来进行这个操作,使用到的函数如下。
关于PBKDF2的描述参考维基百科PBKDF或者RFC2898(PBKDF2)
头文件
#include <openssl/evp.h>
函数
int PKCS5_PBKDF2_HMAC(const char *pass, int passlen,
const unsigned char *salt, int saltlen, int iter,
const EVP_MD *digest,
int keylen, unsigned char *out);
该函数使用PKKDF2算法利用口令生成指定长度的密钥,其参数描述如下:
参数 | 描述 |
---|---|
pass | 用于生成密钥的口令 |
passlen | 口令的长度 |
salt | 用于生成密钥的盐值(建议4字节以上),当然也可以设置为NULL 表示不使用 |
saltlen | 盐值的长度,如果不使用则为0 |
iter | 迭代次数(openssl建议设置到1000以上,用于增加暴力破解的难度) |
digest | 单向hash函数,在openssl/evp.h 中定义了许多以算法命名的函数 这些函数的返回值作为此参数使用,比如 EVP_sha256() |
keylen | 输出的密钥的长度 |
out | 保存输出的密钥的缓冲区 |
返回值为1
表示成功,0
表示失败。
示例
我写了一个加密和解密文件的小例子,有兴趣的朋友可以看一下, 包含主要加密解密流程的操作在代码中的encrypt
和decrypt
函数中。
#include <stdlib.h>
#include <stdio.h>
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include <string.h>
//#define DEBUG
#define printBytesN(a,b) printBytes(a,b,NULL)
#define handleErrors() handleErrorsP("")
void printBytes(unsigned char* bytes, int len, const char* prompt){
int i=0;
if (prompt != NULL) {
printf("%s:\n",prompt);
}
for(i=0; i<len; i++){
printf("%02x ", *(bytes + i));
if ((i + 1) % 10 == 0) {
printf("\n");
}
}
if( i % 10 != 0 ) {
printf("\n");
}
}
void handleErrorsP(const char* prompt)
{
ERR_print_errors_fp(stderr);
puts(prompt);
exit(1);
}
// 加密函数,主要部分,包含openssl api
void encrypt(const char* input_file, const char* out_put_file)
{
EVP_CIPHER_CTX *ctx;
int len;
char password[10];
char salt[PKCS5_SALT_LEN];
char key_iv[48];
// output buffer should be a block size longer thant input buffer
char plain_buffer[4096];
char cipher_buffer[4112];
FILE* plain_file = NULL;
FILE* cipher_file = NULL;
int imm_len;
/* Create and initialise the context */
if(!(ctx = EVP_CIPHER_CTX_new()))
handleErrors();
// read password
for(;;){
printf("please input your password\n");
scanf("%10s", password);
if(strlen(password) > 5){
break;
}
printf("password too short, at least six letters.\n");
}
// generate salt
// function in openssl/rand.h, generate random bytes
RAND_bytes(salt, sizeof(salt));
#ifdef DEBUG
printBytes(salt, sizeof(salt), "The salt is");
#endif
// generate key and iv (generate 48 bytes and the first 32 bytes are for key and the last 16 bytes are for iv)
if (0 == PKCS5_PBKDF2_HMAC(password, strlen(password), salt, sizeof(salt),
1000, EVP_sha256(), sizeof(key_iv), key_iv)) {
handleErrors();
}
// initialise encryption
if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key_iv, key_iv + 32))
handleErrors();
plain_file = fopen(input_file, "rb");
cipher_file = fopen(out_put_file, "wb");
if (plain_file == NULL) {
printf("file open error\n");
exit(1);
}
if(8 != (len = fwrite(salt, 1, 8, cipher_file))) {
printf("error write file, size %d\n", len);
exit(1);
}
while ((imm_len = fread(plain_buffer, 1, 4096, plain_file)) != 0) {
if(1 != EVP_EncryptUpdate(ctx, cipher_buffer, &len, plain_buffer, imm_len)) {
handleErrors();
}
#ifdef DEBUG
printf("imm_len %d\n", imm_len);
printf("cipher_len:%d\n", len);
#endif
if(len != fwrite(cipher_buffer, 1, len, cipher_file)) {
printf("file write error\n");
exit(0);
}
}
/*
* Finalise the encryption. Further ciphertext bytes may be written at
* this stage.
*/
if(1 != EVP_EncryptFinal_ex(ctx, cipher_buffer, &len))
handleErrors();
if(len != fwrite(cipher_buffer, 1, len, cipher_file)) {
printf("file write error\n");
exit(0);
}
#ifdef DEBUG
printf("cipher final:%d\n", len);
#endif
/* Clean up */
EVP_CIPHER_CTX_free(ctx);
OPENSSL_cleanse(plain_buffer, 4096);
OPENSSL_cleanse(password, 10);
fclose(cipher_file);
fclose(plain_file);
}
// 解密函数,主要部分,包含openssl api
int decrypt(const char* input_file, const char* output_file)
{
EVP_CIPHER_CTX *ctx;
int len;
char password[10];
char salt[PKCS5_SALT_LEN];
char key_iv[48];
// output buffer should be a block size longer thant input buffer
char plain_buffer[4112];
char cipher_buffer[4096];
FILE* plain_file = NULL;
FILE* cipher_file = NULL;
int imm_len;
/* Create and initialise the context */
if(!(ctx = EVP_CIPHER_CTX_new()))
handleErrors();
// read password
for(;;){
printf("please input your password\n");
scanf("%10s", password);
if(strlen(password) > 5){
break;
}
printf("password too short, at least six letters");
}
plain_file = fopen(output_file, "wb");
cipher_file = fopen(input_file, "rb");
if (plain_file == NULL || cipher_file == NULL) {
printf("file open error\n");
exit(1);
}
if(8 != (len = fread(salt, 1, 8, cipher_file))) {
printf("error write file, size %d\n", len);
exit(1);
}
// generate key and iv
if (0 == PKCS5_PBKDF2_HMAC(password, strlen(password), salt, sizeof(salt),
1000, EVP_sha256(), sizeof(key_iv), key_iv)) {
handleErrors();
}
#ifdef DEBUG
printBytesN(salt, sizeof(salt));
#endif
// initialise decryption
if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key_iv, key_iv + 32)){
handleErrors();
}
// decrypt
while(0 != (imm_len = fread(cipher_buffer, 1, 4096, cipher_file))) {
if(1 != EVP_DecryptUpdate(ctx, plain_buffer, &len, cipher_buffer, imm_len)) {
handleErrorsP("error decrypt, maybe password not corret, or the file is corrupted");
}
#ifdef DEBUG
printf("imm_len %d\n", imm_len);
printf("plain_len %d\n", len);
#endif
fwrite(plain_buffer, 1, len, plain_file);
}
// Finalise decryption
if(1 != EVP_DecryptFinal_ex(ctx, plain_buffer, &len)) {
handleErrorsP("error decrypt, maybe password not corret, or the file is corrupted");
}
#ifdef DEBUG
printf("plain_final %d\n", len);
#endif
if(len != fwrite(plain_buffer, 1, len, plain_file)) {
printf("file write error\n");
exit(0);
}
/* Clean up */
EVP_CIPHER_CTX_free(ctx);
OPENSSL_cleanse(plain_buffer, 4096);
OPENSSL_cleanse(password, 10);
fclose(plain_file);
fclose(cipher_file);
}
int main() {
printf("encrypt plain.txt to cipher.txt:\n")
encrypt("plain.txt", "cipher.txt");
printf("encrypt plain.txt to new_plain.txt:\n")
decrypt("cipher.txt", "new_plain.txt");
return 0;
}