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),即解码得到每个字节;

Read More: