C语言中AES-256-CTR模式跨平台加密UART流数据及解密异常排查
问题分析与解决方案
加密代码的核心问题
1. AES-CTR模式上下文复用错误
AES-CTR是流加密模式,依赖连续递增的计数器生成唯一密钥流。当前代码每次调用aes_encrypt都新建EVP_CIPHER_CTX并重新初始化,会导致计数器每次从初始IV开始,重复生成相同的密钥流块。当数据流分多次加密时,后续块的密钥流与前面的重复,解密时必然出现数据错乱(这就是你看到的一半正常一半乱码的核心原因)。
2. 输入长度计算错误
代码中用strlen((char *)input)获取输入数据长度,这仅适用于纯文本数据。如果UART流数据包含\0字节,strlen会提前截断,导致实际加密的数据比输入少,解密时对应位置的数据必然异常。
3. Base64编码实现隐患
当前通过BIO_read读取Base64数据的方式不可靠,BIO_push后的内存BIO读指针位置可能不正确,导致读取的Base64数据不完整或错误。直接使用BUF_MEM的data指针拷贝更安全。
修正后的加密流程实现
1. 持久化AES上下文
对于持续的流数据,应该全局复用一个EVP_CIPHER_CTX,仅在初始化时调用一次EVP_EncryptInit_ex,后续每次加密数据只调用EVP_EncryptUpdate。
示例修正后的加密框架:
#include <openssl/evp.h> #include <string.h> #include <stdio.h> // 全局上下文,仅初始化一次 static EVP_CIPHER_CTX *aes_ctx = NULL; unsigned char key_aes_2[32]; // 256位密钥 unsigned char iv_aes_2[16]; // CTR模式IV为128位 // 初始化AES上下文,仅调用一次 int aes_init(void) { aes_ctx = EVP_CIPHER_CTX_new(); if (!aes_ctx) { fprintf(stderr, "Failed to create EVP_CIPHER_CTX\n"); return -1; } if (EVP_EncryptInit_ex(aes_ctx, EVP_aes_256_ctr(), NULL, key_aes_2, iv_aes_2) != 1) { EVP_CIPHER_CTX_free(aes_ctx); fprintf(stderr, "Failed to initialize encryption\n"); return -1; } return 0; } // 加密流数据块,可多次调用 void aes_encrypt_stream(const unsigned char *input, int input_len, unsigned char *output, int *output_len) { if (!aes_ctx) { fprintf(stderr, "AES context not initialized\n"); *output_len = 0; return; } int len; if (EVP_EncryptUpdate(aes_ctx, output, &len, input, input_len) != 1) { fprintf(stderr, "Encryption update failed\n"); *output_len = 0; return; } *output_len = len; } // 关闭AES上下文,程序结束时调用 void aes_cleanup(void) { if (aes_ctx) { EVP_CIPHER_CTX_free(aes_ctx); aes_ctx = NULL; } }
2. 修正Base64编码
直接使用BUF_MEM的data指针拷贝数据,避免BIO_read的潜在问题:
#include <openssl/bio.h> #include <openssl/buffer.h> void base64_encode(const unsigned char *input, int input_length, unsigned char *output, int *output_length) { BIO *b64 = BIO_new(BIO_f_base64()); BIO *bio = BIO_new(BIO_s_mem()); if (!b64 || !bio) { fprintf(stderr, "Failed to create BIO objects\n"); *output_length = 0; return; } bio = BIO_push(b64, bio); BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); // 不添加换行符 BIO_write(bio, input, input_length); BIO_flush(bio); BUF_MEM *buffer_ptr; BIO_get_mem_ptr(bio, &buffer_ptr); *output_length = buffer_ptr->length; memcpy(output, buffer_ptr->data, *output_length); output[*output_length] = '\0'; // 字符串终止符 BIO_free_all(bio); }
Windows端解密方案
解密端需要和加密端保持完全一致的上下文状态:
- 同样复用一个EVP_CIPHER_CTX,仅初始化一次,每次解密数据块调用
EVP_DecryptUpdate。 - 先Base64解码,再传入AES解密函数。
- 确保解密端的密钥和IV与加密端完全相同,CTR模式不需要
EVP_DecryptFinal_ex(因为不会产生额外的填充数据)。
示例解密代码框架:
#include <openssl/evp.h> #include <openssl/bio.h> #include <openssl/buffer.h> #include <string.h> #include <stdio.h> static EVP_CIPHER_CTX *decrypt_ctx = NULL; unsigned char key_aes_2[32]; unsigned char iv_aes_2[16]; int decrypt_init(void) { decrypt_ctx = EVP_CIPHER_CTX_new(); if (!decrypt_ctx) { fprintf(stderr, "Failed to create decrypt context\n"); return -1; } // CTR模式加密和解密的初始化调用一致 if (EVP_DecryptInit_ex(decrypt_ctx, EVP_aes_256_ctr(), NULL, key_aes_2, iv_aes_2) != 1) { EVP_CIPHER_CTX_free(decrypt_ctx); fprintf(stderr, "Failed to initialize decryption\n"); return -1; } return 0; } void base64_decode(const unsigned char *input, int input_length, unsigned char *output, int *output_length) { BIO *b64 = BIO_new(BIO_f_base64()); BIO *bio = BIO_new_mem_buf(input, input_length); if (!b64 || !bio) { fprintf(stderr, "Failed to create BIO objects\n"); *output_length = 0; return; } bio = BIO_push(b64, bio); BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); *output_length = BIO_read(bio, output, input_length * 3 / 4); // Base64解码后的最大长度 BIO_free_all(bio); } void aes_decrypt_stream(const unsigned char *input, int input_len, unsigned char *output, int *output_len) { if (!decrypt_ctx) { fprintf(stderr, "Decrypt context not initialized\n"); *output_len = 0; return; } int len; if (EVP_DecryptUpdate(decrypt_ctx, output, &len, input, input_len) != 1) { fprintf(stderr, "Decryption update failed\n"); *output_len = 0; return; } *output_len = len; } void decrypt_cleanup(void) { if (decrypt_ctx) { EVP_CIPHER_CTX_free(decrypt_ctx); decrypt_ctx = NULL; } }
额外注意事项
- 密钥与IV管理:确保加密和解密端使用完全相同的256位密钥和16位IV。IV应该随机生成,每次会话使用不同的IV,但不需要保密,可随加密数据一起传输(比如放在Base64数据的开头)。
- 数据流分块:UART流数据分块传输时,每块的加密/解密都要复用同一个上下文,不能每次新建。
- 数据长度传递:加密时必须传递实际的输入数据长度,不能依赖
strlen,确保所有字节都被加密。
内容的提问来源于stack exchange,提问作者Sumedh Vidhate
相关产品推荐
相关产品推荐

