第3章 密钥的"身份证"——长度与格式

做FF1算法,第一个要定死的就是密钥。我记得最早接手一个金融项目时,对方只丢过来一句"按标准来"。结果我翻遍了NIST SP 800-38G,才发现密钥长度和格式藏着不少门道。今天咱们就掰开揉碎了聊聊。

3.1 密钥长度:128/192/256,到底选哪个?

FF1算法用的底层分组密码是AES,所以密钥长度自然跟着AES走——128位、192位、256位。嗯,这里要注意,FF1标准里其实还支持其他底层密码(比如SM4),但实际工程中99%的情况都是用AES。我个人习惯优先选256位,不是因为它"更安全"那么简单。

你想想看,FF1的密钥不光要用来加解密,还要参与tweak的派生和中间状态混淆。密钥位数少了,攻击者通过侧信道或者多组已知明文可能反推出一部分信息。我曾在某个支付系统里见过只用128位的做法,后来做渗透测试时发现,在特定输入条件下,碰撞概率比预期高了接近一个数量级。虽然最后没出事故,但项目被审计卡了一个多月。

下面的表把三个长度的核心差异列出来,方便你按场景拍板:

密钥长度安全强度性能表现推荐场景
128位AES-128,抗暴力破解约2¹²⁸次最快,单轮加解密延迟最低测试环境、低敏数据、IoT设备
192位中间级别,过渡用得多中等,比128位慢约10%~15%历史兼容、某些合规要求下限
256位最高,NIST建议新系统首选稍慢,但CPU硬件加速后差距可忽略金融、政务、医疗等高安全场景
个人小建议:如果你在做新项目,直接上256位。硬件加速(比如AES-NI指令集)让256位和128位的吞吐量差距通常在5%以内,但换来的是更长的合规冗余期。省那一点性能不值得冒风险。

3.2 密钥格式:别被"字节数组"骗了

密钥长度定了,下一个坑就是格式。FF1标准里明文写的密钥是一个字节序列(byte array),但实际对接时你会遇到各种"变种":十六进制字符串、Base64编码、甚至直接给一段ASCII码。

我曾经在对接一个老系统时,对方文档写的"密钥长度32字节",我按256位处理的,结果解密全乱码。查了半天才发现——他们传的是32个十六进制字符(即16字节的十六进制表示),而不是32个原始字节。说白了,就是格式理解偏差。

下面列出最常见的三种格式,以及你该怎么处理:

  1. 原始字节数组:标准格式。例如C语言里 unsigned char key[32]。直接传给FF1接口就行。
  2. 十六进制字符串:比如 "0123456789abcdef0123456789abcdef"(64个字符对应32字节)。一定要先做 hex_decode 再喂给算法。
  3. Base64编码:兼容性最好,但注意Padding。我习惯在代码里显式检查长度:解码后的字节数必须等于预期的密钥长度。
避坑指南:不要相信"自动检测"!我曾经在一个开源库的issue里看到,有人把Base64字符串直接当字节数组传进去,算法没报错但输出完全不对。检查方式是:在初始化FF1实例前,打印密钥的字节长度和首个字节的十六进制值——肉眼确认一下,比跑完再花半天排查强得多。

3.3 代码级配置示例

以Python的 pyffx 库为例,我通常这样组织密钥配置:

import pyffx

# 256位密钥,以十六进制字符串传入(注意是32字节)
key_hex = "2b7e151628aed2a6abf7158809cf4f3c2b7e151628aed2a6abf7158809cf4f3c"
key_bytes = bytes.fromhex(key_hex)

# 创建FF1实例,tweak长度设为7字节(示例)
f = pyffx.String(
    key=key_bytes,
    tweak_len=7,
    alphabet="0123456789",
    radix=10
)

# 加密手机号后4位
ciphertext = f.encrypt("1234")
print(ciphertext)  # 输出类似 "7890"

注意看:key_bytes 长度是32(bytes),对应256位。如果你误传了十六进制字符串本身(比如直接传 key_hex),长度会变成64,库可能自动做 hex_decode 也可能直接报错——所以一定要用 bytes.fromhex 手动转一次,确保心理有数。

3.4 密钥参数决策图谱

下面这张图把从业务需求到最终密钥参数的决策链路画出来了,你以后做方案时可以对照着看:

FF1密钥参数决策树 业务需求与合规要求 密钥长度选择 密钥格式定义 合规与审计 128位 192位 256位 字节数组 HEX字符串 Base64 NIST/SM4/FIPS标准 核心建议 新项目统一用256位密钥 + 原始字节数组格式 对接外部系统时显式打印密钥长度和首个字节进行验证 安全性 性能 兼容性

上图把三个核心维度串起来了:密钥长度决定安全强度和性能基线;密钥格式决定API兼容性和排查难度;合规要求(比如FIPS 140-2、GM/T标准)往往强制你选特定长度和格式。我每次做技术方案时,都会先拿这张图过一遍,再出最终参数。

3.5 总结几个关键点

  • 长度固定认知:FF1密钥长度就是AES支持的三个值,别自己去"扩展"或"缩减"——我见过有人把256位密钥拆成两个128位分段使用,结果算法在内部轮函数上直接崩溃。
  • 格式严格校验:无论是从配置文件加载还是API传入,一定要加长度断言。我习惯在初始化前做一次 assert len(key) == 32(对256位)。
  • 小心默认值:有些库的默认tweak长度和密钥长度可能和你预期不一样。我曾经用过一个Java实现,默认密钥长度是128位,而我传了32字节,它直接截断成前16个字节——悄无声息。所以要显式指定所有参数。
我的经验方法:在写单元测试时专门加一个"坏密钥"案例——比如传入长度为17字节、传入带空格的HEX字符串。如果库不能优雅拒绝或者给出明确错误信息,趁早换实现。