替代方案概览:加密、哈希、截断、掩码

说到令牌化的替代方案,我首先得承认:令牌化不是万能的。早年做某支付平台合规改造时,我们遇到过资金流上游系统死活接不了令牌接口的情况。当时就一句话——不上令牌,但必须合规。那怎么办?

今天聊的四个方案:加密、哈希、截断、掩码,说白了都是同一件事:让原始卡号不出现在不该出现的地方,同时尽可能保留业务可用性。

核心判断标准:

  • 我需要还原原始数据吗?→ 选加密
  • 我只做等值比较,不关心原文?→ 选哈希
  • 我只保留最后几位,其他丢掉?→ 截断
  • 我只在显示/打印时遮挡部分数字?→ 掩码

加密:可逆但不可轻用

加密是唯一可逆的替代方案。你存储了密文,业务需要时用密钥还原。这个我熟——当年给某银行的交易日志做合规改造,他们坚持“必须能查到完整卡号以便客服核账”。

我个人习惯用的对称加密算法是AES-256,配合GCM模式。

from cryptography.fernet import Fernet

# 生成一个密钥(一次性)
key = Fernet.generate_key()
cipher = Fernet(key)

# 加密
pan = "4111111111111111"
encrypted = cipher.encrypt(pan.encode())

# 解密
plain = cipher.decrypt(encrypted).decode()

注意:PCI DSS对加密有硬性要求。

  • 密钥必须存储在HSM或专用密钥管理系统中,不能硬编码
  • 加密后的数据仍属于“持卡人数据”,存储环境需符合CDE要求
  • 定期轮换密钥,建议最多一年一次

加密的最大问题是:你保留了风险。一旦私钥泄露,所有历史数据就全完了。我曾经见过一个案例,某公司把密钥放在配置文件里,还提交到了Git仓库……嗯,那场事故处理了整整三周。

哈希:不可逆但撞库风险大

如果业务只需要“比较两笔交易是不是同一个卡号”,哈希就够了。比如防重复支付、风控建模、黑名单匹配。

但注意——不要用明文哈希。为什么?因为卡号只有16位数字,穷举空间很小。攻击者可以预先算好所有可能卡号的哈希值,这就是彩虹表攻击。

我的避坑习惯:

  • 一律使用加盐哈希:SHA-256(salt + PAN)
  • 盐必须足够长(至少32字节)且每张卡独立
  • 哈希值仍属于持卡人数据——PCI的CDE范围没变
import hashlib, os

def hash_pan(pan, salt=None):
    if salt is None:
        salt = os.urandom(32)
    h = hashlib.pbkdf2_hmac('sha256', pan.encode(), salt, 100_000)
    return salt + h  # 存储盐+哈希

你想想看,如果只用SHA-256(PAN),攻击者做一次预计算就能匹配所有卡号。加盐之后,每个卡号都要独立计算,成本上升数百万倍。

截断:最简单的减法

截断就是只保留卡号的前几位或后几位,其余丢掉。PCI DSS允许保留不超过6位且不包含完整BIN+完整账号的最后4位——说白了,你不能留着前6位和后4位,那样就是完整卡号了。

我在项目中唯一见过截断的合理场景是交易分析报表:只需要卡BIN(前6位)判断发卡行归属,完全不需要完整PAN。这时候用截断就够了。

千万别误会:

  • 截断是不可逆的,数据永久丢失
  • 如果业务未来可能需要完整卡号,截断会害死你
  • 截断后的数据依然在《PCI DSS适用性矩阵》范围内

掩码:只遮挡不存储

掩码和上面三者完全不同。掩码不是一种存储方案,它只用于显示/打印时的实时遮挡。比如客服界面只显示“411111******1111”,但数据库里存的是完整PAN。

合规经验谈:掩码显示本身不受PCI DSS限制——因为它不存储、不传输。但你用于生成掩码的原始数据必须在CDE中保护。

def mask_pan(pan):
    # 显示前6位 + 后4位,中间掩码
    return pan[:6] + "******" + pan[-4:]

print(mask_pan("4111111111111111"))
# 输出: 411111******1111

有一次审计,对方问我们客服系统API返回的完整PAN为什么没有走加密传输——我解释说“这只是掩码,后端实际返回的还是密文,前端只在浏览器里实时掩码”。嗯,差点被判定为违规,从此我学乖了:掩码必须在显示层完成,不要在服务端或传输层暴露原始数据。

四者对比:一张表说清

令牌化替代方案核心对比
特性 加密 哈希 截断 掩码
是否可逆 是 否(理论上) 否 否(仅显示)
是否需要密钥 是 否(但需盐值) 否 否
撞库风险 无(密文不可识别) 高(无盐时极高) 低 低
是否属于持卡人数据 是 是 是(剩余部分) 否(仅显示)
典型场景 需要还原完整PAN 去重、匹配、风控 统计分析 客服界面、打印小票

一张图理解四者的关系

我习惯把四种方案画成一个“保护程度-保留可用性”的二维图。你看下面这张SVG就明白了——越往上,数据越安全但越难用;越往右,业务越好用但越不安全。

替代方案分类:保护程度 vs 业务可用性 保护程度(高 → 低) 业务可用性(低 → 高) 截断 哈希(加盐) 对称加密 掩码(显示) 令牌化(参考) 箭头方向:保护程度越高 → 业务可用性越低(反之亦然)

从图上你也能看出来,没有“最佳”方案,只有“最适合当前场景”的方案。如果有机构告诉你“只用一种方案就能全面合规”,那建议你多问一句:你们做过风险分析了吗?

我的个人判断框架(给团队用过多次):

  1. 如果未来可能需要还原→加密(同时规划密钥管理)
  2. 如果只做等值比较且能容忍不可逆→加盐哈希
  3. 如果数据只用于粗粒度统计且能接受信息损失→截断
  4. 如果只在界面展示或打印→掩码(不要存掩码后的值)

选对方案,能省下至少80%的合规改造工作量。我也曾在这上面吃过亏,选错了方案导致后期返工,那感受真不好受。