随着物联网与O2O业务的发展,NFC在小额支付领域的前景越来越广阔。本文结合多个公开文档,简要介绍了NFC的工作场景,同时使用RxAndroid实现了对NFC字节流的读取与异常捕捉;介绍了卡模拟过程中与Android底层的交互原理与简单示例。
NFC开发是大坑 ,适配很累,而且在第三方联调对接上很费时间,属于那种吃力不讨好最后又啥都没学到的。我不建议深入折腾。
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),比如
SD卡(银联主推) 手机内置(Embeded,终端厂商主推) SIM卡内置(运营商主推),经过测试我的电信4G天翼卡就是支持卡模拟的 这些芯片的内部实际上运行一个微型JAVA虚拟机,,一个卡(手机)可以装多个applet,它有自己的证书,上层协议是卡片与芯片内置的Applet进行加密交互。通过OAT(空中发卡)业务可以实现把服务器中的Applet二进制文件下载到手机芯片中(是不是有点像HotFix技术?),俗称“卡包”。
举个例子,我通过软件下载了“深圳通”“招商银行”两个applet到手机内置芯片中。当我要刷公交卡时,公交车上的机器将进行如下操作:
1 2 3 select 公交卡applet from appletsselect 余额文件块 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的数据结构如图,本质是一种编码,网上有很多序列化/反序列化的工具
发送的数据报
返回值是
3. NFC读卡器的Android开发 3.1. 配置Manifest 配置权限与feature
1 2 3 4 5 <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" > ..... <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 <resources > <tech-list > <tech > android.nfc.tech.IsoDep</tech > <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) { Log.d(TAG, "handleIntent: " + intent.getAction()); if (!intent.getAction().equals(NfcAdapter.ACTION_TECH_DISCOVERED)) { Log.d(TAG, "handleIntent: no valid action" ); return ; } tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); Log.d(TAG, "Id:" + Util.byteArraytoHexString(tag.getId())); 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; if (iso == null ) { subscriber.onError(new NullPointerException ("Tech was not enumerated in NfcTechList" )); return ; } iso.setTimeout(5000 ); 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 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 files('libs/org.simalliance.openmobileapi.jar' ) }
剩下的事情都交个openmobileapi的SDK 了,SDK提供了基于字节流的通信,我们可以使用okio这样的工具来转换字节与string,当然更高级的使用涉及到厂商的私钥,一般通过TSM发卡厂商提供的私有SDK与密钥进行更高层的封装(反正这些都有SDK),就完成开发了。
注意两点:
回调是在Binder线程池中,所以注意谨慎操作UI组件 onDestory时注意释放连接 常见金融协议 PBOC3.0: 国内银联协议 EMV: MasterCard, VISA, JCB和国际银联等 5. Source Code 代码详见我的Github示例 ,只保留了基于字节流的查询功能。
参考 http://wiki.mbalib.com/wiki/主机卡模拟 http://www.oracle.com/technetwork/java/embedded/javacard/index2-136727.html http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_5_basic_organizations.aspx#table18 http://www.wendangpan.com/702645731/