Skip to Content
全部文章Node服务端从Base64窥探Base编码大家族

从Base64窥探Base编码大家族

为什么会有Base编码

前几天在写TOTP文章时,看到密钥采用Base32编码,于是又想到对于http中常见的Base64编码,就想来聊聊Base编码家族的一些情况。

在计算机数据处理和传输过程中,经常需要将二进制数据以一种文本形式来表示,这是因为文本形式在某些场景下具有更好的通用性、可读性以及兼容性。 base 编码应运而生,它能够将二进制数据转换为特定的字符集表示,使得数据在不同系统、不同应用之间的传递更加便捷,同时也方便了数据的存储和展示。

比如Base64就通过编码,允许一些特殊字符出现在url中,而不会导致处理错误,又比如Base32去掉了一些比较像的字符,这样编码后的字符串更容易阅读、手写。

总归来说,Base编码就是把原字符串转换成二进制,然后通过特定的编码方式,对照编码集转换成一个新的字符串,后面我们来举例说明它是如何做到的。

Base16 编码

base16 编码,也称为十六进制编码。它将每 4 位二进制数据作为一组进行转换。

有计算机基础的朋友肯定学过十六进制、十进制、二进制的换算,Base16恰好和十六进制一个意思。 base16会把原数据转换成二进制,然后一次取4位去编码集表里找到转换后的字符,直到所有字符全部转换完毕,最后将转换后的字符拼接起来, 得到最终的编码结果。

base16 编码集

base16 编码集及其对应的二进制表示:

字符二进制字符二进制
0000081000
1000191001
20010A1010
30011B1011
40100C1100
50101D1101
60110E1110
70111F1111

编码过程

假如我有一个字符串 “base编码”,它的编码过程是:

先把 “base编码” 转换成utf8编码的二进制得到:

utf8 中ASCII字符使用1个字节表示,中文需要3个字节表示,并且是1110开头的,遇到1110开头就表示要连续读取3个字节

我们来分析,转换结果是:

字符二进制
b01100010
a01100001
s01110011
e01100101
11100111 10111100 10010110
11100111 10100000 10000001

最终的二进制编码连起来就是:

01100010 01100001 01110011 01100101 11100111 10111100 10010110 11100111 10100000 10000001

现在我们要把它转换成Base16,上面的编码集中可以看到,base16的16个字符,每个字符使用4个位来表示,因此我们重新划分一下上面的二进制位:

0110 0010 0110 0001 0111 0011 0110 0101 1110 0111 1011 1100 1001 0110 1110 0111 1010 0000 1000 0001

按照上面的码表对应起来,我们就得到了一个base16的编码字符串:

4位base16字符
01106
00102
10008
00011

base编码” 转换后的base16字符串就是:

62617365e7bc96e7a081

可以看到转换之后,实际上字符串更长了,原数据字节是10个字节,转换后字符串长度是20个字节,长度翻了一倍,但是它使用的就是普通ascii字符, 因此它的兼容性是最好的,随便在哪里什么系统、设备上都是可识别的,这就是编码的意义,当然,确实也牺牲了空间。

base32 编码

base32 编码和base16类似,但是它它将每 5 位二进制数据作为一组进行转换。因为 2^5 = 32,所以每 5 位二进制数可以映射到 32 个不同的字符, 其编码集由 32 个字符组成。比起base16加了其他的字母进来,去掉了一些相似的字符,比如数字0和字母O,I和1,所以base32扩充编码集的同时也避免 不易读的问题。

编码集

base32 编码集及其对应的二进制表示:

字符二进制字符二进制
A00000Q10000
B00001R10001
C00010S10010
D00011T10011
E00100U10100
F00101V10101
G00110W10110
H00111X10111
I01000Y11000
J01001Z11001
K01010211010
L01011311011
M01100411100
N01101511101
O01110611110
P01111711111

编码过程

转二进制

base32二进制位不是5的倍数,比如我现在字符串是 “base编码1”,他的二进制是:

01100010 01100001 01110011 01100101 11100111 10111100 10010110 11100111 10100000 10000001 00110001

按5位分组

按照5位分组后:

01100 01001 10000 10111 00110 11001 01111 00111 10111 10010 01011 01110 01111 01000 00100 00001 00110 001

最后只有3位,不足5位,怎么办? 那就在最后不足的地方补0,把他补成5位:

01100 01001 10000 10111 00110 11001 01111 00111 10111 10010 01011 01110 01111 01000 00100 00001 00110 00100
对照码表转换

这个时候他的base32编码后就是:

MJQXGZPHXSLOPIEBGE
补=号

原始数据字节%5=X

  1. 如果X为0字节,表示不需要补充=。
  2. 如果X为1字节,表示还需要补4字节,凑到5字节,则编码后需要6个=填充。
  3. 如果X为2字节,表示还需要补3字节,凑到5字节,则编码后需要4个=填充。
  4. 如果X为3字节,表示还需要补2字节,凑到5字节,则编码后需要3个=填充。
  5. 如果X为4字节,表示还需要补1字节,凑到5字节,则编码后需要1个=填充。

11位%5=1,根据上面规则,需要补6个等号,补=号后的最后编码完成的字符串:

MJQXGZPHXSLOPIEBGE======

补=号规则

记住上面的公式就行,这一段也可以不看。

根据Base32编码规则:

  • 每5个字节的输入会产生 8 个Base32字符(5个字节=40位, base32是5位=1个字符,因此40/5=8个字符)

对于不足5个字节的原始输入数据,需要根据缺少的字节数来决定需要补充多少个等号作为填充。

原始数据字节%5=X

  1. 如果X为0字节,表示不需要补充=。
  2. 如果X为1字节,表示还需要补4字节,凑到5字节,则编码后需要6个=填充。
  3. 如果X为2字节,表示还需要补3字节,凑到5字节,则编码后需要4个=填充。
  4. 如果X为3字节,表示还需要补2字节,凑到5字节,则编码后需要3个=填充。
  5. 如果X为4字节,表示还需要补1字节,凑到5字节,则编码后需要1个=填充。

如果你要计算公式看到这里就行了,下面不用看了,如果要看原理请继续

上面的例子是11字节二进制如下,补4字节(32位),下面最后4字节的0表示补的字节:

01100010 ... 00110001 00000000 00000000 00000000 00000000

按照5位划分,借2位补到前面11字节数据中,最后还剩下30位(30个0),划分5位一组,刚好6组,这6组没有意义,所以就是填充的=

01100 01001 10000 ... 00100 00001 00110 00100 00000 00000 00000 00000 00000 00000

转换后补6个=

MJQXGZPHXSLOPIEBGE======

ps: 这里的00000不能直接到码表翻译,因为是补的,所以就是=

解码过程

就是上面过程的逆向过程。

编码 MJQXGZPHXSLOPIEBGE====== 解码过程:

去掉等号

得到

MJQXGZPHXSLOPIEBGE

对照码表转换成5位二进制

01100 01001 10000 10111 00110 11001 01111 00111 10111 10010 01011 01110 01111 01000 00100 00001 00110 00100

重新8位分组

01100010 01100001 01110011 01100101 11100111 10111100 10010110 11100111 10100000 10000001 00110001 00

最后一段不足8位就表示是补的,丢弃,得到

01100010 01100001 01110011 01100101 11100111 10111100 10010110 11100111 10100000 10000001 00110001

转成字符串

得到

base编码1

五、base64 编码

base64 编码和base16/32类似,将每 6 位二进制数据作为一组进行转换,由于 2^6 = 64,所以每 6 位二进制数对应 64 个不同的字符。

编码集

base64 编码集及其对应的二进制表示:

Base64 编码是使用 6 位二进制来对应一个字符的编码方式,以下是完整的 Base64 码表,其中每个字符对应 6 位二进制:

字符6 位二进制值字符6 位二进制值字符6 位二进制值字符6 位二进制值
A000000Q010000g100000w110000
B000001R010001h100001x110001
C000010S010010i100010y110010
D000011T010011j100011z110011
E000100U010100k1001000110100
F000101V010101l1001011110101
G000110W010110m1001102110110
H000111X010111n1001113110111
I001000Y011000o1010004111000
J001001Z011001p1010015111001
K001010a011010q1010106111010
L001011b011011r1010117111011
M001100c011100s1011008111100
N001101d011101t1011019111101
O001110e011110u101110+111110
P001111f011111v101111/111111

编码过程

转二进制

同样的,我有一个字符串 “base编码”,他的的二进制编码是(编码过程在上面base16已经说过,此处不重复解释):

01100010 01100001 01110011 01100101 11100111 10111100 10010110 11100111 10100000 10000001

重新6位分组

现在我们要把它转换成Base64,上面的编码集中可以看到,base64的64个字符,每个字符使用6个位来表示,因此我们重新划分一下上面的二进制位:

011000 100110 000101 110011 011001 011110 011110 111100 100101 101110 011110 100000 100000 01

因为最后不足6位,补4个0:

011000 100110 000101 110011 011001 011110 011110 111100 100101 101110 011110 100000 100000 010000

对照码表转换

按照上面的码表对应起来,我们就得到了一个base64的编码字符串:

YmFzZee8lueggQ

补=号

原始数据字节%3=X

  1. 如果X为0字节,表示不需要补充=。
  2. 如果X为1字节,表示还需要补2字节,凑到3字节,则编码后需要2个=填充。
  3. 如果X为2字节,表示还需要补1字节,凑到3字节,则编码后需要1个=填充。

上面的数据原始字节是10字节, 3-(10%3)=2, 因此编码后需要补2个=号:

YmFzZee8lueggQ==

补=号规则

记住上面的公式就行,这一段也可以不看。

根据Base64编码规则:

  • 每3个字节的输入会产生 4 个Base64字符(3个字节=24位, base64是6位=1个字符,因此24/6=4个字符)

对于不足3个字节的原始输入数据,需要根据缺少的字节数来决定需要补充多少个等号作为填充。

原始数据字节%3=X

  1. 如果X为0字节,表示不需要补充=。
  2. 如果X为1字节,表示还需要补2字节,凑到3字节,则编码后需要2个=填充。
  3. 如果X为2字节,表示还需要补1字节,凑到3字节,则编码后需要1个=填充。

如果你要计算公式看到这里就行了,下面不用看了,如果要看原理请继续

上面的例子是10字节二进制如下,补2字节(16位)才能到12字节,刚好整除3,下面最后2字节的0表示补的字节:

01100010 ... 10000001 00000000 00000000

按照6位划分,借4位补到前面10字节数据中,最后还剩下12位(12个0),划分6位一组,刚好2组,这2组没有意义,所以就是填充的=

011000 ... 100000 100000 010000 000000 000000

转换后补2个=

YmFzZee8lueggQ==

ps: 这里的000000不能直接到码表翻译,因为是补的,所以就是=

解码过程

就是上面过程的逆向过程。

编码 YmFzZee8lueggQ== 解码过程:

去掉等号

得到

YmFzZee8lueggQ

对照码表转换成6位二进制

011000 100110 000101 110011 011001 011110 011110 111100 100101 101110 011110 100000 100000 010000

重新8位分组

01100010 01100001 01110011 01100101 11100111 10111100 10010110 11100111 10100000 10000001 0000

最后一段不足8位就表示是补的,丢弃,得到

01100010 01100001 01110011 01100101 11100111 10111100 10010110 11100111 10100000 10000001

转成字符串

得到

base编码

其他base

还有base85,base58等,不太常用,就不说了,下次碰到再来补充。

填充=号的必要性

可以通过计算当前Base64字符串的长度来推断出原本应有的填充量,然后去掉最后不足8位的部分来解码, 而不需要等号。实际上,许多解码器确实能够处理没有等号的情况,因为它们可以基于输入字符串 长度自动确定需要忽略的多余位数。

尽管如此,添加等号作为填充是Base64标准的一部分,这样做有几个好处:

  • 兼容性:确保了编码字符串与各种实现之间的兼容性,因为并非所有解码器都实现了自动处理缺少填充的情况。
  • 清晰度:明确指出数据的实际长度,使得人工阅读或调试时更容易理解。
  • 完整性:遵循标准做法有助于避免由于不同解释而导致的潜在错误或数据损坏。

Base只是编码

严格上来说Base只是编码,不是加密,它只是将二进制数据转换成可打印的字符,它并不提供加密功能,因此, 如果需要加密数据,还需要使用其他加密算法。

即便你自己定义了一套码表,它仍然不能算是加密,因为加密算法的优势是你知道了算法,你没有密钥依然无法解开数据。

结论

可以看到base字符集越大,编码后字符串长度越短,编码主要是为了解决一些不同系统等兼容性问题, 开发人员在实际应用中,应根据具体的需求,如数据量大小、传输效率、安全性以及可读性等因素,选择合适的 base 编码方案, 从而优化软件系统的性能和功能。

最后编辑于

hi