3. FF1 算法详解:标准规范、参数定义、加密与解密过程、边界条件处理
FF1 算法,说白了就是 NIST 专门为格式保留加密定制的 Feistel 网络方案。我第一次接触它,是在做一个银行卡号脱敏项目——客户要求加密后卡号还得是 16 位数字,而且前 6 位和后 4 位保持明文。传统的 AES 一加密,数据全乱套了,根本没法用。后来查了半天,发现 FF1 正好解决这个问题。
它的核心思路其实不复杂:用 Feistel 网络把 AES 和取模运算组合起来,保证加密后的字符集跟输入一模一样。你想想看,普通的加密算法哪管你什么字符集,吐出来全是乱码。FF1 却能让你加密完的身份证号还是 18 位,银行卡号还是 16 位数字。
3.1 标准规范:NIST SP 800-38G
FF1 写在了 NIST SP 800-38G 标准里。这套标准 2016 年正式定稿,但算法本身在 2010 年左右就由 Voltage Security 提出来了。我个人习惯把 SP 800-38G 当成 FPE 领域的"核心参考书"——里面的 FF1 和 FF3 是两个主力算法。
标准里明确了几个要点:
- FF1 基于 Feistel 网络结构,轮数固定为 10 轮
- 底层加密原语使用 AES(128/192/256 位密钥)
- 支持任意字符集,通过 radix 参数控制
- 提供可选的 Tweak 参数,用于区分不同的加密上下文
我在项目里翻标准的时候,发现一个容易忽略的细节:FF1 的轮函数里用到了 模幂运算,这个在标准附录里有专门的实现算法。如果你自己手写实现,千万别图省事用 Math.pow(),精度会出问题。
3.2 参数定义
FF1 的参数其实不多,但每个都很关键。我整理了一张表,方便你对照:
| 参数 | 符号 | 说明 | 约束 |
|---|---|---|---|
| 密钥 | K | AES 密钥 | 128 / 192 / 256 位 |
| 调整值 | T | Tweak,公开的字节串 | 长度 0 ~ 2³²-1 字节 |
| 明文 | X | 待加密字符串 | 长度 n ≥ 2 |
| 字符集基数 | radix | 如数字 10,字母数字 36 | 2 ≤ radix ≤ 2¹⁶ |
| 分组大小 | u, v | u = floor(n/2), v = n - u | u + v = n |
我记得第一次调 FF1 的时候,忘了限制 radix 的范围,传了个 100 进去,结果算法直接崩了。后来一查标准,radix 必须 ≤ 65536,而且 radixn ≥ 100 这个条件很多人都会忽略。
3.3 加密过程
下面这张图展示了 FF1 加密的完整流水线。我建议你先把图看一遍,再读后面的步骤,会清晰很多。
加密的具体步骤如下:
- 拆分明文:将明文 X 拆分为 A 和 B,长度分别为 u = floor(n/2) 和 v = n - u
- 编码 Tweak:将 Tweak T 转换为固定格式的字节串,作为轮函数的额外输入
- 执行 10 轮 Feistel 变换:
- 构造轮输入:将 B、轮序号 i、Tweak 编码拼接成 16 字节块
- 用 AES 加密,得到 16 字节密文
- 将加密结果转换为整数,对 radix 取模
- 更新 A = B, B = (A + mod_result) % radixv
- 输出密文:最终 Y = A || B
举个例子,加密 16 位数字卡号:n=16, radix=10, u=8, v=8。前 8 位作为 L₀,后 8 位作为 R₀,经过 10 轮迭代后合并。我在项目中遇到过性能问题:每轮都要做一次 AES 加密,数据量大的话开销不小。后来用了 AES-NI 指令集加速,性能提升了将近 4 倍。
3.4 解密过程
FF1 的解密和加密是高度对称的。说白了,就是把加密流程倒过来走一遍,轮函数本身不用改。我习惯用一个比喻:加密是"正着转 10 圈",解密就是"反着转 10 圈",齿轮本身没有变。
解密步骤对比加密:
| 步骤 | 加密 | 解密 |
|---|---|---|
| 输入 | 明文 X | 密文 Y |
| 拆分 | X 拆为 A, B | Y 拆为 A, B |
| 轮序 | i = 0 → 9 | i = 9 → 0 |
| 轮函数 | AES(B || i || T) | AES(B || i || T) — 完全相同 |
| 更新规则 | A = B, B = (A + d) % r | A = B, B = (A - d) % r |
| 输出 | A || B | A || B |
注意更新规则里,加密用的是加法,解密用的是减法(取模减法)。这个差别很容易搞反——我曾经在一个项目里调试了整整一下午,最后发现就是把加号写成了减号。
3.5 边界条件处理
边界条件往往是坑最多的地方。我在几个项目里踩过的雷,帮你总结出来了:
- 最小长度限制:n 必须 ≥ 2,且 radixn ≥ 100。如果 n=1,不管 radix 多大都不行。我建议在接口层做校验,提前拦截非法输入。
- Tweak 长度:标准允许 0 到 2³²-1 字节,但实际实现中建议控制 ≤ 256 字节。太长会影响性能——每轮都要把 Tweak 编进轮输入里。
- 字符集映射:需要建立字符到数字的双向映射表。例如身份证号包含数字和 X,radix=11。映射表必须是一一对应、满射的,不能有重复。
- 空字符串处理:n=0 时算法未定义。我曾经接手过一个遗留系统,传了空字符串过去,底层 C 库直接段错误。后来加了个前置判断才解决。
总结一下 FF1 的使用要点:
- radixn ≥ 100 是硬约束,输入前必须检查
- 解密时轮序反转,加/减运算互换
- Tweak 用于区分上下文,同一密钥下不同场景用不同 Tweak
- 字符集映射表必须是一一对应且满射的