为什么是 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 编码。关键规范性要求:

标准字母表

code
值  字符    值  字符    值  字符    值  字符
 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 位)为一组处理输入:

code
输入字节:   [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 通常所做的)。

python
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% 开销。

code
输入:  0xDE 0xAD 0xBE 0xEF
输出:  "DEADBEEF"

用于:十六进制转储、颜色代码、密码学哈希、MAC 地址。

Base32(RFC 4648 §6)

每 5 字节输出 8 个字符(40 位 ÷ 5 位/字符)。使用 A–Z 和 2–7(避免 0/O/1/I 混淆):

code
字母表:ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
开销:60%

用于:TOTP 密钥(Google Authenticator)、洋葱地址(Tor v3 使用 Base32 编码的 ed25519 密钥)、要求大小写不敏感的文件系统。

Base64(RFC 4648 §4)

每 3 字节输出 4 个字符。主力编码。

code
字母表:A-Za-z0-9+/
填充:  =
开销:  33.3%

Base64URL(RFC 4648 §5)

URL 和文件名安全变体:

code
字母表:A-Za-z0-9-_
填充:  可选(通常省略)

用于:JWT、URL 查询参数、文件名。

Base85 / Ascii85

每 4 字节输出 5 个字符(32 位,因为 85⁵ > 2³² > 85⁴):

code
开销: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 个字节,但输入以任意块到达(网络缓冲区、文件读取)。流式编码器必须在调用之间维护状态:

python
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 允许编码数据中的换行):

python
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:

code
Content-Transfer-Encoding: base64

SGVsbG8sIFdvcmxkIQ0KVGhpcyBpcyBhIHRlc3QgbWVzc2FnZSB0aGF0IGlz
IGxvbmcgZW5vdWdoIHRvIHdyYXAgYWNyb3NzIG11bHRpcGxlIGxpbmVzLg==

76 字符限制加 CRLF 在 33.3% 编码开销之上额外增加约 2.6%。

PEM(RFC 7468)

加密密钥和证书使用 PEM 格式:64 字符行的 Base64,包裹在标签边界中:

code
-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJALx0SqICBVLGMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMM
BnRlc3RDQTAEFW0yNTA3MTkwMDAwMDBaFw0yNjA3MTkwMDAwMDBaMBExDzAN
BgNVBAMMBnRlc3RDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABC...
-----END CERTIFICATE-----

解析器必须剥离头部,拼接各行,然后解码拼接后的 Base64。

面向安全的常量时间解码

时序攻击向量

朴素 Base64 解码器使用查找表或分支:

c
// 存在时序侧信道漏洞:
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 编码"的数据时可能泄漏明文。

常量时间实现

c
// 常量时间 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 字节:

code
方法                   | 吞吐量 (x86-64) | 技术
-----------------------|-----------------|-------
标量查找表             | ~800 MB/s       | 256 字节 LUT
SSE2 向量化            | ~3.5 GB/s       | 并行 6 位提取
AVX2 向量化            | ~7 GB/s         | 32 字节通道

关键洞察:Base64 字母表映射可以分解为对 6 位值的算术运算,这些运算能很好地向量化:

code
// 伪 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 编码:

code
Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload:   eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ
Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

解码器必须在解码前补回填充,或使用对填充无感的解码器:

python
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 请求权衡

code
data:[<mediatype>][;base64],<data>

盈亏平衡点取决于 HTTP/2 多路复用和压缩:

  • HTTP/1.1:< ~1 KB 的资产内联有利(避免连接开销)
  • HTTP/2+:独立请求几乎免费;仅 < 200 字节的资产适合内联
  • 关键渲染路径:无论大小都内联首屏 CSS/SVG 以消除阻塞渲染的请求

HTTP Basic Auth:不是安全边界

code
Authorization: Basic dXNlcjpwYXNzd29yZA==
             ↓ 解码
             user:password

此处的 Base64 纯粹是格式化——它确保冒号分隔的凭据在 HTTP 头传输中存活。安全性完全来自 TLS。没有 TLS,凭据对任何网络观察者都是明文。

常见实现陷阱

1. Unicode → Base64 未显式编码

javascript
// 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

python
import base64

token = "eyJhbGciOi..."  # 来自 JWT

# BUG:标准解码器拒绝 '-' 和 '_'
base64.b64decode(token)  # binascii.Error

# 正确:使用 URL 安全解码器
base64.urlsafe_b64decode(token + '==')

3. 忽略非字母表字符

python
# 危险:静默忽略垃圾
base64.b64decode("SGVs\x00bG8=")  # 某些实现会成功

# 安全:严格验证
base64.b64decode("SGVsbG8=", validate=True)  # 拒绝非字母表字节

4. 大输入导致内存耗尽

python
# 对不可信输入危险:
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 技术