密钥派生与变量分配:K、T、L、R
好,我们正式开始讲 FF1 算法里最绕不开的一块——键盘派生和变量分配。说实话,很多初学者栽就栽在这里。你想想看,主密钥直接拿去算?不行的,FF1 需要从主密钥 K 派生出若干子密钥,然后还要把输入 X 分成左右两半 L 和 R,再搭配一个可选调整值 T。这一章我们把这块彻底理清楚。
主密钥 K 与子密钥派生
FF1 的密钥派生过程,说白了就是用主密钥 K 和调整值 T 一起,通过一个伪随机函数(通常是 AES-CMAC 或 HMAC-SHA256)生成两个子密钥 K1 和 K2。为什么需要两个?因为每一轮 Feistel 结构里,轮函数 F 要用到两个不同的密钥来避免对称性。我接触的第一个 FF1 项目里,有人偷懒只用一个密钥,结果测试向量全挂……后来 debug 到半夜才找到原因。
派生的通用步骤很简单:
- 将主密钥 K 和调整值 T 拼接成字节串 (K || T)。
- 用 AES 加密(或 CMAC)对拼接结果做固定轮数的伪随机计算。
- 截断或拼接输出,得到 K1(长度与 K 相同)和 K2(同样长度)。
下面是一段 Python 风格的伪代码,你一看就明白:
from cryptography.hazmat.primitives.cmac import CMAC
from cryptography.hazmat.primitives.ciphers.algorithms import AES
def derive_key(K, T):
# 1. 拼接
data = K + T
# 2. 使用 AES-CMAC 生成 16 字节伪随机输出
c = CMAC(AES(K))
c.update(data)
prf_out = c.finalize() # 16 字节
# 3. 截断或直接使用前 16 字节作为 K1
K1 = prf_out
# 4. 再用同样的方法,但数据拼接 (K || T || 0x01)
c2 = CMAC(AES(K))
c2.update(data + b'\x01')
K2 = c2.finalize()
return K1, K2
请注意,这里我用的 CMAC,但 FF1 标准里推荐的是 AES-CMAC。实际项目中我还见过用 HMAC-SHA256 的,必须确认你依赖的库支持哪个。嗯,这个细节很容易踩坑。
Tweak T——调整值的分配
调整值 T 是 FF1 的一个灵活度体现。你可以传任意长度(0 到 216-1 字节)的额外数据进去,比如用户 ID、时间戳、银行账户类型。T 会影响整个加密结果,但又不改变明文长度。我的经验是:尽量把 T 设计成可还原的信息,这样解密时不需要额外存储。
T 在密钥派生阶段就被用上了(就像上面代码里拼接进去),而且在每轮 Feistel 中也会作为轮函数的输入一部分。具体地,FF1 会把 T 编码成固定长度的字节串 T',然后分成左右两块,分别喂给左右两半的计算。这部分细节有点绕,我建议你记两个要点:
- T 的长度会影响子密钥的生成顺序。
- T 的尾端一定要带长度标识,防止多组 T 拼接后歧义。
左右半部分 L 和 R 的分配
输入 X(要加密的明文)首先要被分成左右两半 L 和 R。怎么分?很简单:根据字符集大小和长度 n,把前一半字符给 L,后一半给 R。但细节里有魔鬼——
FF1 的 n 是字符个数,不是字节长度。比如:数字 0-9 是 10 个字符,那么 L 的长度是 floor(n/2),R 的长度是 ceil(n/2)。然后按字符索引拆分,而不是按字节。这一点和很多其他加密方案不一样。我见过有人直接用字节切分,结果加密结果完全不对,折腾了一天才找到原因。
| 字符集类型 | 示例 | n=7 时 L 长度 | n=7 时 R 长度 |
|---|---|---|---|
| 数字 [0-9] | 银行卡号 | 3 | 4 |
| 字母 [A-Z] | 车牌代码 | 3 | 4 |
| 十六进制 | ID | 3 | 4 |
变量的生命周期与初始化
在 FF1 的一轮循环开始前,我们需要保证以下变量的值已经分配好:
K:主密钥,长度 16、24 或 32 字节。K1, K2:子密钥,长度与 K 相同。T:调整值,已编码为字节串。L, R:已经按字符集分割好的两个列表(注意是字符索引,不是字节)。n:明文长度(字符数)。radix:字符集基数(数字为 10,字母为 26 等)。
list(map(int, r_str)) 就能搞定。
核心流程图:密钥派生与变量分配全景
下面我用一张 SVG 图把整个流程串起来。你看完后应该能一目了然:
实践避坑指南
1. 子密钥派生时,拼接顺序必须严格遵守标准:先 K 后 T,而不是反过来。有一次我图省事直接 T+K,结果和标准库对不上。
2. T 的长度编码方式:如果 T 长度不是 0,一定要在 T 前面加上 2 字节大端长度。忘加这个,你的实现和任何其他 FF1 实现都不兼容。
3. 分割 L 和 R 时,注意 n 是字符索引数量,不是字节长度。多字节字符(如中文)必须先转成单字节字符集(如 Base32),否则分割位置会错。
好了,这一章把密钥派生和变量分配的骨架搭起来了。你做完这些基础设置后,下一步就可以直接开始 Feistel 轮循环了。在实际开发中,我建议你先把子密钥派生写成一个独立的函数,方便单元测试。调试的时候打印出 K1 和 K2 的十六进制值,看一眼就知道派生逻辑对不对。