base64 原理解密

什么是 base64

我们知道一个字节可以表示的范围是 0 ~ 255,并且在 ASCII 码表中会对应一个字符,比如:字符 97 对应字符 'a'、90 对应字符 'Z' 等等。而在 ASCII 码表中有很多字符都是不可见字符,那么当数据在网络上传输时,由于不同的设备对字符的处理会有一些不同,那些不可见字符就有可能被错误处理。所以这是不利于数据传输的,而解决办法就是先将数据进行一个 base64 编码,将其统统变成可见字符,这样出错的概率就大大降低了。

base64 应用在很多领域当中,比如:根证书、电子邮件、图片传输等等,那么下面就来分析一下 base64 到底是如何对数据进行编码的。

这里需要提一嘴,很多人会把编码(encode)和加密(encrypt)搞混,尽管它们都是将数据变成另一种格式,但是两者还是不一样的。首先编码是为了方便数据传输,不是为了保证数据的机密性,比如这里的 base64,它的编码规则是公开的,只要你的数据是 base64 编码之后的,那么任何人都可以进行解码;而加密才是为了数据不被别人知道,所采取的安全策略。

base64 原理解密

我们说由于存在不可见的字符的问题,所以需要将每一个 ASCII 码都映射成可见字符。那么同 ASCII 码表一样,base64 肯定也有一张相应的表,记录了数字和字符之间的映射关系。

+----+---+----+---+----+---+----+---+
| 0  | A | 16 | Q | 32 | g | 48 | w |
| 1  | B | 17 | R | 33 | h | 49 | x |
| 2  | C | 18 | S | 34 | i | 50 | y |
| 3  | D | 19 | T | 35 | j | 51 | z |
| 4  | E | 20 | U | 36 | k | 52 | 0 |
| 5  | F | 21 | V | 37 | i | 53 | 1 |
| 6  | G | 22 | W | 38 | m | 54 | 2 |
| 7  | H | 23 | X | 39 | n | 55 | 3 |
| 8  | I | 24 | Y | 40 | o | 56 | 4 |
| 9  | J | 25 | Z | 41 | p | 57 | 5 |
| 10 | K | 26 | a | 42 | q | 58 | 6 |
| 11 | L | 27 | b | 43 | r | 59 | 7 |
| 12 | M | 28 | c | 44 | s | 60 | 8 |
| 13 | N | 29 | d | 45 | t | 61 | 9 |
| 14 | O | 30 | e | 46 | u | 62 | + |
| 15 | P | 31 | f | 47 | v | 63 | / |
+----+---+----+---+----+---+----+---+

我们看到总共由 26 个大写英文字符、26 个小写英文字符、10 个阿拉伯数字、1 个加号、1 个斜杠,总共 64 个字符组成,所以是 base64。这些字符很明显都是我们熟知的可见的字符,任何设备对它们的处理显然都是没有歧义的。

我们以字节串 b"satori" 为例,看看它 base64 编码之后的值是多少?

import base64

s = b"satori"
print(base64.b64encode(s))  # b'c2F0b3Jp'

但是问题来了,到底要怎么映射呢?下面我们来解释一下。

第一步

将待转换的字节串每三个字节分为一组,由于每个字节有 8 位,那么总共是 24 个位。

s = b"satori"
print([c for c in s])
"""
[115, 97, 116, 111, 114, 105]
"""

以上是每个字节对应的 ASCII 码,我们转成二进制格式。

第二步

然后第一步中的 24 个位再每 6 个分为一组,因此可以分为四组。

因此总共我们得到了 8 组数据:011100、110110、000101、110100、011011、110111、001001、101001。

第三步

每组数据有 6 个位,然后在高位补两个 0,于是可以得到:00011100、00110110、00000101、00110100、00011011、00110111、00001001、00101001。

将其转成十进制,得到对应的值:

lst = ['00011100', '00110110', '00000101', '00110100', '00011011', '00110111', '00001001', '00101001']
print([int(_, 2) for _ in lst])  # [28, 54, 5, 52, 27, 55, 9, 41]

8 组数据对应的十进制数为 28、54、5、52、27、55、9、41。

第四步

根据每组得到的码值在 base64 编码对照表中映射出对应的字符,28 对应 c、54 对应 2、5 对应 F、52 对应 0、27 对应 b、55 对应 3、9 对应 J、41 对应 p,因此最终得到的结果就是 c2F0b3Jp,可以看到我们推算的结果和 Python 计算的结果是一样的。

为什么要 3 个字节分为一组,原因是 6 和 8 的最小公倍数是 24,而 3 个字节正好是 24 个位。并且 24 个字节,6 个一组可以分为 4 组,所以 base64 编码之后的数据大小会比原来多大约三分之一。

整体来说还是比较简单好理解的,就是将原来的一组 3 个字节变成 4 个字节。并且此时每个字节只用了 6 个位(前两位补 0),取值范围正好是 0 ~ 63,和 base64 编码对照表匹配。

但是这里还存在一个问题,就是我们上面的字节串正好是 6 个字节,是 3 的倍数。那如果不是 3 的倍数怎么办?假设字节串只有一个字节,比如:b"A",对应的 ASCII 码为 65,二进制格式为 01000001。

即使只有 1 个字节,那么还是当成 3 个字节来处理,而多余的两个字节暂时先不管,然后依旧分为 4 组。第一组是 010000,前面补 0 之后得到 00010000、对应的结果为 16,根据 base64 对照表我们得到对应的字符为 Q;第二组只有 01,后面没东西了,那么此时后面全部按 0 处理,即 010000,然后前面补 0 得到 00010000,因此对应的字符也是 Q;而第三组和第四种组则是完全没有内容,这种情况直接对应 =。因此 b"A" 在 base64 编码之后得到的结果就是 QQ==,同理 b"satoriA" 在 base64 编码之后得到的结果就是 c2F0b3JpQQ==

import base64

print(base64.b64encode(b"satori"))  # b'c2F0b3Jp'
print(base64.b64encode(b"A"))  # b'QQ=='
print(base64.b64encode(b"satoriA"))  # b'c2F0b3JpQQ=='

除了一个字节之外,还有一种特殊情况,就是每 3 个字节一组之后还剩下两个字节,显然两者是类似的。我们还是来分析一下,以 b"satoriAV" 为例,显然我们只需要分析 AV 即可。

第一组:补 0 之后 00010000 对应十进制为 16,根据对照表得到字符 Q;第二组:补 0 之后 00010101 对应十进制为 21,根据对照表得到字符 V;第三组:补 0 之后 00011000 对应十进制为 24,根据对照表得到字符 Y;第四组全部为空,因此直接得到 =。所以 b"AV" 在 base64 编码之后对应的字符为 QVY=

import base64

print(base64.b64encode(b"satori"))  # b'c2F0b3Jp'
print(base64.b64encode(b"AV"))  # b'QVY='
print(base64.b64encode(b"satoriAV"))  # b'c2F0b3JpQVY='

小结

以上就是 base64 的原理,当然我们除了可以进行编码、也可以进行解码,不过是把过程逆过来了而已。

base64 编码数据主要是为了传输和存储,正如我们最开始所说,base64 只能说是一种编码、不能算是加密。

实现 base64

绝大部分语言都内置了 base64 相关的库,可以直接对数据进行编码和解码,那么我们可不可以自己实现呢?显然是可以的,我们来试一下。

import base64


def b64encode(data: bytes):
    if not isinstance(data, bytes):
        raise TypeError("data 需要一个 bytes 对象")
    # 构造码值和字符之间的映射
    base64_table = dict(zip(range(64), "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"))
    # 转成二进制直接拼接起来
    data = "".join([f"{_:0>8b}" for _ in data])
    # 24 个位(3 个字符)一组,计算可以分多少组
    count = len(data) // 24
    # 分为两部分计算
    idx = count * 24
    part1 = [base64_table[int(f"00{data[i: i + 6]}", 2)] for i in range(0, idx, 6)]
    part2 = [base64_table[int(f"00{data[idx + i: idx + i + 6]:0<6}", 2)] for i in range(0, len(data[idx:]), 6)]
    part2.extend(["="] * (4 - len(part2)))
    part1.extend(part2)
    return "".join(part1).encode("utf-8")


print(b64encode(b"satoriAV"))
print(base64.b64decode(b"satoriAV"))  # b'c2F0b3JpQVY='

结果显然是正确的,当然解码也很简单,按照编码的流程反推回去即可。

posted @ 2020-09-14 20:50  古明地盆  阅读(2533)  评论(0编辑  收藏  举报