2003 年的 RFC 3548 第一次把 Base64、Base32、Base16 写成一份通用传输编码规范。三年后的 RFC 4648 取代了它,文本主体延续,但几处关键改动影响了后来编解码器的行为边界。下面按 RFC 4648 的原文结构逐节读,标清每一句“标准规定”对应的节号,以及 MUST、SHOULD、MAY 各自规定了什么、允许了什么。

一、RFC 3548(2003)第一次把 Base64/Base32/Hex 写成通用传输编码

RFC 3548 之前,Base64 的权威定义散落在 MIME(RFC 2045)等应用层协议里,每个协议自己抄一段字母表、自己规定填充行为。RFC 3548 做的是收拢:把 Base16、Base32、Base64 三种编码放进同一份文档,统一给出字母表、分组规则和填充机制。

但 3548 有一个当时没被充分意识到的遗留问题:它没有明确引用 RFC 2119 的关键词约定。这意味着文档里出现的 “must”、“should” 大小写混杂,读者无法确定哪些是真正的互操作性硬约束,哪些只是行文习惯。这个问题在 4648 里被修正了。

二、RFC 4648 为什么取代 3548:术语、字母表和命名

RFC 4648 在 §1 和 §13 “Changes Since RFC 3548” 里说明了改版原因,主要有三条。

术语强度。 3548 没有按 RFC 2119 标注 MUST/SHOULD/MAY,实现者面对 “padding may be omitted” 这类句子时,分不清是允许还是建议。4648 在 §1.1 声明:“The key words ‘MUST’, ‘MUST NOT’, ‘SHOULD’, ‘SHOULD NOT’, and ‘MAY’ in this document are to be interpreted as described in RFC 2119.” 规范里的义务因此有了明确的等级。

base32hex 字母表缺失。 3548 只有一套 Base32 字母表(A-Z + 2-7)。某些场景需要编码后的字符串保持与原始字节相同的排序顺序,标准表做不到。4648 §7 新增了 “Extended Hex” 字母表(0-9 + A-V),选择它作为索引时,编码结果的字典序与输入字节的数值序一致。这是 4648 相对 3548 最实质的扩展。

base64url 的正式命名。 3548 时代,URL 安全变体已经在非正式使用,但没有标准名称。4648 §5 正式定义了 “base64url”,规定用 - 替代 +、用 _ 替代 /,并明确说 “This encoding may be referred to as ‘base64url’”。有了标准术语,实现者才有一个共同的锚点去讨论和测试。

三、标准里的关键定义:base 64 与 base 64url 两套字母表、canonical encoding 与 encoded octet stream

RFC 4648 §4 定义了 base 64 字母表,§5 定义了 base 64url。两者索引 0–61 完全一致,只在索引 62 和 63 处分叉:

  • 索引 62: base 64 用 +,base 64url 用 -
  • 索引 63: base 64 用 /,base 64url 用 _

§3.5 “Canonical Encoding” 是整份文档里最容易被跳过的技术核心。它规定:当输入字节数不是 3 的倍数时,编码器在末尾补零形成完整的 6-bit 组,但这些补的零位 MUST 被置为 0。这个约束的意义在于:如果补位可以是任意值,同一段二进制数据就会产生多个不同的 Base64 字符串,解码回来却都是同一份数据。canonical encoding 保证了“一个字节序列 ↔ 一个编码字符串”的双向唯一映射。

拿一段标准输出在 Base64 转 Hex 里解回十六进制,就能看到 §3.5 说的补零位置。

§1.3 还定义了 “encoded octet stream” 这个概念:编码的输出被视为一个八位组流,但其中每个字节都限制在 US-ASCII 可打印范围内。这解释了为什么 Base64 可以安全穿过邮件、URL、JSON 这些对字节值有过滤的通道。

四、§3.5 与 §4:填充“=”允许省略,解码端“SHOULD”忽略多余换行

关于填充,RFC 4648 的措辞值得逐字读。§3.5 说:“Implementations MUST include appropriate pad characters at the end of encoded data unless the specification referring to this document explicitly states otherwise.” 默认规则是编码器必须加填充。省略填充不是 Base64 本身的自由,而是必须由引用 4648 的某个上层规范来明确豁免。

解码端的措辞则是另一回事。§3.3 说解码器 “MAY reject” 非零补位的编码,但如果不拒绝,就必须正确处理。而关于填充字符本身,§3.5 的表述是解码器 SHOULD 在必要时接受省略填充的输入。SHOULD 意味着“有理由可以不这么做,但你需要理解后果”。一个严格的解码器可以拒绝无填充输入,只要它声明了这一点。

关于换行:§3.3 没有要求 MUST 忽略所有非字母表字符。原文说的是解码器 SHOULD 忽略换行符(CRLF),但对字母表外其他字符的处理没有 MUST 级别的统一要求。这给实现留了一个口子:有的解码器遇到 $ 直接报错,有的静默跳过。在 Base64 在线编码解码 里输入一段带换行的 Base64,可以看实现是否带填充、是否容忍换行。用 JSON 转 Base64 做同一件事,载体不同,但填充策略是同一个实现决定。

五、§6 安全问题:为什么 RFC 自己写明白“这不是加密”

§6 开篇第一句就是:“Base encoding is not encryption.” 后面接着解释:Base64 只做表示层转换,不提供任何机密性;把敏感数据 Base64 之后传输,和明文传输没有安全区别。

§6 还提了一个具体风险:canonical encoding 不成立时,攻击者可以构造多个不同的 Base64 字符串,解码后得到同一份数据。如果某个系统把 Base64 字符串当作唯一标识(比如用作缓存键或权限令牌的一部分),就可能出现绕过。这和第 3.5 节的补零 MUST 要求有关。

另一个实践提醒:base64url 的 - 和 _ 在正则表达式、Shell 变量、文件名里通常不需要转义,但标准表的 + 和 / 在 URL 查询串里会被解释为空格或路径分隔符。一段包含 - 或 _ 的串,通常是 URL 安全变体;包含 + 或 / 的,是标准表。拿不准时,在 Base64 转文件在线工具 里两种都试一次,看哪一种能还原出可读文件,是最直接的验收方式。

六、对开发者的实际影响:严格模式、大小写、非法字符处理都留给实现决定

RFC 4648 用 MUST 规定了编码器的核心行为(补零、字母表、分组),但对解码器的宽容度留了大量 MAY 和 SHOULD 的空间。具体来说:

  • 大小写: Base64 字母表本身区分大小写(a 和 A 索引不同),但 §3.4 说解码器 MAY 接受大小写不敏感的输入。多数实现选择不接受,因为那会破坏双向映射。
  • 非法字符: 除换行外的字母表外字符,4648 没有给出 MUST 级别的统一指令。严格解码器直接拒绝,宽松解码器跳过。两者都可以自称“符合 RFC 4648”,只要行为是自洽的。
  • 填充省略: 编码器默认 MUST 加填充;解码器 SHOULD 接受省略填充的输入。这意味着一个“标准”的编码器输出,交给另一个“标准”的解码器,通常能工作;但如果你自己写编码器省略填充,不能指望所有解码器都接受。

这就是读原始 RFC 的价值:它规定的是行为,不规定用哪个库。选择一个库的严格模式或宽松模式时,你能在规范层面看到自己放弃了什么、保留了什么。

速查小结: 3548 建立了三合一的编码框架,4648 补齐了关键词强度、base32hex 扩展和 base64url 命名。补零 MUST 是 canonical encoding 的根基。填充由编码器 MUST 加,解码器 SHOULD 接受省略。换行解码器 SHOULD 忽略,其他非法字符留给实现。§6 明说不是加密,安全边界不在编码层。