Android下的基于NFC的金融业务开发
2016-09-23 / modified at 2024-03-03 / 2.8k words / 11 mins

随着物联网与O2O业务的发展,NFC在小额支付领域的前景越来越广阔。本文结合多个公开文档,简要介绍了NFC的工作场景,同时使用RxAndroid实现了对NFC字节流的读取与异常捕捉;使用了MVP(好吧,其实是一个简单的静态代理)转移了Activity下的代码量;介绍了卡模拟过程中与Android底层的交互原理与简单示例。


1. NFC工作模式

1.1. 读卡器模式

直接操作“NFC Tag”对象对实体卡片进行I/O操作,目前的NFC卡片大多都是运行Applet服务的嵌入式JAVA虚拟机(俗称CPU卡,比较贵,这个卡的性能非常弱,只能进行写改删查操作),我们通过AndroidAPI连接后,就可以使用各种字节命令(APDUS)控制卡片。比如网上的“公交卡充值/查询”APP就是与公交公司合作的读卡器。

1.2. 点对点模式

使用 Android Bean 技术进行点对点通信,NFC本身速度很慢,所以这项技术最多的场景就是通过两台同品牌手机的NFC私有的应用协议数据单元(APDU)配对,然后建立高速的蓝牙/WIFI通信进行数据交换,以节约适配蓝牙/WIFI的时间。比如蓝牙耳机QC35,华为路由器就支持NFC快速配对。

1.3. 卡模拟模式

这个场景下,我们可以把手机看成一张“公交卡/银行卡”。真正的应用过程在上级应用层与物理加密芯片之间。建行的"Quick Pass"/ApplePay等技术就是基于此的。

1.3.1. 虚拟卡模式(Virtual Card Mode)

某些手机内置了安全芯片(SE,Secure Element),比如

  1. SD卡(银联主推)
  2. 手机内置(Embeded,终端厂商主推)
  3. SIM卡内置(运营商主推),经过测试我的电信4G天翼卡就是支持卡模拟的

这些芯片的内部实际上运行一个微型JAVA虚拟机,,一个卡(手机)可以装多个applet,它有自己的证书,上层协议是卡片与芯片内置的Applet进行加密交互。通过OAT(空中发卡)业务可以实现把服务器中的Applet二进制文件下载到手机芯片中(是不是有点像HotFix技术?),俗称“卡包”。

举个例子,我通过软件下载了“深圳通”“招商银行”两个applet到手机内置芯片中。当我要刷公交卡时,公交车上的机器将进行如下操作:

1
2
3
select 公交卡applet from applets
select 余额文件块 from 公交卡applet
...i/o work...

它最大的好处就是写入了Applet后,手机没电了同样可以刷卡使用(取决于电路设计)。而且它支持多张卡,节约空间。

1.3.2. 主机卡模式(Host Card Mode)

Android文档中可以看到,谷歌在Kitkat后,提供了一种HCE(Host Card Emulation)的方法,只要在Android开发中继承HostApduService服务(类似于J2EE中的Applet),就可以实现软件上进行卡模拟,当然这类的应用层的安全性非常重要,最终还是通过云服务的一些Token/证书进行交互。以后移动互联网公司可能会在这个地方发力。

1
CardReader <----> NFCAdapter <----> HostApduService <----> BackendCloudServer

2. NFC的传输层协议栈

JAVA中 byte 是有符号一字节的,而char是编码过的两个字节的;C中byte(也就是#define byte (unsigned char))是无符号一个字节的,而char是有符号一字节的; 为了方便,我们全部使用byte与16进制进行表示

APDU的数据结构如图,本质是一种编码,网上有很多序列化/反序列化的工具

发送的数据报

简书不支持table标签!!

返回值是

居然不支持table!


3. NFC读卡器的Android开发

3.1. 配置Manifest

配置权限与feature

1
2
3
4
5
<!--Use NFC feature and Permissions-->
<uses-permission android:name="android.permission.NFC"/>
<uses-feature
android:name="android.hardware.nfc"
android:required="true"/>

注意配置Activity的模式为singleTop,配置Activity不可转变屏幕(防止Intent丢失,支付宝也是这样做的),配置NFCTech过滤器,配置Intent接收器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<activity
android:name=".NfcReaderActivity"
android:launchMode="singleTop"
android:alwaysRetainTaskState="true"
android:label="@string/title_activity_nfcscanner">

.....

<!--nfc filter-->
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" />
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

配置NFC卡片技术过滤器,每个卡对应一个<tech-list>

1
2
3
4
5
6
7
8
9
10
11
<!--file nfc_tech_filter.xml -->
<resources>

<!--重庆一卡通-->
<tech-list>
<!--ISO 14443-4-->
<tech>android.nfc.tech.IsoDep</tech>
<!--ISO 14443-3A-->
<tech>android.nfc.tech.NfcA</tech>
</tech-list>
</resources>

3.2. 获取Intent

在Activity中针对收到的Intent进行处理,onCreate是从其他App进入的情况,onNewIntent是已经在本App中收到新的Intent的情况

1
2
3
4
5
6
7
8
9
10
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nfcscanner);
handleIntent(getIntent());
}

@Override protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}

接下来处理Intent,获取到Tag对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void handleIntent(Intent intent) {
//获取到Intent的Action,注意多打Log
Log.d(TAG, "handleIntent: " + intent.getAction());
if (!intent.getAction().equals(NfcAdapter.ACTION_TECH_DISCOVERED)) {
Log.d(TAG, "handleIntent: no valid action");
return;
}
//获取Tag对象
tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
//获取卡ID,这个ID一般没什么用,有可能是卡自动生成的
Log.d(TAG, "Id:" + Util.byteArraytoHexString(tag.getId()));
//NFC卡片所支持的技术标准
Log.d(TAG, "TechList:" + Arrays.toString(tag.getTechList()));
}

3.3. I/O处理

接下来,我们通过Tag获取到I/O对象,通过字节流进行处理,这里的例子按照如下的标准进行解析

1
ISO-DEP (ISO 14443-4) 注意它与ISO-7816也是兼容的

目前最完善的代码如下,基于RxAndroid进行异步处理,可以处理所有的异常,注意这里的subscriber是一个RxAndroid的回掉接口,它处理成功下一个异常这三个回掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public final Observable<ResponseAPDU> getResponseAPDUObservable(final Tag tag,
final byte... bytes) {

return Observable.create(new Observable.OnSubscribe<ResponseAPDU>() {
@WorkerThread @Override public void call(Subscriber<? super ResponseAPDU> subscriber) {
if (tag == null) {
subscriber.onError(new NullPointerException(
"Tag is null,try again to turn on NFC and keep your card close to your phone!"));
return;
}
if (bytes == null || bytes.length == 0) {
subscriber.onError(
new NullPointerException("apdu is null or empty, cheak your command!"));
return;
}
IsoDep iso = IsoDep.get(tag);
byte[] result_all;
//NfcA iso = NfcA.get(tag);
if (iso == null) {
subscriber.onError(new NullPointerException("Tech was not enumerated in NfcTechList"));
return;
}
iso.setTimeout(5000);//ms
try {
if (!iso.isConnected()) {
iso.connect();
} else {
iso.close();
iso.connect();
}
result_all = iso.transceive(bytes);
subscriber.onNext(ResponseAPDU.createFromPdu(result_all));
subscriber.onCompleted();
} catch (IOException e) {
subscriber.onError(e);
} finally {
if (iso != null) {
try {
iso.close();
} catch (IOException ignored) {
subscriber.onError(ignored);
}
}
}
}
});
}

在UI线程下,进行如下的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//RxAndroid的回掉
Subscriber subscriber = new Subscriber<ResponseAPDU>() {
@Override public void onCompleted() {
Log.d(TAG, "onCompleted:");
}

@Override public void onError(Throwable e) {
mTextView_response.setText(e.getMessage());
}

@Override public void onNext(ResponseAPDU responseAPDU) {
Log.d(TAG, "onNext:" + responseAPDU.toString());
mTextView_response.setText(responseAPDU.toString());
}
};

getResponseAPDUObservable(tag, data)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber);

这里还有很大的优化空间,比如加入 keep-alive 机制,避免每读写一次都重新打开关闭流,当然这个需要与场景进行适配

3.4. 封装成高级对象(DTO)

通过以上步骤,我们能够得到一个简单的基于字节流的NFC读取器,但是更加进一步的开发(比如查询命令,SDK等)需要卡厂商提供内部公开的SDK,均有私钥的,就不能具体写了。有兴趣的可以去网上搜索PBOC2.0,网上有泄露的代码,貌似是GPL协议,各位慎重学一下。这个开发一点都不难,封装好I/O后,写改删查即可,主要难点在能够与公交公司谈拢(人家说不定看不上你,当然这个活不是程序员干的),以及谈拢后与对方开发进行联调。


4. NFC的卡模拟

从12年开始,随着O2O的发展,国外的品牌手机都开始加入了对SE芯片的支持,中华酷联也开始加入了对SE卡的支持。

4.1. 判断手机是否支持SE

手机中必须有如下的文件,这是一个开源的项目,各大手机厂商都毫不吝啬的使用了,注意SmartcardService需要在后台运行才能调用,某些ROM被精简了,12年以后的高档机(比如三星,Sony大法)到现在的千元机(比如魅蓝NOTE)的ROM中一般都有这个文件

1
2
/system/framework/org.simalliance.openmobileapi.jar 
/system/app/SmartcardService.apk

如果没有的话,会报错ClassNoFoundException,这里我也没有比较好的解决方案,建议先放着,优先把支持的机子给适配好。

4.2. AndroidSE的通信架构

经过阅读开源代码,交互流程如下,NFC卡模拟开发实际上是基于Binder的,对AIDL与Binder通信需要有理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Activity(调用者)

| ServiceConnection

SmartcardService:remote

| Parcel

SmartcardService

| ServiceConnection

com.android.internal.telephony.ITelephony:remote

| Parcel

com.android.internal.telephony.ITelephony

| transmitIccBasicChannel()

Framwork(JNI/HAL/Kernal)

上文中的 :remote实际上就是 Stub.asInterface()实现远程打桩的代理调用,类似于Spring中的依赖查找。当然在SDK端,只用写AIDL即可,甚至SDK都被写好了。

Binder机制实际上是基于 ASHMEM(匿名共享内存) 的IPC

4.3. 开发卡模拟

在AndroidStudio中,需要注意jar包的依赖形式,由于这个包是已经在系统框架中使用了,防止classloader重复加载,所以这个jar包不能打包到app中,只用在编译时设置为provided即可

1
2
3
4
5
6
dependencies {
compile fileTree(include: ['*.jar'], exclude: ['org.simalliance.openmobileapi.jar'], dir: 'libs')
//provided表示只在编译时使用jar包,运行时默认环境中已经存在这个jar包了
//jar包在网上有下载,注意要与ROM版本一直,不要一味求最新
provided files('libs/org.simalliance.openmobileapi.jar')
}

剩下的事情都交个openmobileapi的SDK了,SDK提供了基于字节流的通信,我们可以使用okio这样的工具来转换字节与string,当然更高级的使用涉及到厂商的私钥,一般通过TSM发卡厂商提供的私有SDK与密钥进行更高层的封装(反正这些都有SDK),就完成开发了。

注意两点:

  1. 回调是在Binder线程池中,所以注意谨慎操作UI组件
  2. onDestory时注意释放连接

常见金融协议

  • PBOC3.0: 国内银联协议
  • EMV: MasterCard, VISA, JCB和国际银联等

5. Source Code

代码详见我的Github示例,只保留了基于字节流的查询功能。
NFC开发是大坑,适配很累,而且在第三方联调上很费时间,属于那种吃力不讨好最后又啥都没学到的。我不建议深入折腾

参考

  1. http://wiki.mbalib.com/wiki/主机卡模拟
  2. http://www.oracle.com/technetwork/java/embedded/javacard/index2-136727.html
  3. http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_5_basic_organizations.aspx#table18
  4. http://www.wendangpan.com/702645731/