Tokenization格式保留加密应用 - 第1节

什么是 Tokenization 格式保留加密

先聊个有意思的事儿。你想想看,现在做数据安全,大家动不动就提“加密”。但加密完的密文,比如一个 16 位的信用卡号,变成了一串乱码——长度变了,格式也变了。业务系统直接炸锅:“这玩意儿没法存数据库!”

我前几年帮一家金融机构做数据脱敏项目,就碰上这问题。对方要求加密后的卡号长度、格式必须和原文一模一样,还要能查询、能做部分掩码。传统的 AES 根本搞不定。后来我引入了 Tokenization 格式保留加密,问题才算解决。

从 Tokenization 说起

Tokenization 原本是支付行业里常用的技术。把敏感的卡号(PAN)替换成一个叫 的随机字符串。 长得跟卡号很像,但没有任何业务价值。不过传统的 Tokenization 得维护一张巨型的映射表,存储成本高,同步也麻烦。

那有没有办法不用查表,直接加密出“保留格式”的密文?有。这就是格式保留加密(FPE,Format-Preserving Encryption)。

格式保留加密的核心思想

通俗点讲,FPE 就是一种加密方式。它加密出的结果,长度和原文一样,字符集也一样。比如原文是 4 位数字 PIN 码“1234”,加密后还是 4 位数字,比如“8912”。业务系统完全不需要改动字段定义。

一句话总结:FPE 让密文看起来像普通数据,又不丢失安全性。

我个人习惯把 FPE 看作一个“黑盒子”。输入:明文 + 密钥 + 可选的 tweak(类似初始向量);输出:保留格式的密文。解密过程反过来。算法保证这个过程是双向的、安全的。

Tokenization 格式保留加密核心流程 明文数据 (如卡号 4111-1111-1111-1111) 密钥 + Tweak (128/256位 key + 可选向量) FPE 加密引擎 FF1 / FF3-1 算法 保留格式密文 (同样 16 位数字) 格式与原文保持一致 位数、字符集、校验位不变 业务系统无需改造

为什么需要 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 格式保留加密是什么、为什么需要、用在哪儿、怎么工作的基本思路。后面的章节会深入算法细节和工程落地,欢迎继续跟紧。