加密与校验: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。这样内存占用就稳定了。
知识体系流程图
下面这张图展示了文件完整性检查的核心逻辑:
实际项目中的建议
嗯,这里有几个我踩过的坑,分享给你:
- 大文件分块处理:别一次性读入内存。用fread循环读,每次更新哈希上下文。
- 二进制模式打开文件:在Windows上,如果不加"rb",文本模式会做换行符转换,导致哈希值不一致。
- 多线程计算:如果文件特别大,可以考虑用多线程分块计算CRC32(因为CRC32可以分段合并),但MD5/SHA1不行,它们是串行的。
总结一下:CRC32适合快速检测随机错误,MD5和SHA1适合防篡改。实际项目中,我经常同时使用CRC32和SHA1——CRC32做快速校验,SHA1做最终确认。这样既保证了速度,又保证了安全性。