校验码:只防意外,不防人

47 阅读 1850 字 · 约 7 分钟

你下载了一个文件,打开,发现数据是错的。

不是下错了版本,是传输过程中某个比特悄悄翻了个身。网线经过变电站旁边被干扰了一下,Wi-Fi 穿墙丢了几帧——数据从 A 传到 B,中间要经过线缆、无线电、磁盘、内存,每个环节都可能出错。

这就是校验要解决的问题。不是防人,是防意外。

校验和哈希长得很像:都是把一段数据压缩成一个短值,贴在原文后面。但目的完全不同——哈希防恶意碰撞,校验防传输错误。哈希的设计前提是「有人会想办法攻击你」,校验的设计前提是「没人攻击你,但物理世界不靠谱」。

这个区别,决定了它们走完全不同的路。

校验和哈希:一个防意外,一个防人


奇偶校验:太简单了

最早的校验,是电报时代的奇偶校验。一个字节八个位,末尾加一个校验位。约定:这一串里 1 的个数永远是偶数。发送前填好,收到后数一遍——对上了,数据没错;对不上,有人翻了。

但这个方案有个致命的盲区:只防单比特错误。一个位翻了,奇偶必变。两个位翻了,奇偶不变——总数还是偶数,校验位告诉你「没问题」,数据已经错了。

物理世界不会只翻一个位。电磁干扰是成片来的,一根线被干扰,上面跑的八个位随便翻几个。奇偶校验能发现第一个,发现不了第二个和第三个。

但它没退出历史。ECC 内存还在用这个思路——不只是发现错误,还能纠正。一个比特翻了,算出哪个位,自己翻回去。这才是它的归宿:不是通用数据传输,是内存制造商替操作系统在硬件层兜底。


校验和:加一遍,不够

奇偶校验太容易漏。校验和换了个思路:不数 1 的个数,把数据当数字,一段一段加起来,末尾贴和。

但这个算法有个更隐蔽的问题:和不变,不代表数据没变。

传两个数:100200,和是 300。传过去翻了——100 成了 199200 成了 101,和还是 300。一增一减,正好抵消。校验和告诉你「没问题」,数据已经面目全非了。

更坑的是:字节顺序错了,校验和也不变。100,200200,100,和一样,但程序读到的顺序全反了。

校验和的问题在于,它假设错误是独立发生的——一个字节翻不翻,跟邻居没关系。 但物理世界的错误经常是抱团来的:一段干扰同时掀翻好几个相邻字节。校验和管不了这个。


CRC:比加法聪明一点

CRC 换了个思路:不再做加法,做除法。

你传一串数据,把它看成一个很长的数字。拿它除一个固定的除数,余数就是 CRC 值。接收方用同一个除数再除一遍——余数对上了,数据没错;对不上,中间有人翻了。

为什么除法比加法强?因为除法有放大效应:改一个位,余数全变,不像加法那样能正负抵消。CRC-32 能抓到所有单比特错误、双比特错误、以及长度不超过 32 个比特的连续错误——这个覆盖范围,加法做不到。

但 CRC 强不强,看的是你选的除数。除数选得好,覆盖广;选得不好,漏一片。CRC 不是「即插即用」,是「选对了除数才够用」。

校验码的进化:越算越聪明


CRC 不是哈希,别拿来当哈希用

这是 CRC 最常见的踩坑:拿 CRC-32 当哈希函数用,做数据去重,做哈希分片。

CRC 的数学结构决定了:输入和输出之间有线性关系。哈希要求改一个位,输出不可预测地全变——这叫雪崩效应。CRC 没有雪崩效应,改一个位,输出变,但变化方式是可预测的。

这意味着,如果有人想攻击你,给一个文件的 CRC 值,他就能算出一个不同的文件也有同样的 CRC——不用暴力试,直接算。故意构造碰撞,CRC 拦不住。

CRC 设计出来是防传输错误的,不是防恶意攻击的。你用 CRC 做哈希分片,攻击者可以构造一批文件全落到同一个片里——分片白分了。这不是 CRC 的 bug,是它不该出现在这个场景。

校验和哈希的边界:校验只管「传输中比特有没有翻」,哈希管「有没有人故意构造两个东西指向同一个输出」。碰错了场景,工具不背锅,选的人背。


怎么选

  • 你只想快速验证文件完整性,不防恶意——CRC-32。网络传输、ZIP 压缩包、TCP/IP 协议栈里都在用它。硬件支持,生态全认。别拿来当哈希,别拿来防篡改。
  • 你要防篡改、防恶意破坏——别用校验,用哈希。SHA-256 才是这个场景的答案。CRC 没有雪崩效应,攻击者可推算。
  • 你要防传输错误 + 纠错——Reed-Solomon 码、Turbo 码、LDPC 码。这些是校验的下一代:不仅发现错了,还能把错的改回来。二维码扫花了能读、CD 划了能放——不是校验没发现错,是纠错码替你修了。

大部分场景选 CRC-32。它用了三十年,够用。但永远记得:CRC 前面的 C 是 Cyclic(循环),不是 Cryptographic(密码学)。 别把它放进任何攻击者可能参与的地方。

校验是物理世界的兜底——它不防人,只防传输意外。拿 CRC 当哈希用,就是给攻击者留了个后门。