加密与校验:MD5/SHA1计算、CRC32校验

说实话,文件完整性检查这事儿,我早年做嵌入式开发时吃过不少亏。那时候传输固件,偶尔会出现几个比特的翻转,设备就莫名其妙死机了。后来我养成了一个习惯——不管传什么文件,先算个校验值再说。

今天咱们就聊聊怎么用标准库实现MD5、SHA1和CRC32这三种常见的校验方式。嗯,说白了就是给文件算个“指纹”,看看它有没有被篡改或者损坏。

为什么需要文件完整性检查?

你想想看,文件在网络传输、磁盘存储过程中,可能因为各种原因出现损坏。比如:

  • 网络丢包导致数据不完整
  • 磁盘坏道导致读取错误
  • 恶意篡改(比如替换了你的可执行文件)

这时候,我们就需要一种机制来验证文件是否“原封不动”。

核心思路:对原始文件计算一个固定长度的哈希值(比如32位的CRC、128位的MD5、160位的SHA1),然后传输或存储这个哈希值。接收方重新计算文件的哈希值,如果一致,说明文件完好无损。

三种校验算法的对比

我个人习惯这样选择:

  • CRC32:速度快,适合检测随机错误(比如传输误码)。但不能防恶意篡改。
  • MD5:曾经很流行,但现在已被证明存在碰撞漏洞。不过做普通完整性检查还是够用的。
  • SHA1:比MD5更安全,但计算稍慢。目前仍是很多场景的标配。
算法 输出长度 安全性 速度 典型用途
CRC32 32位 低(不可防篡改) 极快 网络传输校验、压缩文件
MD5 128位 中(有碰撞风险) 文件完整性、密码存储(已不推荐)
SHA1 160位 较高(理论上可碰撞) 中等 Git版本控制、数字签名

CRC32的实现

CRC32其实不依赖标准库的加密函数,它本质上是多项式除法。我当年第一次手写CRC32时,对着查表法看了半天才明白——说白了就是预先算好256个余数,然后逐字节查表。

#include <stdio.h>
#include <stdint.h>

// CRC32查表法实现
static uint32_t crc32_table[256];

void crc32_init() {
    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;
        }
        crc32_table[i] = crc;
    }
}

uint32_t crc32_calc(const unsigned char *buf, size_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (size_t i = 0; i < len; i++) {
        uint8_t index = (crc ^ buf[i]) & 0xFF;
        crc = (crc >> 8) ^ crc32_table[index];
    }
    return crc ^ 0xFFFFFFFF;
}

int main() {
    crc32_init();
    unsigned char data[] = "Hello, file integrity check!";
    uint32_t crc = crc32_calc(data, sizeof(data) - 1);
    printf("CRC32: 0x%08X\n", crc);
    return 0;
}

小技巧:实际项目中,我一般把CRC32表放在全局静态区,只初始化一次。如果频繁计算小文件,查表法比逐位计算快几十倍。

MD5与SHA1的实现

MD5和SHA1的实现比较复杂,标准C库并没有直接提供。不过,很多系统自带了OpenSSL或者libcrypto库。我个人建议直接用这些现成的库,别自己手写——我曾经手写过一次MD5,调试了三天才发现一个字节序问题。

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>
#include <openssl/sha.h>

void print_hash(const unsigned char *hash, int len) {
    for (int i = 0; i < len; i++)
        printf("%02x", hash[i]);
    printf("\n");
}

int main() {
    const char *msg = "Hello, file integrity check!";
    unsigned char md5_out[MD5_DIGEST_LENGTH];
    unsigned char sha1_out[SHA_DIGEST_LENGTH];

    // MD5
    MD5((unsigned char*)msg, strlen(msg), md5_out);
    printf("MD5:  ");
    print_hash(md5_out, MD5_DIGEST_LENGTH);

    // SHA1
    SHA1((unsigned char*)msg, strlen(msg), sha1_out);
    printf("SHA1: ");
    print_hash(sha1_out, SHA_DIGEST_LENGTH);

    return 0;
}

注意:编译时需要链接OpenSSL库:-lssl -lcrypto。如果你在Windows上开发,可以考虑用BCrypt或CNG API,或者直接集成一个轻量级的哈希库。

基于标准库实现文件完整性检查

好了,现在我们把上面的知识串起来,写一个完整的文件校验工具。这个工具可以计算文件的CRC32、MD5和SHA1值,然后跟预期的值对比。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

// 假设上面已经实现了crc32_init和crc32_calc
// 以及OpenSSL的MD5/SHA1函数

int check_file_integrity(const char *filename,
                         const char *expected_md5,
                         const char *expected_sha1,
                         uint32_t expected_crc32) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) {
        perror("fopen");
        return -1;
    }

    // 读取文件到内存(小文件直接读,大文件建议分块)
    fseek(fp, 0, SEEK_END);
    long fsize = ftell(fp);
    rewind(fp);

    unsigned char *buf = (unsigned char*)malloc(fsize);
    if (!buf) {
        fclose(fp);
        return -1;
    }
    fread(buf, 1, fsize, fp);
    fclose(fp);

    // 计算CRC32
    crc32_init();
    uint32_t crc = crc32_calc(buf, fsize);
    printf("CRC32: 0x%08X (expected: 0x%08X)\n", crc, expected_crc32);
    if (crc != expected_crc32) {
        printf("CRC32 mismatch!\n");
        free(buf);
        return 1;
    }

    // 计算MD5
    unsigned char md5_out[MD5_DIGEST_LENGTH];
    MD5(buf, fsize, md5_out);
    char md5_str[33];
    for (int i = 0; i < 16; i++)
        sprintf(md5_str + i*2, "%02x", md5_out[i]);
    md5_str[32] = '\0';
    printf("MD5:   %s (expected: %s)\n", md5_str, expected_md5);
    if (strcmp(md5_str, expected_md5) != 0) {
        printf("MD5 mismatch!\n");
        free(buf);
        return 1;
    }

    // 计算SHA1
    unsigned char sha1_out[SHA_DIGEST_LENGTH];
    SHA1(buf, fsize, sha1_out);
    char sha1_str[41];
    for (int i = 0; i < 20; i++)
        sprintf(sha1_str + i*2, "%02x", sha1_out[i]);
    sha1_str[40] = '\0';
    printf("SHA1:  %s (expected: %s)\n", sha1_str, expected_sha1);
    if (strcmp(sha1_str, expected_sha1) != 0) {
        printf("SHA1 mismatch!\n");
        free(buf);
        return 1;
    }

    free(buf);
    printf("All checks passed! File is intact.\n");
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc < 5) {
        fprintf(stderr, "Usage: %s <file> <md5> <sha1> <crc32_hex>\n", argv[0]);
        return 1;
    }
    uint32_t expected_crc32 = (uint32_t)strtoul(argv[4], NULL, 16);
    return check_file_integrity(argv[1], argv[2], argv[3], expected_crc32);
}

避坑指南:我曾经在处理大文件时,直接把整个文件读到内存里,结果一个2GB的文件把服务器内存撑爆了。后来我改用分块读取的方式——每次读64KB,更新哈希上下文,最后再finalize。这样内存占用就稳定了。

知识体系流程图

下面这张图展示了文件完整性检查的核心逻辑:

文件完整性检查流程 原始文件 计算 CRC32 / MD5 / SHA1 哈希值 存储哈希值(如 .md5 / .sha1 文件) 重新计算并对比哈希值

实际项目中的建议

嗯,这里有几个我踩过的坑,分享给你:

  • 大文件分块处理:别一次性读入内存。用fread循环读,每次更新哈希上下文。
  • 二进制模式打开文件:在Windows上,如果不加"rb",文本模式会做换行符转换,导致哈希值不一致。
  • 多线程计算:如果文件特别大,可以考虑用多线程分块计算CRC32(因为CRC32可以分段合并),但MD5/SHA1不行,它们是串行的。

总结一下:CRC32适合快速检测随机错误,MD5和SHA1适合防篡改。实际项目中,我经常同时使用CRC32和SHA1——CRC32做快速校验,SHA1做最终确认。这样既保证了速度,又保证了安全性。

专注资料整理