openss-aes加密

本文详细介绍了OpenSSL中高级别的EVP接口,特别是AES加密算法(包括CBC模式和GCM/CCM扩展),以及如何使用EVP_Cipher_CTX和相关函数进行加解密操作,特别强调了填充机制、加密模式选择和解密注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

描述

EVP接口是一个高级别的对称加密接口,该接口封装了所有对称加密的算法。
以下内容来自openssl1.0.2:/docs/man1.0.2/man3/EVP_aes_256_cbc_hmac_sha1.html

AES加密

AES是基于块的对称加密算法,基于块的加密算法又称cbc加密,所以AES也叫做AES_CBC。

根据其密码长度128,192,256,则类型进一步扩展为AES_128_CBC,AES_192_CBC,AES_256_CBC。

在AES基础上又可以扩展出增加2种模式:GCM和CCM,这2中模式都是增加了额外的认证内容,实际使用中完全可以不用。

所以简单的使用,应该是,AES,128位密码,cbc,不启用GCM或者CCM,则:AES_128_CBC。

说明:基于块的加密,则加密原文必须是块大小,小于块的原文是不能直接加密的,所以需要填充。

过期接口

这些接口是过期的,不建议使用:

EVP_EncryptInit(), EVP_EncryptFinal(), EVP_DecryptInit(), EVP_CipherInit() and EVP_CipherFinal()

最新的加密建议使用下面的接口:

EVP_EncryptInit_ex(), EVP_EncryptFinal_ex(), EVP_DecryptInit_ex(), EVP_DecryptFinal_ex(), EVP_CipherInit_ex() and EVP_CipherFinal_ex()

aes加密相关函数

EVP_CIPHER_CTX_init()
初始化加密套件的上下文环境,必须第一个调用。

EVP_CipherInit_ex(), EVP_CipherUpdate() and EVP_CipherFinal_ex()
这组函数可以用来加解密,比起纯粹的加密、解密函数,这组更好用,所以加解密采用这组函数。其他纯粹的只加密、只解密函数就不再关注了。

EVP_CIPHER_CTX_cleanup()
清空加密上下文环境。

块大小:
AES的固定加密块大小是128bit。

明文和输出密文大小对应关系:
加密前块大小=加密后块大小。

明文和密文大小对于关系:
明文可能需要填充,则明文加密后的长度最大值是明文加上一个填充块:
密文长度 <= 明文长度 + 一个填充块长度

加密流程

加密的流程:
1.调用EVP_CIPHER_CTX_init创建上下文;
2.调用EVP_CipherInit_ex,设置上下文参数,例如密码等,该函数可以调用多次,设置完就可以加密了;
3.设置完参数就可以加密了,加密用到EVP_CipherUpdate()、EVP_CipherFinal_ex();
4.加密完成后,清空上下文,必须调用EVP_CIPHER_CTX_cleanup() ;
对于启用了填充的加密模式(启用填充,是默认模式);

加密说明:
1.对于启用了填充的加密模式,EVP_CipherFinal_ex()是用来加密最终的需要填充的数据的,如果最后没有多余的源数据剩下,则该函数也必须调用,且会全部填充一个数据块,并加密,同时清理一些东西及收尾。
2.对于没有启用填充的加密模式,调用EVP_CipherFinal_ex()时,如果加密上下文中保存有未加密的原文(一般是因为原文不是加密块整数倍导致有原文剩余)则会出错,否则什么也不会做(同样,可能有清理、收尾的动作,在老版本中应该是这样)。所以未启用填充模式的,自己要保存原文是加密块的整数倍,这个函数还是要调,但必须保证buff无剩余的原数据。
3.EVP_CipherFinal_ex调用后,标志加密结束了,开始销毁流程了。就不要在调用EVP_CipherUpdate()了。
4.EVP_CipherUpdate可以反复调用,用来加密数据。

解密说明:
1.同加密一样,EVP_CipherUpdate()进行解密数据,完成后调用EVP_CipherFinal_ex()收尾;
2.在启用了填充的模式下EVP_CipherFinal_ex()还会吐出数据,如果原文非块大小的整数倍;
3.在未启用填充模式下,EVP_CipherFinal_ex()也要调用,进行收尾;

cbc模式说明:
1.因为是cbc模式,所以加密必须是块大小才能加密,否则就出错,进一步的区别就是这个填充是自己做还是让openssl做;
2.解密也同理,必须是块大小,否则解密在最后一步EVP_CipherFinal_ex()就会校验出错;
3.在启用填充模式下,EVP_CipherFinal_ex()负责解密最后的填充块(及EVP_CipherUpdate()不解密填充块)同时校验最后的填充块格式,返回校验结果和解密数据;
4.在未启用填充块的模式下,调用EVP_CipherFinal_ex()时,加密上行文中应该没有任何缓存的带解密数据,否则就出错;

加解密流程图:

openssl中的一个例子:

int do_crypt(FILE *in, FILE *out, int do_encrypt)
        {
        /* Allow enough space in output buffer for additional block */
        unsigned char inbuf[1024], outbuf[1024 + EVP_MAX_BLOCK_LENGTH];
        int inlen, outlen;
        EVP_CIPHER_CTX ctx;
        /* Bogus key and IV: we'd normally set these from
         * another source.
         */
        unsigned char key[] = "0123456789abcdeF";
        unsigned char iv[] = "1234567887654321";

        /* Don't set key or IV right away; we want to check lengths */
        EVP_CIPHER_CTX_init(&ctx);
        EVP_CipherInit_ex(&ctx, EVP_aes_128_cbc(), NULL, NULL, NULL,
                do_encrypt);
        OPENSSL_assert(EVP_CIPHER_CTX_key_length(&ctx) == 16);
        OPENSSL_assert(EVP_CIPHER_CTX_iv_length(&ctx) == 16);

        /* Now we can set key and IV */
        EVP_CipherInit_ex(&ctx, NULL, NULL, key, iv, do_encrypt);

        for(;;) 
                {
                inlen = fread(inbuf, 1, 1024, in);
                if(inlen <= 0) break;
                if(!EVP_CipherUpdate(&ctx, outbuf, &outlen, inbuf, inlen))
                        {
                        /* Error */
                        EVP_CIPHER_CTX_cleanup(&ctx);
                        return 0;
                        }
                fwrite(outbuf, 1, outlen, out);
                }
        if(!EVP_CipherFinal_ex(&ctx, outbuf, &outlen))
                {
                /* Error */
                EVP_CIPHER_CTX_cleanup(&ctx);
                return 0;
                }
        fwrite(outbuf, 1, outlen, out);

        EVP_CIPHER_CTX_cleanup(&ctx);
        return 1;
        }

支持的所有对称加密列表(cipher list)

支持的每种加密类别,都可以通过函数获取其类型,不同的加密类型,通过不同的函数获取,这里我们只关注AES类型的加密类型获取函数。

EVP_aes_128_cbc()

密码长度128位的aes加密算法。

GCM和CCM

GCM和CCM是2种加密模式,这2种加密模式加密时需要一些额外的信息,这里不关注,忽略。

说明

加密有启用填充和不启用填充两种模式,默认是启用填充的。

如果启用填充,当加密数据总长度不是加密块的倍数时,缺失的数据会用填充填补,如果是整数倍,则会额外增加一个填充块

当启用填充后看,解密是否成功,可以在解密最后一个数据块时根据解密函数的返回结果判断出来,但是这个也不是100%准确的,错误数据仍然有1/256的概率被识别成成功解密,这个要注意。

如果不启用填充,当源数据长度是加密块的倍数时,解密总是返回成功。

结论:解密操作发生失败,则肯定是出错了,成功则未必表示数据已经解密成功。

对称算法主要有四种加密模式

(1)   电子密码本模式    Electronic Code Book(ECB)

这种模式是最早采用和最简单的模式,它将加密的数据分成若干组,每组的大小跟加密密钥长度相同,然后每组都用相同的密钥进行加密。

其缺点是:电子编码薄模式用一个密钥加密消息的所有块,如果原消息中重复明文块,则加密消息中的相应密文块也会重复,因此,电子编码薄模式适于加密小消息。

(2)加密块链模式     Cipher Block Chaining(CBC)

CBC模式的加密首先也是将明文分成固定长度的块,然后将前面一个加密块输出的密文与下一个要加密的明文块进行异或操作,将计算结果再用密钥进行加密得到密文。第一明文块加密的时候,因为前面没有加密的密文,所以需要一个初始化向量。跟ECB方式不一样,通过连接关系,使得密文跟明文不再是一一对应的关系,破解起来更困难,而且克服了只要简单调换密文块可能达到目的的攻击。

(3)加密反馈模式          Cipher Feedback Mode(CFB)

面向字符的应用程序的加密要使用流加密法,可以使用加密反馈模式。在此模式下,数据用更小的单元加密,如可以是8位,这个长度小于定义的块长(通常是64位)。其加密步骤是:

a)  使用64位的初始化向量。初始化向量放在移位寄存器中,在第一步加密,产生相应的64位初始化密文;
b)    始化向量最左边的8位与明文前8位进行异或运算,产生密文第一部分(假设为c),然后将c传输到接收方;

c)    向量的位(即初始化向量所在的移位寄存器内容)左移8位,使移位寄存器最右边的8位为不可预测的数据,在其中填入c的内容;
d)    第1-3步,直到加密所有的明文单元。

解密过程相反

(4)输出反馈模式          Output Feedback Mode(OFB)

输出反馈模式与CFB相似,惟一差别是,CFB中密文填入加密过程下一阶段,而在OFB中,初始化向量加密过程的输入填入加密过程下一阶段。

测试代码:


#include <openssl/evp.h>
#include <malloc.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#define data_base_len 16 * 1000 * 10
#define process_times 1000 * 10
#define debug 0
#define run_test_1 0
#define run_test_2 1

/*
  编译: g++  encryption.cpp -o my_program -lssl -lcrypto 
  time ./my_program
  real    0m3.590s
  user    0m3.541s
  sys     0m0.001s
    
  real:表示的是墙上时间,说白了,其实就是从程序运行开始到结束所经历的时间;
  user:表示程序运行期间,cpu 在用户态所花费的时间;
  sys:表示程序运行期间,cpu 在内核态所花费的时间;
*/



int do_crypt_1(unsigned char inbuf[], int inbuf_len, 
               unsigned char outbuf[], int outbuf_len,
               unsigned char key[], 
               unsigned char iv[], 
               int do_encrypt)
{
    EVP_CIPHER_CTX ctx;
    EVP_CIPHER_CTX_init(&ctx);

    EVP_CipherInit_ex(&ctx, EVP_aes_128_cbc(), NULL, NULL, NULL, do_encrypt);
    OPENSSL_assert(EVP_CIPHER_CTX_key_length(&ctx) == 16);
    OPENSSL_assert(EVP_CIPHER_CTX_iv_length(&ctx) == 16);

    /* Now we can set key and IV */
    EVP_CipherInit_ex(&ctx, NULL, NULL, key, iv, do_encrypt);

    if(!EVP_CipherUpdate(&ctx, outbuf, &outbuf_len, inbuf, inbuf_len))
    {
        /* Error */
        EVP_CIPHER_CTX_cleanup(&ctx);
        printf("Error in EVP_CipherUpdate do_encrypt:%u\n", do_encrypt);
        return 0;
    }

    int rr = EVP_CipherFinal_ex(&ctx, outbuf + outbuf_len, &outbuf_len);
    // printf("Error in EVP_CipherFinal_ex do_encrypt:%u rr:%d\n", do_encrypt, rr);

    EVP_CIPHER_CTX_cleanup(&ctx);
    return 1;
}

int test_1()
{
   unsigned char inbuf[data_base_len], outbuf[data_base_len + EVP_MAX_BLOCK_LENGTH], decryption_buff[data_base_len + EVP_MAX_BLOCK_LENGTH + EVP_MAX_BLOCK_LENGTH];

   memset(inbuf, 'c', data_base_len);
   inbuf[data_base_len - 1] = 0;
   
   memset(outbuf, 0, data_base_len + EVP_MAX_BLOCK_LENGTH);
   memset(decryption_buff, 0, data_base_len + EVP_MAX_BLOCK_LENGTH + EVP_MAX_BLOCK_LENGTH);

   unsigned char key[] = "0123456789abcdeF";
   unsigned char iv[] = "1234567887654321";

    int32_t times = process_times;
    while (times-- > 0) {
        do_crypt_1(inbuf, data_base_len, outbuf, data_base_len + EVP_MAX_BLOCK_LENGTH, key, iv, 1);
        do_crypt_1(outbuf, data_base_len + EVP_MAX_BLOCK_LENGTH, decryption_buff, data_base_len + EVP_MAX_BLOCK_LENGTH + EVP_MAX_BLOCK_LENGTH, key, iv, 0);
        if (debug) printf("do_crypt_1[%u->%zu]:\n%s\n", data_base_len - 1, strlen((const char *)decryption_buff), decryption_buff);
    }
}

int test_2()
{
   unsigned char inbuf[data_base_len], outbuf[data_base_len + EVP_MAX_BLOCK_LENGTH], decryption_buff[data_base_len + EVP_MAX_BLOCK_LENGTH + EVP_MAX_BLOCK_LENGTH];

   memset(inbuf, 'c', data_base_len);
   inbuf[data_base_len - 1] = 0;
   
   memset(outbuf, 0, data_base_len + EVP_MAX_BLOCK_LENGTH);
   memset(decryption_buff, 0, data_base_len + EVP_MAX_BLOCK_LENGTH + EVP_MAX_BLOCK_LENGTH);

   unsigned char key[] = "0123456789abcdeF";
   unsigned char iv[] = "1234567887654321";

    EVP_CIPHER_CTX ctx1;
    EVP_CIPHER_CTX_init(&ctx1);

    EVP_CipherInit_ex(&ctx1, EVP_aes_128_cbc(), NULL, NULL, NULL, 1);
    OPENSSL_assert(EVP_CIPHER_CTX_key_length(&ctx1) == 16);
    OPENSSL_assert(EVP_CIPHER_CTX_iv_length(&ctx1) == 16);

    /* Now we can set key and IV */
    EVP_CipherInit_ex(&ctx1, NULL, NULL, key, iv, 1);

    EVP_CIPHER_CTX ctx2;
    EVP_CIPHER_CTX_init(&ctx2);

    EVP_CipherInit_ex(&ctx2, EVP_aes_128_cbc(), NULL, NULL, NULL, 0);
    OPENSSL_assert(EVP_CIPHER_CTX_key_length(&ctx2) == 16);
    OPENSSL_assert(EVP_CIPHER_CTX_iv_length(&ctx2) == 16);

    /* Now we can set key and IV */
    EVP_CipherInit_ex(&ctx2, NULL, NULL, key, iv, 0);

    int times = process_times;
    while (times-- > 0) {
        int outbuf_len = 0;
        if(!EVP_CipherUpdate(&ctx1, outbuf, &outbuf_len, inbuf, data_base_len))
        {
            /* Error */
            EVP_CIPHER_CTX_cleanup(&ctx1);
            printf("err!\n");
            return 0;
        }

        int decryption_buff_len = 0;
        if(!EVP_CipherUpdate(&ctx2, decryption_buff, &decryption_buff_len, outbuf, outbuf_len))
        {
            /* Error */
            EVP_CIPHER_CTX_cleanup(&ctx2);
            printf("err!\n");
            return 0;
        }

        if (debug) printf("do_crypt_2[%u->%zu]:\n%s\n", data_base_len - 1, strlen((const char *)decryption_buff), decryption_buff);
    }

    int outbuf_len;
    EVP_CipherFinal_ex(&ctx1, inbuf, &outbuf_len);
    EVP_CipherFinal_ex(&ctx2, inbuf, &outbuf_len);

    EVP_CIPHER_CTX_cleanup(&ctx1);
    EVP_CIPHER_CTX_cleanup(&ctx2);
}


int main(int argc, char *argv[])
{
    if (run_test_1) test_1();
    if (run_test_2) test_2();

    return 0;
}

0、此例程调试环境 运行uname -a的结果如下: Linux freescale 3.0.35-2666-gbdde708-g6f31253 #1 SMP PREEMPT Thu Nov 30 15:45:33 CST 2017 armv7l GNU/Linux 简称2017 armv7l GNU/Linux 1、openssl 直接处理AES的API 在openssl/aes.h定义。是基本的AES库函数接口,可以直接调用,但是那个接口是没有填充的。而如果要与Java通信,必须要有填充模式。所以看第2条。 2、利用openssl EVP接口 在openssl/evp.h中定义。在这个接口中提供的AES是默认是pkcs5padding方式填充方案。 3、注意openssl新老版本的区别 看如下这段 One of the primary differences between master (OpenSSL 1.1.0) and the 1.0.2 version is that many types have been made opaque, i.e. applications are no longer allowed to look inside the internals of the structures. The biggest impact on applications is that: 1)You cannot instantiate these structures directly on the stack. So instead of: EVP_CIPHER_CTX ctx; you must instead do: EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); .... EVP_CIPHER_CTX_free(ctx); 2)You must only use the provided accessor functions to access the internals of the structure. 4、注意加密的内容是数据不限制是否为字符串 openssl接口加密的是数据,不限制是否为字符串,我看到有些人在加密时使用strlen(),来获取要加密的长度,如果是对字符串加密的话没有问题,如果不是字符串的话,用它获取的长度是到第一个0处,因为这个函数获取的是字符串长度,字符串是以零为终止的。 5、在调用EVP_EncryptFinal_ex时不要画蛇添足 正常加解密处理过程,引用网友的代码如下,经测试正确。 int kk_encrypt(unsigned char *plaintext, int plaintext_len, unsigned char *key, unsigned char *iv, unsigned char *ciphertext) { EVP_CIPHER_CTX *ctx; int len; int ciphertext_len; ctx = EVP_CIPHER_CTX_new(); EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv); //EVP_EncryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, iv); EVP_EncryptUpdate(ctx, ciphertext, &len;, plaintext, plaintext_len); ciphertext_len = len; EVP_EncryptFinal_ex(ctx, ciphertext + len, &len;); ciphertext_len += len; EVP_CIPHER_CTX_free(ctx); return ciphertext_len; } int kk_decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key, unsigned char *iv, unsigned char *plaintext) { EVP_CIPHER_CTX *ctx; int len; int plaintext_len; ctx = EVP_CIPHER_CTX_new(); EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv); //EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, iv); EVP_DecryptUpdate(ctx, plaintext, &len;, ciphertext, ciphertext_len); plaintext_len = len; EVP_DecryptFinal_ex(ctx, plaintext + len, &len;); plaintext_len += len; EVP_CIPHER_CTX_free(ctx); return plaintext_len; } 我看到有人提供的代码在加密长度正好是16字节的整数倍时特意不去调用EVP_EncryptFinal_ex,这实在是画蛇添足啊,不论什么情况下,最后一定要调用EVP_EncryptFinal_ex一次,然后结束加密过程。 6、Base64陷阱 如果用到了base64,要注意如下: 1)base64算法是将3个字节变成4个可显示字符。所以在如果数据长度不是3字节对齐时,会补0凑齐。 2)在解密时先要解base64,再解AES。在解base64后,要减掉补上的0。算法就去查看base64后的字符串尾处有几个=号,最多是2个,如果正好要加密的数据是3的倍数,不需要补0,那么base64后的数据尾处就没有=,如果补了1个0,就有一个=号。 算法如下: int encode_str_size = EVP_EncodeBlock(base64, en, el); int length = EVP_DecodeBlock(base64_out, base64, encode_str_size ); //EVP_DecodeBlock内部同样调用EVP_DecodeInit + EVP_DecodeUpdate + Evp_DecodeFinal实现,但是并未处理尾部的'='字符,因此结果字符串长度总是为3的倍数 while(base64[--encode_str_size] == '=') length--; 算法网友提供,测试正确。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值