简介压缩包内含一份 C/C 实现的 AES 文件加解密源码 aes.cpp面向正在学习密码学、信息安全或 C/C 程序设计的读者也可作为相关课程设计与项目实践参考。代码基于 NIST 发布的 AES 高级加密标准采用 128 位数据块和可变密钥长度完整覆盖密钥扩展、初始轮、中间轮、最后轮以及逆变换过程具体实现了字节代换、行移位、列混淆和轮密钥加等核心环节便于对照算法原理逐步剖析。资源共 1 个 cpp 文件压缩包约 4KB体积小巧适合快速阅读和二次开发。已有 243 人浏览学习适合作为从原理到工程实践的过渡样例。通过这份代码读者可以掌握文件加解密中的二进制读写、内存管理、错误处理等落地技巧同时理解密钥安全存储与传输的必要性为后续在 TLS/SSL、数据保护等场景中运用 AES 打下基础。1. 用 C 语言做 AES 文件加密难的不是算法而是文件边界处理拿到“aes.zip”这个标题我第一反应不是代码怎么写而是大部分人在 AES 文件加密上栽的跟头根本不在 AES 本身——轮函数、S 盒、密钥扩展这些算法细节OpenSSL 或 mbedTLS 早就封装好了真正让项目卡壳的是文件怎么切块、填充怎么做、IV 怎么存、密文怎么和原文长度对齐。C 语言做 AES 文件加密的典型场景有两类一类是嵌入式环境跑 Linux 都要精打细算只能自己调 AES 库另一类是安全工具开发比如给配置文件加密、给固件分包加密需要把加密逻辑和文件格式一起设计。适合读这篇文章的人是那种已经能写文件读写、懂一点指针和内存布局但没系统摸过分组加密工程化的开发者——你缺的不是 AES 算法本身而是把它塞进文件流里的那套完整方案。C 语言在文件加解密上有天然优势内存可控、零依赖、交叉编译方便但也因为太接近底层所有坑都藏不住。接下来我在标题“aes.zip_AES_AES文件加密_aes c语言_aes 文件_文件加解密”的语义范围内从 AES 的工程选型讲起落到可编译的代码和排错思路。2. AES 文件加密的选型模式、填充与密钥长度先定死2.1 为什么 ECB 模式不能用于文件加密很多初学者第一次写 AES 文件加密用的是 ECB 模式因为写起来最简单——每个 16 字节块独立加密不需要 IV。但 ECB 的致命缺陷是相同的明文块产生相同的密文块。对文件来说如果文件里有大量重复片段比如全零的空白区域、位图文件头部、结构化日志加密后这些重复模式直接暴露在密文里文件的信息熵没有真正被抹平这等于告诉攻击者文件结构没有变化。工程上文件加密至少要选 CBC 或 CTR。CBC 每个明文块先和上一个密文块异或再加密解决了重复模式问题CTR 则是把计数器加密后和明文异或可以并行处理也天然支持随机读。对于我自己的项目如果加密后要支持按块解密比如分片下载校验我会选 CTR如果只是整文件加解密CBC 更常见因为 OpenSSL 和 mbedTLS 对 CBC 的优化最完善。2.2 填充模式文件加密必须用 PKCS7AES 是分组密码块大小固定 16 字节而文件长度几乎不可能正好是 16 的倍数。C 语言里处理文件填充我一般用 PKCS7剩余字节数 n1 到 16就往尾部补 n 个值为 n 的字节。比如明文最后一块还差 5 字节满 16就补 5 个 0x05。解密后取最后一个字节的值把它当成填充长度去掉即可。这里有个容易错的点如果文件长度恰好是 16 的倍数PKCS7 也要额外补满一整块16 个 0x10。因为解密时需要靠最后一个字节判断填充长度如果原始文件正好对齐而不补块解密时无法区分“没有填充”和“最后一个字节本来就是填充长度”。这个细节在文件加密里必须处理否则边界文件会在解密后多出或缺失 16 字节。2.3 密钥长度与轮数AES-128、AES-192、AES-256 怎么选AES 支持三种密钥长度128 位10 轮、192 位12 轮、256 位14 轮。做文件加密我建议直接用 AES-256-CBC除非你的目标 CPU 没有 AES 硬件指令且性能受限。注意了这不是因为 AES-128 不安全——它在当前算力下仍然安全——而是因为文件加密往往是静态落盘数据加密攻击者可以离线拿密文做暴力破解或侧信道分析密钥长度多一些冗余是值得的。/* 密钥长度宏定义统一走 AES-256 */ #define AES_KEY_LEN 32 /* 256 bit 32 bytes */ #define AES_BLOCK_LEN 16 /* AES 块大小固定 */ unsigned char key[AES_KEY_LEN]; unsigned char iv[AES_BLOCK_LEN]; /* key 和 iv 从哪里来见 2.4 节 */逻辑说明密钥是 32 字节的数组IV 是 16 字节数组。这里只是定义内存布局真正的密钥派生逻辑在后面。参数上需要留意 AES_KEY_LEN 不要随意改成 16 或 24否则调用底层 AES_set_encrypt_key 时传的位数参数也要同步修改否则越界读内存是必然的。2.4 密钥和 IV 怎么来从口令到密钥的派生另一个常见误区是直接拿用户输入的字符串当密钥。用户口令长度往往不是 32 字节且可预测性太强。正确做法是用 PBKDF2 或 scrypt 从口令派生密钥和 IV。C 语言里 OpenSSL 的 PKCS5_PBKDF2_HMAC 函数可以实现这一点。#include openssl/evp.h const char *pass user-passphrase; unsigned char salt[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; PKCS5_PBKDF2_HMAC(pass, strlen(pass), salt, sizeof(salt), 100000, /* 迭代次数越大越慢但更抗暴力 */ EVP_sha256(), AES_KEY_LEN AES_BLOCK_LEN, key_iv_buffer); /* 前 32 字节当 key后 16 字节当 IV */ memcpy(key, key_iv_buffer, AES_KEY_LEN); memcpy(iv, key_iv_buffer AES_KEY_LEN, AES_BLOCK_LEN);参数说明迭代次数这里写了 100000在嵌入式环境可以降到 10000但这是安全性和性能的权衡——100000 次 SHA-256 在普通 PC 上约 0.1 秒在 200MHz 的 MCU 上可能要吃 3 到 5 秒。盐值 salt 应该随机生成并随密文存储绝不能硬编码成固定值否则攻击者可以预计算彩虹表。函数输出写进 key_iv_buffer 时注意缓冲区大小必须是 48 字节。2.5 常见的文件加密库选择C 语言里做 AES 文件加密常见的选择有几个库包/获取方式特点适合场景OpenSSL EVPapt install libssl-dev功能全支持 PBKDF2、所有模式Linux 桌面端/服务端工具mbedTLSGitHub 源码编译体积小可裁剪嵌入式、RTOS自己写 AES 轮函数代码公开学习用途不推荐直接用于生产理解算法细节笔试/竞赛自己手写 AES 算法做文件加密我只在一种情况下推荐你所在的环境不允许链接任何动态库或者编译器太老旧。否则用 OpenSSL 的 EVP 接口是最省心的它帮你封装了模式切换、填充处理和硬件 AES 加速指令AES-NI的调用。3. C 语言实现 AES 文件加密整文件读入与流式分块3.1 两种文件处理模型对比AES 文件加密的代码组织取决于文件大小的假设。小文件比如配置文件、密钥材料可以一次性读入内存加密后再写回。大文件比如磁盘镜像、日志归档不能整文件读入必须流式分块处理。两个模型的最大差别在于内存峰值和错误恢复能力。我一般先判断文件大小。如果小于 64MB直接走内存模型超过 64MB走分块模型。阈值设 64MB 不是 AES 的限制而是 32 位嵌入式系统上 malloc 容易失败的边界。/* 判断使用哪种模型 */ FILE *fp fopen(input_path, rb); fseek(fp, 0, SEEK_END); long fsize ftell(fp); fseek(fp, 0, SEEK_SET); if (fsize 64 * 1024 * 1024) { encrypt_file_memory(fp, output_path); } else { encrypt_file_stream(fp, output_path); } fclose(fp);逻辑说明先获取文件大小再决定处理路径。这里 fseek/ftell 是传统做法注意大于 2GB 的文件 ftell 返回 long 在 Windows 上是 32 位要换 _ftelli64 或 fseeko。Linux 上 long 是 64 位没这个问题。参数上 64MB 的阈值不是硬性规定如果你的系统内存富裕可以调到 256MB但 malloc 失败时的错误处理必须写全。3.2 内存模型的完整实现内存模型适合小文件代码短、边界逻辑简单。加密流程读全部明文到缓冲区、PKCS7 填充、按块 AES-CBC 加密、写入密文和 IV 头。#include openssl/evp.h #include string.h int encrypt_file_memory(FILE *in, const char *out_path) { unsigned char *plain_buf, *cipher_buf; long fsize, padded_len; int outlen, tmplen; fseek(in, 0, SEEK_END); fsize ftell(in); fseek(in, 0, SEEK_SET); int pad_len AES_BLOCK_LEN - (fsize % AES_BLOCK_LEN); padded_len fsize pad_len; plain_buf malloc(padded_len); cipher_buf malloc(padded_len AES_BLOCK_LEN); if (!plain_buf || !cipher_buf) return -1; fread(plain_buf, 1, fsize, in); memset(plain_buf fsize, pad_len, pad_len); /* PKCS7 填充 */ EVP_CIPHER_CTX *ctx EVP_CIPHER_CTX_new(); EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv); EVP_EncryptUpdate(ctx, cipher_buf, outlen, plain_buf, padded_len); EVP_EncryptFinal_ex(ctx, cipher_buf outlen, tmplen); EVP_CIPHER_CTX_free(ctx); FILE *out fopen(out_path, wb); /* 文件格式IV(16) 密文 */ fwrite(iv, 1, AES_BLOCK_LEN, out); fwrite(cipher_buf, 1, outlen tmplen, out); fclose(out); free(plain_buf); free(cipher_buf); return 0; }代码逻辑拆解第一步算填充长度并申请 padding 后的缓冲区这里 memset 的第三个参数是 pad_len值等于 pad_len这正是 PKCS7 的语义。第二步 EVP_EncryptUpdate 一次传入 padded_len因为 CBC 模式可以一次加密多块。第三步写文件时先写 IV 再写密文这个顺序不是随便定的后面解密要从同一个文件中恢复 IV 才能开始解密。这里有几个参数值得注意。EVP_EncryptUpdate 的 outlen 是本次输出长度EVP_EncryptFinal_ex 的 tmplen 在 CBC 模式下通常为 0。但养成从 final 拿返回值再相加的习惯因为以后切到 GCM 模式时 final 会输出 TAG。cipher_buf 申请了 padded_len 16是因为 OpenSSL 的 EVP 接口文档要求输出缓冲区必须比输入多一个块长否则可能缓冲区溢出。3.3 流式分块的实现不把大文件塞进内存大文件加密要按固定大小读取、加密、写入。关键在于处理好最后一块的填充逻辑。我用的块大小是 1MB这个值对磁盘 I/O 和 AES-NI 指令都算友好。#define CHUNK_SIZE (1024 * 1024) int encrypt_file_stream(FILE *in, const char *out_path) { unsigned char *in_chunk malloc(CHUNK_SIZE AES_BLOCK_LEN); unsigned char *out_chunk malloc(CHUNK_SIZE AES_BLOCK_LEN); FILE *out fopen(out_path, wb); /* 先写 IV 头 */ fwrite(iv, 1, AES_BLOCK_LEN, out); EVP_CIPHER_CTX *ctx EVP_CIPHER_CTX_new(); EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv); size_t bytes_read; int outlen; while ((bytes_read fread(in_chunk, 1, CHUNK_SIZE, in)) CHUNK_SIZE) { EVP_EncryptUpdate(ctx, out_chunk, outlen, in_chunk, bytes_read); fwrite(out_chunk, 1, outlen, out); } /* 处理最后一块并做 PKCS7 填充 */ int pad_len AES_BLOCK_LEN - (bytes_read % AES_BLOCK_LEN); memset(in_chunk bytes_read, pad_len, pad_len); EVP_EncryptUpdate(ctx, out_chunk, outlen, in_chunk, bytes_read pad_len); fwrite(out_chunk, 1, outlen, out); EVP_EncryptFinal_ex(ctx, out_chunk, outlen); fwrite(out_chunk, 1, outlen, out); EVP_CIPHER_CTX_free(ctx); fclose(in); fclose(out); free(in_chunk); free(out_chunk); return 0; }边界情况说明while 循环读到正好 CHUNK_SIZE 时继续最后一次 fread 的返回值小于 CHUNK_SIZE这个残留数据就是最后一块。PKCS7 填充在最后一次调用 EVP_EncryptUpdate 时交给 OpenSSL 处理——不对这里有个细节要注意OpenSSL EVP_CBC 模式不会自动填充需要自己填。上面的做法是自己计算 pad_len 然后 memset这是对的。最后一轮 Update 传入的数据含填充块加密后 outlen 也会相应增加 16 字节。这段代码里最关键的思路是OpenSSL 的 EVP_EncryptUpdate 可以反复调用每次放入任意长度的数据只要不超过块大小的整数倍约束实际并不存在内部会缓存未满块但明文最后一次必须由 EncryptFinal 收尾。在流式模型中如果你能保证 Update 每次都是 16 字节倍数Final 就不会再产生输出。上面的实现里循环中的 CHUNK_SIZE 是 1MB天然是 16 的倍数所以没问题。3.4 解密函数的对称实现解密和加密代码几乎对称只改两个地方EVP_DecryptInit_ex 替换 Encrypt 版本文件头先读 IV 而不是写 IV。int decrypt_file_stream(FILE *in, const char *out_path) { unsigned char file_iv[AES_BLOCK_LEN]; fread(file_iv, 1, AES_BLOCK_LEN, in); /* 读回 IV */ unsigned char *in_chunk malloc(CHUNK_SIZE AES_BLOCK_LEN); unsigned char *out_chunk malloc(CHUNK_SIZE AES_BLOCK_LEN); FILE *out fopen(out_path, wb); EVP_CIPHER_CTX *ctx EVP_CIPHER_CTX_new(); EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, file_iv); size_t bytes_read; int outlen; while ((bytes_read fread(in_chunk, 1, CHUNK_SIZE, in)) CHUNK_SIZE) { EVP_DecryptUpdate(ctx, out_chunk, outlen, in_chunk, bytes_read); fwrite(out_chunk, 1, outlen, out); } EVP_DecryptUpdate(ctx, out_chunk, outlen, in_chunk, bytes_read); fwrite(out_chunk, 1, outlen, out); EVP_DecryptFinal_ex(ctx, out_chunk, outlen); /* 这里会校验 PKCS7 填充 */ fwrite(out_chunk, 1, outlen, out); EVP_CIPHER_CTX_free(ctx); fclose(in); fclose(out); free(in_chunk); free(out_chunk); return 0; }解密时 PKCS7 校验由 EVP_DecryptFinal_ex 完成。如果密文被篡改或密钥不对Final 返回值会是 0填充校验失败。你必须在代码里检查这个返回值——常见的错误是忽略它导致解密攻击者改过的密文时输出了带填充的垃圾数据却报成功。注意解密后文件大小会比加密前的原文多出 0 到 16 字节的填充需要按 PKCS7 规则在原位删除。4. 文件格式设计与安全增强CBC 之外还要想的事4.1 自描述文件头盐、IV、版本号用 2.4 节的方式派生密钥时盐值是随机生成的IV 也是随机生成的。但解密端需要拿到盐和 IV 才能重新派生密钥并开始解密。这意味着文件格式必须自带这些参数我前文的实现里只把 IV 写在文件头这还不够完整。推荐的文件格式设计typedef struct { unsigned char magic[4]; /* A, E, S, F */ uint32_t version; /* 格式版本当前为 1 */ unsigned char salt[8]; /* PBKDF2 盐值 */ unsigned char iv[16]; /* AES IV */ uint64_t orig_size; /* 原始文件长度解密后截断用 */ } aes_file_header_t;这个头一共 40 字节通过 fwrite 一次性写入。orig_size 字段解决了我之前讲的填充问题——解密后只要按 orig_size 截断即可不用依赖 PKCS7 结构去推断原始长度等于双重保障。version 字段用来应对以后加密算法升级比如从 CBC 切到 GCM旧文件还能识别版本并走老解密路径。4.2 完整性校验从 Ciphertext 到 Authenticated Encryption写到这里我必须给标题里没写但工程上绕不开的话题一个明确立场只用 CBC 模式的 AES 加密无法保证密文完整性。攻击者翻转密文中的某个 bit解密后对应明文位会翻转且不报错。对配置文件或固件包来说这等于给了攻击者修补字节的空间。替代方案有两个。第一个是 Crypto 的 GCM 模式密文后追加 16 字节认证标签解密时校验标签OpenSSL EVP 接口直接支持。第二个是加密后单独算 HMAC-SHA256把 HMAC 值放在文件尾部。我个人倾向 GCM因为接口统一、不需要额外管理 HMAC 密钥。如果沿用 CBC至少要加 HMACunsigned char hmac[EVP_MAX_MD_SIZE]; unsigned int hmac_len; HMAC(EVP_sha256(), key, AES_KEY_LEN, cipher_buf, cipher_len, hmac, hmac_len); /* 把 hmac 追加到密文末尾解密前先校验 */参数说明HMAC 的密钥应该和 AES 密钥分开派生比如 PBKDF2 输出 32 字节后扩到 64 字节前 32 字节给 AES后 32 字节给 HMAC。如果同一个密钥既做加密又做认证在理论上存在相关攻击风险工程实践中要避免。4.3 C 语言内存安全密钥销毁与缓冲区清零C 语言做加密最容易在内存上留下密钥痕迹。malloc 出来的 key 和 iv 用完如果不主动清零程序退出时这些敏感数据留在堆内存里core dump 或调试器即可读取。AES 文件加密工具应该养成一个习惯不再使用的敏感缓冲区用 secure_zero 函数覆盖。void secure_zero(void *ptr, size_t len) { volatile unsigned char *p (volatile unsigned char *)ptr; while (len--) *p 0; }这里用 volatile 指针而不是 memset原因在于编译器可能优化掉对即将释放的内存的 memset 调用认为它无效。volatile 强制写操作真正落在内存上。在 Linux 上还可以用 explicit_bzero 或 OPENSSL_cleanse后者能避免某些架构上的编译器重排风险。5. 性能调优、常见返回码排查与最小可验证命令5.1 启用 AES-NI 硬件加速现代 x86 和 ARMv8 处理器都带 AES 硬件指令OpenSSL 在编译时默认启用。你不需要改代码加密速度就能提升好几倍。我见过有人因为在自己代码里手工展开 AES 轮函数结果反而绕过了 AES-NI性能差 5 倍以上。验证硬件加速是否生效检查你的编译环境和运行环境# 查看 openssl 是否支持 aes-ni openssl speed -evp aes-256-cbc输出里如果看到“aes-256-cbc”的吞吐量在几百 MB/s 到几 GB/s 之间说明硬件加速在工作如果只有几十 MB/s可能是库的编译配置问题。这条命令同时能测出你目标机器上 AES 加密的实际吞吐量上限对预估大文件加密耗时很有用。5.2 必须处理的 OpenSSL 返回码EVP 接口几乎每个函数都有返回值0 表示失败1 表示成功。文件加密代码里至少这几个位置要检查返回值否则失败时静默出错很难定位。if (EVP_EncryptUpdate(ctx, out_chunk, outlen, in_chunk, bytes_read) ! 1) { fprintf(stderr, EVP_EncryptUpdate failed: %s\n, ERR_error_string(ERR_get_error(), NULL)); goto cleanup; }常见失败原因对照返回错误可能原因排查方向EVP_EncryptFinal 返回 0前面 Update 有未提交的数据或参数长度非法检查每个 Update 的输入长度是否为 16 的倍数需在 Final 前补齐EVP_DecryptFinal 返回 0密钥/IV 不对或密文被篡改导致的 PKCS7 校验失败对比头部的盐值和 IV 是否一致fwrite 返回短写磁盘满或权限不足检查 ferror(out) 和磁盘剩余空间PKCS5_PBKDF2_HMAC 返回 0迭代次数传入 0 或密钥长度非法确认迭代次数为正数输出缓冲区容量不小于 keyiv5.3 用命令行全链路验证加解密写完全部代码用一个最小的 shell 命令序列验证加密-解密的正确性# 生成 1MB 随机测试文件 dd if/dev/urandom ofplain.bin bs1M count1 # 编译你的程序后执行加密 ./aes_tool -e -i plain.bin -o cipher.bin -p test-pass # 解密回明文 ./aes_tool -d -i cipher.bin -o decrypted.bin -p test-pass # 对比原始明文和解密结果 cmp plain.bin decrypted.bin echo PASScmp输出为空且返回码为 0 则说明文件完全一致。这一步验证的是整个加密-解密回路包括 PKCS7 填充和截断逻辑。建议再补一个破坏测试用 dd 随便改密文的中间一个字节再解密密文预期是 EVP_DecryptFinal 报错或输出乱码——这个测试验证的正是 4.2 节里提到的完整性校验如果你还想测试 GCM 或 HMAC 的追加逻辑可以把篡改点放在认证标签上解出来的结果不会一样。5.4 调试时打印关键中间值文件加密 bug 里最高频的问题就是填充长度算错。调试时我习惯在代码里加打印看加密前后的长度变化printf(orig_size%ld, pad_len%d, padded_len%ld\n, fsize, pad_len, padded_len); printf(cipher_len%d, last_byte0x%02x\n, outlen tmplen, cipher_buf[outlen tmplen - 1]);对照关系cipher_len 应该等于 padded_len解密时 last_byte 对应填充长度值解密截断后文件应该回到 orig_size。如果发现 cipher_len 比 padded_len 长了 16 字节说明你的缓冲区申请或 Update 调用里多了一次块加密回头查是不是把 Final 的输出和 Update 重叠计算了。本文还有配套的精品资源点击获取