FPE 核心原理:Feistel 网络、块大小与 Tweak

这一章我们来啃 FPE 最核心的骨架。别看外面包装得花里胡哨,剥开看就三个事儿:

  • Feistel 网络结构 — FPE 的“发动机”
  • 块大小与字符集 — 决定了你能加密多长的数据
  • Tweak 的作用与设计原则 — 这是个“安全调味料”,用不好会翻车

Feistel 网络结构:FPE 的发动机

先问个问题:普通加密算法(比如 AES)为什么不能直接用?因为它们要求的输入是固定位数的二进制。而 FPE 要加密的是“信用卡号”、“身份证号”、“手机号”这种特定字符集的数据。你没法把一串 16 位数字直接塞进 AES 里,对吧?

Feistel 网络就是为了解决这个问题的。它最早用在 DES 里,后来被 FPE 借过来。核心思想很简单——把数据切成两半,轮番处理。

我举个简化例子。假设我们要加密一个 6 位数字“123456”:

  1. 分割:切成左半“123”和右半“456”。
  2. 轮函数:把右半“456”和密钥放进一个函数 F,算出个伪随机数,比如“789”。
  3. 异或/映射:把左半“123”和这个“789”做一次“加法”(其实是模10加法,因为数字集只有0-9),得到“802”。
  4. 交换:左半变成原来的右半“456”,右半变成新值“802”。
  5. 重复:再做第二轮……做够 8~10 轮。

最终的左右拼起来就是密文。整个过程是可逆的——解密时反向走一遍就行。

关键点:Feistel 网络能保证输入输出格式一致,因为它处理的是“位置”而不是“比特”。轮函数 F 必须是强伪随机函数,但 F 的输出可以映射到任意字符集上。

我做过一个项目,加密 18 位的统一社会信用代码。客户说“不能多一位也不能少一位”。用 Feistel 网络,把 18 位切成 9+9,每轮用 SM3 做哈希再映射到字符集——完美。

块大小与字符集:细粒度决定一切

FPE 不是只能处理整字节。你可以定义“块大小”为任意长度,只要它能被 Feistel 切分。

具体来说:

  • 字符集:比如数字 [0-9],大小是 10;英文字母 [a-zA-Z],大小是 52;字母数字混合 [0-9a-zA-Z],大小是 62。
  • 块大小:每个块包含的字符个数。比如加密一个 16 位银行卡号,块大小就是 16。
  • 比特位 vs 字符位:Feistel 工作在整数域上,不是二进制域。密钥和轮函数的输出要换算到字符集的模。

一个常见的坑:块大小必须是偶数。为什么?因为 Feistel 把块切成左右两半,两半长度必须相等。如果你要加密奇数个字符(比如 15 位手机号),有两种处理办法:

  • 用 FF1 算法(NIST SP 800-38G):支持奇数长度,内部做了平衡处理。
  • 或者自己补一位,加密完再截掉。我建议别自己造轮子——直接用 FF1。

注意:块大小不是可以无限大。太大会导致 Feistel 的轮数不够,安全强度下降。经验值是:字符集大小 × 块长度 对应的比特位数,应不小于 128 位。

我曾经在一个金融系统里看到他们用 FPE 加密 32 位数字串,没设 block size 上限,结果上线后性能爆炸。查了一下,32 位的 Feistel 网络要做 10 轮轮函数计算,每轮要哈希一个 32 字符的字符串……后来改成 8+8+8+8 分块加密,性能好了 10 倍。你想想看,块大小不是越大越好。

Tweak 的作用与设计原则

Tweak 是 FPE 特有的一把“盐”。它像 AES 的 IV,但更灵活。同一个密钥,用不同的 Tweak 加密相同的明文,会得到完全不同的密文。但 IV 是随机的,Tweak 可以是确定的——比如数据库行号、用户 ID、当前时间戳。

核心作用:

  • 提供上下文分离:加密用户 A 的身份证号和用户 B 的身份证号,用不同的 Tweak(比如用户ID),即使明文相同,密文也不同。
  • 绕过格式长度限制:Tweak 不影响密文长度,它只是 Feistel 网络的一个额外输入。
  • 防重放攻击:如果用交易流水号做 Tweak,攻击者没法把一个密文拿到另一个上下文里解密。

我的建议:Tweak 的长度至少 12 字节(96 位),熵够用。如果 Tweak 太短,例如只有 1 字节,那只有 256 种可能,穷举空间太小。

设计原则有哪些?我踩过几个坑,这里直接列给你:

原则 解释 反面案例
不可预测性 攻击者不能控制 Tweak 的取值 用自增 ID 做 Tweak,攻击者可枚举
唯一性 同一个密钥下,Tweak 不应重复 用当前日期做 Tweak,一天内重复
长度合理 太短丧失安全性,太长浪费算力 用 4 字节做 Tweak,熵不够
可复现 解密时要能重新构造相同的 Tweak 随机生成并且不存储,解密时没法再次拿到

特别提醒:Tweak 不能当密钥用。它只是“调味料”,密钥才是主料。我曾经接过一个代码 review,有人把 Tweak 设成和密钥一样……那就是双重加密,没什么意义,反而浪费了 Tweak 的设计优势。

一张图看懂 FPE 核心架构

FPE 核心原理:Feistel 网络 + 块大小 + Tweak 明文 (M) 密钥 (K) Tweak (T) 步骤1: 分割明文 → 左半 (L₀) 与右半 (R₀),块大小与字符集确定 Feistel 轮循环(通常 8~16 轮) 轮函数 F(R₀, K, T) L₁ = R₀ ; R₁ = L₀ ⊕ F → 重复执行直到第 n 轮 → 最后一轮后交换 得到 (Lₙ, Rₙ) 密文 (C)

避坑指南·经验谈

写到这里,我想起以前刚接触 FPE 时犯的一个错。我在一个支付系统里加密用户邮箱——邮箱地址包含 '@' 和 '.',字符集是 [a-zA-Z0-9.@_-]。我用的是 FF1,但忘记把字符集大小设对。默认的 FF1 实现假设字符集是 [0-9],大小 10。结果一跑,加密出的结果虽然格式对,但解密失败。查了一天,发现是字符集映射表没配好。

所以我的建议:

  • 字符集定义要按 ASCII 码排好顺序,不要跳字符。
  • 块大小必须能被 2 整除。如果非要奇数,老老实实用 FF1 或 FF3(NIST 标准)。
  • Tweak 的构造最好做成字符串拼接,比如 "user_"+userId+"_seq_"+sequence,这样可读性强,方便调试。
  • 轮数不能低于 8。我曾经偷懒设 4 轮,结果用 chosen-plaintext attack 两下就破了。教训惨痛。

小技巧:生产环境建议直接使用成熟的库,比如 Python 的 ff3 或 pyffx,别自己写 Feistel。标准实现的轮函数 HMAC-SHA256 已经经过经年累月的安全验证。自己写容易被侧信道攻击盯上。