为什么是 64?数学基础
64 的选择不是任意的。它是对应字符数能容纳在可打印 ASCII 范围(95 个字符,编码 32–126)内的最大 2 的幂:
| 基数 | 每字符位数 | 所需字符数 | 开销 | 适合可打印 ASCII? |
|---|---|---|---|---|
| 16 | 4 | 16 | 100% | 是 |
| 32 | 5 | 32 | 60% | 是 |
| 64 | 6 | 64 | 33.3% | 是 |
| 128 | 7 | 128 | 14.3% | 否(仅 95 个可用) |
Base64 实现了最优权衡:在保持使用普遍安全的可打印字符的同时,最大化信息密度。Base85(Ascii85)通过使用 85 个字符将 4 字节表示为 5 个字符(25% 开销)进一步压缩,但它需要在许多协议中不安全的字符。
RFC 4648:规范标准
RFC 4648(2006)取代 RFC 3548,定义了规范的 Base16、Base32 和 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 l 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 /
填充:=
编码算法
编码器以 3 字节(24 位)为一组处理输入:
输入字节: [byte₀] [byte₁] [byte₂]
位布局: XXXXXXXX YYYYYYYY ZZZZZZZZ
拆分为 6 位组:
char₀ = XXXXXX (byte₀ >> 2)
char₁ = XXYYYY ((byte₀ & 0x03) << 4) | (byte₁ >> 4)
char₂ = YYYYZZ ((byte₁ & 0x0F) << 2) | (byte₂ >> 6)
char₃ = ZZZZZZ (byte₂ & 0x3F)
当输入长度不是 3 的倍数时:
| 剩余字节 | 输出字符 | 填充 |
|---|---|---|
| 0 | 0 | 无 |
| 1 | 2 + == |
char₁ 中 2 位未使用 |
| 2 | 3 + = |
char₂ 中 4 位未使用 |
填充语义
填充不是装饰。它携带信息:告知解码器最后一个量子是 1 字节还是 2 字节。RFC 4648 §3.2 规定标准 Base64 必须添加填充,但指出在输入长度已知的上下文中可以省略(如 Base64URL 通常所做的)。
import base64
# 标准:始终填充
base64.b64encode(b"A") # b'QQ==' (1 字节 → 2 字符 + ==)
base64.b64encode(b"AB") # b'QUI=' (2 字节 → 3 字符 + =)
base64.b64encode(b"ABC") # b'QUJD' (3 字节 → 4 字符,无填充)
# URL 安全无填充(JWT 中常用):
base64.urlsafe_b64encode(b"A").rstrip(b'=') # b'QQ'
BaseN 编码族
Base16(十六进制)
每字节 2 个十六进制字符。简单但 100% 开销。
输入: 0xDE 0xAD 0xBE 0xEF
输出: "DEADBEEF"
用于:十六进制转储、颜色代码、密码学哈希、MAC 地址。
Base32(RFC 4648 §6)
每 5 字节输出 8 个字符(40 位 ÷ 5 位/字符)。使用 A–Z 和 2–7(避免 0/O/1/I 混淆):
字母表:ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
开销:60%
用于:TOTP 密钥(Google Authenticator)、洋葱地址(Tor v3 使用 Base32 编码的 ed25519 密钥)、要求大小写不敏感的文件系统。
Base64(RFC 4648 §4)
每 3 字节输出 4 个字符。主力编码。
字母表:A-Za-z0-9+/
填充: =
开销: 33.3%
Base64URL(RFC 4648 §5)
URL 和文件名安全变体:
字母表:A-Za-z0-9-_
填充: 可选(通常省略)
用于:JWT、URL 查询参数、文件名。
Base85 / Ascii85
每 4 字节输出 5 个字符(32 位,因为 85⁵ > 2³² > 85⁴):
开销:25%
字母表:ASCII 33–117(! 到 u)
变体:btoa(原始)、Adobe Ascii85、Z85(ZeroMQ)、RFC 1924(IPv6)
用于:PostScript/PDF 流、Git 二进制补丁、ZeroMQ。
比较
| 编码 | 位/字符 | 开销 | 字母表安全性 |
|---|---|---|---|
| Base16 | 4 | 100% | 十六进制,到处安全 |
| Base32 | 5 | 60% | 大小写不敏感安全 |
| Base64 | 6 | 33.3% | URL 中需要转义 |
| Base64URL | 6 | 33.3% | URL/文件名安全 |
| Base85 | ~6.4 | 25% | 包含 shell 元字符 |
流式编码器与解码器
状态机问题
Base64 编码每次处理 3 个字节,但输入以任意块到达(网络缓冲区、文件读取)。流式编码器必须在调用之间维护状态:
class StreamingBase64Encoder:
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
def __init__(self):
self._buffer = bytearray()
def update(self, data: bytes) -> str:
self._buffer.extend(data)
output = []
while len(self._buffer) >= 3:
b0, b1, b2 = self._buffer[0], self._buffer[1], self._buffer[2]
del self._buffer[:3]
output.append(self.ALPHABET[b0 >> 2])
output.append(self.ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)])
output.append(self.ALPHABET[((b1 & 0x0F) << 2) | (b2 >> 6)])
output.append(self.ALPHABET[b2 & 0x3F])
return ''.join(output)
def finalize(self) -> str:
if not self._buffer:
return ''
b0 = self._buffer[0]
output = [self.ALPHABET[b0 >> 2]]
if len(self._buffer) == 1:
output.append(self.ALPHABET[(b0 & 0x03) << 4])
output.append('==')
else:
b1 = self._buffer[1]
output.append(self.ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)])
output.append(self.ALPHABET[(b1 & 0x0F) << 2])
output.append('=')
self._buffer.clear()
return ''.join(output)
流式解码器
解码器每次处理 4 个字符,必须处理空白字符(MIME 允许编码数据中的换行):
class StreamingBase64Decoder:
DECODE_TABLE = {c: i for i, c in enumerate(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
)}
def __init__(self):
self._buffer = []
def update(self, text: str) -> bytes:
output = bytearray()
for c in text:
if c in (' ', '\n', '\r', '\t'):
continue
if c == '=':
self._buffer.append(-1)
elif c in self.DECODE_TABLE:
self._buffer.append(self.DECODE_TABLE[c])
else:
raise ValueError(f"无效 Base64 字符: {c!r}")
if len(self._buffer) == 4:
v0, v1, v2, v3 = self._buffer
self._buffer.clear()
output.append((v0 << 2) | (v1 >> 4))
if v2 != -1:
output.append(((v1 & 0x0F) << 4) | (v2 >> 2))
if v3 != -1:
output.append(((v2 & 0x03) << 6) | v3)
return bytes(output)
MIME 与 PEM:行折叠的 Base64
MIME(RFC 2045)
邮件附件使用强制 76 字符行折叠的 Base64:
Content-Transfer-Encoding: base64
SGVsbG8sIFdvcmxkIQ0KVGhpcyBpcyBhIHRlc3QgbWVzc2FnZSB0aGF0IGlz
IGxvbmcgZW5vdWdoIHRvIHdyYXAgYWNyb3NzIG11bHRpcGxlIGxpbmVzLg==
76 字符限制加 CRLF 在 33.3% 编码开销之上额外增加约 2.6%。
PEM(RFC 7468)
加密密钥和证书使用 PEM 格式:64 字符行的 Base64,包裹在标签边界中:
-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJALx0SqICBVLGMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMM
BnRlc3RDQTAEFW0yNTA3MTkwMDAwMDBaFw0yNjA3MTkwMDAwMDBaMBExDzAN
BgNVBAMMBnRlc3RDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABC...
-----END CERTIFICATE-----
解析器必须剥离头部,拼接各行,然后解码拼接后的 Base64。
面向安全的常量时间解码
时序攻击向量
朴素 Base64 解码器使用查找表或分支:
// 存在时序侧信道漏洞:
int decode_char(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A'; // 分支 1
if (c >= 'a' && c <= 'z') return c - 'a' + 26; // 分支 2
if (c >= '0' && c <= '9') return c - '0' + 52; // 分支 3
if (c == '+') return 62; // 分支 4
if (c == '/') return 63; // 分支 5
return -1;
}
分支预测时序可以泄漏正在解码的字符,在解码"先加密再 Base64 编码"的数据时可能泄漏明文。
常量时间实现
// 常量时间 Base64 解码(无数据相关分支):
static int ct_decode_char(unsigned char c) {
unsigned int val = 0;
// 每个比较都无条件运行
val |= ((unsigned int)(('A' - 1 - c) & (c - ('Z' + 1))) >> 8) & (c - 'A');
val |= ((unsigned int)(('a' - 1 - c) & (c - ('z' + 1))) >> 8) & (c - 'a' + 26);
val |= ((unsigned int)(('0' - 1 - c) & (c - ('9' + 1))) >> 8) & (c - '0' + 52);
val |= ((unsigned int)(('+' - 1 - c) & (c - ('+' + 1))) >> 8) & 62;
val |= ((unsigned int)(('/' - 1 - c) & (c - ('/' + 1))) >> 8) & 63;
return (int)val;
}
实现常量时间 Base64 的库:libsodium(sodium_bin2base64)、OpenSSL(EVP_EncodeBlock)和 Go 的 encoding/base64(自 Go 1.12 起使用常量时间查找表)。
性能:SIMD 加速
查找表 vs 算术
传统实现使用 256 字节查找表。SIMD 实现(SSE2、AVX2、NEON)每条指令处理 12–48 字节:
方法 | 吞吐量 (x86-64) | 技术
-----------------------|-----------------|-------
标量查找表 | ~800 MB/s | 256 字节 LUT
SSE2 向量化 | ~3.5 GB/s | 并行 6 位提取
AVX2 向量化 | ~7 GB/s | 32 字节通道
关键洞察:Base64 字母表映射可以分解为对 6 位值的算术运算,这些运算能很好地向量化:
// 伪 SIMD:同时编码 12 字节 → 16 字符
for 并行的每个 6 位值 v:
if v < 26: char = v + 'A'
elif v < 52: char = v - 26 + 'a'
elif v < 62: char = v - 52 + '0'
elif v == 62: char = '+'
else: char = '/'
相关库:Turbo-Base64(C)、base64-simd(Rust)、Node.js Buffer(自 v16 起内部使用 SIMD)。
生产边界
JWT:无填充的 Base64URL
JWT 对所有三个段使用无填充的 Base64URL 编码:
Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ
Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
解码器必须在解码前补回填充,或使用对填充无感的解码器:
import base64
def base64url_decode(s: str) -> bytes:
# 添加填充:Base64 输出长度始终是 4 的倍数
padding = 4 - len(s) % 4
if padding != 4:
s += '=' * padding
return base64.urlsafe_b64decode(s)
Data URL:体积 vs 请求权衡
data:[<mediatype>][;base64],<data>
盈亏平衡点取决于 HTTP/2 多路复用和压缩:
- HTTP/1.1:< ~1 KB 的资产内联有利(避免连接开销)
- HTTP/2+:独立请求几乎免费;仅 < 200 字节的资产适合内联
- 关键渲染路径:无论大小都内联首屏 CSS/SVG 以消除阻塞渲染的请求
HTTP Basic Auth:不是安全边界
Authorization: Basic dXNlcjpwYXNzd29yZA==
↓ 解码
user:password
此处的 Base64 纯粹是格式化——它确保冒号分隔的凭据在 HTTP 头传输中存活。安全性完全来自 TLS。没有 TLS,凭据对任何网络观察者都是明文。
常见实现陷阱
1. Unicode → Base64 未显式编码
// BUG:btoa 无法处理码位 > 255
btoa("Hello 🌍") // 抛出:InvalidCharacterError
// 正确:先显式编码为 UTF-8
function toBase64(str) {
const bytes = new TextEncoder().encode(str);
const binary = Array.from(bytes, b => String.fromCharCode(b)).join('');
return btoa(binary);
}
2. 混淆 Base64 与 Base64URL
import base64
token = "eyJhbGciOi..." # 来自 JWT
# BUG:标准解码器拒绝 '-' 和 '_'
base64.b64decode(token) # binascii.Error
# 正确:使用 URL 安全解码器
base64.urlsafe_b64decode(token + '==')
3. 忽略非字母表字符
# 危险:静默忽略垃圾
base64.b64decode("SGVs\x00bG8=") # 某些实现会成功
# 安全:严格验证
base64.b64decode("SGVsbG8=", validate=True) # 拒绝非字母表字节
4. 大输入导致内存耗尽
# 对不可信输入危险:
decoded = base64.b64decode(user_provided_string) # 可能是数 GB
# 安全:解码前强制大小限制
MAX_ENCODED_SIZE = 10 * 1024 * 1024 # 10 MB 编码 ≈ 7.5 MB 解码
if len(user_provided_string) > MAX_ENCODED_SIZE:
raise ValueError("输入超过最大允许大小")
参考文献
- RFC 4648 — The Base16, Base32, and Base64 Data Encodings(Josefsson, 2006)
- RFC 2045 — MIME Part One: Format of Internet Message Bodies(§6.8 Base64 Content-Transfer-Encoding)
- RFC 7468 — Textual Encodings of PKIX, PKCS, and CMS Structures(PEM 格式)
- RFC 7515 — JSON Web Signature (JWS) — 定义 JWT 中的 Base64URL 用法
- Langley, A. (2019). "Constant-Time Character Classification" — 时序安全实现
- Muła, W. & Lemire, D. (2018). "Faster Base64 Encoding and Decoding Using AVX2 Instructions" — SIMD 技术