PAYATHON 2026

通过 HCE 实现 Tap & Pay 实际支付的后续步骤

支付小周

结论

仅让 NFC 终端选中 HostApduService,并不能完成真实支付。HCE 只是让 Android 手机具备收发 ISO-DEP/APDU 的能力。要实现非接支付,还需要:

  • 实现终端要求的 EMV Contactless 应用协议;
  • 使用发卡机构或支付网络合法分配的支付 AID、卡片数据和密钥;
  • 为每笔交易生成可由发卡后台验证的动态密码;
  • 接入发卡行、收单机构、支付处理商或 Token Service Provider;
  • 通过支付网络、安全和兼容性认证。

自行生成 AID、PAN 或固定的 APDU 响应,无法让普通 POS 接受交易并完成清算。要支持 Visa、Mastercard、银联等真实银行卡支付,通常需要与发卡机构或具备资质的支付服务商合作,取得相应规范和密钥材料。

当前代码实际完成了什么

目前的服务只处理了 SELECT 指令,并在 AID 匹配时返回:

9000

9000 只表示命令执行成功,其中没有终端继续交易所需的 FCI、应用能力、处理选项、卡片记录和动态密码。即使终端连接成功,也会在后续的 EMV 指令阶段终止交易。

代码中还有一处明显的不一致:

val AID = "A0000002471001"

而 apduservices.xml 注册的是:

<aid-filter android:name="325041592E5359532E4444463031"/>

325041592E5359532E4444463031 对应 ASCII 字符串 2PAY.SYS.DDF01,也就是非接触式支付环境 PPSE,并不是代码中的支付应用 AID。

Android 会根据已注册的 AID 路由 SELECT APDU。如果只注册 PPSE,终端随后选择 A0000002471001 时,不一定会继续把指令路由给这个服务。即使两个 AID 都成功注册,服务仍然需要返回符合支付规范的完整响应。

一次真实 EMV 非接交易通常包含的流程

具体的指令顺序取决于支付网络和终端配置,常见流程大致如下:

  1. 终端选择 PPSE:

    SELECT 2PAY.SYS.DDF01
    
  2. 手机返回 PPSE 的 FCI,其中列出可用的支付应用,例如应用 AID、应用标签和优先级。

  3. 终端选择具体的支付应用:

    SELECT <payment AID>
    
  4. 手机返回应用 FCI,其中包括 PDOL 等处理信息。

  5. 终端发送:

    GET PROCESSING OPTIONS
    
  6. 手机返回 AIP 和 AFL,说明支持哪些功能,以及终端应读取哪些记录。

  7. 终端根据 AFL 发送一个或多个:

    READ RECORD
    
  8. 手机返回与支付凭证相关的 EMV TLV 数据。

  9. 终端请求生成交易动态密码,例如发送:

    GENERATE AC
    

    某些非接触内核使用的具体命令和数据流可能不同,应以对应支付网络的规范为准。

  10. 手机根据交易计数器、终端不可预测数、金额和货币等交易数据生成一次性密码。

  11. POS 将交易提交给收单机构,再经支付网络发送到发卡系统。

  12. 发卡系统验证动态密码、凭证状态、风控结果和可用余额,最终批准或拒绝交易。

APDU 格式只是流程的一部分。真正决定交易能否完成的是有效的支付凭证和动态密码。如果终端无法验证数据,或者发卡后台不认可这些数据,交易就不能进入清算。

推荐的实施路径

1. 先确定支付业务模式

首先要明确准备实现哪种产品:

  • 自有闭环支付,例如园区卡、会员余额、交通或门禁系统;
  • 由银行发行的银行卡钱包;
  • 支付网络的 Tokenized Wallet;
  • 仅用于实验室的 EMV/HCE 模拟器。

如果 POS 和后台都由你控制,可以定义自己的 AID 和 APDU 协议,用于闭环支付。若目标是普通商户 POS,就必须接入该终端支持的支付网络,不能用自定义协议代替 EMV 支付应用。

2. 与发卡行或支付服务商合作

真实的开放式支付一般需要合作方提供:

  • 合法分配的支付应用 AID;
  • 对应支付网络的 EMV Contactless 规范;
  • 卡片或支付令牌的生命周期管理接口;
  • 凭证个性化数据;
  • 动态密码生成方案及密钥;
  • 挂失、暂停、删除和重新发放能力;
  • 发卡授权及交易验证后台;
  • 测试环境、测试卡配置和认证要求。

许多相关规范和测试材料需要签署协议后才能取得,并非全部公开。

3. 建立安全的凭证下发机制

不要将支付凭证硬编码在 APK 中。通常应由可信后台完成以下流程:

  1. 校验用户身份和设备;
  2. 向发卡机构或 TSP 申请设备支付令牌;
  3. 将受保护的凭证和有限用途密钥下发到设备;
  4. 控制设备完整性、密钥使用次数和有效期;
  5. 在设备丢失或风险升高时撤销令牌。

现代方案通常使用网络令牌,不会把实体银行卡 PAN 和长期主密钥直接存放在应用中。

4. 实现完整的 APDU 状态机

不要通过固定偏移截取字符串来解析 APDU,例如:

hexCommandApdu.substring(10, 24)

APDU 的 Lc、扩展长度、Le 以及不同形式的 SELECT 指令都可能改变数据位置。至少应按照 APDU 结构解析 CLA、INS、P1、P2、Lc 和 Data,并根据当前交易状态判断下一条指令是否有效。

下面的代码只演示如何安全识别 SELECT by name,不能直接用于真实支付:

private const val SW_SUCCESS = "9000"
private const val SW_WRONG_LENGTH = "6700"
private const val SW_FILE_NOT_FOUND = "6A82"
private const val SW_INS_NOT_SUPPORTED = "6D00"

private fun selectName(command: ByteArray): ByteArray? {
    if (command.size < 5) return null

    val cla = command[0].toInt() and 0xFF
    val ins = command[1].toInt() and 0xFF
    val p1 = command[2].toInt() and 0xFF
    val lc = command[4].toInt() and 0xFF

    if (cla != 0x00 || ins != 0xA4 || p1 != 0x04) return null
    if (command.size < 5 + lc) return null

    return command.copyOfRange(5, 5 + lc)
}

override fun processCommandApdu(
    commandApdu: ByteArray?,
    extras: Bundle?
): ByteArray {
    if (commandApdu == null) {
        return hexToBytes(SW_WRONG_LENGTH)
    }

    val ins = commandApdu.getOrNull(1)?.toInt()?.and(0xFF)
        ?: return hexToBytes(SW_WRONG_LENGTH)

    if (ins != 0xA4) {
        return hexToBytes(SW_INS_NOT_SUPPORTED)
    }

    val selectedName = selectName(commandApdu)
        ?: return hexToBytes(SW_WRONG_LENGTH)

    return when {
        selectedName.contentEquals(PPSE_NAME) -> {
            // 必须返回合法的 PPSE FCI TLV,不能只返回 9000。
            buildPpseFciResponse()
        }

        selectedName.contentEquals(PAYMENT_AID) -> {
            // 必须返回该支付应用规范要求的 FCI TLV。
            buildApplicationFciResponse()
        }

        else -> hexToBytes(SW_FILE_NOT_FOUND)
    }
}

之后还要实现 GET PROCESSING OPTIONS、READ RECORD,以及对应支付内核要求的动态密码生成流程。响应中的 BER-TLV 标签、长度、数据来源和密码算法必须严格遵循合作支付网络的规范,不能使用示例值随意拼装。

5. 正确声明支付服务

真实支付服务通常应使用 payment 分类,而不是 other:

<host-apdu-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/hce_service"
    android:requireDeviceUnlock="true">

    <aid-group
        android:description="@string/aid_groups"
        android:category="payment">

        <aid-filter android:name="325041592E5359532E4444463031"/>
        <aid-filter android:name="YOUR_ASSIGNED_PAYMENT_AID"/>
    </aid-group>
</host-apdu-service>

必须将 YOUR_ASSIGNED_PAYMENT_AID 替换为合作机构允许使用的 AID,不能把示例值用于生产环境。

支付分类还会影响系统默认钱包、前台优先级和用户选择机制。不同 Android 版本和设备厂商的行为可能存在差异,需要在目标设备上实际验证。至于 requireDeviceUnlock 能否设为 false,应根据支付网络规则、风险模型和产品安全要求决定,不能只考虑使用是否方便。

测试方式

开发阶段不要直接使用生产 POS 和真实账户试错。可以按以下顺序测试:

  1. 使用 NFC/APDU 测试工具验证选择和路由;
  2. 使用脚本或测试读卡器验证 APDU 状态机;
  3. 使用支付合作方提供的测试凭证和测试密钥;
  4. 在 EMV Contactless 内核测试环境中验证 TLV 和交易流程;
  5. 测试重复交易、断场、超时、锁屏、计数器回滚和网络不可用等异常情况;
  6. 使用支付网络认可的测试工具完成一致性和认证测试;
  7. 获得合作方批准后再进入生产环境。

测试时应确认终端发出的完整 APDU 日志。只看到 PPSE SELECT 并收到 9000,不能证明终端已经识别支付应用。

安全注意事项

  • 不要将 PAN、长期密钥、主密钥或固定动态密码密钥硬编码进 APK。
  • 不要把完整支付凭证、敏感认证数据或密钥写入 Logcat。
  • 交易计数器必须防止回滚和重复使用。
  • 设备端密钥应存放在受保护的存储中,并采用受控的使用策略。具体能力取决于合作方案和设备。
  • 服务端必须能够撤销设备凭证并执行交易风控。
  • 如果系统存储、处理或传输真实银行卡数据,需要评估适用的 PCI DSS 等合规要求。
  • HCE 应用能否在锁屏状态下支付、是否要求用户验证以及单笔限额,都应遵守支付网络和发卡方的规则。

如果目标只是自有闭环 Tap & Pay,可以自行设计 APDU、签名和后台扣款流程。如果目标是在普通银行卡 POS 上完成实际付款,下一步不是继续补几个 APDU 返回值,而是先确定支付网络,并取得发卡方或支付服务商的正式接入支持。

备注:内容仅供参考。