You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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端解密方案

解密端需要和加密端保持完全一致的上下文状态:

  1. 同样复用一个EVP_CIPHER_CTX,仅初始化一次,每次解密数据块调用EVP_DecryptUpdate。
  2. 先Base64解码,再传入AES解密函数。
  3. 确保解密端的密钥和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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 17:55:04