四、常见脱敏算法(二):加扰、加密、幻影(FPE)——安全性分析

脱敏算法选错,比不脱敏更危险。这话听着夸张,但我在项目里见过太多「脱敏后数据还能反推原文」的案例了。今天咱们就把加扰、加密、FPE这三类算法的安全性掰开揉碎讲透。

4.1 加扰:看起来乱了,其实很脆弱

加扰,说白了就是「假装脱敏」。它通过固定规则打乱字符顺序、替换部分字符,但保留原始数据的格式和统计特征。

  • 字符乱序:比如把手机号 13812345678 变成 18236547138
  • 固定替换:把名字里的「张」统一换成「王」
  • 随机偏移:每个数字加一个固定值(比如+3,1变成4)

关键认知:加扰的安全性≈0。它只防肉眼,不防机器。

我参与过一家金融公司的数据审计,他们用「姓替换、随机打乱中间四位」的方式脱敏身份证号。结果呢?攻击者用统计分布一算,就把前六位地址码和后四位校验码还原了。因为打乱的是中间区位码,但前六位和后四位根本没动。

避坑指南:

  • 千万别用加扰处理高敏感字段(身份证、银行卡号、社保号)
  • 加扰只能用于测试数据中「非关键」字段,比如用户昵称、邮编
  • 加扰后的数据不可逆转——你找不到原始值

4.2 加密:安全,但格式会变

加密大家都熟。AES-256,SM4,这才是正经的密码学算法。安全性依赖密钥,只要密钥不泄露,密文就是一堆二进制乱码。

但问题来了:加密后的数据格式变了。比如一个18位身份证号,AES加密后变成32字节的十六进制字符串。你原来的数据库字段存的是 varchar(18),现在得改成 varchar(64) 甚至更大的 blob。

我遇到过最典型的情况:一家电商平台要对用户手机号加密存储,但下游的短信营销系统需要直接读取原始号码发验证码。他们没做解密接口,而是直接把密文拷贝过去了……结果是所有营销短信都发给了不可读的乱码串。

算法 安全性等级 格式保持 性能 典型场景
AES-256 极高 否(输出二进制/hex) 快(硬件加速) 数据库列级加密、传输加密
SM4 极高(国密) 否(输出16字节倍长) 快 国内合规场景
RSA-2048 高 否(输出256字节) 慢(仅适合小数据) 密钥交换、数字签名

注意:加密 ≠ 脱敏。加密是可逆的,只要拿到密钥就能恢复。在很多监管要求中(比如GDPR、个人金融信息保护),加密只能算「基本防护」,不能替代脱敏。脱敏要求的是「即使攻击者拿到密钥,也无法还原原始数据」。

我曾经和合规团队争论过这件事,后来监管现场检查时,他们明确表示「加密存储后仍然需要做脱敏处理,否则视为未脱敏」。

4.3 幻影(FPE):既要安全,又要保持格式

FPE(Format Preserving Encryption)就是来解决「加密后格式变化」的痛点。它采用保留格式的加密模式——输出数据和原始数据长度相同,字符集相同。

举个例子:信用卡号 4111 1111 1111 1111,FPE加密后仍然是16位数字,且前4位还能保留(BIN码)。这样数据库字段不用改,接口不需要适配,下游系统照常工作。

我个人习惯:评估一个字段是否用FPE,先问自己三个问题:

  1. 这个字段的格式是否必须保留?(比如手机号、身份证、银行卡号)
  2. 应用层是否有现成的解密能力?(没有就回流到下一条)
  3. 字段长度是不是固定且较小?(超过128位变慢)

如果都是"是",FPE首选。

FPE 的安全性到底怎么样?

FPE 的底层本质还是加密。主流实现 FF1 和 FF3-1 都基于 AES,用的是 NIST SP 800-38G 标准。安全强度等价于相同密钥长度的 AES——这是严格证明过的。

但有个坑:FPE 不能抵御「选择明文攻击」中的某些侧信道。举例:如果你把 FPE 用在了「自增ID」上(比如用户ID 10001 → FPE后还是10001),攻击者很容易通过已知明文对推断出密钥。这不是算法本身的问题,而是使用场景不当。

// 举例:Java 中使用 FF3-1 (Bouncy Castle)
import org.bouncycastle.crypto.fpe.FPEParameters;
import org.bouncycastle.crypto.fpe.FF3p1Engine;
import org.bouncycastle.crypto.params.KeyParameter;

// 初始化密钥和Tweak
byte[] key = hexStringToByteArray("2B7E151628AED2A6ABF7158809CF4F3C");
byte[] tweak = hexStringToByteArray("CBD09F18");

KeyParameter keyParam = new KeyParameter(key);
FF3p1Engine engine = new FF3p1Engine();
engine.init(true, new FPEParameters(keyParam, tweak, 10)); // radix=10 表示数字

// 加密 16 位银行卡号
String plain = "4111111111111111";
byte[] cipher = engine.processBlock(plain.getBytes(), 0, plain.length());
System.out.println(new String(cipher)); // 输出仍是16位数字

4.4 安全性对照一览

我直接画了个对比表格,方便你选型时快速对照:

维度 加扰 标准加密(AES) FPE(FF1/FF3-1)
可逆性 不可逆(无密钥) 可逆(有密钥) 可逆(有密钥+相同tweak)
格式保持 是 否 是
抗统计攻击 弱(保留分布) 强(均匀分布) 强(密文分布与明文无关)
密钥管理需求 无 需要 需要(+ tweak管理)
典型脱敏等级 低(仅用于非敏感字段) 中-高(配合其他措施) 高(适合合规场景)
性能(10万条/秒) 极快(几乎无开销) 快(~50μs/条) 中等(~200μs/条,FF3-1比FF1更快)

选型结论:

  • 需要用明文原样做查询/索引 → FPE(配合保留明文索引前缀)
  • 只存储不允许原样读出 → 标准加密 + KMS管理密钥
  • 仅做展示脱敏(比如页面显示138****5678)→ 加扰就已经够了

4.5 一张图看懂三类算法的安全性定位

加扰、加密、FPE 安全性对比图 加扰 / 加密 / FPE 安全性层级与边界 加扰(Scrambling) 格式保持 ✅ 抗统计攻击 ❌ 可逆性 ❌ 无密钥管理 适用:非敏感展示 安全性等级:低 标准加密(AES/SM4) 格式保持 ❌ 抗统计攻击 ✅ 可逆性 ✅(需密钥) 密钥管理:必须 适用:存储/传输加密 安全性等级:高 FPE (FF1/FF3-1) 格式保持 ✅ 抗统计攻击 ✅ 可逆性 ✅(需密钥+tweak) 密钥管理:必须 适用:合规脱敏/格式保留 安全性等级:很高 📌 核心差异总结 1. 加扰仅用于「看起来脱敏了」,实则经不起任何统计审计。 2. 标准加密(AES/SM4)安全但破坏格式,适合底层存储加密。 3. FPE 在安全性和格式保持之间取平衡,但需要额外管理 tweak。 4. 我记得有个客户误把加扰当成加密用于信用卡号脱敏,结果PCI-DSS审计 直接判定不合规,被迫回滚重做。这个教训让我一直保留这张对照图。

4.6 避坑清单:我的实战教训

  • 不要认为「加密了就能过合规」——很多行业标准(PCI-DSS、等保三级、金融数据安全分级指南)明确要求「不可逆脱敏」或「仅暴露必要部分」,加密后等于没脱敏。
  • FPE 不要用在序列号上——前面提到过,如果明文本身是连续自增ID,FPE加密后依然可能被猜出密钥。我曾经在项目中因此被DBA追着问「为什么用户ID加密后还是连续的?」
  • 加扰不要用在 JSON 或 XML 字段里——你会破坏数据结构的完整性。有一次下游解析时因为少了逗号直接报错。
  • 密钥和tweak要分开存储——很多团队把tweak硬编码在代码里,等于给攻击者开了一扇后门。

我的习惯:

  • 对于生产环境的数据复制品(比如测试库),我优先使用 FPE 加上动态过滤规则,这样既保留了数据可用性,又能在运行时拦截敏感查询。
  • 如果条件允许,我会在数据库层面做「透明脱敏网关」——应用层感知不到脱敏,数据库返回时自动替换为加扰或FPE结果。这需要 DBA 配合,但效果很干净。

4.7 小结

嗯,这一章我们聊了三种算法的「安全底牌」。加扰像层薄纱——看着挡了,一戳就破。加密是铁桶,但桶的形状变了,管线和接口都得跟着改。FPE 是定制化铁桶——既要安全又要保持原样,代价是需要多掏一份 tweak 的管理成本。

选哪个?看你的业务场景和合规要求。我自己的经验是:能用FPE优先用FPE,因为改字段长度的代价远比多维护一个tweak大得多。当然,如果你只是给报表做个展示脱敏,加扰就足够——别为了炫技引入不必要的复杂度。