PAYATHON 2026

如何确保 Android app 支付系统符合 PCI DSS 安全要求?

支付阿杰

结论

不要把 Secure Element、Java Card Applet 或 Host-based Card Emulation(HCE)作为保存信用卡信息或满足 PCI DSS 要求的主要手段。这些技术主要处理 NFC、智能卡应用和凭据模拟,无法替代完整的持卡人数据保护体系。使用它们,也不会让 Android app 自动符合 PCI DSS。

更合适的做法是:

  1. 尽量不要在 Android 设备上保存完整卡号。
  2. 优先让伊朗当地银行、收单机构或支付服务商采集卡信息,再向应用返回 token 或交易标识。
  3. 如果当地支付系统不支持 tokenization,只有在业务确实需要时才保存 PAN,并将其放在受控的服务端环境中,不要存入手机本地。
  4. 授权完成后,不得保存 CVV/CVC、PIN、磁道数据等敏感认证数据,即使这些数据已经加密。
  5. 请当地收单机构或具备资质的 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 评估。不能只凭技术选型或一份自评问卷认定系统已经合规。

备注:内容仅供参考。