FF3 算法参数详解:密钥、Tweak、基数与长度要求
好了,咱们现在正式进入 FF3 的核心地带。前面聊了 FPE 的概念和 Feistel 网络的骨架,但真正让 FF3 跑起来的,是这四个参数:Key(密钥)、Tweak(调柄)、Radix(基数)以及明文/密文的长度。
说白了,FF3 就是个“带着镣铐跳舞”的加密算法。它既要保证安全,又得让输出格式和输入一模一样。这四个参数,就是那副“镣铐”的具体尺寸。
我在做金融系统字段加密的时候,被这些参数坑过不止一次。你想想看,参数配错了,整个系统跑出来的数据都是废的,那叫一个酸爽。今天咱们就一个一个捋清楚。
4.1 Key(密钥):128位的定海神针
FF3 的密钥固定是 128 位,也就是 16 个字节。这个长度是标准写死的,你不能多给,也不能少给。为什么呢?
因为 FF3 底层用的是 AES-128 加密,AES-128 的轮数、密钥编排都是为 128 位量身定做的。你用 192 位或者 256 位的密钥,标准就不认了。
核心要点:不要尝试用 256 位密钥来“增强” FF3 的安全性。FF3 的安全性依赖于底层的 AES 分组和 Feistel 轮数,不是密钥越长就越安全。反而可能因为不符合标准,导致算法实现不兼容。
我记得早些年在做一个数据库列加密的项目,项目经理非要我用 256 位密钥,觉得这样“更保险”。我跟他说了标准限制,他不信。好家伙,他自己查了三天资料,最后还是改回 128 位。嗯,我那时就想,有些坑真的是必须自己踩一次才记得住。
密钥的生成也是有讲究的。我个人习惯用安全的随机数生成器来生成,不会用 password 或者简单的哈希字符串。在 Java 里,我会用 SecureRandom;在 Python 里,就是 os.urandom(16)。
# Python 示例:生成一个合法的 128 位密钥
import os
key = os.urandom(16) # 返回 16 字节的随机字节串
print(key.hex()) # 输出类似:c9a7b3d8e1f04a2b5c6d7e8f90123456
一个小技巧:生产环境中,密钥一定要存储在专门的密钥管理服务(KMS)或硬件安全模块(HSM)中。你想想看,如果密钥硬编码在代码里,那加密就等于白做了。
4.2 Tweak(调柄):让相同明文产生不同密文
Tweak 这个词,翻译成“调柄”有点拗口,你就理解成一个公开的、可变的辅助参数。
它的长度为 64 位,也就是 8 个字节。这是 FF3 和 FF1 的一个重要区别——FF1 的 Tweak 长度可变,而 FF3 固定为 64 位。
那这个 Tweak 有什么用呢?
你想想看,假设你用同一个密钥加密一个身份证号“110101199001011234”。不管加多少次,结果都一样。这叫确定性加密,在某些场景下是好事,但在很多场景下,这是个灾难。
- 场景一:数据库索引。如果每个字段都密文相同,攻击者就能通过频率分析猜出哪些记录是同一个人。
- 场景二:数据分片。如果你把密文当分片键,相同的明文永远落在一个分片上,导致热点问题。
这时候,Tweak 就派上用场了。你可以把记录的自增 ID、时间戳、或者字段在表中的列号当成 Tweak。这样一来,相同明文在不同位置,密文就完全不同了。
实用推荐:我个人习惯把 Tweak 设计为一个 64 位的整数。前 32 位存表 ID 或实体类型,后 32 位存行号。这样既保证了唯一性,又方便后期调试。
你可能会问:Tweak 是公开的,会不会不安全?
不会。因为 Tweak 是由 AES 加密后,再与 Feistel 网络中间值做异或运算的。即使攻击者知道 Tweak,也不知道密钥,所以还是无法恢复明文。但 Tweak 必须保证唯一性,同一个密钥下,永远不要用重复的 (Tweak, 明文) 组合。
我曾经踩过的坑:在一次项目中,我用了时间戳取模作为 Tweak。结果同一毫秒内生成了多条记录,Tweak 重复,密文也就重复了。幸好只是在测试环境发现的,不然线上数据就废了。记住:Tweak 必须保证唯一性,不能偷懒。
# Tweak 的构造示例(Python)
table_id = 0x00000001 # 4 字节,表标识符
row_id = 0x00000042 # 4 字节,行号
tweak = (table_id << 32) | row_id # 组合成一个 64 位整数
# 将整数转换为 8 字节的字节串
tweak_bytes = tweak.to_bytes(8, byteorder='big')
4.3 Radix(基数):定义字符集的“进制”
Radix 这个词,直译是“基数”或“进制”。它决定了你加密的字符集有多大。
比如:
- 数字:Radix = 10。字符集是 0-9,共 10 个字符。
- 小写字母:Radix = 26。字符集是 a-z。
- 大写字母:Radix = 26。字符集是 A-Z。
- 数字+大写字母:Radix = 36。字符集是 0-9, A-Z。
- 字母+数字(不区分大小写):Radix = 62。
FF3 标准规定 Radix 的最小值是 2,最大值是 2^16(65536)。在实际应用中,Radix 通常是 10、36 或 62。你想想看,谁会去加密一个二进制的 FPE?那还不如直接用 AES 呢。Radix 为 2 的时候,FPE 就退化成普通的块加密了,失去了保格式的意义。
| Radix 值 | 字符集示例 | 典型应用场景 |
|---|---|---|
| 10 | 0-9 | 身份证号、银行卡号、手机号 |
| 16 | 0-9, A-F | 十六进制颜色码、机器编码 |
| 26 | A-Z 或 a-z | 英文姓名缩写、车牌字母部分 |
| 36 | 0-9, A-Z | 订单号、优惠券码(不区分大小写) |
| 62 | 0-9, a-z, A-Z | 通用短码、访问令牌 |
Radix 直接影响了加密过程中数值编码的方式。FF3 内部会把字符映射成 0 到 (Radix-1) 的整数,然后进行算术运算。Radix 越大,每个字符携带的信息量就越多,但密文的计算复杂度也越高。
4.4 明文/密文的长度要求
这是 FF3 的一个硬约束,也是很多人容易搞错的地方。
FF3 要求:明文的长度必须至少为 2 * ceil(log_Radix(2^32))。听起来很绕是吧?
咱们用白话翻译一下:
- FF3 内部把明文分成左右两半,每一半的“数值”必须能塞进一个 32 位整数里。也就是说,每一半的最大值不能超过 2^32 - 1。
- 所以,每一半的字符数 n,必须满足:Radix^n <= 2^32。
- 因此,n <= floor(32 / log2(Radix))。
我直接给你列几个常见 Radix 下的最小长度吧,这样最实用:
| Radix | 最小明文长度 | 单一半最大字符数 |
|---|---|---|
| 10(数字) | 4 | 9 |
| 16(十六进制) | 4 | 8 |
| 26(小写字母) | 4 | 6 |
| 36(字母数字) | 4 | 6 |
| 62(大小写+数字) | 4 | 5 |
你可能会注意到,最小长度基本都是 4。但这是 最小 长度。标准没有规定最大长度,但因为 Feistel 网络的计算量随着长度线性增长,实际应用中一般不超过几百个字符。
避坑指南:我曾经在一个项目中,要加密一段 10 位的数字。Radix=10,最小长度要求是 4,10 肯定是够了。结果明文里有 2 位长度小于 2 的数字(比如“05”),我当时没做填充,直接报错了。坑就在这:FF3 要求每一半的数值必须有足够的“比特位”来表达,如果你的数据太短,需要做填充。
如果你要加密的字段长度小于最小长度,怎么办?
- 方法一:拼接一个固定的后缀(比如时间戳的哈希值),达到最小长度。解密后丢弃后缀。
- 方法二:使用 FF3-1 的变体(虽然标准里不推荐,但某些实现支持放宽约束)。
4.5 参数相互关系:一个简单的框架图
这张图把参数关系和流程串起来了。简单来说,Key 和 Tweak 是 Feistel 网络的“燃料”,Radix 决定了明文每个字符的“数值范围”,而长度约束则决定了能不能安全地拆分成左右两半去执行 Feistel 轮函数。
4.6 总结:参数配置的检查清单
好了,四个参数都讲完了。我给你整理一个检查清单,以后配置 FF3 的时候,直接对着看:
- Key:长度必须是 16 字节(128位)。用 KMS 管理,不要硬编码。
- Tweak:长度必须是 8 字节(64位)。必须保证在同一个密钥下唯一。
- Radix:范围 2~65536。根据你的字符集选择(10/26/36/62 最常见)。
- 明文长度:必须 ≥ 2 × ceil(log_Radix(2^32))。不够长就做填充。
- 密文长度:与明文长度完全相同。输出格式 100% 保真。
嗯,掌握了这四个参数,FF3 的骨架你就搭起来了。下一轮咱们要深入 Feistel 网络的内部,看看这些参数是怎么被“喂”进去,又是怎么搅和在一起的。