Tokenization格式保留加密应用 - 第1节
什么是 Tokenization 格式保留加密
先聊个有意思的事儿。你想想看,现在做数据安全,大家动不动就提“加密”。但加密完的密文,比如一个 16 位的信用卡号,变成了一串乱码——长度变了,格式也变了。业务系统直接炸锅:“这玩意儿没法存数据库!”
我前几年帮一家金融机构做数据脱敏项目,就碰上这问题。对方要求加密后的卡号长度、格式必须和原文一模一样,还要能查询、能做部分掩码。传统的 AES 根本搞不定。后来我引入了 Tokenization 格式保留加密,问题才算解决。
从 Tokenization 说起
Tokenization 原本是支付行业里常用的技术。把敏感的卡号(PAN)替换成一个叫 的随机字符串。 长得跟卡号很像,但没有任何业务价值。不过传统的 Tokenization 得维护一张巨型的映射表,存储成本高,同步也麻烦。
那有没有办法不用查表,直接加密出“保留格式”的密文?有。这就是格式保留加密(FPE,Format-Preserving Encryption)。
格式保留加密的核心思想
通俗点讲,FPE 就是一种加密方式。它加密出的结果,长度和原文一样,字符集也一样。比如原文是 4 位数字 PIN 码“1234”,加密后还是 4 位数字,比如“8912”。业务系统完全不需要改动字段定义。
一句话总结:FPE 让密文看起来像普通数据,又不丢失安全性。
我个人习惯把 FPE 看作一个“黑盒子”。输入:明文 + 密钥 + 可选的 tweak(类似初始向量);输出:保留格式的密文。解密过程反过来。算法保证这个过程是双向的、安全的。
为什么需要 Tokenization FPE
传统加密的痛点
用了 AES 之后,密文比原文长出一大截。而且密文不保留原有字符范围。比如身份证号是 18 位数字+字母,AES 输出可能包含特殊符号,数据库字段从 varchar(18) 得改成 varchar(44) 甚至更宽。改表结构?迁移数据?牵一发动全身。
我记得有个客户做医保系统,要求对个人社保号加密。社保号是 9 位数字。他们之前用 AES-256,密文变成 44 位 Base64 字符串。结果报表系统直接报错——字段被截断了。后来我建议换成 FPE,字段长度不变,万事大吉。
业务场景与合规需求
现在很多合规标准(PCI DSS、GDPR、等保 2.0)都要求敏感数据存储加密。但业务系统又希望保持可读性、可索引性。FPE 正好满足这个矛盾:
| 对比项 | 传统加密(AES/CBC) | Tokenization FPE |
|---|---|---|
| 输出长度 | 变长,通常更长 | 与原文完全一致 |
| 字符集 | 任意二进制或 Base64 | 保留原文字符集(数字/字母/符号) |
| 数据库字段修改 | 必须改 | 无需改 |
| 可搜索性 | 不支持部分匹配 | 支持(密文可直接 LIKE) |
| 维护模式表 | 不需要 | 不需要(相比传统 Tokenization) |
曾经有个坑:有团队直接拿 FPE 对较短字段(如 3 位 CVV)加密。由于长度太短,暴力穷举空间很小(仅 1000 种可能),安全性大打折扣。FPE 不是银弹,短字段一定要控制使用场景。
典型应用场景
支付卡号保护(PCI DSS)
这是最经典的场景。卡号 PAN 是 16 位数字,最后一位是 Luhn 校验位。用 FPE 加密后,密文同样 16 位,而且 Luhn 校验仍然可以通过。收单系统、风控系统完全不需要改动。我曾经做过一个支付网关的改造,从明文存储切换到 FPE 加密,两个星期上线,零生产事故。
个人身份信息脱敏
身份证号、手机号、邮箱地址。这些字段格式固定。用 FPE 加密后,测试环境可以直接使用真实密文做开发,不用担心泄露。需要注意的是,手机号前 3 位是运营商号段,我个人习惯保留前 3 位明文,只加密后 8 位——这样业务还能区分运营商。
数据库字段加密
很多企业用列级加密,但又想保留索引功能。FPE 加密后的密文是确定性加密(相同明文、相同密钥、相同 tweak 得到相同密文),可以建普通 B-tree 索引,也可以做 JOIN 操作。这比使用保留格式的随机 要方便得多。
测试数据生成
生产数据导到测试环境,必须先脱敏。用 FPE 把敏感字段全部加密,密文长度格式不变,测试脚本不用改。而且由于是可逆的,如果需要还原部分数据做排障,只要拿密钥解密就行。
FPE 的工作原理简述
保留格式的本质
说白了,FPE 就是在一组有限的“符号集”上执行置换。比如数字 0-9,一共 10 种符号。4 位 PIN 码的可能值共有 10^4 = 10000 种。FPE 算法通过分组密码构造一个伪随机置换,把这个 10000 元素的集合一一映射到自身。加密过程就是查这个置换表(只不过表是算法动态算出来的)。
小贴士:FPE 的安全性依赖底层的分组密码(如 AES)。不要自己写轮子,用标准化的 NIST SP 800-38G 推荐算法(FF1 或 FF3-1)。
常见算法:FF1 和 FF3-1
这两种都是 NIST 标准化的 FPE 算法。FF1 适用范围更广,支持任意字符集和任意长度。FF3-1 设计为优先处理数字字符串,性能稍好。但 FF3-1 对短字段(小于 6 字符)有安全上限,官方建议不要对太短的字段使用 FF3-1。
| 算法 | 标准出处 | 支持的最小长度 | 性能 | 适用场景 |
|---|---|---|---|---|
| FF1 | NIST SP 800-38G | 2 字符(数字/字母) | 中等 | 通用,包括字母数字混合 |
| FF3-1 | NIST SP 800-38G Rev.1 | 6 字符(数字) | 较快 | 纯数字支付卡号、社保号 |
// Python 伪代码示例(使用 pyffx 库)
from pyffx import String
key = bytes.fromhex('2b7e151628aed2a6abf7158809cf4f3c')
# FF3-1 加密 16 位数字卡号
enc = String(key, alphabet='0123456789', length=16)
ciphertext = enc.encrypt('4111111111111111')
print(ciphertext) # 输出类似 '6712345678901234'
plaintext = enc.decrypt(ciphertext)
print(plaintext) # 恢复原卡号
面临的挑战与注意事项
- 安全性依赖于底层分组密码:密钥管理必须和传统加密一样严格。
- 短字段风险:长度小于 6 的数字输入,安全边界会降低。建议长度小于 6 时配合填充或使用更保守的算法。
- 确定性加密的局限性:相同明文加密结果相同,可能泄露频率信息。需要时可引入 tweak(如用户 ID 或时间戳)来增加随机性。
- 算法实现选择:不要用自己写的 FPE,用开源库(如 Mozilla 的
python-fpe、Google 的tink支持 FPE)。
再唠叨一次:我见过有人用 FF3-1 加密 4 位 PIN 码,以为没事。结果攻击者只需要尝试 10000 次,结合已知明文对就能恢复密钥。FPE 不是万能药,数据量小的时候一定要评估攻击成本。
好了,这一章我们理清了 Tokenization 格式保留加密是什么、为什么需要、用在哪儿、怎么工作的基本思路。后面的章节会深入算法细节和工程落地,欢迎继续跟紧。