深度解密 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。
将其转成十进制,得到对应的值:
print(
[0b00011100, 0b00110110, 0b00000101, 0b00110100,
0b00011011, 0b00110111, 0b00001001, 0b00101001]
) # [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 个位。
整体来说还是比较简单好理解的,就是将原来的一组 3 个字节变成 4 个字节(所以 base64 编码之后的数据大小会比原来多大约三分之一)。并且此时每个字节只用了 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='
所以假设原始字节串长度为 N,那么编码成 base64 之后的字节串长度 M 就等于 N * 8 // 6,当然这是在 n 是 3 的倍数的情况下,如果不是的话。
如果 N % 3 == 1,即 N * 8 % 6 == 2,那么 M 要在原来的基础上再加上 3
如果 N % 3 == 2,即 N * 8 % 6 == 4,那么 M 要在原来的基础上再加上 2
以上就是 base64 的原理,当然我们除了可以进行编码、也可以进行解码,不过是把过程逆过来了而已。base64 编码数据主要是为了传输和存储,正如我们最开始所说,base64 只能说是一种编码、不能算是加密。
C 语言实现 base64
绝大部分语言都内置了 base64 相关的库,可以直接对数据进行编码和解码,那么我们可不可以自己实现呢?显然是可以的,我们来试一下,由于 Python 太简单了,所以这里我们就用 C 来实现。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// base64 映射表
static const unsigned char base64_enc_map[64] =
{
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '+', '/'
};
/* 将字符串 data 编码成 base64 格式的字符串
* @data: 原始字符串
* @len: 原始字符串的长度,不包括 \0
*/
unsigned char * base64_encode(const unsigned char *data, size_t len)
{
size_t i, n;
int c1, c2, c3;
unsigned char *b64_data, *p;
// 如果长度为 0,直接返回空字符串
if (len == 0) {
b64_data = (unsigned char *)malloc(1);
b64_data[0] = 0;
return b64_data;
}
// 长度为 len 的字符串,总共有 len * 8 个位,再除以 6,便可计算出要分多少组
n = (len << 3) / 6;
switch ((len << 3) - n * 6) { // 等价于 len * 8 % 6
case 2:
n += 3;
break;
case 4:
n += 2;
break;
default:
break;
}
// 为 base64 字符串申请空间
b64_data = (unsigned char *)malloc(n + 1);
memset((void *) b64_data, 0, n + 1);
// 遍历原始字符串,一次遍历 3 个字节
// 如果原始字符串的长度少于 3 字节,那么就无需遍历了
// 所以中止条件是 i < len / 3 * 3
for (i = 0, p = b64_data; i < len / 3 * 3; i += 3) {
c1 = *data++;
c2 = *data++;
c3 = *data++;
// 3 个字节,24 个位,每 6 个为一组
// 用 c1 的前 6 个位作为 8 位整数(高位补零)
*p++ = base64_enc_map[c1 >> 2];
// 用 c1 的后 2 个位和 c2 的前 4 个位组合作为 8 位整数(高位补零)
*p++ = base64_enc_map[((c1 & 0b11) << 4) + (c2 >> 4)];
// 用 c2 的后 4 个位和 c3 的前 2 个位组合作为 8 位整数(高位补零)
*p++ = base64_enc_map[((c2 & 0b1111) << 2) + (c3 >> 6)];
// 用 c3 的后 6 个位作为 8 位整数(高位补零)
*p++ = base64_enc_map[c3 & 0b111111];
}
// 如果 len 是 3 的倍数,那么此时 i 应该和 len 相等
// 如果 len 不是 3 的倍数,那么 i = len - 1 或 len - 2
if (i < len) {
c1 = *data++;
// 如果 i == len - 2,说明多出来两个字节,赋给 c1 和 c2
// 否则多出来一个字节(此时 c2 == 0)
c2 = i == len - 2 ? *data++ : 0;
*p++ = base64_enc_map[c1 >> 2];
*p++ = base64_enc_map[((c1 & 0b11) << 4) + (c2 >> 4)];
// 如果多出来两个字节,那么使用 c2 的后 4 个位,否则用 '=' 填充
if (i == len - 2)
*p++ = base64_enc_map[(c2 & 0b1111) << 2];
else
*p++ = '=';
// 原始字符串的三个字节,编码之后会对应四个字节,所以还差一个 '='
*p++ = '=';
}
return b64_data;
}
int main(void) {
char *s1 = "satori";
char *s2 = "satoriA";
char *s3 = "satoriAV";
printf("%s\n", base64_encode((const unsigned char *)s1, strlen(s1)));
printf("%s\n", base64_encode((const unsigned char *)s2, strlen(s2)));
printf("%s\n", base64_encode((const unsigned char *)s3, strlen(s3)));
/*
c2F0b3Jp
c2F0b3JpQQ==
c2F0b3JpQVY=
*/
// free
}
以上就是 base64 的编码过程,再来看看解码,直接反过来就行。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 从原始字符串到 base64 字符串的映射关系为
// 0 -> 'A',1 -> 'B',2 -> 'C',······,62 -> '+',63 -> '/',使用数组再合适不过
// 现在则是反过来,变成 'A' -> 0,'B' -> 1,'C' -> 2,····,'+' -> 62,'/' -> 63
// 要维护这个映射关系,采用字典是最合适的,但 C 里面没有字典这种复杂的数据结构,所以需要另想办法
// 由于 C 里面字符就是一个整数,所以依旧可以使用数组
// 比如 '+',它就是一个整数,值为 43,由 62 映射得到
// 那么在下面这个数组中,索引为 43 的元素就是 62,剩余用不到的元素就用 255 代替
static const unsigned char base64_dec_map[128] = {
255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 62, 255, 255, 255, 63, 52, 53,
54, 55, 56, 57, 58, 59, 60, 61, 255, 255,
255, 64, 255, 255, 255, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 255, 255, 255, 255, 255, 255, 26, 27, 28,
29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 255, 255, 255, 255, 255
};
unsigned char *base64_decode(const unsigned char *b64_data, size_t len) {
size_t i, n;
int c1, c2, c3, c4;
unsigned char *data, *p;
if (len == 0) {
data = (unsigned char *)malloc(1);
data[0] = 0;
return data;
}
// base64 字符串的长度除以 4、再乘以 3,即可得到原始字符串的长度
n = len / 4 * 3;
data = (unsigned char *)malloc(n + 1);
memset((void *)data, 0, n + 1);
// 每 4 个字节为一组,
for (i = 0, p = data; i < len; i += 4) {
c1 = base64_dec_map[*b64_data++];
c2 = base64_dec_map[*b64_data++];
c3 = *b64_data == '=' ? 0 : base64_dec_map[*b64_data++];
c4 = *b64_data == '=' ? 0 : base64_dec_map[*b64_data++];
*p++ = (c1 << 2) + ((c2 & 0b110000) >> 4);
*p++ = ((c2 & 0b1111) << 4) + ((c3 & 0b111100) >> 2);
*p++ = ((c3 & 0b11) << 6) + c4;
}
return data;
}
int main(void) {
char *s1 = "c2F0b3Jp";
char *s2 = "c2F0b3JpQQ==";
char *s3 = "c2F0b3JpQVY=";
printf("%s\n", base64_decode((const unsigned char *)s1, strlen(s1)));
printf("%s\n", base64_decode((const unsigned char *)s2, strlen(s2)));
printf("%s\n", base64_decode((const unsigned char *)s3, strlen(s3)));
/*
satori
satoriA
satoriAV
*/
}
以上我们就用 C 实现了字符串的 base64 编解码,还是不难的。
封装 Python 扩展
我们将上面实现的的功能封装成 Python 扩展,然后暴露给 Python,并测试它和内置的 base64 模块之间的性能差异。需要用到以下几个文件:
b64module.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const unsigned char base64_enc_map[64] = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '+', '/'
};
static const unsigned char base64_dec_map[128] = {
255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 62, 255, 255, 255, 63, 52, 53,
54, 55, 56, 57, 58, 59, 60, 61, 255, 255,
255, 64, 255, 255, 255, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 255, 255, 255, 255, 255, 255, 26, 27, 28,
29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 255, 255, 255, 255, 255
};
/* 将字符串 data 编码成 base64 格式的字符串 */
unsigned char *base64_encode(const unsigned char *data, size_t len) {
size_t i, n;
int c1, c2, c3;
unsigned char *b64_data, *p;
if (len == 0) {
b64_data = (unsigned char *)malloc(1);
b64_data[0] = 0;
return b64_data;
}
n = (len << 3) / 6;
switch ((len << 3) - n * 6) {
case 2:
n += 3;
break;
case 4:
n += 2;
break;
default:
break;
}
b64_data = (unsigned char *)malloc(n + 1);
memset((void *) b64_data, 0, n + 1);
for (i = 0, p = b64_data; i < len / 3 * 3; i += 3) {
c1 = *data++;
c2 = *data++;
c3 = *data++;
*p++ = base64_enc_map[c1 >> 2];
*p++ = base64_enc_map[((c1 & 0b11) << 4) + (c2 >> 4)];
*p++ = base64_enc_map[((c2 & 0b1111) << 2) + (c3 >> 6)];
*p++ = base64_enc_map[c3 & 0b111111];
}
if (i < len) {
c1 = *data++;
c2 = i == len - 2 ? *data++ : 0;
*p++ = base64_enc_map[c1 >> 2];
*p++ = base64_enc_map[((c1 & 0b11) << 4) + (c2 >> 4)];
if (i == len - 2)
*p++ = base64_enc_map[(c2 & 0b1111) << 2];
else
*p++ = '=';
*p++ = '=';
}
return b64_data;
}
/* 将 base64 格式的字符串 b64_data 解码成原始字符串 */
unsigned char *base64_decode(const unsigned char *b64_data, size_t len) {
size_t i, n;
int c1, c2, c3, c4;
unsigned char *data, *p;
if (len == 0) {
data = (unsigned char *)malloc(1);
data[0] = 0;
return data;
}
n = len / 4 * 3;
data = (unsigned char *)malloc(n + 1);
memset((void *)data, 0, n + 1);
for (i = 0, p = data; i < len; i += 4) {
c1 = base64_dec_map[*b64_data++];
c2 = base64_dec_map[*b64_data++];
c3 = *b64_data == '=' ? 0 : base64_dec_map[*b64_data++];
c4 = *b64_data == '=' ? 0 : base64_dec_map[*b64_data++];
*p++ = (c1 << 2) + ((c2 & 0b110000) >> 4);
*p++ = ((c2 & 0b1111) << 4) + ((c3 & 0b111100) >> 2);
*p++ = ((c3 & 0b11) << 6) + c4;
}
return data;
}
b64module.h
#include <stdio.h>
unsigned char *base64_encode(const unsigned char *data, size_t len);
unsigned char *base64_decode(const unsigned char *b64_data, size_t len);
my_base64.c
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "b64module.h"
static PyObject *b64encode(PyObject *self, PyObject *data) {
if (!PyBytes_CheckExact(data)) {
PyErr_Format(PyExc_ValueError,
"data must be a bytes, not '%s'\n", Py_TYPE(data)->tp_name);
return NULL;
}
char *src = PyBytes_AsString(data);
ssize_t len = strlen(src);
unsigned char *dst = base64_encode((unsigned char *)src, len);
PyObject *b64_data = PyBytes_FromString((const char *)dst);
free(dst);
return b64_data;
}
#define is_valid_b64_character(c) \
(((c) >= 'A' && (c) <= 'Z') || \
((c) >= 'a' && (c) <= 'z') || \
((c) >= '0' && (c) <= '9') || \
((c) == '+') || ((c) == '/'))
static PyObject *b64decode(PyObject *self, PyObject *b64_data) {
if (!PyBytes_CheckExact(b64_data)) {
PyErr_Format(PyExc_ValueError,
"b64_data must be a bytes, not '%s'", Py_TYPE(b64_data)->tp_name);
return NULL;
}
char *src = PyBytes_AsString(b64_data);
ssize_t len = strlen(src);
// 检验一下 base64 字符串的合法性
// 长度必须是 4 的整数倍
if (len % 4 != 0) {
PyErr_Format(PyExc_ValueError, "b64_data length must be 4n");
return NULL;
}
// 字符必须是 'a' ~ 'z'、'A' ~ 'Z'、'+'、'/'、'=',并且 '=' 要在字符串的最后
int flag = 1;
int i;
for (i = 0; i < len - 2; i++) {
if (!is_valid_b64_character(src[i])) {
flag = 0;
break;
}
}
// 单独判断最后两个字符,如果倒数第二个字符为 '=',那么最后一个必须也是 '='
if (src[i] == '=') {
if (src[i + 1] != '=')
flag = 0;
} else if (!is_valid_b64_character(src[i])) {
flag = 0;
} else {
// 到这里说明倒数第二个字符不为 '='
if (!(src[i + 1] == '=' || is_valid_b64_character(src[i + 1]))) {
flag = 0;
}
}
if (!flag) {
PyErr_Format(PyExc_ValueError, "invalid b64_data");
return NULL;
}
unsigned char *dst = base64_decode((unsigned char *)src, len);
PyObject *data = PyBytes_FromString((const char *)dst);
free(dst);
return data;
}
static PyMethodDef methods[] = {
{"b64encode", (PyCFunction) b64encode, METH_O, NULL},
{"b64decode", (PyCFunction) b64decode, METH_O, NULL},
{NULL, NULL, 0, NULL}
};
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"my_base64",
"我们自己实现的 base64",
-1,
methods,
NULL, NULL, NULL, NULL
};
PyMODINIT_FUNC PyInit_my_base64(void) {
return PyModule_Create(&module);
}
以上我们就封装了一个解析 base64 字符串的模块,将其编译成扩展,然后来测试一下:
import base64
import my_base64
data = "这是一段很长的字符串,到底有多长我也不知道,总之就是非常长".encode("utf-8")
print(
base64.b64encode(data) == my_base64.b64encode(data)
) # True
b64_data = base64.b64encode(data)
print(my_base64.b64decode(b64_data) == data) # True
结果没有问题,那么速度呢?我们实现的和解释器自带的,哪个速度更快呢?
在解码方面势均力敌,但编码是我们赢了。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏