通过 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 非接交易通常包含的流程
具体的指令顺序取决于支付网络和终端配置,常见流程大致如下:
-
终端选择 PPSE:
SELECT 2PAY.SYS.DDF01 -
手机返回 PPSE 的 FCI,其中列出可用的支付应用,例如应用 AID、应用标签和优先级。
-
终端选择具体的支付应用:
SELECT <payment AID> -
手机返回应用 FCI,其中包括 PDOL 等处理信息。
-
终端发送:
GET PROCESSING OPTIONS -
手机返回 AIP 和 AFL,说明支持哪些功能,以及终端应读取哪些记录。
-
终端根据 AFL 发送一个或多个:
READ RECORD -
手机返回与支付凭证相关的 EMV TLV 数据。
-
终端请求生成交易动态密码,例如发送:
GENERATE AC某些非接触内核使用的具体命令和数据流可能不同,应以对应支付网络的规范为准。
-
手机根据交易计数器、终端不可预测数、金额和货币等交易数据生成一次性密码。
-
POS 将交易提交给收单机构,再经支付网络发送到发卡系统。
-
发卡系统验证动态密码、凭证状态、风控结果和可用余额,最终批准或拒绝交易。
APDU 格式只是流程的一部分。真正决定交易能否完成的是有效的支付凭证和动态密码。如果终端无法验证数据,或者发卡后台不认可这些数据,交易就不能进入清算。
推荐的实施路径
1. 先确定支付业务模式
首先要明确准备实现哪种产品:
- 自有闭环支付,例如园区卡、会员余额、交通或门禁系统;
- 由银行发行的银行卡钱包;
- 支付网络的 Tokenized Wallet;
- 仅用于实验室的 EMV/HCE 模拟器。
如果 POS 和后台都由你控制,可以定义自己的 AID 和 APDU 协议,用于闭环支付。若目标是普通商户 POS,就必须接入该终端支持的支付网络,不能用自定义协议代替 EMV 支付应用。
2. 与发卡行或支付服务商合作
真实的开放式支付一般需要合作方提供:
- 合法分配的支付应用 AID;
- 对应支付网络的 EMV Contactless 规范;
- 卡片或支付令牌的生命周期管理接口;
- 凭证个性化数据;
- 动态密码生成方案及密钥;
- 挂失、暂停、删除和重新发放能力;
- 发卡授权及交易验证后台;
- 测试环境、测试卡配置和认证要求。
许多相关规范和测试材料需要签署协议后才能取得,并非全部公开。
3. 建立安全的凭证下发机制
不要将支付凭证硬编码在 APK 中。通常应由可信后台完成以下流程:
- 校验用户身份和设备;
- 向发卡机构或 TSP 申请设备支付令牌;
- 将受保护的凭证和有限用途密钥下发到设备;
- 控制设备完整性、密钥使用次数和有效期;
- 在设备丢失或风险升高时撤销令牌。
现代方案通常使用网络令牌,不会把实体银行卡 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 和真实账户试错。可以按以下顺序测试:
- 使用 NFC/APDU 测试工具验证选择和路由;
- 使用脚本或测试读卡器验证 APDU 状态机;
- 使用支付合作方提供的测试凭证和测试密钥;
- 在 EMV Contactless 内核测试环境中验证 TLV 和交易流程;
- 测试重复交易、断场、超时、锁屏、计数器回滚和网络不可用等异常情况;
- 使用支付网络认可的测试工具完成一致性和认证测试;
- 获得合作方批准后再进入生产环境。
测试时应确认终端发出的完整 APDU 日志。只看到 PPSE SELECT 并收到 9000,不能证明终端已经识别支付应用。
安全注意事项
- 不要将 PAN、长期密钥、主密钥或固定动态密码密钥硬编码进 APK。
- 不要把完整支付凭证、敏感认证数据或密钥写入 Logcat。
- 交易计数器必须防止回滚和重复使用。
- 设备端密钥应存放在受保护的存储中,并采用受控的使用策略。具体能力取决于合作方案和设备。
- 服务端必须能够撤销设备凭证并执行交易风控。
- 如果系统存储、处理或传输真实银行卡数据,需要评估适用的 PCI DSS 等合规要求。
- HCE 应用能否在锁屏状态下支付、是否要求用户验证以及单笔限额,都应遵守支付网络和发卡方的规则。
如果目标只是自有闭环 Tap & Pay,可以自行设计 APDU、签名和后台扣款流程。如果目标是在普通银行卡 POS 上完成实际付款,下一步不是继续补几个 APDU 返回值,而是先确定支付网络,并取得发卡方或支付服务商的正式接入支持。
备注:内容仅供参考。
