基于tiny-AES-c实现与CryptoJS AES等效的C加解密代码
等效CryptoJS AES加解密的C实现方案(基于tiny-AES-c)
首先得明确:你当前的问题大概率是没匹配上CryptoJS的默认行为逻辑——比如你用了ECB模式,但CryptoJS默认是CBC;没处理PKCS#7填充;也没把密文字节转成Base64可读字符串。下面我分场景给出和CryptoJS完全等效的C代码实现,解决你提到的两个核心问题。
先理清CryptoJS AES的关键默认规则
- 默认模式是CBC(不是ECB,除非你JS代码显式指定了
mode: CryptoJS.mode.ECB) - 默认填充是PKCS#7(自动补全到16字节块的整数倍)
- 若用字符串密码而非原始字节密钥,会通过OpenSSL EVP_BytesToKey算法(MD5哈希、1次迭代)生成实际AES密钥和IV
- 加密输出默认是包含
salt+IV+密文的Base64编码字符串(直接用密钥时则是密文的Base64)
场景1:JS显式使用ECB模式+直接密钥
假设你的JS代码是这样的:
const key = CryptoJS.enc.Utf8.parse('your-16-byte-key-here'); // 16/24/32字节对应AES-128/192/256 const plaintext = 'Hello, this is a long message that needs encryption'; const ciphertext = CryptoJS.AES.encrypt(plaintext, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }).toString();
对应的C实现代码
tiny-AES-c本身不带填充和Base64转换,所以我们需要自己补充这部分逻辑:
1. PKCS#7填充/去填充函数
#include "aes.h" #include <string.h> #include <stdlib.h> // PKCS#7填充:将数据补全到AES块大小(16字节)的整数倍 void pkcs7_pad(uint8_t *data, size_t data_len, size_t *padded_len) { size_t block_size = AES_BLOCKLEN; size_t pad_len = block_size - (data_len % block_size); *padded_len = data_len + pad_len; memset(data + data_len, pad_len, pad_len); } // PKCS#7去填充:移除末尾的填充字节 void pkcs7_unpad(uint8_t *data, size_t *data_len) { uint8_t pad_len = data[*data_len - 1]; *data_len -= pad_len; }
2. AES-ECB加密(输出Base64可读字符串)
// 你需要实现或引入一个标准Base64编码函数,这里省略具体实现 char* base64_encode(const uint8_t* data, size_t len); // AES-ECB加密,等效CryptoJS的ECB+PKCS7逻辑 char* aes_ecb_encrypt(const char* plaintext, const uint8_t* key, size_t key_len) { // 1. 把明文转成UTF-8字节数组 size_t plain_len = strlen(plaintext); uint8_t* plain_data = (uint8_t*)malloc(plain_len + AES_BLOCKLEN); // 预留填充空间 memcpy(plain_data, plaintext, plain_len); // 2. 执行PKCS#7填充(解决长消息块不匹配问题) size_t padded_len; pkcs7_pad(plain_data, plain_len, &padded_len); // 3. 初始化AES上下文,执行分块加密 AES_ctx ctx; AES_init_ctx(&ctx, key); for (size_t i = 0; i < padded_len; i += AES_BLOCKLEN) { AES_ECB_encrypt(&ctx, plain_data + i); } // 4. 密文字节转Base64,得到可读字符串 char* cipher_b64 = base64_encode(plain_data, padded_len); free(plain_data); return cipher_b64; }
3. AES-ECB解密函数
// 标准Base64解码函数,需自行实现或引入 uint8_t* base64_decode(const char* b64_str, size_t* out_len); char* aes_ecb_decrypt(const char* cipher_b64, const uint8_t* key, size_t key_len) { // 1. Base64解码得到密文字节数组 size_t cipher_len; uint8_t* cipher_data = base64_decode(cipher_b64, &cipher_len); // 2. 初始化AES上下文,分块解密 AES_ctx ctx; AES_init_ctx(&ctx, key); for (size_t i = 0; i < cipher_len; i += AES_BLOCKLEN) { AES_ECB_decrypt(&ctx, cipher_data + i); } // 3. 移除PKCS#7填充 size_t plain_len = cipher_len; pkcs7_unpad(cipher_data, &plain_len); // 4. 转成UTF-8字符串 char* plaintext = (char*)malloc(plain_len + 1); memcpy(plaintext, cipher_data, plain_len); plaintext[plain_len] = '\0'; free(cipher_data); return plaintext; }
场景2:JS使用默认CBC模式(带自动salt/IV)
如果你的JS代码是最常见的默认调用(用密码而非直接密钥):
const password = 'your-password'; const plaintext = 'Long message to encrypt'; const ciphertext = CryptoJS.AES.encrypt(plaintext, password).toString();
这种情况需要先实现EVP_BytesToKey算法,再处理CBC模式加密。
补充EVP_BytesToKey实现(MD5版本)
#include <md5.h> // 需引入MD5库,可自行实现或用开源版本 // 从密码和salt生成AES密钥和IV,完全匹配CryptoJS默认逻辑 void evp_bytes_to_key(const char* password, const uint8_t* salt, uint8_t* key, uint8_t* iv, size_t key_len, size_t iv_len) { uint8_t md5_buf[MD5_DIGEST_LENGTH]; uint8_t* temp = (uint8_t*)malloc(strlen(password) + 8); // 密码 + 8字节salt size_t offset = 0; memcpy(temp, password, strlen(password)); memcpy(temp + strlen(password), salt, 8); // 第一次MD5哈希 MD5(temp, strlen(password) + 8, md5_buf); memcpy(key + offset, md5_buf, MD5_DIGEST_LENGTH); offset += MD5_DIGEST_LENGTH; // 若密钥+IV长度不足,补充迭代(CryptoJS默认1次,这里简化处理) if (offset < key_len + iv_len) { memset(temp, 0, strlen(password) + 8); memcpy(temp, md5_buf, MD5_DIGEST_LENGTH); memcpy(temp + MD5_DIGEST_LENGTH, password, strlen(password)); memcpy(temp + MD5_DIGEST_LENGTH + strlen(password), salt, 8); MD5(temp, MD5_DIGEST_LENGTH + strlen(password) + 8, md5_buf); memcpy(key + offset, md5_buf, key_len + iv_len - offset); } // 拆分密钥和IV memcpy(iv, key + key_len, iv_len); key[key_len] = '\0'; free(temp); }
CBC模式加密函数
char* aes_cbc_encrypt(const char* plaintext, const char* password) { // 1. 生成随机8字节salt(CryptoJS默认行为) uint8_t salt[8]; // 生产环境请用安全随机源(如/dev/urandom),这里用rand()做示例 for (int i = 0; i < 8; i++) salt[i] = rand() % 256; // 2. 生成AES-128密钥(16字节)和IV(16字节) uint8_t key[16], iv[16]; evp_bytes_to_key(password, salt, key, iv, 16, 16); // 3. 明文转UTF-8并PKCS#7填充 size_t plain_len = strlen(plaintext); uint8_t* plain_data = (uint8_t*)malloc(plain_len + AES_BLOCKLEN); memcpy(plain_data, plaintext, plain_len); size_t padded_len; pkcs7_pad(plain_data, plain_len, &padded_len); // 4. CBC模式加密 AES_ctx ctx; AES_init_ctx_iv(&ctx, key, iv); AES_CBC_encrypt_buffer(&ctx, plain_data, padded_len); // 5. 按CryptoJS格式组装:Salted__ + salt + 密文,再转Base64 size_t total_len = 8 + 8 + padded_len; // "Salted__"占8字节 uint8_t* output = (uint8_t*)malloc(total_len); memcpy(output, "Salted__", 8); memcpy(output + 8, salt, 8); memcpy(output + 16, plain_data, padded_len); char* cipher_b64 = base64_encode(output, total_len); free(plain_data); free(output); return cipher_b64; }
解决你现有问题的关键总结
- 加密结果转可读字符串:CryptoJS默认输出Base64,你需要把C代码里的密文字节数组转成Base64编码,而不是直接输出原始字节。
- 长消息异常:tiny-AES-c的加密是按16字节块处理的,必须先对长消息做PKCS#7填充,确保长度是块大小的整数倍,否则会出现块不匹配导致的异常。
额外注意事项
- 若使用AES-256,需要在tiny-AES-c的头文件中定义
AES256宏。 - Base64编码/解码要使用标准实现,确保和CryptoJS的Base64兼容。
- 生产环境生成salt/IV时,一定要用安全的随机数源,不要用
rand()。
内容的提问来源于stack exchange,提问作者Roni Gadot
相关产品推荐
相关产品推荐

