基于主机的卡模拟概览

许多提供 NFC 功能的 Android 设备已经支持 NFC 卡模拟。在大多数情况下,卡由设备中的一个单独的芯片(称为安全元件)模拟。无线运营商提供的许多 SIM 卡也包含安全元件。

Android 4.4 及更高版本提供了一种不涉及安全元件的额外卡模拟方法,称为“基于主机的卡模拟”。这样一来,任何 Android 应用都可以模拟卡并直接与 NFC 读取器通信。本主题介绍了基于主机的卡模拟 (HCE) 在 Android 上的工作原理,以及如何开发使用此技术模拟 NFC 卡的应用。

使用安全元件进行卡模拟

使用安全元件提供 NFC 卡模拟时,要模拟的卡会通过 Android 应用配置到设备上的安全元件中。然后,当用户将设备靠近 NFC 终端时,设备中的 NFC 控制器会将来自读取器的所有数据直接路由到安全元件。图 1 说明了这一概念:

NFC 读取器通过 NFC 控制器从安全元件检索信息的示意图
图 1. 使用安全元件进行卡模拟。

安全元件本身与 NFC 终端进行通信,交易中不涉及任何 Android 应用。交易完成后,Android 应用可以直接查询安全元件以获取交易状态并通知用户。

基于主机的卡模拟

使用基于主机的卡模拟来模拟 NFC 卡时,系统会将数据直接路由到主机 CPU,而不是路由到安全元件。图 2 说明了基于主机的卡模拟的工作原理:

NFC 读取器通过 NFC 控制器从 CPU 检索信息的示意图
图 2. 不涉及安全元件的 NFC 卡模拟。

支持的 NFC 卡和协议

HCE 协议堆栈示意图
图 3. Android 的 HCE 协议栈。

NFC 标准支持许多不同的协议,并且您可以模拟不同类型的卡。

Android 4.4 及更高版本支持目前市场上常见的多种协议。许多现有的感应式卡都已经基于这些协议,例如感应式支付卡。目前市场上的许多 NFC 读取器也支持这些协议,包括本身充当读取器的 Android NFC 设备(请参阅 IsoDep 类)。这样,您只需使用 Android 设备即可围绕 HCE 构建和部署端到端 NFC 解决方案。

具体而言,Android 4.4 及更高版本支持模拟基于 NFC-Forum ISO-DEP 规范(基于 ISO/IEC 14443-4)的卡,并处理应用协议数据单元 (APDU)(如 ISO/IEC 7816-4 规范中所定义)。Android 要求在 Nfc-A (ISO/IEC 14443-3 Type A) 技术上仅模拟 ISO-DEP。可选择是否支持 Nfc-B(ISO/IEC 14443-4 Type B)技术。图 3 说明了所有这些规范的分层。

HCE 服务

Android 中的 HCE 架构基于 Android Service 组件(称为“HCE 服务”)。服务的主要优势之一是,它可以在后台运行,而无需任何界面。这非常适合许多 HCE 应用(例如会员卡或公交卡),用户无需启动应用即可使用。相反,如果设备尚未运行,则用设备触碰 NFC 读取器会启动正确的服务,并在后台执行事务。当然,在适当情况下,您可以随意从服务启动其他界面(例如用户通知)。

服务选择

当用户用设备触碰 NFC 读取器时,Android 系统需要知道 NFC 读取器想要与哪项 HCE 服务通信。ISO/IEC 7816-4 规范定义了一种应用选择方式,该方式以应用 ID (AID) 为中心。一个 AID 最多可包含 16 个字节。如果您要模拟现有 NFC 读取器基础架构的卡,这些读取器要寻找的 AID 通常为众所周知且已公开注册(例如,支付网络的 AID,如 Visa 和 MasterCard)。

如果您想为自己的应用部署新的读取器基础架构,则必须注册自己的 AID。AID 的注册程序在 ISO/IEC 7816-5 规范中定义。如果您要部署 Android 版 HCE 应用,我们建议您根据 7816-5 注册 AID,因为它可以避免与其他应用发生冲突。

AID 群组

在某些情况下,HCE 服务可能需要注册多个 AID 并被设置为所有 AID 的默认处理程序,以便实现特定应用。群组中的某些 AID 会转到其他服务不受支持。

一组 AID 就保存在一个 AID 群组中。对于 AID 群组中的所有 AID,Android 会保证以下某一项:

  • 群组中的所有 AID 都会路由到此 HCE 服务。
  • 群组中的任何 AID 都不会路由到此 HCE 服务(例如,因为用户更喜欢另一个请求了群组中的一个或多个 AID 的服务)。

也就是说,不会出现群组中的一些 AID 会路由到一项 HCE 服务,而另一部分会路由到另一项 HCE 服务这种介于两者之间的状态。

AID 群组和类别

您可以将每个 AID 群组与一个类别相关联。这样一来,Android 就可以按类别对 HCE 服务进行分组,进而允许用户在类别级别(而不是 AID 级别)设置默认值。避免在应用任何面向用户的部分提及 AID,因为它们对普通用户而言没有任何意义。

Android 4.4 及更高版本支持两个类别:

实现 HCE 服务

如需使用基于主机的卡模拟来模拟 NFC 卡,您需要创建一个用于处理 NFC 交易的 Service 组件。

检查 HCE 支持

您的应用可以通过检查 FEATURE_NFC_HOST_CARD_EMULATION 功能来检查设备是否支持 HCE。在应用清单中使用 <uses-feature> 标记声明您的应用会使用 HCE 功能,以及是否需要此功能才能正常运行。

服务实现

Android 4.4 及更高版本提供了一个便捷的 Service 类,即 HostApduService 类,可用作实现 HCE 服务的基础。

第一步是扩展 HostApduService,如以下代码示例所示:

Kotlin

class MyHostApduService : HostApduService() {

    override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
       ...
    }

    override fun onDeactivated(reason: Int) {
       ...
    }
}

Java

public class MyHostApduService extends HostApduService {
    @Override
    public byte[] processCommandApdu(byte[] apdu, Bundle extras) {
       ...
    }
    @Override
    public void onDeactivated(int reason) {
       ...
    }
}

HostApduService 声明了两个您必须替换和实现的抽象方法。其中一项就是,每当 NFC 读取器向您的服务发送应用协议数据单元 (APDU) 时,系统都会调用 processCommandApdu()。APDU 在 ISO/IEC 7816-4 规范中定义。APDU 是在 NFC 读取器和 HCE 服务之间交换的应用级数据包。该应用级协议是半双工的:NFC 读取器向您发送命令 APDU,然后等待您发送响应 APDU。

如前所述,Android 使用 AID 来确定读取器想要与哪项 HCE 服务通信。通常,NFC 读取器向设备发送的第一个 APDU 是 SELECT AID APDU;此 APDU 包含读取器要与之通信的 AID。Android 会从 APDU 中提取该 AID,将其解析为 HCE 服务,然后将该 APDU 转发到解析后的服务。

您可以通过从 processCommandApdu() 返回响应 APDU 的字节来发送响应 APDU。请注意,此方法是在应用的主线程上调用的,您不应阻塞该线程。如果您无法立即计算并返回响应 APDU,则返回 null。然后,您可以在另一个线程上执行必要的操作,并在操作完成后使用 HostApduService 类中定义的 sendResponseApdu() 方法发送响应。

Android 会不断将新的 APDU 从读取器转发到您的服务,直到发生以下任一情况:

  • NFC 读取器会发送另一个 SELECT AID APDU,操作系统会将其解析为其他服务。
  • NFC 读取器和设备之间的 NFC 链接断开。

在这两种情况下,系统都会调用类的 onDeactivated() 实现,并提供一个参数来指示发生了哪种情况。

如果您使用的是现有的读取器基础架构,则必须实现读取器在 HCE 服务中期望的现有应用级协议。

如果您要部署同样由您控制的新读取器基础架构,则可以定义自己的协议和 APDU 序列。尽量限制 APDU 的数量和要交换的数据大小:这可以确保您的用户只需短时间就能将其设备靠近 NFC 读取器。合理的上限是大约 1 KB 的数据,通常可以在 300 毫秒内交换。

服务清单声明和 AID 注册

您必须像往常一样在清单中声明服务,但您还必须在服务声明中添加一些其他部分:

  1. 如需告知平台它是实现 HostApduService 接口的 HCE 服务,请在服务声明中添加针对 SERVICE_INTERFACE 操作的 intent 过滤器。

  2. 如需告知平台此服务会请求哪些 AID 群组,请在该服务的声明中添加 SERVICE_META_DATA <meta-data> 标记,该标记指向提供 HCE 服务相关额外信息的 XML 资源。

  3. android:exported 属性设置为 true,并在服务声明中要求 android.permission.BIND_NFC_SERVICE 权限。前者可确保外部应用可以绑定该服务。然后,后者强制要求只有拥有 android.permission.BIND_NFC_SERVICE 权限的外部应用可以绑定到您的服务。由于 android.permission.BIND_NFC_SERVICE 是系统权限,因此这会有效地强制规定只有 Android OS 可以绑定到您的服务。

以下是 HostApduService 清单声明的示例:

<service android:name=".MyHostApduService" android:exported="true"
         android:permission="android.permission.BIND_NFC_SERVICE">
    <intent-filter>
        <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
    </intent-filter>
    <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
               android:resource="@xml/apduservice"/>
</service>

此元数据标记指向一个 apduservice.xml 文件。以下是此类文件的示例,其中有一个 AID 群组声明包含两个专有 AID:

<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
           android:description="@string/servicedesc"
           android:requireDeviceUnlock="false">
    <aid-group android:description="@string/aiddescription"
               android:category="other">
        <aid-filter android:name="F0010203040506"/>
        <aid-filter android:name="F0394148148100"/>
    </aid-group>
</host-apdu-service>

<host-apdu-service> 标记必须包含 <android:description> 属性,其中包含可在应用界面中显示的服务简单易懂的说明。在调用此服务处理 APDU 之前,您可以使用 requireDeviceUnlock 属性指定设备已解锁。

<host-apdu-service> 必须包含一个或多个 <aid-group> 标记。每个 <aid-group> 标记都必须执行以下操作:

  • 包含一个 android:description 属性,其中包含适合在界面中显示的简单易懂的 AID 群组说明。
  • 设置其 android:category 属性设置为指示 AID 群组所属的类别,例如由 CATEGORY_PAYMENTCATEGORY_OTHER 定义的字符串常量。
  • 包含一个或多个 <aid-filter> 标记,每个标记都包含一个 AID。 请以十六进制格式指定 AID,并确保其包含的字符数为偶数。

您的应用还需要拥有 NFC 权限才能注册为 HCE 服务。

AID 冲突解决

一台设备上可以安装多个 HostApduService 组件,但同一个 AID 可由多项服务注册。Android 解决 AID 冲突的方式取决于 AID 所属的类别。每个类别可能具有不同的冲突解决政策。

对于某些类别(例如付款),用户或许可以在 Android 设置界面中选择默认服务。对于其他类别,政策可能会始终询问用户在发生冲突时要调用哪项服务。如需了解如何查询特定类别的冲突解决政策,请参阅 getSelectionModeForCategory()

检查您的服务是否为默认服务

应用可以使用 isDefaultServiceForCategory() API 检查其 HCE 服务是否为特定类别的默认服务。

如果您的服务不是默认服务,您可以使用 ACTION_CHANGE_DEFAULT 请求将其设为默认服务。

付款应用

Android 会将声明了 payment 类别的 AID 群组的 HCE 服务视为付款应用。Android 4.4 及更高版本包含一个名为“触碰付款”的顶级设置菜单项,其中枚举了所有此类付款应用。在此设置菜单中,用户可以选择在触碰付款终端时调用的默认付款应用。

付款应用必需的资源

为了提供更具视觉吸引力的用户体验,HCE 付款应用必须提供服务横幅。

Android 13 及更高版本

为了更好地适应“设置”界面中的默认付款方式选择列表,请将横幅要求调整为方形图标。理想情况下,此图标应与应用启动器图标设计相同。这种调整可实现更高的一致性和更清晰的外观。

Android 12 及更低版本

将服务横幅的尺寸设置为 260x96 dp,然后通过将 android:apduServiceBanner 属性添加到指向可绘制资源的 <host-apdu-service> 标记,在元数据 XML 文件中设置服务横幅的尺寸。示例如下:

<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
        android:description="@string/servicedesc"
        android:requireDeviceUnlock="false"
        android:apduServiceBanner="@drawable/my_banner">
    <aid-group android:description="@string/aiddescription"
               android:category="payment">
        <aid-filter android:name="F0010203040506"/>
        <aid-filter android:name="F0394148148100"/>
    </aid-group>
</host-apdu-service>

屏幕关闭和屏幕锁定行为

HCE 服务的行为因设备搭载的 Android 版本而异。

Android 12 及更高版本

在以 Android 12(API 级别 31)及更高版本为目标平台的应用中,您可以通过将 requireDeviceScreenOn 设置为 false,在设备屏幕未开启的情况下启用 NFC 付款。

Android 10 及更高版本

搭载 Android 10(API 级别 29)或更高版本的设备支持安全 NFC。当安全 NFC 处于开启状态时,当设备屏幕关闭时,所有卡模拟器(主机应用和脱离主机的应用)都不可用。当安全 NFC 处于关闭状态时,脱离主机的应用在设备屏幕关闭时可用。您可以使用 isSecureNfcSupported() 检查安全 NFC 支持情况。

在搭载 Android 10 及更高版本的设备上,将 android:requireDeviceUnlock 设置为 true 的功能与搭载 Android 9 及更低版本的设备相同,但仅在关闭安全 NFC 时适用。也就是说,如果安全 NFC 处于开启状态,则无论 android:requireDeviceUnlock 设置如何,HCE 服务都无法从锁定屏幕运行。

Android 9 及更低版本

在搭载 Android 9(API 级别 28)及更低版本的设备上,当屏幕关闭时,NFC 控制器和应用处理器会完全关闭。因此,当屏幕关闭时,HCE 服务将无法正常运行。

同样在 Android 9 及更低版本中,HCE 服务可以在锁定屏幕上运行。不过,这由 HCE 服务的 <host-apdu-service> 标记中的 android:requireDeviceUnlock 属性控制。默认情况下,设备不要求解锁,即使设备处于锁定状态,系统也会调用您的服务。

如果您针对 HCE 服务将 android:requireDeviceUnlock 属性设置为 true,Android 会在发生以下情况时提示用户解锁设备:

  • 用户点按 NFC 读取器。
  • NFC 读取器会选择一个解析为您的服务的 AID。

解锁后,Android 会显示一个对话框,提示用户再次点按以完成交易。系统必须执行此操作,因为用户可能为了解锁设备而将设备从 NFC 读取器上移开。

与安全元件卡共存

此部分适用于部署了依赖安全元件进行卡模拟的应用的开发者。Android 的 HCE 实现旨在与实现卡模拟的其他方法并行工作,包括使用安全元件。

这种共存基于一项称为 AID 路由的原则。NFC 控制器会保留一个路由表,该表由一个(有限)路由规则列表组成。每个路由规则都包含一个 AID 和一个目的地。目的地可以是运行 Android 应用的主机 CPU,也可以是连接的安全元件。

当 NFC 读取器发送带有 SELECT AID 的 APDU 时,NFC 控制器会对其进行解析,并检查 AID 是否与其路由表中的任何 AID 匹配。如果匹配,该 APDU 及其后面的所有 APDU 都会发送到与 AID 关联的目的地,直到收到另一个 SELECT AID APDU 或 NFC 链接中断为止。

图 4 展示了此架构:

NFC 读取器与安全元件和 CPU 通信的示意图
图 4. 通过安全元件和主机卡模拟运行的 Android。

NFC 控制器通常还包含 APDU 的默认路由。如果在路由表中找不到某个 AID,则使用默认路由。虽然此设置可能因设备而异,但 Android 设备需要确保应用注册的 AID 正确路由到主机。

实现 HCE 服务或使用安全元件的 Android 应用无需担心路由表的配置;这些配置由 Android 自动处理。Android 只需知道 HCE 服务可以处理哪些 AID,以及安全元件可以处理哪些 AID 即可。路由表会根据已安装的服务以及用户配置为首选的服务自动配置。

以下部分介绍了如何为使用安全元件进行卡模拟的应用声明 AID。

安全元件 AID 注册

使用安全元件进行卡模拟的应用可以在其清单中声明脱离主机的服务。此类服务的声明与 HCE 服务的声明几乎完全相同。但存在以下情况:

  • intent 过滤器中使用的操作必须设置为 SERVICE_INTERFACE
  • 元数据名称属性必须设置为 SERVICE_META_DATA
  • 元数据 XML 文件必须使用 <offhost-apdu-service> 根标记。

    <service android:name=".MyOffHostApduService" android:exported="true"
           android:permission="android.permission.BIND_NFC_SERVICE">
      <intent-filter>
          <action android:name="android.nfc.cardemulation.action.OFF_HOST_APDU_SERVICE"/>
      </intent-filter>
      <meta-data android:name="android.nfc.cardemulation.off_host_apdu_service"
                 android:resource="@xml/apduservice"/>
    </service>
    

以下是注册两个 AID 的相应 apduservice.xml 文件的示例:

<offhost-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
           android:description="@string/servicedesc">
    <aid-group android:description="@string/subscription" android:category="other">
        <aid-filter android:name="F0010203040506"/>
        <aid-filter android:name="F0394148148100"/>
    </aid-group>
</offhost-apdu-service>

android:requireDeviceUnlock 属性不适用于脱离主机的服务,因为主机 CPU 不参与事务,因此无法阻止安全元件在设备锁定时执行事务。

对于作为付款应用的脱离主机的服务,android:apduServiceBanner 属性是必需的,并且可以选择作为默认付款应用。

脱离主机的服务调用

Android 绝不会启动或绑定到声明为“脱离主机”的服务,因为实际交易由安全元件执行,而不是由 Android 服务执行。服务声明仅允许应用注册安全元件上的 AID。

HCE 和安全

HCE 架构提供一项核心安全防护:由于您的服务受 BIND_NFC_SERVICE 系统权限保护,因此只有操作系统可以绑定到您的服务并与之通信。这样可以确保您接收的任何 APDU 实际上是操作系统从 NFC 控制器接收的 APDU,并且您发回的任何 APDU 都只会发送到操作系统,而操作系统又会直接将 APDU 转发到 NFC 控制器。

剩下的一个问题是获取应用发送到 NFC 读取器的数据。我们在 HCE 设计中有意分离了此机制;它不关心数据来自哪里,只要确保数据安全地传输到 NFC 控制器和 NFC 读取器。

例如,为了安全地存储和检索要从 HCE 服务发送的数据,您可以依赖 Android 应用沙盒,它将您的应用数据与其他应用隔离开来。如需详细了解 Android 安全性,请参阅安全提示

协议参数和详情

此部分适用于想要了解 HCE 设备在 NFC 协议的防冲突和激活阶段使用哪些协议参数的开发者。这样可以构建与 Android HCE 设备兼容的读取器基础架构。

Nfc-A(ISO/IEC 14443 A 类)协议防冲突和激活

作为 Nfc-A 协议激活的一部分,系统会交换多个框架。

在交换的第一部分中,HCE 设备会显示其 UID;应假定 HCE 设备具有随机 UID。这意味着,在每次点按时,呈现给读取器的 UID 都是随机生成的 UID。因此,NFC 读取器不应依赖 HCE 设备的 UID 作为身份验证或识别形式。

NFC 读取器随后可以通过发送 SEL_REQ 命令来选择 HCE 设备。HCE 设备的 SEL_RES 响应至少设置了第 6 位 (0x20),表明设备支持 ISO-DEP。请注意,SEL_RES 中的其他位也可以设置,例如表明支持 NFC-DEP (p2p) 协议。由于可能会设置其他位,因此希望与 HCE 设备交互的读取器应仅明确检查第 6 位,而不要将完整的 SEL_RES 与值 0x20 进行比较。

ISO-DEP 激活

Nfc-A 协议启用后,NFC 读取器会启动 ISO-DEP 协议激活。它会发送 RATS(对选择应答的请求)命令。NFC 控制器会生成 RATS 响应,即 ATS;ATS 无法由 HCE 服务配置。不过,HCE 实现必须满足 NFC Forum 对 ATS 响应的要求,以便 NFC 读取器可以依赖根据 NFC Forum 要求针对任何 HCE 设备设置的这些参数。

以下部分详细介绍了 NFC 控制器在 HCE 设备上提供的 ATS 响应的各个字节:

  • TL:ATS 响应的长度。不得指明大于 20 个字节的长度。
  • T0:所有 HCE 设备上都必须设置位 5、位 6 和位 7,表示 ATS 响应中包含 TA(1)、TB(1) 和 TC(1)。位 1 到位 4 表示 FSCI,对最大帧大小进行编码。在 HCE 设备上,FSCI 的值必须介于 0h 到 8h 之间。
  • T(A)1:定义读取器和模拟器之间的比特率,以及它们是否可以是非对称的。没有针对 HCE 设备的比特率要求或保证。
  • T(B)1:位 1 到位 4 表示启动帧保护时间整数 (SFGI)。在 HCE 设备上,SFGI 必须小于等于 8h。位 5 到位 8 表示帧等待时间整数 (FWI),并对帧等待时间 (FWT) 进行编码。在 HCE 设备上,FWI 必须小于等于 8h。
  • T(C)1:位 5 表示支持“高级协议功能”。HCE 设备不一定支持“高级协议功能”。位 2 表示支持 DID。HCE 设备不一定支持 DID。位 1 表示支持 NAD。HCE 设备不得支持 NAD,也不得将位 1 设为零。
  • 历史字节:HCE 设备最多可返回 15 个历史字节。愿意与 HCE 服务交互的 NFC 读取器不应对历史字节的内容或其存在做出假设。

请注意,许多 HCE 设备可能符合 EMVCo 中的支付网络在其“感应式通信协议”规范中指定的协议要求。具体而言:

  • T0 中的 FSCI 必须在 2h 到 8h 之间。
  • T(A)1 必须设置为 0x80,表示仅支持 106 kbit/s 比特率,而不支持读取器和模拟器之间的非对称比特率。
  • T(B)1 中的 FWI 必须小于等于 7h。

APDU 数据交换

如前所述,HCE 实现仅支持单个逻辑通道。尝试在不同逻辑通道上选择应用不适用于 HCE 设备。