FF1 算法详解:参数、步骤与安全约束归纳总结
FF1 是目前应用最广泛的格式保持加密算法,没有之一。我最早接触它是在一个信用卡号脱敏项目里,需求是「加密后长度不变、字符集不变,还要能反向解密」。当时调研了一圈,最后选了 NIST SP 800-38G 里定义的 FF1。今天咱们就把它拆开揉碎了讲——参数、加密步骤、还有那些容易踩坑的安全约束。
1. 核心参数:你得先认清这五个
FF1 的参数长得有点啰嗦,但每个都不能少。我在一次方案评审里见过有人把 tweak 长度设成 0,结果合规检查直接不通过。下面这张表帮你理清脉络:
| 参数 | 类型 | 说明 | 我踩过的坑 |
|---|---|---|---|
| radix | 整数 (≥2) | 字符集基。数字用10,字母用26,字母数字混合用36 | 基设错会导致格式校验失败,我记得一个短信验证码项目用了16进制却设了radix=10,解密后全是乱码 |
| 密钥K | 字节串,128/192/256位 | AES 密钥。长度决定了安全强度 | 千万别用随机数生成替代密钥派生,我见过有人直接把用户密码当K传进去,立马被安全审计打回 |
| tweak T | 字节串 (0..2^64-1 字节) | 类似于 IV,提供上下文隔离 | 长度不够时用 0x00 补齐,但 tweak 本身可为空——不过我个人建议永远别用空 tweak,否则同一密钥加密相同明文会得出相同密文,数据泄露风险极大 |
| minlen | 整数 (≥2) | 最小明文长度。通常设为2 | 有次生产上明文长度为1,FF1直接抛异常,因为算法要求 n ≥ minlen |
| maxlen | 整数 (≤2^32-1) | 最大明文长度。受内存和性能限制 | 处理超长字段时(比如身份证号18位)没问题,但若达到几百上千字符,每个分组迭代会变慢,建议分段处理 |
关键点: radix、tweak 和 minlen 必须事前约定好,解密端一旦传错,结果全部不可逆。我通常会在配置中心用 JSON Schema 把这三个参数固化下来,上线前做 100 组正向+逆向测试。
2. 加密步骤:一个迭代的 Feistel 结构
FF1 本质上是一个经过调整的 Feistel 网络。你想想看,一个 Feistel 结构把明文分成左右两半,一轮一轮地做非线性变换——FF1 在此基础上加了 tweak 和 radix 的适配。步骤拆解如下:
- 分拆明文:将长度为 n 的明文(数字串或字符串)拆成左半 A(长度
u = floor(n/2))和右半 B(长度v = n - u)。 - 计算中间值:用
PRF(key, tweak + A_integer)生成一个伪随机数流,转成 radix 进制表示,长度等于 v。 - 更新右半:将上一步的伪随机数串与 B 按位模加(radix 下的加法),得到新的 B'。
- 交换左右:将 A 和 B' 交换,作为下一轮的输入。
- 重复 10 轮:FF1 固定迭代 10 轮。每轮使用不同的 round counter 参与 PRF 计算。
- 最终输出:将最后一轮的 A 和 B 拼接成加密后的字符串。
解密步骤刚好反过来——轮函数用同样的 PRF,但加减法互换。说白了就是把加密流程倒着跑一遍。我曾在一个安全 SDK 里实现过两端自动切换,核心逻辑不超过 30 行。
我的开发习惯: 在代码注释里用“S1 → S2 → ... → S6”标出以上六个阶段,这样同事 review 时一眼就能看出哪段对应哪个 Feistel 轮次。
3. 安全约束:这些红线不能碰
FF1 虽然经过了标准化,但使用不当照样会翻车。我记得有一篇专门指出了 FF1 在特定 radix 和 tweak 组合下的攻击面。下面是几条必须遵守的约束:
- 最小明文长度 ≥ 2:算法内部依赖左右拆分,如果
n=1则无法执行 Feistel 结构。NIST 标准直接规定minlen至少为 2。 - tweak 应具备唯一性:在同一个密钥下,尽量保证每个密文使用不同的 tweak。例如可以用记录 ID + 时间戳拼接。我曾经遇到过线上一个缓存键冲突,就是因为多个加密记录用了同一个静默 tweak(全是空字节),蜜文全部重复,排查了很久才发现。
- 禁止直接使用明文作为 tweak:这样做会破坏 FPE 的安全性基础,攻击者很容易构造选择明文攻击。
- Radix 上限 2^16:官方建议 radix ≤ 65536,否则内部算术运算可能溢出或性能骤降。一般场景下我们只用 10/16/36/62 这几个常用值。
- 密钥周期管理:FF1 的加密速度较慢(比 AES-GCM 慢一个数量级),所以密钥复用次数需要控制。我建议单个密钥加密的记录数不超过 10^8 条,并定期轮换。
⚠ 我曾经踩过的大坑: 在实现FF1的C库时,忽略了模加法中的进位处理。结果加密完美,但解密时某些边界字符(例如 radix=10 下数字 '9' 加 '1' 后应为 '0' 并进位到前一位)没有正确处理进位传递,导致解密后部分字符偏移了一位。从那以后,我要求每个FF1实现都必须通过 NIST 官方测试向量全量验证,一条都不能少。
// FF1 加密的伪代码(简化)
function FF1_Encrypt(key, tweak, radix, plaintext):
n = len(plaintext)
u = floor(n/2)
v = n - u
A = plaintext[0:u]
B = plaintext[u:n]
for i in range(10):
// 生成伪随机数流(转为 radix 进制,长度 v)
prf_output = AES_CMAC(key, tweak + int_to_bytes(A) + i)
prf_radix = bytes_to_radix(prf_output, radix, v)
// 模加法
B_new = (B + prf_radix) mod radix
// 交换
A, B = B_new, A
return A + B
总结: FF1 的参数决定了它能在哪些场景下安全使用,步骤清晰但实现细节(模加法、边界处理)容易被忽略。记住那条安全规则:tweak 需要随机化、密钥不重用、minlen 不小于 2。我每次做 FF1 的集成测试都会用 NIST 官方测试向量跑一遍,确保加密解密对 10 万条以上数据无误。