ID TECH
全部技术文章

技术文章

如何计算校验和

各类校验和在数据通信协议中被广泛使用,使消息的接收方能够快速、便捷地判断数据在传输过程中是否可能发生了损坏。例如,若你将一条消息中所有字节相加,得到(忽略溢出)的总和为 96,然后在发送前将该数值附加到消息末尾,接收方便可对消息的前 N – 1 个字节重复同样的求和运算,并将结果与最后一个字节进行比较,看是否等于 96。若相等,接收方就可以推断该消息在传输过程中很可能(或者说大致上)未被篡改。

常见的校验和技术种类繁多。其中最常用的三种是:传统校验和、LRC(纵向冗余校验)以及 CRC(循环冗余校验)。严格来说,CRC 并非通常意义上的校验和,而是属于"线性同余生成器"家族的一种单向 散列

请注意,前文中我们提到这些数据完整性技术能告诉你数据"是否可能已损坏"。对于任意长度的数据而言,没有任何一种校验和技术在通用情形下能够做到 100% 万无一失。但有些技术确实优于其他技术。

下面让我们看看如何使用 JavaScript 计算 LRC、校验和及 CRC。

传统的 8 位校验和顾名思义:将输入中所有字节的数值相加,并丢弃任何溢出(来自进位操作)部分。用 JavaScript 实现如下:

该函数的输入应为类似 "48656C6C6F20776F726C6421" 的十六进制字符串(在本例中,这是 ASCII 字符串 "Hello world!" 的十六进制形式)。若将 "48656C6C6F20776F726C6421" 作为上述函数的输入,输出将是 "5d",即最终 8 位求和结果的十六进制值。

代码非常简单。我们首先(第 3 行)使用正则表达式 /../g 将十六进制输入解析为两位半字节的片段——这意味着查找符合"任意字符后跟任意字符"模式的子串(这就是两个点的含义),并在全局范围内进行匹配(这就是 'g' 的含义)。结果是一个数组 s,包含两位十六进制数值。

在第 5 行,我们进入一个循环(使用 forEach 迭代结构),将两位半字节的十六进制字符串转换为可用于运算的实际数字。在第 7 行执行实际的求和操作。请注意, sum 在底层本质上是一个 32 位整数,意味着我们的最终值可能远大于 255。循环结束后,我们需要确保将和值约束为 8 位。我们在第 9 行通过与 255 进行逻辑与运算来实现这一点。同时,使用 toString() 方法(参数为 16,表示我们希望在最终结果中使用 16 进制表示,即"基数为 16")将其转换回十六进制。

在第 10 行和第 11 行,我们需要检查最终十六进制值是否为两位半字节长度。对于小于 10 的值,JavaScript 的 toString(16) 操作会返回单个字符。在这种情况下,我们需要在结果前添加 '0'。

如果你想试运行这段代码,请将以上代码复制粘贴到 JS 控制台(在 Chrome 中,使用 Shift-Cmd-J 打开控制台),然后在底部(函数外)添加一行:CHECKSUM("48656C6C6F20776F726C6421")。按下回车键后,控制台应显示返回值 '5d'。

如何使用 LRC 计算校验和

纵向冗余校验(LRC)是 8 位校验和的一种变体,其唯一区别在于"求和"使用的是 XOR(异或)运算,而非数值加法。

在第 6 行,你可以看到原位异或运算符 (^=)。

由于 XOR 永不溢出,我们无需通过 AND 运算将最终结果约束为 8 位。只需检查长度是否为两位半字节,然后返回最终值即可。

如果你在控制台中使用前文所示的字符串运行代码,应得到结果 '21'。

对校验和与 LRC 的评析

校验和与 LRC 都不能视为可可靠地抵御消息损坏。例如,考虑原始消息("48656C6C6F20776F726C6421");假设我们将消息的最后两个字节从 6421 改为 6520。LRC 和校验和都保持不变!(我们只是在前面的某个字节中把某一位置 1,并在后面的字节中将相同位置的位置 0,从而在校验和计算时两处变化互相抵消。)

同样,考虑一下若你将消息反向排列(即按字节倒序,使其以 21 开头并以 48 结尾)会发生什么。同样,LRC 和校验和与原消息保持一致。这是因为 XOR 和加法都满足交换律。A + B 总是等于 B + A。

此外,请考虑这样一个事实:由于 8 位 LRC 或校验和只能有 256 种不同的值,因此任何一条消息与随机选取的另一条消息产生完全相同的 LRC(或校验和)的概率为 1/256。

通常情况下,"欺骗"校验和与 LRC 算法非常容易,因此它们在消息完整性检查方面的可靠性其实并不高。

所幸,确实存在比 LRC 或校验和更优秀的完整性检查算法,但代价是更高的计算开销。

如何使用 CRC 计算校验和

当完整性检查至关重要时,一般而言需要使用某种不满足交换律的哈希算法。这通常意味着采用诸如 SHA-1 或 MD5 之类的加密哈希算法,但它们计算开销较大,在许多场景下可视为"过度设计"。

在计算开销和可靠性之间取得良好平衡的方案是循环冗余校验(CRC),它有多种版本,不过下文介绍的 16 位 CRC 对于短消息(最大约 4 KB)已足够(且相当流行)。

CRC 是一个有趣的话题,但篇幅所限,本文无法对其进行全面阐述。(可自行 Google 查阅。)从实用角度而言,可以说两字节 CRC 对数据中随机比特翻转具有非常良好的灵敏度,并且对于包含多处比特翻转的数据也不太可能产生误判。正因如此,加之它易于以硬件或软件方式实现,并能在极少内存中快速运行,你会发现它在多种数据通信环境中被使用,包括存储磁盘驱动器(其中磁盘错误通常通过 CRC 检测)、调制解调器以及小型电子设备(包括所有 ID TECH 的 ViVOpay 系列信用卡读卡器)。

以下 JavaScript 代码演示如何计算 16 位 CRC 值(以四个半字节的十六进制 ASCII 形式返回)。

CRC 实现的哈希算法可以用通俗语言描述如下:

  1. 将起始 crc 值设为 0xFFFF — 第 10 行
  2. 读取一个字节的输入数据(作为 8 位数值)— 第 13 行
  3. 将已有的 crc 值右移 8 位 — 第 14 行
  4. 将右移后的 crc 与输入字节进行异或运算 — 第 14 行
  5. 将所得值 j(仅取低 8 位)作为表偏移量,从名为 crcTable 的表中查找一个"替换字节"— 第 15 行
  6. crc 值左移 8 位,然后与"替换字节"进行异或运算 — 第 15 行
  7. 使用下一个输入字节,从第 13 行起重复以上操作
  8. 当所有输入都按此方式处理完成后,将结果与零做异或运算,并保留 crc 的低 16 位 — 第 17 行

我们使用一个小型的辅助函数,将最终的数字从整数转换为十六进制字符串:

如果你在浏览器的 JS 控制台中加载这两个函数(numToHex 和 CRC),并执行 CRC( "48656C6C6F20776F726C6421" ),那么对于我们的"Hello world!"输入数据,应当得到 CRC 值为 'BD22'。

作为练习,你可以尝试翻转输入中的某一位,看看输出会发生什么变化。例如,使用我们的"Hello world!"字符串作为输入,将数据的最后一个字节由 21 改为 20,CRC 就会变成 'AD03',与 'BD22' 毫无关联。若将最后两个字节改为 '6520',则 CRC 变为 '9E32'。(请记住,相同的改动并不会影响 LRC 或校验和。)

设想一个由十个零(空)字节组成的字符串。该字符串的 LRC 值为零,校验和显然也为零。但 CRC 却是 E139。

你应该不难确认,反转输入所得到的 CRC 与按原方向输入所得到的 CRC 完全不同。(这一点对 LRC 或校验和并不成立。)CRC 不具备交换律,因为在所有内容左移之前,CRC 的高 8 位会先与输入字节进行异或运算(其原理类似于 密码块链接 的工作方式,只不过此时"密码块"大小为 8 位)。

请注意,虽然 CRC 是一种单向哈希,但严格来说并非加密哈希,因为实际上可以相当容易地计算出一个"修正值",将其附加到数据后,便能使数据的某种特定篡改产生所期望的最终 CRC。(这一点对所谓的加密哈希并不成立,因为很难计算出一个能将被篡改的数据块反向修正为目标哈希的"修正因子"。)因此,CRC 适用于检测无意造成的数据损坏。

可用于监测数据包损坏的"完整性校验"算法有很多。在某些情况下,简单的校验和或 LRC 即可胜任。但若涉及大量数据,且对数据完整性有严格要求,那么几乎必须考虑采用 "线性同余生成器" 类型的哈希算法。CRC 系列算法经过充分调优和优化,能够提供良好的完整性识别能力,同时易于实现、运行迅速、内存占用低,因此在从硬盘到信用卡读卡器等各种数据校验场景中都极具吸引力。

想要进一步了解校验和?ID TECH 为您提供全方位支持!

立即开始!