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 参数相互关系:一个简单的框架图

FF3 算法参数关系图 FF3 Feistel 网络 Key (128位) Tweak (64位) 明文(长度 ≥ minLen) 密文(长度 = 明文长度) Radix(基数) 决定字符集大小与最小长度 长度约束条件 minLen = 2 × ceil(log_Radix(2^32))

这张图把参数关系和流程串起来了。简单来说,Key 和 Tweak 是 Feistel 网络的“燃料”,Radix 决定了明文每个字符的“数值范围”,而长度约束则决定了能不能安全地拆分成左右两半去执行 Feistel 轮函数。

4.6 总结:参数配置的检查清单

好了,四个参数都讲完了。我给你整理一个检查清单,以后配置 FF3 的时候,直接对着看:

  1. Key:长度必须是 16 字节(128位)。用 KMS 管理,不要硬编码。
  2. Tweak:长度必须是 8 字节(64位)。必须保证在同一个密钥下唯一。
  3. Radix:范围 2~65536。根据你的字符集选择(10/26/36/62 最常见)。
  4. 明文长度:必须 ≥ 2 × ceil(log_Radix(2^32))。不够长就做填充。
  5. 密文长度:与明文长度完全相同。输出格式 100% 保真。

嗯,掌握了这四个参数,FF3 的骨架你就搭起来了。下一轮咱们要深入 Feistel 网络的内部,看看这些参数是怎么被“喂”进去,又是怎么搅和在一起的。