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加密/解密函数。它一次读取一个字节,与密钥异或后写回。注意,这里我用了freadfwrite,而不是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;
}
注意:XOR加密是对称加密,密钥必须妥善保管。我曾经见过有人把密钥写在注释里,结果代码上传到GitHub……嗯,那画面太美我不敢看。

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;
}
关键点:初始值取0xFFFFFFFF,最后再异或0xFFFFFFFF,这是CRC-32的标准做法。别问我为什么,这是前辈们定下的规矩,照做就行。

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,我们就可以构建一套完整的文件完整性检查方案了。说白了,就是三步走:

  1. 写入时:先加密数据,再计算CRC,把CRC值附加到文件末尾(或单独存一个校验文件)。
  2. 读取时:先计算当前文件的CRC,与之前保存的CRC对比。如果一致,再解密;如果不一致,说明文件被改了。
  3. 报警: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;
}
我曾经踩过的坑:一开始我把CRC放在文件开头,结果读取时先读CRC,再读数据,逻辑上没问题。但后来发现,如果文件只写了前半部分就断电了,CRC还在,数据却丢了——校验通过了,但数据不完整。所以,CRC一定要放在文件末尾,这样只有数据全部写完,CRC才会被写入。

20.4 知识体系总览

下面这张图总结了本章的核心逻辑:加密和校验如何协同工作。

文件加密与校验知识体系 写入流程 ① 原始数据 ② XOR加密 ③ 计算CRC-32 ④ 写入文件 + CRC 读取流程 ① 读取文件 + CRC ② 重新计算CRC ③ CRC是否匹配? 匹配 ④ XOR解密 → 输出数据 不匹配 报错 存储/传输

你看,整个流程其实不复杂。写入时先加密再校验,读取时先校验再解密。这个顺序不能乱——如果先解密再校验,万一文件被篡改,解密出来的就是一堆垃圾数据,CRC校验反而可能通过(因为垃圾数据也有自己的CRC)。

我的建议:在实际项目中,不要把CRC和加密数据放在同一个文件里。我习惯的做法是:数据文件叫 data.bin,校验文件叫 data.bin.crc。这样即使校验文件丢了,数据本身还能用(只是无法验证完整性)。如果混在一起,文件结构就绑死了。

好了,这一章的内容就到这里。加密和校验是文件操作中容易被忽视但极其重要的环节。你想想看,如果你的设备存储着用户的隐私数据,或者控制着关键设备的配置参数,没有加密和校验,那简直就是裸奔。希望今天的内容能帮你把文件安全这块短板补上。

公众号:蓝海资料掘金营,微信deep3321