第三章 FF1 算法的参数、符号与加解密流程

讲真,搞懂 FF1 算法的第一步,就是把那堆参数和符号记扎实。我最早看规范时也觉得符号有点多,后来在项目里踩了几次坑才明白——每个符号背后都有讲究。这一章咱们就把它掰开揉碎,然后把加密解密的完整流程过一遍。

3.1 算法参数

FF1 需要四个核心参数,少了哪个都不行。你想想看,这四个参数决定了你的格式空间有多大、怎么加解密。

参数含义典型取值
radix字符集的基数10(数字)、16(十六进制)、26(字母)
key加密密钥128 / 192 / 256 位 AES 密钥
tweak调整值(公开参数)长度建议 8~16 字节
plaintext/ciphertext明文/密文字符串长度 ≥ radixminLen 且 ≤ maxLen

核心观点:radix 决定了每个位置有多少种可能。数字 radix=10,大写字母 radix=26,全数字+字母 radix=36。我自己做社保卡号加密项目时就犯过这个错——把 radix 设成了 10,结果客户需要保留字母,一跑就崩。后来老老实实按实际字符集来。

3.2 符号定义

规范里一堆拉丁字母和希腊符号,别被吓到。说白了就是几个关键记号:

  • n — 明文字符串长度
  • t — tweak 长度(字节)
  • u = ⌊n/2⌋ — 左半部分长度
  • v = n - u — 右半部分长度
  • A, B — 左右两个子串
  • P, Q, R — 中间计算变量

∑ 和 Δ 符号在 Feistel 轮函数里频繁出现。我记得第一次看规范的伪代码时,被 NUM 和 STR 两个转换函数绕了半天——其实就是一个把字符串转成整数,一个把整数转回字符串,base 就是 radix。

提示:个人习惯把符号定义做成一张速查表贴在办公室白板上。你开发时随时扫一眼,省得来回翻规范。

3.3 加密流程

FF1 用的是 8 轮 Feistel 网络。每一轮分成左半和右半,右半经过轮函数 F 加密后与左半异或(这里的异或是按 数值加减模 radixu 操作,不是比特异或)。

  1. 分割:把明文 X 分成左 A(前 u 个字符)和右 B(后 v 个字符)。
  2. 迭代:执行 8 轮,每轮:
    • 计算 P = [ key 派生出来的块 + 轮数 + tweak 相关信息 ]
    • 计算 Q = NUM(B) 等中间值
    • 调用 AES(或 CMAC)生成 R
    • 用 R 更新 A 和 B
  3. 合并:最后 A || B 就是密文。

警告:轮数固定 8 轮,不能瞎改。我曾经试过减到 6 轮想省性能,结果安全性评估直接挂了。NIST 设定这个数字是有道理的。

3.4 解密流程

解密就是加密的逆过程,注意轮函数和密钥顺序是相反的:

  1. 把密文分割成左 A 和右 B(与加密时相同)。
  2. 从第 8 轮开始向下迭代到第 1 轮,每轮用的密钥派生值和加密时相反。
  3. 最后合并得到原始明文。

嗯,这里要注意:解密时左右交换的逻辑和加密正好镜像。我在写代码时直接把加密的流程倒过来,结果最后两位总是不对——仔细排查才发现轮函数里有一个 tweak 的计数器方向搞反了。

3.5 完整加解密流程图

FF1 加解密流程图(Feistel 结构) 加密 明文 X 分割 A | B 8 轮 Feistel 轮函数含 AES-ECB & 模运算 tweak 每轮参与 合并 A || B 密文 Y 解密 密文 Y 分割 A | B 反向 8 轮 Feistel 轮密钥顺序逆序 tweak 需与加密一致 合并 A || B 明文 X 密钥派生和 tweak 在两分支中保持对称,否则解密结果无效

3.6 轮函数细节(简版)

轮函数 F 是 FF1 的心脏,可以拆成三步:

  • 构造输入块:把当前轮号、tweak、右边子串 B 的数值拼成 16 字节输入。
  • AES 加密:用 AES-ECB 模式加密这个块,得到 16 字节输出。
  • 数值模运算:把 16 字节输出转成整数,然后对 radixv 取模,再转回字符串加到左半上。

个人经验:最开始我用 OpenSSL 的 AES-ECB 接口,发现输出总是多两字节。仔细一看,规范要求的是「原始 AES 加密」而不是带填充的 ECB—数据块正好 16 字节,不需要填充。去掉填充选项后,结果就对了。

3.7 常见踩坑点

我把这些年在 FF1 实现中遇到的坑列一下,你写代码时多留个心眼:

  • tweak 长度不一致:加密和解密用的 tweak 必须完全相同,有位差异都会导致解密失败。我曾经在数据库里存 tweak 时把最后一位截断了,查了两天才找到原因。
  • radix 与字符集不匹配:radix 必须能覆盖输入字符里的所有可能值。比如你允许输入小写字母,但 radix 设成 26(只支持大写),那就等着解密异常吧。
  • 明文长度上限:FF1 要求 n ≤ maxLen(由 radix 和 AES 密钥长度决定)。超出时算法会断言失败,不能自动截断。
  • 多线程安全问题:密钥和 tweak 是共享的,但每个加解密实例内部状态独立。我一开始没注意,多线程并发跑时轮函数用到同一个临时缓冲区,结果互相覆盖。后来用线程局部存储解决。

3.8 小结

这一章我们把 FF1 的骨架搭起来了。记住四个参数、六个符号、八轮流程。下一节咱们会用实际代码把这些流程落下来——不过那是下一章的事了。你先消化好本章的内容,特别是那张流程图,理解加密和解密是怎样镜像对称的。

最后啰嗦一句:ff1 的数学公式看着复杂,但实现时百分之九十的 bug 都出在模运算的边界上。写代码时多写几个单元测试,用 NIST 的测试向量验证,能省下大量 Debug 时间。