【数据加解密】CRC检验算法的原理及实现

CRC检验算法的原理及实现

循环冗余校验(英语:Cyclic redundancy check,通称“CRC”)是一种根据网络数据包或计算机文件等数据产生简短固定位数校验码的一种散列函数,主要用来检测或校验数据传输或者保存后可能出现的错误。生成的数字在传输或者存储之前计算出来并且附加到数据后面,然后接收方进行检验确定数据是否发生变化。一般来说,循环冗余校验的值都是32位的整数。由于本函数易于用二进制的计算机硬件使用、容易进行数学分析并且尤其善于检测传输通道干扰引起的错误,因此获得广泛应用。此方法是由W. Wesley Peterson于1961年发表[1]。

简介[编辑]


CRC是基于有限域**GF(2)**的校验和算法。它的机制是 将原始信息数据流被解释为一个长二进制流,这个二进制流被另一个固定的预定义(短)的二进制数所除,这个除法的余数部分就是检验和。

这里的二进制数(除数和被除数)并不是正常的整数值,而是将原始数据的二进制的多个位作为数。

比如,原始数据 0x25 = 0010 0101 可以解释成生成多项式 0x^7 +0x6+1*5+0*4+0*x3+1x2+0*x1+1x^0

有限域GF(2),2会变成0,因为对系数的加法运算都会再取2的模数 2 Mod 2=0 (商=1)
例如:
image.png
乘法也是类似的:
image.png
我们同样可以对生成多项式作除法并且得到商和余数。例如,如果我们用_x_ + x + x_除以_x + 1。我们会得到:
image.png

也就是说:
image.png

等价于:
image.png

这里除法得到了商_x_ + 1和余数-1,因为是奇数所以最后一位是1。
字符串中的每一位其实就对应了这样类型的生成多项式的系数。为了得到CRC,我们首先将其乘以image.png,这里 n 是一个固定生成多项式的**阶数,然后再将其除以这个固定的生成多项式,余数的系数就是CRC。
在上面的等式中,x^2 +x+1表示了本来的信息位是111, x+1是所谓的
钥匙**,而余数1(也就是x^0就是CRC. key的最高次为1,所以我们将原来的信息乘上x^1来得到 x3+x2+x,也可视为原来的信息位补1个零成为1110

一般来说,其形式为:
image.png
这里M(x)是原始的信息生成多项式。K(x)是 n 阶的“钥匙”生成多项式。 image.png表示了将原始信息后面加上n个0。R(x)是余数,即时CRC“检验和”。

在通信中,发送者在原始的信息数据M后附加上n位的R(替换本来附加的0)再发送。接受者受到M和R后,检查image.png是否能被K(x)整除。如果是,那么接受者认为该心事正确的。
值得注意的是image.png就是发送者所想要发送的数据。这个串又叫做 codeword.

CRCs经常被叫做“校验和”,但是这样的说法严格来说并不是准确的,因为技术上来说,校验“和”是通过加法来计算的,而不是CRC这里的除法。

“错误纠正编码”(Error–Correcting Codes,简称ECC)常常和CRCs紧密相关,其语序纠正在传输过程中所产生的错误。这些编码方式常常和数学原理紧密相关。例如常见于通信或信息传递上BCH码、前向错误更正、Error detection and correction等。

二进制的除法不同于整数除法。CRC计算的底层是基于XOR(异或)操作的,因为在二进制中,XOR操作等于不借位的减法。

  • 初始时输入的原始数据(以二进制刘标识)
  • 被除数是是由CRC算法定义预先定义的固定生成多项式. 比如 CRC-n 使用一个固定的(n+1)位二进制数
  • CRC检验和的值 = 除数%被除数的余数

在做除法之前,需要要在信息数据之后先加上n个0.

Example:
源数据是 0xC2 =b11000010.
CRC-8生成多项式(被除数)假设为 b100011101
被除数大小为9bits(因此这是一个 CTC-8检验函数),因此在源数据后添加8位0 bits数据
我们将被除数和源数据对齐,手工进行除法(二进制异或)运算
             H
1100001000000000
100011101

010011001
  100011101
  -----------
  000101111
        100011101       (*)
        ----------
  001100101
      100011101
      ----------
      010001001
        100011101
  ----------
        000001111   =0x0F

求得 CRC检验值为 0x0F

示例要点:

  • 在手动进行除法的每一步中,除数的前导“1”总是被除数的第一个“1”对齐。这意味着每步计算可能并不只是向右移动1位,有时也会移动多位(如 line (*))
  • 计算过程中如果除数将实际输入数据的每个位归零(不包含填充字节,即最后一位为H所示位置),则算法将停。在最后一步中,列H和所有先前的列都包含0,因此算法停止

CRC 验证

在数据传输过程中,CRC检验值随源数据一起发送给接受端,数据接收端接收到数据后可以使用相同的方式对源数据计算得到CRC 并验证计算得到的CRC值和发送过来的CRC值是否一致。另一种更常见的做法是将CRC值附加到实际的数据上,接受端接受到整个数据后再次计算CRC值,如果值为0则说明数据传输过程中应该没有发生错误。

Example verfication
源数据加上CRC值为 b1100001000001111 ,我们使用的是 8bit的CRC 因此CRC值同样为8bit (00001111)。检验和过程使用的生成多项式值(被除数)是预先定义的,所以在接收端可以直接获取到。

1100001000001111
100011101

0100110010
  100011101
  ----------
  000101111001
        100011101
        ----------
        00110010011
            100011101
           -----------
            010001110
              10001110
              ---------
             00000000   ->       余数为0,验证数据应该是完整的

CRC移位计算原理

我们已经手动验证了CRC算法,但是具体如何变成实现呢? 计算的源数据可能很长(超过1个字节),因此不能直接通过"Inpudate data % generator polynomial"就得到结果。计算过程必须一步步来(移位计算),这里使用了移位寄存器的概念

设定移位寄存器有固定的宽度,每次将寄存器移动一个位时,及删除最右边或者最左边的位,在空出的位置上移入一个新的位数据(来自源数据)。CRC使用左移寄存器:当移位时,最高有效位弹出,MSB-1的位向左移动一个位置到MSB,位置MSB-2的位移动到MSB-1的位置,以此类推。输入流的下个位插入移动到因为左移位而空出来的位置(最右的位)

image.png

CRC 使用左移位的具体就算过程如下

  1. 初始化“寄存器”为0
  2. 原始数据流依次移动到“寄存器”中,每次移动1 bit,移动后如果最高位(MSB)为1,则执行执行一次 XOR
  3. 如果源数据的所有位都处理了,则“寄存器”中的值就是 CRC value

示例

源数据 = 0xC2=b11000010( 添加8bit的0 后: b1100001000000000),生成多项式= b100011101

  1. CRC-8 寄存器初始化

— --- — --- — --- — ---
       |  0 |  0 | 0  | 0  | 0  | 0  | 0 | 0 |  <-- b1100001000000000
        — --- — --- — --- — ---

  1. 寄存器左移一位 ,MSB为0 ,因此不做任何操作,数据源最高位移动到空出的LSB位

— --- — --- — --- — ---
       |  0 |  0 | 0  | 0  | 0  | 0  | 0 | 1 |  <-- b1000010000000000
        — --- — --- — --- — ---

  1. 重复步骤,直到MSB为1,此时数据如下

— --- — --- — --- — ---
       |  1|  1 | 0  | 0  | 0  | 0  | 1 | 0 |  <-- b00000000
        — --- — --- — --- — ---

  1. 再次左移寄存器时, MSB 1将被移除,因此需要执行XOR运算

— --- — --- — --- — ---
 1 <-|  1|  0 | 0  | 0  | 0  | 1  | 0 | 0 |  <-- b0000000
        — --- — --- — --- — --- 
执行 b110000100^b100011101=b010011001  最高位被丢弃,因此新的CRC寄存器的值为 b010011001
        — --- — --- — --- — ---
       |  1 |  0 | 0  | 1 | 1  | 0  | 0  | 1 |  <-- b0000000
        — --- — --- — --- — ---

  1. 左移寄存器, MSB 1被移除,执行:b100110010 ^ b 100011101 = b000101111

— --- — --- — --- — ---
       |  0 |  0 | 1  | 0 | 1  | 1  | 1  | 1 |  <-- b000000
        — --- — --- — --- — ---

  1. 重复以上步骤直到 源数据全部被移入寄存器

— --- — --- — --- — ---
       |  0 |  0 | 0  | 0 | 1  | 1  | 1  | 1 |  <--
        — --- — --- — --- — --- 
此时寄存器中的值即为计算得出的CRC检验值 0x0F.


代码实现示例

以下是一个Java实现的CRC-8检验和,CRC-8检验和应该使用一个9位的生成项,但是在算法实现中跟踪计算这样一个未对齐(8位)的数据比较麻烦。幸运的是,由于多项式的最高位总是1,而且在运算过程中总是将除数的第一个1和被除数的第一个1对齐,并且进行异或后它的结果总是0,因此在算法时间中我们可以丢弃最高位的1。因此在算法中我们可以直接使用 00011101 =0x1D作为生成项

public class CRC_8_DEMO {

    public static byte Compute_CRC8_Simple_OneByte_ShiftReg(byte byteVal) {
        byte generator = 0x1D;
        byte crc = 0; /* 初始化CRC寄存器为0 */
        /*添加8位0即0x00在数据的末尾 */
        byte[] inputstream = new byte[]{byteVal, 0x00};
        /* 循环字节输入流,每次处理一个字节,每个字节的处理 从最高位每次向左移动1个位 */
        for (byte b : inputstream) {
            for (int i = 7; i >= 0; i--) {
                /* 检查 crc寄存器中最高位是否为1 */
                if ((crc & 0x80) != 0) {   /* 如果最高位为1,则左移一位*/
                    crc = (byte) (crc << 1);
                    /* shift in next bit of input stream:
                     * If it's 1, set LSB of crc to 1.
                     * If it's 0, set LSB of crc to 0. */
                    /* 从 b 获取下一位(最高位),如果为1,则填充crc的最低位为 1
                     * 如果最高位为1,则填充crc的最低位为 1
                     * 如果最高位为0,则填充crc的最低位为 0   */
                    crc = ((byte) (b & (1 << i)) != 0) ? (byte) (crc | 0x01) : (byte) (crc & 0xFE);
                    /* Perform the 'division' by XORing the crc register with the generator polynomial */
                    /* 通过^操作 执行二进制除法 */
                    crc = (byte) (crc ^ generator);
                } else {   /* MSB不为1,则crc寄存器 左移一位,并且字节b left shift移除最高位填充crc的 LSB */
                    crc = (byte) (crc << 1);
                    crc = ((byte) (b & (1 << i)) != 0) ? (byte) (crc | 0x01) : (byte) (crc & 0xFE);
                }
            }

        }
        return crc;
    }
}

输入流只有一个字节时 优化CRC-8 实现

从目前的CRC-8的实现上来看 还有一些复杂,那么能否进一步简化呢?

  • 最开始的8次 左移是无用的,因此 CRC 寄存器被初始化为0 因此,至少需要向左移动8次 ,CRC的首位才可能为1;因此我们可以直接使用 input stream 填充crc
  • 此时inputstream中只剩下8位0, 因为 在crc执行左移操作时 ,默认会在最低字节填充0,因此我们不必执行 shift in操作 (即把inpustream中的位移入crc)

优化后的单字节 CRC算法实现如下

public class CRC_8_DEMO {

    public static byte Compute_CRC8_Simple_OneByte(byte byteVal) {
        byte generator = 0x1D;

        byte crc = byteVal; /*直接使用输入字节替代, 省略掉了不必要的8次从输入字节移位到crc寄存器的操作*/

        for (int i = 0; i < 8; i++) {
            if ((crc & 0x80) != 0) {
                /* 最高有效位为1, 左移crc寄存器 并执行 XOR操作*/
                crc = (byte) ((crc << 1) ^ generator);
            } else { /* 最高有效位不为1,则只执行移位 */
                crc <<= 1;
            }
        }

        return crc;
    }
}

通用的 CRC8实现

上面 我们讨论并实现了只有一个字节时的CRC8的实现,那么如果输入是一个字节数组呢。回到最初的CRC8处理字节数组的函数 _Compute_CRC8_Simple_OneByte_ShiftReg() ,该函数可以很容易适配只有一个字节的情况,只要我们把一个字节固定为 0x00。 那么对于Compute_CRC8_Simple_OneByte()函数呢? 

两者的边界在于:如果一个字节已经被计算(处理)过了,那么另一个字节的处理如何整合到现有的计算过程呢?让我们看一个简单的例子(手动计算实例)


假设输入流字节为{0x01,0x02}, 多项式值为 0x1D
000000010000001000000000
             100011101
             -----------
             0000111110000
                     100011101
                     -----------
                     0111011010
                       100011101
                       -----------
                       0110001110
                         100011101
                       -----------
                         0100100110
                           100011101
                           ----------
                           0001110110  =0x76
                 


该算法一次处理一个字节码并且在当前字节完全处理完之前不会考虑下一个字节
回到 函数  Compute_CRC8_Simple_OneByte, crc的值设置为 0x01 ,输入流为 0000000100000000…我们看下只处理首个字节时的处理过程

0000000100000000
             100011101
             ----------
             000011101

比对下 包含第二个字节 0x02时的处理过程
000000010000001000000000
             100011101
             -----------
             000011111

我们可以发现第一个示例中只处理了首个字节,第二个示例同时处理了2个字节,如果将第一个示例中的CRC值(处理结果) 000011101 当做下一个字节,然后重新使用异或计算得出 000011101 ^ 00000010 = 000011111 。
因此,我们可以很容易拓展目前的算法以处理容易长度的输入字节数组

public class CRC_8_DEMO {


    public static byte Compute_CRC8_Simple(byte[] bytes) {
        byte generator = 0x1D;
        byte crc = 0; /* start with 0 so first byte can be 'xored' in */

        for (byte currByte : bytes) {
            crc ^= currByte; /* XOR-in the next input byte */

            for (int i = 0; i < 8; i++) {
                if ((crc & 0x80) != 0) {
                    crc = (byte) ((crc << 1) ^ generator);
                } else {
                    crc <<= 1;
                }
            }
        }

        return crc;
    }
}

CRC-8查表法实现

到目前为止,该算法的效率很低,因为它是逐位工作的。对于较大的输入数据,这可能非常慢。但是我们的CRC-8算法如何才能被加速呢?
我们知道除数总是当前的crc字节值——一个字节只能取256个不同的值。并且生成多项式(=除数)是固定的。为什么不通过固定的多项式预先计算每个可能的字节的除法并将这些结果存储在一个查找表中呢?显而易见因为对于相同的除数和被除数,余数总是相同的!然后输入流可以按字节处理,而不是按位处理。
让我们用我们常用的例子来手动演示这个过程:


假设输入流字节为{0x01,0x02}, 多项式值为 0x1D
1.初始化 crc = 0x00
2.异或下一个输入字节 0x00^0x01 =0x01
3.使用CRC-8计算 数据0x01的结果,从查找表中 查找异或0x01对应的结果:table[0x01]=crc= 0x1D
4.异或处理下一个输入字节byte 0x02:0x1D^0x02 = 0x1F
5.使用CRC-8计算 数据0x01F的结果: table[0x1F] = crc= 0x76


至于表的数据,我们可以根据定义的 多项式值提前生成,比如我们生成一个CRC-32的查找表,其中生成多项式为0xedb88320

CRC 算法规范

下列标准参数用于定义CRC算法实例:

  • Name: 一个CRC算法实例必须以某种方式标识,因此每个公开定义的CRC参数集都有一个名称,例如CRC-16/CCITT。
  • Width: 定义计算出的CRC值的字节数(n bites);同时也定义了使用的多项式生成器的字节数(n=1 bits)。最常用的宽度是8、16和32位。但从理论上讲,从1开始的所有宽度都是可能的。在实践中,甚至使用非常大的(80位)或不均匀的(5位或31位)宽度。
  • Polynomia: 使用的多项式生成器的值。存在不同的形式,包括正常、反转等
  • Initial Value: CRC寄存器的初始值,在上面的例子中都是0,但是理论上任何值都是可以的,但是不同的值检验的精度可能有差异
  • **Input reflected: **如果这个值是true,则输入字节在使用前应该是反转的,即以相反的殊勋使用输入字节的位。举个例子 byte 0x82 = b10000010 ,Reflected(0x82)=Reflected(b10000010)=b01000001=0x41
  • Result reflected: 如果这个值是true,则最终的计算的CRC结果在返回前被反转。
  • Final XOR value: 对最终的CRC结果值进行异或操作,这个操作在 "Result reflected"之后。
  • Check value [可选的]:这个值不是必须的,该值的作用是 用于帮助验证实现的CRC检验算法是否正确。这个值通常是字符"123456789"或字节数组:[0x31,0x32,0x33,0x34,x035,036,x037,0x38,0x39] 进行CRC检验运算后的结果

补充说明(值得注意的地方)

CRC的基本数学原理

首先让我们回顾以下CRC定义中的一些基础数学知识

CRC-n 使用了一个生成多项式 G(x),这个多项式有n阶以及n+1个项式
n+1项表示多项式的长度为n+1(使用了普通的多项式表示法,即最高项在左边)

比如:一个 CRC-8的生成多项式值为0x07 = 100000111 =  1x8+0*x7+0x6+0*x5+0x4+0*x3+1x2+1*x1+1*x^0

CRC的计算依赖于多项式的除法,可以表述为 M(x)*x^n =G(x)*Q()+R(X) ,其中

  • M(x) 是输入的二进制串,M(x)*x^n是输入字节补上n个0位
  • G(x)是 n阶的生成多项式
  • Q(x)是除法的商,并且该数据后面不会再用到
  • R(x) 是余数 ,也是 CRC 检验值

CRC-1 等同于 奇偶检验位

奇偶检验位表示一个二进制数中位值为1的数量是奇数还是偶数。
比如:

0x34 = 0011 0100 有3个1的位,因为3为奇数,因此奇偶检验结果为1;

CRC-1 阶级为1,并且有2个多项式:a*1+b*x0. 因为CRC检验的最高位总是1,因此a=1; 如果b=0则该多项式没有任何意义,因为 与多项式0x10最终计算的结果总是为0(读者可以试验下)。因此CRC-1使用的生成多项式为0x11(二进制),而因为在程序的实际运算过程中我们可以省略第一位(上文有介绍过)因此在程序中表达为 0x01。


输入数据为0x34的运算过程
001101000000
    11
---------
    0001000
          11
           -------
          0100
            11
             —
            010
              11
               –
               01 = CRC = 1


为什么在CRC 算法中,加法等于减法

CRC 计算使用的是多项式算法。CRC的多项式算法是基于有限域上两个元素(0和1)的除法

要定义加法运算,只有四种不同的情况:
0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 0 -注意这里没有进位,因为有限域为(0,1)

定义减法如下:
0 - 0 = 0
0 - 1 = 1
1 - 0 = 1
1 - 1 = 0

我们看到加法和减法是一样的:基本上这就是我们在上面已经使用过几次的XOR操作。( 因为 1+1=0,1-1=0,1^1=0)
这就是为什么我们说M(x)*^n - R(x)和M(x) *x^n+ R(x)在CRC算法中是相同的。

为什么乘以x^n等于添加n个0

这是因为每在末尾加一个0,等于多项式乘以一个n,比如我们有一个多项式 xn+xn-1+…x1+x0.乘以x后等于
x*(xn+xn-1+…x1+x0) =x(n+1)+xn+…x^1 ,通过与x相乘,多项式的层级增加了1,这就等于在右边加上一个0。举个例子可能会更清楚


假设有多项式 01010010 = x^6 + x^4 + x ,添加一个0后:
010100100 = x^7 + x^5 + x^2 = x * (x^6 + x^4 + x). 因此多项式每乘于x 等于添加一个0


"移位寄存器"的初始值是否可以不为0

在目前所有的实现示例中,crc寄存器中的值总是为0的,那么如果crc寄存器的值不为0这个结果是否依旧是正确的?或者说寄存器为0和为非0两者结果是否相同?

让我们先假设寄存器初始值为0 和为非0的结果是相同的,那么看下面这样一个例子


case A: crc初始值 = 0x00, 多项式 = 0x9B,输入数据为 [0xFF,0x01]
case B: crc初始值为 = 0FF, 多项式 = 0x9B,输入数据为 [0x01]

分析:因为case A的初始值为0x00,因此在计算过程中,首先移动8个位,因此等价于 crc初始值为 0xFF  输入数据为[0x01],此时 case A = case B,所以两个的计算结果应该是一样的,对吧.


但是实际上两种情况计算出(使用程序运算)的CRC结果是不一样的,分别是 0x2A和0xE0。
这表明了,先移位改变crc寄存器的值,再使用crc程序运行的结果是不一样的。事实上,在CRC32的实现中,寄存器的初始值通常为0xFFFFFFFF。

让我们分析下一个的 CRC-8 实现,并从中寻找原因

public class CRC_8_DEMO {


    public static byte Compute_CRC8_Simple(byte[] bytes) {
        byte generator = 0x1D;
        byte crc = 0; /* start with 0 so first byte can be 'xored' in */

        for (byte currByte : bytes) {
            //注意,这里对输入的字节先执行了 ^=currByte的操作
            crc ^= currByte; /* XOR-in the next input byte */

            for (int i = 0; i < 8; i++) {
                if ((crc & 0x80) != 0) {
                    crc = (byte) ((crc << 1) ^ generator);
                } else {
                    crc <<= 1;
                }
            }
        }

        return crc;
    }
}

注意第10行的代码,在进行 crc寄存器和generator的异或操作前,先对crc和currByte执行了异或操作,因此如果 crc初始值不为0,当第一次执行 crc的初始值是0时,执行异或操作后的结果是crc = currByte即首个字节,但当crc初始值不为0时,第一次循环内 crc^=currByte的结果则不是currByte。

该实验并不是说 crc的初始值不能为非0,只是为了说明初始值不同,计算的结果也不同。

Normal、Reversed的生成项形式

我们可能在一些有关CRC中的文章中知道 crc 生成项有 正常、反转、镜像等形式,其实原理是一样的,我们上文介绍的计算方式都是 normal的,即处理数据是从 从左到右、从高位到低位,但是在计算的现实情况中,数据可能是从低位到高位(比如在很多底层硬件的数据通信中),因此有了reversed的形式,数据的处理是从右向左移位的。

CRC与数据完整性[编辑]

尽管CRC在数据错误检测中非常有用,但CRC并不能可靠地校验数据完整性(即数据没有发生任何变化),这是因为CRC多项式是线性结构,可以非常容易地_故意_改变量据而维持CRC不变,参见CRC and how to Reverse it中的证明。我们可以用Message authentication code校验数据完整性。

CRC发生碰撞的情况[编辑]

与所有其它的散列函数一样,在一定次数的碰撞测试之后CRC也会接近100%出现碰撞。CRC中每增加一个数据位,碰撞几率就会减少接近50%,如CRC-20与CRC-21相比。

  • 理论上来讲,CRC64的碰撞概率大约是每18×10个CRC码出现一次。
  • 由于CRC的不分解多项式特性,所以经过合理设计的较少位数的CRC可能会与使用较多数据位但是设计很差的CRC的效率相媲美。在这种情况下CRC-32几乎同CRC-40一样优秀。

设计CRC多项式[编辑]

生成多项式的选择是CRC算法实现中最重要的部分,所选择的多项式必须有最大的错误检测能力,同时保证总体的碰撞概率最小。多项式最重要的属性是它的长度,也就是最高非零系数的数值,因为它直接影响着计算的校验和的长度。
最常用的多项式长度有

  • 9位(CRC-8)
  • 17位(CRC-16)
  • 33位(CRC-32)
  • 65位(CRC-64)

在构建一个新的CRC多项式或者改进现有的CRC时,一个通用的数学原则是使用满足所有模运算不可分解多项式约束条件的多项式。

  • 这种情况下的不可分解是指多项式除了1与它自身之外不能被任何其它的多项式整除。

生成多项式的特性可以从算法的定义中推导出来:

  • 如果CRC有多于一个的非零系数,那么CRC能够检查出输入消息中的所有单数据位错误。
  • CRC可以用于检测短于2k的输入消息中的所有双位错误,其中k是多项式的最长的不可分解部分的长度。
  • 如果多项式可以被x+1整除,那么不存在可以被它整除的有奇数个非零系数的多项式。因此,它可以用来检测输入消息中的奇数个错误,就像奇偶校验函数那样。

参考

https://www.cnblogs.com/masonzhang/p/10261855.html
http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html#ch1

你可能感兴趣的:(计算机通用技术,--算法,crc,数据加解密,数据验证,检验算法)