补位操作,是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进制,补足16位
pad = "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位,虽然也是一个限制,但应该也是足够使用了的。