Base64 编码算法 - Python实现

算法描述

Base64编码的目标:将任意字节转为可读字符的编码。

Base64 不是为了安全,而是为了显示。因为计算机世界最开始的时候,只支持 26 个字母和一些符号,所以 1个 字节就足够编码了,但后来,计算机世界的不止多了中文、日文等文字,更多了视频、图片、程序等一样以字节为单位的数据,这些字节大多数不仅仅是 2^7 以内的可显示的文字字符编码,还有可能是大于127(有符号数小于0)的字节,这些字节没办法用字符显示出来,Base64就是将他们显示出来的算法。

编码规则

Base64编码的思想: 采用64个基本的ASCII码字符对数据进行重新编码。

步骤如下

  • 首先,我们得准备一个密文表,一般选这个
    • ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
    • 一共有 64 的字符,这些字符称为 Base 字符,基本的 ASCII 码字符,这也是 Base64 名称的由来
    • 64 个也就是 2^6 个字符,也就是说我们需要 6 位二进制来显示
  • 接下来将需要编码的数据拆分成字节数组,以三个字节为一组,每个字节8位,共24位。当所要编码的数据的字节数不是3的整数倍,也就是说在分组时最后一组不足3个字节。这时在最后一组填充120字节。并在最后编码完成后在结尾添加12‘=’(可回忆一下之前碰到的 base64 编码,确实是不少在最后有 =
  • 按顺序排列这24位数据
  • 把这24位数据分成四组,即每组6
  • 在每组的最高位前补两个0凑足一个字节。这样就把一个3字节为一组的数据重新编码成了4个字节
  • 并且每个字节的值都是 0-63 以内,对应我们的 密文表 的字符位置,编码成功

示例:Hello

  • Hello 包含 5 个字节,取出对应的 ASCII 编码
    • 16进制 H (0x48) e (0x64) l (0x6c) l (0x6c) o(0x6f)
    • 二进制 H (0100 1000) e (0110 0101) l (0110 1100) l(0110 1100) o(0110 1111)
  • 三个字节为 1 组,加一个 0 补为两组:Hello0,这里补了一个0,所以,编完码后需要在编码后面加一个 =
  • 分为两组后,按顺序排列这两组的编码
    • 第1组:01001000 01100101 01101100
    • 第2组:01101100 01101111 00000000
  • 将这两组的 24 位数据各自按 6 位一组,分为 4 组
    • 第1组:010010 000110 010101 101100
    • 第2组:011011 000110 111100 000000
  • 给每组的最高位补两个 0,就完成了 3 个字节重新编码为 4 个字节
    • 第1组:00010010 00000110 00010101 00101100
    • 第2组:00011011 00000110 00111100 00000000
  • 找出每组的二进制对应的密文表的字符
    • 第1组:00010010 -> 18 -> S, 00000110 -> 6 -> G, 00010101 -> 21 -> V, 00101100 -> 44 -> s
      • SGVs
    • 第2组:00011011 -> 27 -> b, 00000110 -> 6 -> G, 00111100 -> 60 -> 8, 00000000 -> 0 -> A
      • bG8A
  • 最终编码为:SGVsbG8A=

解码过程

以上就是编码过程,解码过程,就是倒过来,先去掉末尾的 =,将剩余的字符按照 4 个字符一组进行分组,然后每组的每个字符去掉最高位的 2 个 0,重新分为 3 个字符一组,在根据 = 的个数,去掉最后一组的 0-20 字符,完成解码(详细过程就不赘述了)。

可以使用系统指令base64进行验证,命令如下:


# encode
printf "Hello" | base64 -i -

# decode
printf "SGVsbG8A=" | base64 -D -

此处不能使用echo,因为echo会在末尾添加换行符。

Python实现

接下来,我们使用 python 实现两个函数,encodedecode,完成 base64 的编解码。



CHARSET = "ASCII"
BASE_CHAR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
CHAR_PRE_GROUP = 3

def encode(msg):
    # 转为二进制
    b_msg = bytes(msg, CHARSET)
    # print("字节数:", len(b_msg))
    # 计算需要添加的 0 的个数
    zero_cnt = CHAR_PRE_GROUP - len(b_msg) % CHAR_PRE_GROUP
    if zero_cnt == CHAR_PRE_GROUP:
        zero_cnt = 0
    # print("需要添加的 0 的个数:", zero_cnt)
    msg += str(chr(0))*zero_cnt
    # print("Msg with zero:", msg)
    # 再一次转化为二进制
    b_msg = bytes(msg, CHARSET)
    # 三个一组,分组进行处理
    encoded = ""
    for i in range(0, len(b_msg), 3):
        i_msg =[int(i) for i in b_msg[i:i+3]]
        # print([bin(i) for i in i_msg])
        # 按照算法,将 3 字节变成 4 字节,位运算会比较绕
        new_msg = [None] * 4
        new_msg[0] = i_msg[0] >> 2
        new_msg[1] = (((i_msg[0] & 0b00000011) << 6) | (i_msg[1] >> 2)) >> 2
        new_msg[2] = (((i_msg[1] & 0b00001111) << 4) | (i_msg[2] >> 4)) >> 2
        new_msg[3] = i_msg[2] & 0b00111111

        encoded += "".join([BASE_CHAR[i] for i in new_msg])
    return encoded + '=' * zero_cnt

def decode(msg):
    zero_cnt = msg.count('=')
    # print("zero cnt:", zero_cnt)

    # remove =
    msg_without_zero = msg.replace('=', '')
    # 将字符转化为对应的索引值
    i_msg = [BASE_CHAR.index(i) for i in msg_without_zero]
    # 转化为 二进制
    decoded = ""
    for i in range(0, len(i_msg), 4):
        i_group_msg =[int(i) for i in i_msg[i:i+4]]
        # print([bin(i) for i in i_group_msg])
        # 将四字节转化为三字节
        original_msg = [None] * 3
        original_msg[0] = (i_group_msg[0] << 2) | ((i_group_msg[1] & 0b00110000) >> 4)
        original_msg[1]= ((i_group_msg[1] & 0b00001111) << 4) | ((i_group_msg[2] & 0b00111100) >> 2)
        original_msg[2]= ((i_group_msg[2] & 0b00000011) << 6) | i_group_msg[3]

        # print(original_msg)
        decoded += "".join([chr(i) for i in original_msg if i])
    # print(len(decoded))
    # print(decoded)
    return decoded


if __name__ == '__main__':
    def test(msg):
        emsg = encode(msg)
        print("Encoded", emsg)


        dmesg = decode(emsg)
        print("Decoded", dmesg)

    test("Hello")
    test("Hello1")

    test("Hello World")

以上就是一个简单的 Base64 编解码算法。

中文如何进行 Base64 编码

需要提醒的是,用上述代码,是无法对中文进行编码和解码的。

如果要对中文进行编解码,我们首先要对原始编码做一定的了解。

中文有多种编码方式:GBK、UTF-8等等,每一种编码的Base64对应值都不一样。比如 UTF-8 用三个字节对中文编码,所以代码里要对此进行优化,所以一个汉字就是一组了,最终会用 4个 Base 字符对其进行展示。

如:


printf "中文" | base64 -i -

结果为:5Lit5paH