许多提供 NFC 功能的 Android 设备已经支持 NFC 卡模拟。在大多数情况下,卡由设备中的一个单独芯片(称为 *安全元件*)模拟。许多无线运营商提供的 SIM 卡也包含安全元件。
Android 4.4 及更高版本提供了一种无需安全元件即可进行卡模拟的附加方法,称为 *基于主机的卡模拟*。这允许任何 Android 应用模拟卡并直接与 NFC 读卡器通信。本主题描述了基于主机的卡模拟 (HCE) 在 Android 上的工作方式,以及如何开发使用此技术模拟 NFC 卡的应用。
使用安全元件进行卡模拟
当使用安全元件提供 NFC 卡模拟时,要模拟的卡通过 Android 应用预配到设备上的安全元件中。然后,当用户将设备放在 NFC 终端上时,设备中的 NFC 控制器将所有来自读卡器的数据直接路由到安全元件。图 1 说明了此概念
安全元件本身执行与 NFC 终端的通信,并且交易中不涉及任何 Android 应用。交易完成后,Android 应用可以直接查询安全元件的交易状态并通知用户。
基于主机的卡模拟
当使用基于主机的卡模拟模拟 NFC 卡时,数据将直接路由到主机 CPU,而不是路由到安全元件。图 2 说明了基于主机的卡模拟的工作方式
支持的 NFC 卡和协议
NFC 标准支持许多不同的协议,您可以模拟不同类型的卡。
Android 4.4 及更高版本支持目前市场上几种常见的协议。许多现有的非接触式卡片都基于这些协议,例如非接触式支付卡。这些协议也受当今市场上许多NFC读卡器支持,包括充当读卡器的Android NFC设备(参见IsoDep
类)。这允许您仅使用Android设备构建和部署围绕HCE的端到端NFC解决方案。
具体来说,Android 4.4 及更高版本支持模拟基于NFC论坛ISO-DEP规范(基于ISO/IEC 14443-4)的卡片,并处理ISO/IEC 7816-4规范中定义的应用协议数据单元 (APDU)。Android 规定仅在Nfc-A(ISO/IEC 14443-3 A型)技术之上模拟ISO-DEP。对Nfc-B(ISO/IEC 14443-4 B型)技术的支持是可选的。图3说明了所有这些规范的分层。
HCE服务
Android 中的HCE架构基于Android Service
组件(称为HCE服务)。服务的关键优势之一是它可以在后台运行而无需任何用户界面。这非常适合许多HCE应用程序,例如会员卡或交通卡,用户无需启动应用程序即可使用。相反,将设备轻触NFC读卡器,如果服务尚未运行,则会启动正确的服务并在后台执行交易。当然,您可以根据需要从您的服务启动其他UI(例如用户通知)。
服务选择
当用户将设备轻触NFC读卡器时,Android系统需要知道NFC读卡器想要与哪个HCE服务通信。ISO/IEC 7816-4规范定义了一种选择应用程序的方法,其核心是应用程序ID (AID)。AID最多由16个字节组成。如果您正在为现有的NFC读卡器基础设施模拟卡片,则这些读卡器查找的AID通常是众所周知的并公开注册的(例如,Visa和MasterCard等支付网络的AID)。
如果您想为自己的应用程序部署新的读卡器基础设施,则必须注册您自己的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服务,而另一些路由到另一个服务。
AID组和类别
您可以将每个AID组与一个类别关联。这允许Android按类别将HCE服务组合在一起,进而允许用户在类别级别而不是AID级别设置默认值。避免在应用程序的任何用户界面部分提及AID,因为它们对普通用户没有任何意义。
Android 4.4及更高版本支持两个类别:
CATEGORY_PAYMENT
(涵盖行业标准支付应用程序)CATEGORY_OTHER
(适用于所有其他HCE应用程序)
实现HCE服务
要使用基于主机的卡模拟来模拟NFC卡,您需要创建一个Service
组件来处理NFC事务。
检查HCE支持
您的应用程序可以通过检查FEATURE_NFC_HOST_CARD_EMULATION
功能来检查设备是否支持HCE。使用应用程序清单中的<uses-feature>
标签声明您的应用程序使用HCE功能,以及它对于应用程序运行是否必不可少。
服务实现
Android 4.4及更高版本提供了一个方便的Service
类,您可以将其用作实现HCE服务的基准:HostApduService
类。
第一步是扩展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
声明了两个必须重写和实现的抽象方法。其中一个,processCommandApdu()
,每当NFC读卡器向您的服务发送应用协议数据单元 (APDU) 时都会调用。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。然后,您可以使用另一个线程完成必要的工作,并使用sendResponseApdu()
(在HostApduService
类中定义的方法)在完成时发送响应。
Android会继续将来自读卡器的新的APDU转发到您的服务,直到发生以下情况之一:
- NFC读卡器发送另一个
SELECT AID
APDU,操作系统将其解析为不同的服务。 - NFC读卡器和您的设备之间的NFC连接中断。
在这两种情况下,都会调用您的类的onDeactivated()
实现,并带有一个参数,指示发生了哪一种情况。
如果您正在使用现有的读卡器基础设施,则必须在您的HCE服务中实现读卡器期望的现有应用程序级协议。
如果您正在部署您也控制的新读卡器基础设施,则可以定义您自己的协议和APDU序列。尝试限制APDU的数量和要交换的数据的大小:这可以确保您的用户只需将设备放在NFC读卡器上很短的时间。合理的上限约为1 KB的数据,通常可以在300毫秒内交换。
服务清单声明和AID注册
您必须像往常一样在清单中声明您的服务,但您还必须在服务声明中添加一些其他部分。
要告诉平台它是一个实现
HostApduService
接口的HCE服务,请向您的服务声明添加针对SERVICE_INTERFACE
操作的意图过滤器。要告诉平台此服务请求哪些AID组,请在服务的声明中包含
SERVICE_META_DATA
<meta-data>
标签,指向包含有关HCE服务的其他信息的XML资源。将
android:exported
属性设置为true
,并在您的服务声明中需要android.permission.BIND_NFC_SERVICE
权限。前者确保可以由外部应用程序绑定到该服务。后者强制只有持有android.permission.BIND_NFC_SERVICE
权限的外部应用程序才能绑定到您的服务。由于android.permission.BIND_NFC_SERVICE
是一个系统权限,因此这有效地强制只有Android操作系统才能绑定到您的服务。
以下是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>
属性,该属性包含服务的易于理解的描述,您可以在应用程序UI中显示。您可以使用requireDeviceUnlock
属性指定在调用此服务来处理APDU之前设备已解锁。
<host-apdu-service>
必须包含一个或多个<aid-group>
标签。每个<aid-group>
标签都需要执行以下操作:
- 包含一个
android:description
属性,该属性包含AID组的易于理解的描述,适合在UI中显示。 - 将其
android:category
属性设置为指示AID组所属的类别,例如CATEGORY_PAYMENT
或CATEGORY_OTHER
定义的字符串常量。 - 包含一个或多个
<aid-filter>
标签,每个标签都包含一个AID。以十六进制格式指定AID,并确保它包含偶数个字符。
您的应用程序还需要持有NFC
权限才能注册为HCE服务。
AID冲突解决
单个设备上可以安装多个HostApduService
组件,同一个AID可以被多个服务注册。Android使用以下步骤确定调用哪个服务:
- 如果用户选择的默认钱包应用已注册该AID,则调用该应用。
- 如果默认钱包应用未注册该AID,则调用已注册该AID的服务。
- 如果多个服务都注册了该AID,则Android会询问用户要调用哪个服务。
检查您的应用是否为默认钱包应用
应用可以通过将RoleManager.ROLE_WALLET
传递给RoleManager.isRoleHeld()
来检查它们是否是默认钱包应用。
如果您的应用不是默认钱包应用,您可以通过将RoleManager.ROLE_WALLET
传递给RoleManager.createRequestRoleIntent()
来请求默认钱包角色。
钱包应用
Android将声明了具有支付类别的AID组的HCE服务视为钱包应用。Android 15及更高版本包含一个默认钱包应用角色,用户可以通过导航到**设置 > 应用 > 默认应用**来选择。这定义了在轻触支付终端时要调用的默认钱包应用。
钱包应用所需资源
为了提供更具视觉吸引力的用户体验,HCE钱包应用需要提供服务横幅。
Android 13及更高版本
为了更好地适应设置UI中的默认支付选择列表,请将横幅要求调整为方形图标。理想情况下,它应该与应用程序启动器图标设计相同。此调整可创建更多一致性和更简洁的外观。
Android 12及更低版本
将服务横幅的大小设置为260x96 dp,然后通过在您的元数据XML文件中添加android:apduServiceBanner
属性到<host-apdu-service>
标签来设置服务横幅的大小,该标签指向可绘制资源。以下是一个示例
<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>
观察模式
Android 15引入了观察模式功能。启用后,观察模式允许设备观察NFC轮询循环,并向相应的HostApduService
组件发送有关这些循环的通知,以便它们可以准备与给定的NFC终端交互。HostApduService
可以通过将true
传递给setObserveModeEnabled()
来将设备置于观察模式。这指示NFC堆栈不允许NFC交易,而是被动地观察轮询循环。
轮询循环过滤器
您可以使用以下任一方法为HostApduService
注册轮询循环过滤器:
registerPollingLoopFilterForService()
用于必须完全匹配轮询帧的过滤器。registerPollingLoopPatternFilterForService()
用于对轮询帧执行正则表达式匹配的过滤器。
当轮询循环过滤器与非标准轮询帧匹配时,NFC堆栈会通过调用其processPollingFrames()
方法将这些轮询帧路由到相应的HostApduService
。这允许服务采取任何必要的步骤来确保用户已准备好进行交易并打算这样做——例如,对用户进行身份验证。如果NFC读取器在其轮询循环中仅使用标准帧,则NFC堆栈会将这些轮询帧路由到首选前台服务(如果该服务在前台)或默认钱包角色持有者。
轮询帧通知还包括您可以通过调用getVendorSpecificGain()
检索到的供应商特定的场强测量值。供应商可以使用他们自己的比例进行测量,只要它适合单个字节即可。
响应轮询循环并退出观察模式
当服务准备好进行交易时,它可以通过将false
传递给setObserveModeEnabled()
来退出观察模式。然后,NFC堆栈将允许交易继续。
HostApduService
组件可以通过在清单中将shouldDefaultToObserveMode
设置为true
或通过调用CardEmulation.setShouldDefaultToObserveModeForService()
来指示每当它们是首选支付服务时应启用观察模式。
HostApduService
和OffHostApduService
组件还可以通过在清单中的PollingLoopFilter
声明中将autoTransact
设置为true
来指示匹配收到的轮询循环帧的轮询循环过滤器应自动禁用观察模式并允许交易继续。
屏幕关闭和锁屏行为
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控制器通常还包含APDU的默认路由。当在路由表中找不到AID时,将使用默认路由。虽然此设置可能因设备而异,但Android设备需要确保您的应用注册的AID被正确路由到主机。
实现HCE服务或使用安全元件的Android应用程序无需担心配置路由表;这由Android自动处理。Android只需要知道哪些AID可以由HCE服务处理,哪些AID可以由安全元件处理。路由表将根据安装的服务以及用户配置为首选的服务自动配置。
下一节将解释如何为使用安全元件进行卡模拟的应用程序声明AID。
安全元件AID注册
使用安全元件进行卡模拟的应用程序可以在其清单中声明一个非主机服务。此类服务的声明几乎与HCE服务的声明相同。例外情况如下:
- 意图过滤器中使用的操作必须设置为
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 不会启动或绑定声明为“off-host”的服务,因为实际事务是由安全元件而不是 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 设备。SEL_RES
HCE 设备的响应至少设置了第 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 论坛对 ATS 响应的要求,因此 NFC 读卡器可以依靠这些参数根据 NFC 论坛对任何 HCE 设备的要求进行设置。
以下部分详细介绍了 NFC 控制器在 HCE 设备上提供的 ATS 响应的各个字节。
- TL:ATS 响应的长度。不得指示长度大于 20 字节。
- T0:所有 HCE 设备必须设置第 5、6 和 7 位,表示 TA(1)、TB(1) 和 TC(1) 包含在 ATS 响应中。位 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 设备上不起作用。