SHA256补位操作

342 阅读6分钟

补位操作,是SHA256算法准备数据的一个重要环节,也是很多信息和数据处理算法的常见环节。笔者在学习这个内容的时候,看到很多资料对这个操作要么语焉不详,要么说得非常复杂,要么只提供代码和原理图,中间过程说的不太清楚。经过一段时间的研究,笔者终于初步搞清楚了其原理和操作过程,这里予以整理记录,并且提供了测试代码来帮助理解。

SHA256补位

SHA256算法的一个核心是要操作一个长度为512位(64byte比特)的数据块。在此之前,对应任意一段信息,我们都可以将其编码成为一个字节数组。显然,这个字节数组的长度,不可能正好被64整除,就需要某种扩展方式,能够将这个字节数组扩展到正好长度是64的整数倍,这个操作就叫做补位。SHA256补码是一种通用规则,无论原始信息的长度如何,即使其刚好能够被512整除,或者补足尾码(后详)后刚好能被512正常,都需要进行补码操作。

我们先来看一个补位计算的一般结果(这里使用了hex编码方式):


原始信息:
48656c6c 6f20576f .... 8c2e2e2e 32

补位后:
48656c6c 6f20576f .... 8c2e2e2e 32800000 0000000 .... 00000000 00000000 000000e0

┗----------原始编码--------------┛┗--------补码(80开头)-------┛ ┗------ 尾码------┛

可以看到,需要将原始的信息,无论长度如何,都要能够扩展到64位的整数倍。扩展后的信息有其相应的编码规则。其中,最后16位,是原始编码的长度(尾码);中间若干位,是需要填充的内容(补码),而且必须以"80"(二进制的1)作为开头。

实例操作

为了更直观的理解,我们以"Hello World:你好,世界...2"这个字符串为例,结合Javascript补位操作代码,来进行说明(还是使用hex编码):

1 对字符串进行编码

一般情况下,原始数据是UTF-8字符串,可以使用TextEncoder,对其进行编码,这里可以一个hex字符串,长度为58:


let buf = new TextEncoder().encode("Hello World:你好,世界...2");

48656c6c 6f20576f 726c643a e4bda0e5 
a5bd2ce4 b896e795 8c2e2e2e 32

2 对长度信息编码

这个内容的字节数组长度为29,位长为29x8=232,用16进制(hex)表示为"e8", 用0补足前面的位数作为尾码(固定长度为16)的字符串为:

00000000 000000e8

3 补位

要把内容补到8的整数倍行(此处需要补到8行),这里中间需要补52个0, 然后第一个字节数组为80,形式如下:


48656c6c 6f20576f
726c643a e4bda0e5
a5bd2ce4 b896e795
8c2e2e2e 32         ------ 原码
           80       ------ 标头
             0000   ------ 补码
00000000 00000000   |
00000000 00000000   |
00000000 00000000   |
00000000 000000e0   ------ 尾码

在很多资料中,补位的开始是1,随后是若干位0,那个是二进制的表达形式,如1000 0000...,我们为了看得清楚,使用16进制表示,实际编码也是以字节形式表示的,就是80...。

4 结果

所以,合并后,最后的结果应该是:


48656c6c 6f20576f 726c643a e4bda0e5
a5bd2ce4 b896e795 8c2e2e2e 32800000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 000000e0

补足后,总共128个hex字符,二进制长度为1024,可以被512整除。显然,补位的重点就是需要计算中间需要补足0的个数。

基于以上操作,如果使用JS实现,参考的代码如下:

通用公式

如果将上述的步骤,在使用字节数组,和hex编码和长度的情况下,可以得到下面的通用公式:

原始信息字节数组为b[l], 长度为l, 尾码长度tl为16, 补码长度为pl, 则有:

r1 = (l*2 + 16) % 64;
pl = r1 ==  0 ? 62 : 62 - r1
tail = (l*8).toString(16).padStart(16,"0") // 16进制,补足16pad = "80" + Array(pl).fill("0")

此处:
l  = 29
r1 = 10
pl = 62 - 10 = 52 

最后为:

48656c6c 6f20576f 726c643a e4bda0e5
a5bd2ce4 b896e795 8c2e2e2e 32
                             80
                               0000
00000000 00000000 00000000 00000000
00000000 00000000 
                  00000000 000000e0

要查看原始信息长度或者补足尾码后长度是512的整数倍的情况,可以使用下列测试用例:


"HelloWorld:你好世界"
48656c6c 6f576f72 6c643ae4 bda0e5a5 
bde4b896 e7958c80 00000000 000000b8


"HelloWorld:你好世界."
48656c6c 6f576f72 6c643ae4 bda0e5a5 
bde4b896 e7958c2e 80000000 00000000 
00000000 00000000 00000000 00000000 
00000000 00000000 00000000 000000c0

"HelloWorld:你好世界.1234ABCD"
48656c6c 6f576f72 6c643ae4 bda0e5a5 
bde4b896 e7958c2e 31323334 41424344 
80000000 00000000 00000000 00000000 
00000000 00000000 00000000 00000100

补位设计的思考

SHA256补位的规则和设计,可以让我们学习到一种补位操作的方法。通过记录原始信息的长度,让我们也可以从补位后的信息进行还原。但目前尚不可知,为什么设计中间补码,是一个"80"(字节)或者"1"(二进制)开头码的填充内容。如果从信息的可还原角度而言,是不需要这个信息的。如果读者知道原因或者有自己的理解,希望不吝赐教。

此外,SHA256设计的内容长度限制,是2^64位,虽然也是一个限制,但应该也是足够使用了的。