Base64, Base58 等算法的编码和解码过程
这里记录一下字节流在表示和传输中使用到的 Base 编码,即把二进制字节流编码成某种格式 ASCII 文本字符。
Base16
最简单的是平时常用到的十六进制编码。即以 0123456789abcdef
(不区分大小写)作为字符表,对于输入字节流的每个字节,按高 4 位和低 4 位分别作为索引来查表并得到相应的字符,比如:
- 输入字节流:两个字节,分别是 0x12 和 0x34;
- 写成二进制:
00010010 00110100
- 按 4 比特分组:
0001 0010 0011 0100
- 转换成十进制,作为索引从字符表里得到四个符:
1、2、3、4
,放在一起即1234
。
按编码前后的容量来看,它从最初的 两个字节 变成了 四个字节,即增加了一倍的容量;好处是得到了可见的字符流。
实际上,十六进制编码,也就是 Base16 编码。概括地说,即以 16(24)个字符作为字符表,输入字节流的每 4 个比特作为索引得到相应的字符。解码过程,则把每个字符对应的索引,按 4 比特(不足时高位补 0)连在一起,即得到了二进制的字节流。
Base64
Base64 编码,以 64(26)个字符 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
作为字符表。
输入字节流中,每三个字节一组,24 比特,分成四个 6 比特,作为四个索引,从字符表中检索到相应字符,即每三个字节编码成四个字符。这样就在保持原输入字节流的信息、并且编码成明文可见字符串的情况下,容量只增加了三分之一。
编码
对于输入字节流,按每 6 位作为索引来查表并得到相应的字符,比如:
- 输入字节流:三个字节,0x41、0x42、0x43,(对应的 ASCII 字符
A、B、C
) - 写成二进制:
01000001 01000010 01000011
- 按 6 比特分组:
010000 010100 001001 000011
- 转换成十进制,作为索引:
16, 20, 9, 3
- 从字符表检索得到:
Q,U,J,D
所以 base64(b'ABC') == b'QUJD'
这里以三个字节举例,是因为 8bit × 3 = 6bit × 4,正好可以全部用于索引。
如果六个字节,那就分成两组分别编码后再合并起来:base64(b'DEF') == b'REVG'
、base64(b'ABCDEF') == b'QUJDREVG'
等等。
补全
如果字节流按每三个字节处理后,还剩下两个字节,是这样处理:
- 剩余字节流:0x41、0x42
- 写成二进制:
01000001 01000010
- 按 6 比特分组:
010000 010100 0010xx xxxxxx
;两个字节只有 16 比特,所以再用 x 补全到四个 6 比特,计算时 x 取 0; - 转换成十进制,作为索引:
16, 20, 8, 0
- 注意到上述两个字节 16 比特的信息只分布在前三组里,所以只索引前三个,第四个取
=
,即:Q,U,I,=
所以 base64(b'AB') == b'QUI='
如果字节流按每三个字节处理后,还剩下一个字节,比如:
- 剩余字节流:0x41
- 写成二进制:
01000001
- 按每 6 比特分组:
010000 01xxxx xxxxxx xxxxxx
;一个字节只有 8 比特,所以再用 x 补全到四个 6 比特,计算时 x 取 0; - 转换成十进制,并作为索引:
16, 16, 0, 0
- 注意到上述一个字节 8 比特的信息只分布在前两组里,所以只索引前两个,第三、四个取
=
,即 :Q,Q,=,=
所以 base64(b'A') == b'QQ=='
举例
把上述编码规则放在一起,举例:
base64(b'ABC') == b'QUJD'
base64(b'DEF') == b'REVG'
base64(b'GH') == b'R0g='
所以 base64(b'ABCDEFGH') == b'QUJDREVGR0g='
。
解码
解码就是按上述步骤的逆运算。即在编码字符串里,每四个字符一组,按字符表解码成四个索引数,并按二进制(不足 6 位时高位补 0)排在一起,按 8 比特理解成三个字节即可。
输入的字符串,必然可以完整地分成若干个四字符组,否则输入是无效的。
对于最后一组四字符组里有=
的情况:
- 如果补了一个
=
,那么前三个字符提供了 6 比特 ×3=18 比特的信息,其中前 16 比特解码出两个字节,另外 2 比特用来作为校验(如果不为 0,说明编码字符串不合理); - 如果补了两个
=
,那么前两个字符提供了 6 比特 ×2=12 比特的信息,其中前 8 比特解码出一个字节,另外 4 比特用来作为校验(如果不为 0,说明编码字符串不合理);
按 64 进制来理解
三个字节,24 位,按 6 位分组,可以用 64 进制来理解,比如:
- 输入字节流:三个字节,0x41、0x42、0x43,(对应的 ASCII 字符
A、B、C
) - 写成二进制:
01000001 01000010 01000011
- 合在一起成为一个数:01000001_01000010_010000112 == 427680310
- 展开:4276803 == 16 × 643 + 20 × 642 + 9 × 641 + 3 × 640,4276803 == (16,20,9,3)64
- 得到的索引分别是 16, 20, 9, 3
- 从字符表检索得到:
Q,U,J,D
所以得到了相同的结果,即 base64(b'ABC') == b'QUJD'
实际上对于 2 的幂(比如 Base16、Base32、Base64 等等),都可以这样理解。 因为这跟之前的比特分组,原理上是一致的。
Base32
Base32 编码,以 32(25)个字符 ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
作为字符表。
输入字节流中,每五个字节一组,40 比特,分成八个 5 比特,作为八个索引,从字符表中检索到相应字符,即每五个字节编码成八个字符。
关于补全,即在剩余字节数小于五个时,按实际剩余字节数 n(1、2、3、4),也就是 n × 8 比特,需要分布在多少个 5 比特里,剩余的补 =
。具体地说:
- n=4 时,索引 7 个字符,补 1 个
=
; - n=3 时,索引 5 个字符,补 3 个
=
; - n=2 时,索引 4 个字符,补 4 个
=
; - n=1 时,索引 2 个字符,补 6 个
=
;
除了这些具体参数包括字符表,实际的编码和解码过程,跟 Base64 一致。
Base58
Base58 编码,以 58 个字符 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
作为字符表。相比 Base64,去掉了几个容易混淆的字符 "0OIl" 以及 "+" 和 "/" 符号。
编码
因为 58 并不是 2 的幂,所以不能按比特分组的方式,只能使用 58 进制的方式来计算。比如:
- 输入字节流:三个字节,0x41、0x42、0x43,(对应的 ASCII 字符
A、B、C
) - 写成二进制:
01000001 01000010 01000011
- 合在一起成为一个数:01000001_01000010_010000112 == 427680310
- 展开:4276803 == 21 × 583 + 53 × 582 + 19 × 581 + 57 × 580,即 4276803 == (21,53,19,57)58
- 得到的索引分别是 21, 53, 19, 57
- 从字符表检索得到:
N,v,L,z
所以 base58(b'ABC') == b'NvLz'
这里要考虑一个特殊情况,如果输入字节流开头连续若干个 0,这个信息在合成大数后会丢失,比如:
- 01000001_01000010_010000112 == 427680310
- 00000000_00000000_01000001_01000010_010000112 == 427680310
所以对于这样的输入(因为没有比特分组的情况,所以这里就是整个的输入字节流),有多少个 0 字节(000000002)开头,就先输出多少个 1
字符;剩下的部分再按 58 进制处理。所以 base58(b'\0\0ABC') == b'11NvLz'
。
解码
对于输入字符串:
- 如果开头有若干个字符
1
,先解码输出若干个\0
; - 剩下部分,每个字符转换成索引,并按 58 进制合成一个数;再按二进制展开(高位不足时补 0),即解码得到每个字节;