base64 编码原理详解

发表于 2018 年 4 月 26 日

之前一直都是直接无脑

1from base64 import b64encode as b64e

只知道大概 base64 是啥东西, 今天突然心血来潮花了几分钟了解了下 base64, 其实原理并不复杂.

所以今天不做个 api caller, 自己实现一下 base64 编码玩玩. 首先先了解一下 base64 的实际用途, 主要是将一个字节 (1 byte、8 bit) 转换为可以直接显示的字符, 比如一个文件内容是 \x66\x12\x13\x14 就可以将其转化为 ZhITFA==. 这样就可以通过一些只能输入可见字符的设备、协议或者格式来传递二进制文件, 比如在 json 中就可以将文件 base64 编码后传递. 在 acsii 中除去空格和两个换行符号, 一共有 93 个可显示字符, 而一个字节一共有 2^8=256 种可能, 无法全部一一与 93 个字符对应, 所以我们可以通过另一种取巧的办法, 将 3 个字节拼在一起, 用 4 个 6 bits 来代替.

这样就可以直接用 2^6=64 个可显示字符一一对应 6 个 bits 的所有可能性. 这也是 base64 里面 64 的来历, 其他 16、32 也是同理, 只是用了更少的字符对应更少的 bits, 但同时对应的存储损耗更大. 比如: base64 转化后每个可见字符保存时占用大小是一个字节, 但却需要 4 个可见字符来表示转化前的 3 个字节, 使得大小是原来的 4/3, 所以一般都使用 base64. 接下来了解了原理后就可以直接实现了, 像图中所示一样, 需要取高低六位、高低四位、高低二位. 我们可以用 python 中的位运算实现.

 1def getHighSix(num):
 2    return (252 & num) >> 2
 3
 4
 5def getLowSix(num):
 6    return (63 & num)
 7
 8
 9def getLowTwo(num):
10    return (3 & num) << 4
11
12
13def getHighTwo(num):
14    return (192 & num) >> 6
15
16
17def getHighFour(num):
18    return (240 & num) >> 4
19
20
21def getLowFour(num):
22    return (15 & num) << 2
23
24接下来我们需要一个字符表来一一对应字符与各个 6 bits 的关系,
25
26codeTable = [
27    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
28    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
29    'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
30    't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
31    '8', '9', '+', '/'
32]
33
34然后我们就可以写一个简单的函数将 3 个字节转换为 4 个字符,
35
36def encodeThreeBytes(bytecodes):
37    if len(bytecodes) != 3:
38        raise Exception("The bytecode's length should be three.")
39    
40    fisrt = getHighSix(bytecodes[0])
41    second = getLowTwo(bytecodes[0]) + getHighFour(bytecodes[1])
42    third = getLowFour(bytecodes[1]) + getHighTwo(bytecodes[2])
43    fourth = getLowSix(bytecodes[2])
44    return codeTable[fisrt] + codeTable[second]  + codeTable[third]  + codeTable[fourth] 
45    '''
46    等价于:
47    return codeTable[getHighSix(bytecodes[0])] + codeTable[getLowTwo(
48        bytecodes[0]) + getHighFour(bytecodes[1])] + codeTable[getLowFour(
49            bytecodes[1]) + getHighTwo(bytecodes[2])] + codeTable[getLowSix(
50                bytecodes[2])]
51    '''

最后需要注意的是当输入的 bytes 长度不是 3 的倍数时, 需要对其补一个或者两个纯零即 \x00 字节后再进行编码,
完成后将结果的倒数一、二个字符替换成 = 号表示这是补零而来即可.

 1def b64encode(bytecodes):
 2    if len(bytecodes) == 0:
 3        return b''
 4        
 5    status = 0
 6    result = ""
 7
 8    try:
 9        bytecodes = bytecodes.encode()
10    except Exception:
11        pass
12
13    if len(bytecodes)% 3 == 2:
14        status = 1
15        bytecodes += b'\x00'
16
17    if len(bytecodes)% 3 == 1:
18        status = 2
19        bytecodes += b'\x00\x00'
20
21    for i in range(int(len(bytecodes) / 3)):
22        result += encodeThreeBytes(bytecodes[i * 3:i * 3 + 3])
23    
24    if status == 1:
25        return result[0:-1] + "="
26    if status == 2:
27        return result[0:-2] + "=="   
28    return result

这样我们就完成了一个简单的 base64 编码, 最后可以写个 tests 来验证.

 1import base64
 2import myBase64
 3
 4bytecodes = b'\x12\x66\x23\xff'
 5org = myBase64.b64encode(bytecodes)
 6std = base64.b64encode(bytecodes).decode()
 7
 8print(org)
 9print(std)
10print(org == std)
11
12'''
13输出:
14EmYj/w==
15EmYj/w==
16True
17'''