20、文件加密与校验:简单文件加密算法、CRC校验实现、文件完整性检查
说到文件加密和校验,我脑子里立刻浮现出几年前的一个项目。当时我们做一款嵌入式数据采集器,SD卡里存着关键的环境监测数据。有一次客户反馈说,读出来的数据偶尔会“跳变”——明明温度传感器没动,记录值却从25度蹦到了80度。排查到最后,发现是文件在写入过程中受到了电磁干扰,CRC校验没做,数据就稀里糊涂存进去了。嗯,从那以后,我所有涉及文件存储的项目,加密和校验都是标配。
这一章,咱们就聊聊文件加密和校验那些事。说白了,加密是为了防窥探,校验是为了防篡改或传输错误。两者结合,你的文件才算真正“安全”。
20.1 简单文件加密算法
加密算法有很多,从简单的XOR到复杂的AES。在嵌入式环境里,资源有限,很多时候我们不需要军事级加密,只需要让普通用户无法直接读懂文件内容。这时候,XOR加密就是个不错的选择。
20.1.1 XOR加密原理
XOR加密的核心思想:用同一个密钥对数据进行异或操作。加密时异或一次,解密时再异或一次,数据就还原了。简单吧?
举个例子:
明文: 0xAB
密钥: 0x55
加密: 0xAB ^ 0x55 = 0xFE
解密: 0xFE ^ 0x55 = 0xAB
你看,只要密钥不变,加密和解密就是同一个操作。我在项目中经常用这个特性,省去了写两个函数的麻烦。
20.1.2 文件XOR加密实现
下面是一个完整的文件XOR加密/解密函数。它一次读取一个字节,与密钥异或后写回。注意,这里我用了fread和fwrite,而不是fgetc/fputc,因为块操作效率更高。
#include <stdio.h>
#include <stdint.h>
#define BUFFER_SIZE 1024
/**
* @brief 对文件进行XOR加密/解密(原地操作)
* @param filename 文件名
* @param key 密钥字节
* @return 0成功,-1失败
*/
int file_xor_crypt(const char *filename, uint8_t key) {
FILE *fp = fopen(filename, "rb+");
if (!fp) {
perror("打开文件失败");
return -1;
}
uint8_t buffer[BUFFER_SIZE];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, fp)) > 0) {
// 对缓冲区每个字节进行XOR
for (size_t i = 0; i < bytes_read; i++) {
buffer[i] ^= key;
}
// 将文件指针回退到刚才读取的位置
fseek(fp, -(long)bytes_read, SEEK_CUR);
fwrite(buffer, 1, bytes_read, fp);
fflush(fp); // 确保写入磁盘
}
fclose(fp);
return 0;
}
20.1.3 增强型XOR——多字节密钥
单字节密钥太容易被暴力破解了。你想想看,总共256种可能,穷举一下分分钟的事。我建议用多字节密钥,比如16字节或32字节,循环使用。
/**
* @brief 多字节密钥XOR加密
* @param filename 文件名
* @param key 密钥数组
* @param key_len 密钥长度
*/
int file_xor_crypt_multi(const char *filename,
const uint8_t *key,
size_t key_len) {
FILE *fp = fopen(filename, "rb+");
if (!fp) return -1;
uint8_t buffer[BUFFER_SIZE];
size_t bytes_read;
size_t key_index = 0;
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, fp)) > 0) {
for (size_t i = 0; i < bytes_read; i++) {
buffer[i] ^= key[key_index % key_len];
key_index++;
}
fseek(fp, -(long)bytes_read, SEEK_CUR);
fwrite(buffer, 1, bytes_read, fp);
fflush(fp);
}
fclose(fp);
return 0;
}
20.2 CRC校验实现
加密解决了“别人看不懂”的问题,但怎么保证文件没被改过?或者传输过程中没丢包?这时候就需要校验了。CRC(循环冗余校验)是嵌入式领域最常用的校验方式之一。
20.2.1 CRC原理简述
CRC的原理说起来有点绕,简单理解就是:把数据看作一个巨大的二进制数,然后除以一个固定的“生成多项式”,余数就是CRC值。接收方用同样的多项式计算,如果余数相同,说明数据没变。
常用的CRC有:
| 类型 | 多项式 | 校验位 | 应用场景 |
|---|---|---|---|
| CRC-8 | 0x07 | 8位 | 小数据包、1-Wire总线 |
| CRC-16 | 0x8005 | 16位 | Modbus、文件校验 |
| CRC-32 | 0x04C11DB7 | 32位 | ZIP、以太网、文件完整性 |
我个人最常用CRC-32,碰撞概率低,适合做文件完整性检查。
20.2.2 CRC-32查表法实现
直接按位计算CRC太慢了。实际项目中都用查表法——提前算好256个表项,运行时直接查表,速度能快几十倍。
#include <stdint.h>
// 生成CRC-32表
void crc32_generate_table(uint32_t table[256]) {
for (uint32_t i = 0; i < 256; i++) {
uint32_t crc = i;
for (int j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc >>= 1;
}
table[i] = crc;
}
}
// 计算缓冲区CRC-32
uint32_t crc32_compute(const uint8_t *data,
size_t len,
const uint32_t table[256]) {
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < len; i++) {
uint8_t index = (crc ^ data[i]) & 0xFF;
crc = (crc >> 8) ^ table[index];
}
return crc ^ 0xFFFFFFFF;
}
20.2.3 文件CRC计算
有了上面的函数,计算整个文件的CRC就简单了:
uint32_t file_crc32(const char *filename) {
uint32_t table[256];
crc32_generate_table(table);
FILE *fp = fopen(filename, "rb");
if (!fp) return 0;
uint8_t buffer[BUFFER_SIZE];
size_t bytes_read;
uint32_t crc = 0xFFFFFFFF;
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, fp)) > 0) {
for (size_t i = 0; i < bytes_read; i++) {
uint8_t index = (crc ^ buffer[i]) & 0xFF;
crc = (crc >> 8) ^ table[index];
}
}
fclose(fp);
return crc ^ 0xFFFFFFFF;
}
20.3 文件完整性检查
有了加密和CRC,我们就可以构建一套完整的文件完整性检查方案了。说白了,就是三步走:
- 写入时:先加密数据,再计算CRC,把CRC值附加到文件末尾(或单独存一个校验文件)。
- 读取时:先计算当前文件的CRC,与之前保存的CRC对比。如果一致,再解密;如果不一致,说明文件被改了。
- 报警:CRC不匹配时,不要尝试解密,直接报错。
20.3.1 写入带校验的加密文件
int write_encrypted_with_crc(const char *filename,
const uint8_t *data,
size_t data_len,
const uint8_t *key,
size_t key_len) {
// 1. 先加密数据(原地加密)
uint8_t *encrypted = malloc(data_len);
if (!encrypted) return -1;
memcpy(encrypted, data, data_len);
for (size_t i = 0; i < data_len; i++) {
encrypted[i] ^= key[i % key_len];
}
// 2. 计算加密后数据的CRC
uint32_t table[256];
crc32_generate_table(table);
uint32_t crc = crc32_compute(encrypted, data_len, table);
// 3. 写入文件:加密数据 + CRC值
FILE *fp = fopen(filename, "wb");
if (!fp) {
free(encrypted);
return -1;
}
fwrite(encrypted, 1, data_len, fp);
fwrite(&crc, sizeof(crc), 1, fp);
fclose(fp);
free(encrypted);
return 0;
}
20.3.2 读取并校验
int read_encrypted_with_crc(const char *filename,
uint8_t *out_data,
size_t *out_len,
const uint8_t *key,
size_t key_len) {
FILE *fp = fopen(filename, "rb");
if (!fp) return -1;
// 获取文件大小
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
rewind(fp);
// 文件至少要有4字节的CRC
if (file_size < (long)sizeof(uint32_t)) {
fclose(fp);
return -1;
}
size_t data_size = file_size - sizeof(uint32_t);
uint8_t *buffer = malloc(data_size);
if (!buffer) {
fclose(fp);
return -1;
}
// 读取加密数据和CRC
fread(buffer, 1, data_size, fp);
uint32_t stored_crc;
fread(&stored_crc, sizeof(stored_crc), 1, fp);
fclose(fp);
// 校验CRC
uint32_t table[256];
crc32_generate_table(table);
uint32_t computed_crc = crc32_compute(buffer, data_size, table);
if (computed_crc != stored_crc) {
// CRC不匹配!文件可能被篡改
free(buffer);
return -2; // 自定义错误码
}
// CRC匹配,解密数据
for (size_t i = 0; i < data_size; i++) {
out_data[i] = buffer[i] ^ key[i % key_len];
}
*out_len = data_size;
free(buffer);
return 0;
}
20.4 知识体系总览
下面这张图总结了本章的核心逻辑:加密和校验如何协同工作。
你看,整个流程其实不复杂。写入时先加密再校验,读取时先校验再解密。这个顺序不能乱——如果先解密再校验,万一文件被篡改,解密出来的就是一堆垃圾数据,CRC校验反而可能通过(因为垃圾数据也有自己的CRC)。
data.bin,校验文件叫 data.bin.crc。这样即使校验文件丢了,数据本身还能用(只是无法验证完整性)。如果混在一起,文件结构就绑死了。
好了,这一章的内容就到这里。加密和校验是文件操作中容易被忽视但极其重要的环节。你想想看,如果你的设备存储着用户的隐私数据,或者控制着关键设备的配置参数,没有加密和校验,那简直就是裸奔。希望今天的内容能帮你把文件安全这块短板补上。