如何确保 Android app 支付系统符合 PCI DSS 安全要求?
结论
不要把 Secure Element、Java Card Applet 或 Host-based Card Emulation(HCE)作为保存信用卡信息或满足 PCI DSS 要求的主要手段。这些技术主要处理 NFC、智能卡应用和凭据模拟,无法替代完整的持卡人数据保护体系。使用它们,也不会让 Android app 自动符合 PCI DSS。
更合适的做法是:
- 尽量不要在 Android 设备上保存完整卡号。
- 优先让伊朗当地银行、收单机构或支付服务商采集卡信息,再向应用返回 token 或交易标识。
- 如果当地支付系统不支持 tokenization,只有在业务确实需要时才保存 PAN,并将其放在受控的服务端环境中,不要存入手机本地。
- 授权完成后,不得保存 CVV/CVC、PIN、磁道数据等敏感认证数据,即使这些数据已经加密。
- 请当地收单机构或具备资质的 PCI QSA 确认适用范围、SAQ 类型和验证方式。
“不保存密码”并不能明显降低 PCI DSS 风险。PCI DSS 保护的重点是 PAN、持卡人姓名、有效期和敏感认证数据,不是应用登录密码。
推荐的支付架构
优先采用以下流程:
Android app
│
├─ 跳转至当地银行/支付机构提供的支付页面或官方 SDK
│
└─ 用户直接向支付机构提交卡信息
│
▼
当地支付机构处理授权
│
▼
app/backend 只收到 token、交易号和支付结果
这样可以减少信用卡数据进入应用和业务服务器的机会。但 PCI DSS 范围能否缩小,还要看当地支付页面或 SDK 的具体集成方式,不能因为使用了支付 SDK 就直接下结论。
如果当地机构完全不提供托管支付页面、SDK 或 tokenization,可以采用下面的结构:
Android app
│ TLS
▼
受控的支付后端
├─ 卡数据只进入隔离的 Cardholder Data Environment
├─ 使用受管理的密钥系统加密 PAN
├─ 将 PAN 替换为内部 token
└─ 通过当地银行协议完成交易
采用这种架构后,Android app、支付后端、数据库、日志系统、运维终端以及能够访问这些系统的人员,都可能被纳入 PCI DSS 评估范围。自行建设这套系统,成本通常远高于接入当地合规的支付机构。
必须先确认当地支付机构的能力
“不能使用国外第三方 API”不代表必须由 app 自己保管银行卡数据。应先向当地银行、收单机构或 PSP 确认是否提供:
- 托管支付页面;
- 官方 Android SDK;
- 卡号 tokenization;
- recurring payment 或 card-on-file 服务;
- 服务端到服务端支付接口;
- PCI DSS 合规证明或责任划分文件;
- 支付回调签名和交易状态查询接口。
如果需要保存卡片供用户以后使用,最好保存支付机构返回的 token,不要保存 PAN:
{
"customerId": "customer_123",
"paymentToken": "tok_local_provider_abc",
"last4": "4242",
"brand": "local-card-network",
"expiryMonth": 12,
"expiryYear": 2028
}
last4 可以用于界面展示,但仍要避免将它与其他数据组合成没有必要的完整支付档案。
如果确实需要保存 PAN
只有在业务和支付机构明确要求时,才考虑保存完整 PAN。至少应采取以下措施:
- 记录保存 PAN 的业务理由和保留期限,并完成审批;
- 不保存 CVV/CVC、PIN、PIN block 或完整磁道数据;
- 使用符合当前安全要求的 TLS 传输 PAN;
- 对静态存储的 PAN 使用强加密;
- 将加密密钥与密文分开保存;
- 使用专用 KMS、HSM 或具有同等控制能力的密钥系统;
- 建立密钥轮换、双人控制和访问审计机制;
- 展示 PAN 时默认使用掩码,例如
************1234; - 禁止完整 PAN 进入日志、崩溃报告、分析平台和客服系统;
- 只允许完成工作所必需的人员和服务访问;
- 数据到期后自动删除,并确认备份中的数据也能按策略清理;
- 按要求完成漏洞扫描、渗透测试和安全审查。
不要把密钥硬编码进 APK。APK 可以被反编译,代码混淆也不能保护密钥。
Android 本地存储示例
最安全的选择仍然是不在设备上保存 PAN。如果只需保存支付 token,可以用 Android Keystore 保护本地加密密钥,再通过 AES-GCM 加密 token。
private const val KEY_ALIAS = "payment_token_key"
fun getOrCreateKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
val existingKey = keyStore.getKey(KEY_ALIAS, null)
if (existingKey is SecretKey) {
return existingKey
}
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
keyGenerator.init(spec)
return keyGenerator.generateKey()
}
data class EncryptedValue(
val ciphertext: ByteArray,
val iv: ByteArray
)
fun encryptToken(token: String, key: SecretKey): EncryptedValue {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
return EncryptedValue(
ciphertext = cipher.doFinal(token.toByteArray(Charsets.UTF_8)),
iv = cipher.iv
)
}
解密时必须使用先前保存的 IV:
fun decryptToken(value: EncryptedValue, key: SecretKey): String {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val spec = GCMParameterSpec(128, value.iv)
cipher.init(Cipher.DECRYPT_MODE, key, spec)
return String(cipher.doFinal(value.ciphertext), Charsets.UTF_8)
}
每次加密都必须使用新的唯一 IV,并同时保存密文和 IV。还要防止 token 出现在日志、备份、剪贴板、截图或崩溃报告中。
这段代码仅用于演示如何在设备端保护 token,不代表系统符合 PCI DSS,也不适合长期保存完整 PAN。设备可能被 root、调试或植入恶意软件,也可能遭遇界面覆盖攻击。Android Keystore 只能降低其中一部分风险。
Secure Element 是否适合
Secure Element 通常用来保存受限制的智能卡凭据,并执行特定的密码学操作。但它有几个实际限制:
- 普通应用通常不能随意访问 Secure Element 或安装 applet;
- 访问权限可能由设备厂商、SIM 运营商或可信服务管理方控制;
- 不同设备使用的硬件和访问策略并不一致;
- 它无法保护服务器、网络、日志、人员权限和整个支付流程;
- 使用 Secure Element 不等于符合 PCI DSS。
因此,Secure Element 不适合充当通用的信用卡数据库。除非当地支付网络、设备厂商和收单机构共同提供经过认证的 Secure Element 支付方案,否则不应自行设计这类方案。
Access Control、Java Card Applet 和 HCE 是否适合
Secure Element Access Control
Access Control 用于决定哪些 Android 应用可以访问 Secure Element 中的资源。它只是一种访问控制机制,不是完整的支付安全方案,也解决不了服务端的合规问题。
Java Card Applet
Java Card Applet 需要受支持的 Secure Element、完整的发行和密钥管理体系,还需要设备厂商或运营商配合。自行开发 applet 不能证明支付系统符合 PCI DSS,而且通常很难部署到普通用户的设备上。
Host-based Card Emulation
HCE 通过 Android 软件模拟 NFC 卡,主要用于非接触式支付或门禁等场景。它不是为保存电商用户输入的信用卡资料而设计的。数据和逻辑运行在普通 Android 环境中,因此 HCE 无法提供与受控 Secure Element 相同的隔离能力。
对于通过互联网购买手机充值额度的业务,这三种技术通常没有解决真正的风险。重点应放在减少卡数据的接触范围、使用当地收单系统、保护后端环境,以及建立可审计的密钥和访问管理机制。
还需要注意的安全问题
- 不要使用普通
WebView自行拼装银行卡输入页,除非当地支付机构明确支持这种方式并给出了安全要求。 - 不要关闭 TLS 证书校验,也不要实现“信任所有证书”的
TrustManager。 - 服务端必须向支付机构查询交易状态或验证签名,不能直接相信客户端传来的
success=true。 - 每笔充值订单都应使用唯一订单号,并进行幂等处理,防止重复回调造成重复充值。
- 不要只依赖设备 root 检测、代码混淆或证书锁定。这些措施可以用于纵深防御,但不能作为合规基础。
- 支付系统应与普通业务系统进行网络和权限隔离,尽量缩小持卡人数据环境。
- PCI DSS 之外,移动支付应用相关的其他标准或软件安全要求也可能适用,具体取决于应用是否直接采集和处理卡数据。
PCI DSS 的验证方式取决于支付架构、交易量、支付品牌和收单机构的要求。最终方案应交由当地银行或收单机构确认,并由熟悉当地支付网络的 PCI QSA 评估。不能只凭技术选型或一份自评问卷认定系统已经合规。
备注:内容仅供参考。
