一、 mbedtls简介
MbedTLS是一个开源、可移植、易使用、可读性高的SSL库,实现了常所用的加解密算法、X.509证书操作以及TLS协议操作。MbedTLS各功能模块独立性高、耦合度低,可以通过配置宏定义进行功能裁剪,非常适合对空间和效率要求高的嵌入式系统。
二、AES算法简介
AES全称为Advanced Encryption Standard,是一种对称分组算法。AES算法是NIST组织公开竞选产生,最终Rijndael力压群雄,在2000年被NIST选择为AES标准算法。通过AES算法可以加密或解密一段数据,其中加密为将明文转换为密文,解密是将密文转换为明文。AES算法的密钥长度有三种:128bits、192bits、256bits,但加密和解密的分组长度必须为128bits。
AES为一个分组密码,密码算法总体可分为分组密码(block cipher)和流密码(stream cipher)两类。其中分组密码是每次只能处理特定长度的一块数据,而流密码是对数据流进行连续处理,无需按照一段分组后再处理。
分组密码算法一次只能处理固定长度的分组,当我们需要加解密的数据超过固定分组长度时,需要不断迭代分组密码算法,以保证对全部数据进行加解密,迭代的过程就成为分组密码的模式。对于AES,存在多种模式,常见有:
- ECB:Electronic CodeBook mode
- CBC:Cipher Block Chaining mode
- CFB:Cipher FeedBack mode
- OFB:Output FeedBack mode
- CTR:CounTeR mode
通常使用CBC模式最多,本文以CBC为示例。关于AES算法,具体可阅读NIST FIPS-197文档。
三、实现
3.1 移植MbedTLS代码
移植自mbedTLS 2.16版本
需移植的文件如下:
修改config.h文件
#ifndef MBEDTLS_CONFIG_H
#define MBEDTLS_CONFIG_H
#define MBEDTLS_ERROR_C
/* AES-256-CBC */
#define MBEDTLS_CIPHER_MODE_CBC
#define MBEDTLS_AES_C
#define MBEDTLS_AES_ROM_TABLES //Use precomputed AES tables stored in ROM
#endif /* MBEDTLS_CONFIG_H */
3.2 引入头文件
引入相应的头文件
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "../crypto/mbedtls/aes.h"
#include "../crypto/mbedtls/error.h"
3.3 填充说明
因AES单个分组长度为16个字节,当分组不足16个字节时,必须填充到16个字节,否则AES无法进行加解密。常用的分组填充方式为PKCS#7,PKCS#7的总体填充思想为,缺少N个字节,则填充N个字节的0x0N值,例如待加密数据长度为10字节,则缺少6个字节,故填充内容为6个字节的0x06。但须额外注意,当最后一个分组长度即便正好为16字节,仍需要填充16个字节的0x0F。具体请阅读相关标准
3.4 一次处理所有数据
一次性加密或解密所有数据,需要加解密的数据都一次作为输入,通常用于数据量不大的情况下。
3.4.1 加密
详细内容见代码中的注释。
注意以下点:
- CBC模式是需要一个初始化向量的IV,此初始化向量IV值,加解密时必须保持一致,否则会导致解密出的数据和原明文不一致。
- 采用PKCS#7的填充方式
- 因进行了填充,故加密后的密文长度必然比明文长度要长,密文长度计算见代码头部注释,需传入足够长度的密文存储空间,否则会导致溢出
/*
* 一次输入所有待加密数据,并使用AES CBC模式进行加密
*
* 输入:
* input:待加密数据
* input_length:待加密数据的长度
* encrypted_data:加密后的密文存放位置,注意此指针指向的区域有效长度计算如下:使用PKCS#7填充的output长度计算应为 decrypted_data_length = ((input_length/16)+1)*16
* 返回值:加密后密文的长度,若为负值,则存在错误。
*
* */
int encrypt_data_by_aes_cbc(unsigned char *input,int input_length,unsigned char *encrypted_data){
mbedtls_aes_context aes_context;
int result = 0;
int padding = 0;
int real_input_length = 0;
unsigned char padding_code;
unsigned char *input_tmp;
//AES密钥
const unsigned char key[16] = "1234567890ABCDEF";
//当使用CBC/CFB/OFB等模式时,需要一个初始化向量IV
unsigned char iv[16] = "0099887766554433";
unsigned char error[64];
/*
AES为分组加密算法,一个分组长度必须为16字节,所有待加密数据必须为16字节的整倍数。
若待加密数据不为16字节的整倍数,则必须先填充到16字节整倍数。常用填充方式有PKCS5和PKCS7,通常使用PKCS7填充
*/
//PKCS7填充到16字节整倍数,若待加密数据正好为16字节整倍数,仍需填充16个字节
//计算当前长度距离16整倍数的差值,此值即为填充值
padding = 16 - (input_length%16);
padding_code = (char)padding;
//real_input_length即为经过填充后待加密数据的长度,值必然为16整倍数,同时此长度也为加密后的密文长度
real_input_length = input_length + padding;
if(real_input_length%16 != 0){
//填充后必然为16字节整倍数,若不为16字节整倍数,则填充存在问题
printf("failed to padding\n");
return -1;
}
input_tmp = calloc(1,real_input_length);
if(input_tmp == NULL){
//分配内存错误
return -2;
}
//进行填充
memcpy(input_tmp,input,input_length);
memset(input_tmp + input_length,padding_code,padding);
mbedtls_aes_init(&aes_context);
//若使用AES-128,则必要长度为128bits。若使用AES-256,则密钥长度为256bits
result = mbedtls_aes_setkey_enc(&aes_context, key, 128);
if(result != 0){
printf("failed to set key:");
mbedtls_strerror(result,error,sizeof(error));
printf("%s\n",error);
free(input_tmp);
mbedtls_aes_free(&aes_context);
return -3;
}
result = mbedtls_aes_crypt_cbc(&aes_context, MBEDTLS_AES_ENCRYPT, real_input_length,iv,input_tmp,encrypted_data);
if(result != 0){
printf("failed to encrypt data:");
mbedtls_strerror(result,error,sizeof(error));
printf("%s\n",error);
free(input_tmp);
mbedtls_aes_free(&aes_context);
return -4;
}
free(input_tmp);
mbedtls_aes_free(&aes_context);
//返回密文长度
return real_input_length;
}
3.4.2 解密
详细内容见代码中的注释。
注意以下点:
- CBC模式是需要一个初始化向量的IV,此初始化向量IV值,加解密时必须保持一致,否则会导致解密出的数据和原明文不一致。
- 因加密采用PKCS#7的填充方式,故解密后需去填充,详见代码注释
- 因进行了填充,故解密后的明文长度比密文长度必然比要短
/*
* 一次输入所有待加密数据,并使用AES CBC模式进行加密
* 使用PKCS#7填充的output长度计算应为 output_length = ((input_length/16)+1)*16
* 输入:
* encrypted_data:待解密的密文数据
* encrypted_length:待解密的密文数据的有效长度
* decrypted_data:解密的明文数据
*返回值:解密后的明文数据的有效长度,即decrypted_data中数据的有效长度。若为负值,则存在错误。
*
* */
int decrypt_data_by_aes_cbc(unsigned char *encrypted_data,int encrypted_length,unsigned char *decrypted_data){
mbedtls_aes_context aes_context;
int result = 0;
int padding = 0;
unsigned char padding_code;
//AES密钥
const unsigned char key[16] = "1234567890ABCDEF";
//当使用CBC/CFB/OFB等模式时,需要一个初始化向量IV
unsigned char iv[16] = "0099887766554433";
unsigned char error[64];
mbedtls_aes_init(&aes_context);
//若使用AES-128,则必要长度为128bits。若使用AES-256,则密钥长度为256bits
result = mbedtls_aes_setkey_dec(&aes_context, key, 128);
if(result != 0){
printf("failed to set key:");
mbedtls_strerror(result,error,sizeof(error));
printf("%s\n",error);
mbedtls_aes_free(&aes_context);
return -1;
}
//解密后包含填充值的明文,后续需去除填充值
unsigned char decrypted_data_include_padding[encrypted_length];
result = mbedtls_aes_crypt_cbc(&aes_context, MBEDTLS_AES_DECRYPT, encrypted_length,iv,encrypted_data,decrypted_data_include_padding);
if(result != 0){
printf("failed to decrypt data:");
mbedtls_strerror(result,error,sizeof(error));
printf("%s\n",error);
mbedtls_aes_free(&aes_context);
return -2;
}
//去除PKCS#7的填充值
//读取最后一个值,此值即为填充值的长度
padding_code = decrypted_data_include_padding[encrypted_length-1];
padding = (int)padding_code;
if(padding < 1 || padding > 16){
printf("padding code is illegal!\n");
return -3;
}
int real_decrypted_data_length = encrypted_length - padding;
memcpy(decrypted_data,decrypted_data_include_padding,real_decrypted_data_length);
mbedtls_aes_free(&aes_context);
return real_decrypted_data_length;
}
3.4.3 运行效果
//待加密数据
unsigned char input[11] = "ABCDEF12345";
//PKCS#7填充的密文长度计算:decrypted_data_length = ((input_length/16)+1)*16,decrypted_data_length = ((11/16)+1)*16 = 16
unsigned char encrypted_data[16];
unsigned char decrypted_data[16];
printf("plain data:\n");
for(int i=0;i<11;i++){
printf("%02x",input[i]);
}
printf("\n");
//加密
int encrypted_data_length = encrypt_data_by_aes_cbc(input,11,encrypted_data);
if(encrypted_data_length < 0){
printf("failed to encrypt data!\n");
return -1;
}
if(encrypted_data_length != 16){
printf("failed to encrypt data!\n");
return -2;
}else{
printf("encrypted data:\n");
for(int i=0;i<encrypted_data_length;i++){
printf("%02x",encrypted_data[i]);
}
printf("\n");
}
//解密
int decrypted_data_length = decrypt_data_by_aes_cbc(encrypted_data,encrypted_data_length,decrypted_data);
if(decrypted_data_length < 0){
printf("failed to decrypt data!\n");
return -3;
}
printf("decrypted data:\n");
for(int i=0;i<decrypted_data_length;i++){
printf("%02x",decrypted_data[i]);
}
printf("\n");
使用openssl验证,openssl默认会进行加盐操作,需添加-nosalt参数
待加密明文保存在00.txt,密文保存在01.txt,可看到密文相同
3.5 基于文件的加解密
当加解密数据较长时,或对文件中内容进行加解密时,需基于文件的加解密操作。总体加解密相同,但需要添加文件读取的控制,尤其是解密时需要去填充的环节,当检测到文件读取完成后,方可进行去填充操作。
每次操作512个字节,每次操作的字节数建议为16字节的整倍数
#define FILE_BUFFER_SIZE 512
3.5.1 加密
/*
* 从文件中读取明文数据,并使用AES CBC模式进行加密,加密后的密文同样输出到文件中
*
* 输入:
* plain_file_path:待加密文件路径
* encrypted_file_path:加密后文件保存路径
* 返回值:0:成功,其他值:失败。
*
* */
int encrypt_data_from_file_by_aes_cbc(const char *plain_file_path,const char *encrypted_file_path){
mbedtls_aes_context aes_context;
int result = 0;
int padding = 0;
int real_input_length = 0;
unsigned char padding_code;
int eof_flag = 0;
//AES密钥
const unsigned char key[16] = "1234567890ABCDEF";
//当使用CBC/CFB/OFB等模式时,需要一个初始化向量IV
unsigned char iv[16] = "0099887766554433";
unsigned char error[64];
//读写文件的buffer,长度应为16整倍数
unsigned char input_buffer[FILE_BUFFER_SIZE];
unsigned char output_buffer[FILE_BUFFER_SIZE];
FILE *plain_file = fopen(plain_file_path,"rb");
FILE *encrypted_file = fopen(encrypted_file_path,"wb");
mbedtls_aes_init(&aes_context);
//若使用AES-128,则必要长度为128bits。若使用AES-256,则密钥长度为256bits
result = mbedtls_aes_setkey_enc(&aes_context, key, 128);
if(result != 0){
printf("failed to set key:");
mbedtls_strerror(result,error,sizeof(error));
printf("%s\n",error);
mbedtls_aes_free(&aes_context);
return -1;
}
while(1){
result = fread(input_buffer,1,FILE_BUFFER_SIZE,plain_file);
real_input_length = result;
if(result != FILE_BUFFER_SIZE){
if(ferror(plain_file) != 0){
printf("failed to read plain file\n");
fclose(plain_file);
fclose(encrypted_file);
mbedtls_aes_free(&aes_context);
return -2;
}
if(feof(plain_file)){
//到达文件尾部,需进行PKCS#7填充
/*
AES为分组加密算法,一个分组长度必须为16字节,所有待加密数据必须为16字节的整倍数。
若待加密数据不为16字节的整倍数,则必须先填充到16字节整倍数。常用填充方式有PKCS5和PKCS7,通常使用PKCS7填充
*/
//PKCS7填充到16字节整倍数,若待加密数据正好为16字节整倍数,仍需填充16个字节
//计算当前长度距离16整倍数的差值,此值即为填充值
padding = 16 - (result%16);
padding_code = (char)padding;
//real_input_length即为经过填充后待加密数据的长度,值必然为16整倍数,同时此长度也为加密后的密文长度
real_input_length = result + padding;
if(real_input_length%16 != 0){
//填充后必然为16字节整倍数,若不为16字节整倍数,则填充存在问题
printf("failed to padding\n");
return -3;
}
//进行填充
memset(input_buffer + result,padding_code,padding);
//设置达到文件尾部标志
eof_flag = 1;
}
}
//进行加密
result = mbedtls_aes_crypt_cbc(&aes_context, MBEDTLS_AES_ENCRYPT, real_input_length,iv,input_buffer,output_buffer);
if(result != 0){
printf("failed to encrypt data:");
mbedtls_strerror(result,error,sizeof(error));
printf("%s\n",error);
fclose(plain_file);
fclose(encrypted_file);
mbedtls_aes_free(&aes_context);
return -4;
}
result = fwrite(output_buffer,1,real_input_length,encrypted_file);
if(result != real_input_length){
printf("failed to write encrypted data to file\n");
fclose(plain_file);
fclose(encrypted_file);
mbedtls_aes_free(&aes_context);
return -5;
}
if(eof_flag == 1){
break;
}
}
mbedtls_aes_free(&aes_context);
fclose(plain_file);
fclose(encrypted_file);
return 0;
}
3.5.2 解密
因解密时,依靠feof判断是否读取到文件末尾,从而确定何时进行去填充的操作,此时需注意一种特殊情况:当加密后的文件正好为FILE_BUFFER_SIZE的整倍数时,在某一轮的读取过程中,正好整个文件读取完成,此时feof无法判断出文件已结束导致无法去除填充,为避免此种情况,需再往后读取一个字节,确保下一轮可读取到内容。
/*
* 从文件中读取密文数据,并使用AES CBC模式进行解密,解密后的明文同样输出到文件中
*
* 输入:
* encrypted_file_path:待解密的密文文件路径
* decrypted_file_path:解密后的明文文件保存路径
* 返回值:0:成功,其他值:失败。
*
* */
int decrypt_data_from_file_by_aes_cbc(const char *encrypted_file_path,const char *decrypted_file_path){
mbedtls_aes_context aes_context;
int result = 0;
int padding = 0;
unsigned char padding_code;
int eof_flag = 0;
int input_length = 0;
//AES密钥
const unsigned char key[16] = "1234567890ABCDEF";
//当使用CBC/CFB/OFB等模式时,需要一个初始化向量IV
unsigned char iv[16] = "0099887766554433";
unsigned char error[64];
//读写文件的buffer,长度应为16整倍数
unsigned char input_buffer[FILE_BUFFER_SIZE];
unsigned char output_buffer[FILE_BUFFER_SIZE];
FILE *encrypted_file = fopen(encrypted_file_path,"rb");
FILE *decrypted_file = fopen(decrypted_file_path,"wb");
mbedtls_aes_init(&aes_context);
//若使用AES-128,则必要长度为128bits。若使用AES-256,则密钥长度为256bits
result = mbedtls_aes_setkey_dec(&aes_context, key, 128);
if(result != 0){
printf("failed to set key:");
mbedtls_strerror(result,error,sizeof(error));
printf("%s\n",error);
mbedtls_aes_free(&aes_context);
return -1;
}
/**
* 当加密后的文件正好为FILE_BUFFER_SIZE的整倍数时,在某一轮的读取过程中,正好整个文件读取完成,此时feof无法判断出文件已结束
* 导致无法去除填充,为避免此种情况,需再往后读取一个字节,确保下一轮可读取到内容。
* */
while(1){
result = fread(input_buffer,1,FILE_BUFFER_SIZE,encrypted_file);
input_length = result;
if(result != FILE_BUFFER_SIZE){
if(ferror(encrypted_file)){
printf("failed to read encrypted file\n");
fclose(encrypted_file);
fclose(decrypted_file);
mbedtls_aes_free(&aes_context);
return -2;
}
}
//进行解密
if(input_length > 0){
//若读取的字节数为0,则表明上一轮正好读取完
result = mbedtls_aes_crypt_cbc(&aes_context, MBEDTLS_AES_DECRYPT, input_length,iv,input_buffer,output_buffer);
if(result != 0){
printf("failed to decrypt data:");
mbedtls_strerror(result,error,sizeof(error));
printf("%s\n",error);
fclose(encrypted_file);
fclose(decrypted_file);
mbedtls_aes_free(&aes_context);
return -3;
}
}
//判断是否到达文件尾部
if(feof(encrypted_file)){
//到达文件尾部
if(input_length%16 != 0){
printf("encrypted file is illegal!\n");
return -4;
}
//设置达到文件尾部标志
eof_flag = 1;
//去除填充
//读取最后一个值,此值即为填充值的长度
padding_code = output_buffer[input_length-1];
padding = (int)padding_code;
if(padding < 1 || padding > 16){
printf("padding code is illegal!\n");
fclose(encrypted_file);
fclose(decrypted_file);
mbedtls_aes_free(&aes_context);
return -6;
}
input_length = input_length - padding;
}else{
//预读取一个字节,查看本次读取是否正好到文件末尾
fgetc(encrypted_file);
if( ferror(encrypted_file)){
printf("failed to read encrypted file\n");
fclose(encrypted_file);
fclose(decrypted_file);
mbedtls_aes_free(&aes_context);
return -2;
}
if(feof(encrypted_file)){
//本次正好读取到文件末尾,需去填充
//设置达到文件尾部标志
eof_flag = 1;
//去除填充
//读取最后一个值,此值即为填充值的长度
padding_code = output_buffer[input_length-1];
padding = (int)padding_code;
if(padding < 1 || padding > 16){
printf("padding code is illegal!\n");
fclose(encrypted_file);
fclose(decrypted_file);
mbedtls_aes_free(&aes_context);
return -6;
}
input_length = input_length - padding;
}else{
//后面还有内容,还原本次预读取的位置
if(fseek(encrypted_file,-1L,SEEK_CUR)){
printf("failed to move file point!\n");
fclose(encrypted_file);
fclose(decrypted_file);
mbedtls_aes_free(&aes_context);
return -5;
}
}
}
result = fwrite(output_buffer,1,input_length,decrypted_file);
if(result != input_length){
printf("failed to write decrypted data to file\n");
fclose(decrypted_file);
fclose(encrypted_file);
mbedtls_aes_free(&aes_context);
return -7;
}
if(eof_flag == 1){
break;
}
}
fclose(decrypted_file);
fclose(encrypted_file);
mbedtls_aes_free(&aes_context);
return 0;
}
3.5.3 运行效果
为测试运行效果,特意准备了三个文件
-
1.txt:文件长度和每次读取的长度一致,即512字节
-
2.txt:长度比每次读取的长度短2个字节,即510字节,实现加密后的密文长度为512字节
-
3.txt:长度大于512字节
int result = encrypt_data_from_file_by_aes_cbc("D:\\tmp\\crypto\\aes\\1.txt","D:\\tmp\\crypto\\aes\\encrypted-1.txt");
if(result == 0){
printf("succeed to encrypt file\n");
}else{
printf("failed to encrypt file\n");
return -4;
}
result = decrypt_data_from_file_by_aes_cbc("D:\\tmp\\crypto\\aes\\encrypted-1.txt","D:\\tmp\\crypto\\aes\\decrypted-1.txt");
if(result == 0){
printf("succeed to decrypt file\n");
}else{
printf("failed to decrypt file\n");
return -5;
}
result = encrypt_data_from_file_by_aes_cbc("D:\\tmp\\crypto\\aes\\2.txt","D:\\tmp\\crypto\\aes\\encrypted-2.txt");
if(result == 0){
printf("succeed to encrypt file\n");
}else{
printf("failed to encrypt file\n");
return -4;
}
result = decrypt_data_from_file_by_aes_cbc("D:\\tmp\\crypto\\aes\\encrypted-2.txt","D:\\tmp\\crypto\\aes\\decrypted-2.txt");
if(result == 0){
printf("succeed to decrypt file\n");
}else{
printf("failed to decrypt file\n");
return -5;
}
result = encrypt_data_from_file_by_aes_cbc("D:\\tmp\\crypto\\aes\\3.txt","D:\\tmp\\crypto\\aes\\encrypted-3.txt");
if(result == 0){
printf("succeed to encrypt file\n");
}else{
printf("failed to encrypt file\n");
return -4;
}
result = decrypt_data_from_file_by_aes_cbc("D:\\tmp\\crypto\\aes\\encrypted-3.txt","D:\\tmp\\crypto\\aes\\decrypted-3.txt");
if(result == 0){
printf("succeed to decrypt file\n");
}else{
printf("failed to decrypt file\n");
return -5;
}
经过对比,解密后的三个文件和原文一致
同样使用openssl验证下,查看加密后的密文是否与openssl加密后的密文一致