关于HCE——Android手机NFC模拟刷卡成果和心得
一、前言
在最近,开始研究了手机模拟NFC刷卡的一些内容,想是自己实现一次手机模拟刷卡。
NFC大家应该都了解,这两年的安卓手机基本都是支持了NFC功能,手机厂商也给出了各自的“钱包”应用,实现卡管理。
门禁卡,公交卡,银行卡等众多功能都可以用一台手机的NFC实现,确实引起了好奇心,所以就开始下面这些研究了。
二、开始摸索
首先呢,还是先去看了看google的官方文档,看看android系统上的nfc到底在技术层面上能干啥:
支持 NFC 的 Android 设备同时支持以下三种主要操作模式:读取器/写入器模式:支持 NFC 设备读取和/或写入被动 NFC 标签和贴纸。
点对点模式:支持 NFC 设备与其他 NFC 对等设备交换数据;Android Beam 使用的就是此操作模式。
卡模拟模式:支持 NFC 设备本身充当 NFC 卡。然后,可以通过外部 NFC 读取器(例如 NFC 销售终端)访问模拟 NFC 卡。*/
这个就是说明:android的nfc的api总共分为三大功能:
1、nfc功能一:支持读取某个nfc设备或者标签中的内容,和给某个nfc设备或者标签写入内容
这个可以联想到淘宝上卖的那种nfc标签的效果。买来的nfc标签可以用手机写入固定的一些内容,然后需要时,手机碰一碰nfc标签,手机跳转固定网址,碰一碰,手机打开支付宝。功能还是很花哨的。
我们要模拟nfc卡片的话,肯定是要先读取被模拟nfc卡片信息的,所以这个api后面肯定要用到。
2、nfc功能二:支持和其他nfc设备交换数据。
官方文档举的Android Beam这个例子,我都有点生疏了。这个功能我还是在android4.0的时代用过,当时是在相册选中一张照片,选择分享,然后选择Android Beam,然后和另一台支持nfc的安卓手机背部挨在一起,图片就传输过去了。这个说实话不是很好用。。。现在好像都没厂商推广这个功能。。。。
我们应该本次不需要用到这个。。
3、nfc功能三:支持 NFC 设备本身充当 NFC 卡
重点来了,就是要这个。
官方文档里介绍,NFC模拟这个,有两种实现原理。
第一种是使用“安全元件”(一种安全芯片),所有与读卡设备的数据交流,由安全芯片管理控制,然后再传给安卓NFC控制器处理。这个是指所有的交互逻辑都在安全芯片里面,是安全芯片在控制NFC进行模拟。
第二种是android4.4系统引入的基于主机模拟——HCE(Host-based Card Emulation),读卡设备的数据,直接传到android系统的应用上交流处理,然后由应用控制NFC进行模拟。
很明显,我们只能使用第二种——HCE。因为我们要模拟复制nfc卡片的代码,android studio写的代码肯定都是应用层的,应用来控制安卓的nfc。要是由安全芯片来控制,我就没法玩了,还能重新设计个芯片不成。。
4、nfc卡片的分类
阻挡我的第一个坎出现了,没错,nfc卡片是有区别的,不是天下大一统的。
因为nfc本质还是一个电磁感应线圈,数据的传输还是要基于某个协议进行的,根据支持的协议不同,卡就有不同的类别。
android系统的api将nfc分为以下这些类别:MifareClassic、IsoDep、MifareUltralight、ndef、NdefFormatable、NfcA、NfcB、NfcF、NfcV、NfcBarcode
而实际上,我们日常使用上好像也只听说过什么加密卡,非加密卡什么的。这个只是用户体验上的说法,不能作为开发依据。
5、开始干活
既然有分类,我们干脆直接就识别一下,我们目前手上的卡是什么就好了,上代码
(1)在res/xml下面新建nfc_tech_filter.xml文件
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"><!-- 可以处理所有Android支持的NFC类型 --><tech-list><tech>android.nfc.tech.IsoDep</tech></tech-list><tech-list><tech>android.nfc.tech.NfcA</tech></tech-list><tech-list><tech>android.nfc.tech.NfcB</tech></tech-list><tech-list><tech>android.nfc.tech.NfcF</tech></tech-list><tech-list><tech>android.nfc.tech.NfcV</tech></tech-list><tech-list><tech>android.nfc.tech.Ndef</tech></tech-list><tech-list><tech>android.nfc.tech.NdefFormatable</tech></tech-list><tech-list><tech>android.nfc.tech.MifareUltralight</tech></tech-list><tech-list><tech>android.nfc.tech.MifareClassic</tech></tech-list>
</resources>
(2)修改AndroidManifest.xml,新增nfc权限,声明minSdkVersion=“10”,新增NfcActivity,在其中增加三个nfc标签的过滤器,用上刚刚的nfc_tech_filter.xml
<uses-permission android:name="android.permission.NFC"/><uses-sdk android:minSdkVersion="10" /><activityandroid:name=".activity.NfcActivity"><intent-filter><action android:name="android.nfc.action.NDEF_DISCOVERED" />--><category android:name="android.intent.category.DEFAULT" />--><data android:mimeType="text/plain" />--></intent-filter><intent-filter><action android:name="android.nfc.action.TAG_DISCOVERED" /></intent-filter><intent-filter><action android:name="android.nfc.action.TECH_DISCOVERED" /></intent-filter><meta-dataandroid:name="android.nfc.action.TECH_DISCOVERED"android:resource="@xml/nfc_tech_filter" /></activity>
(3)NfcActivity,处理nfc读取逻辑,功能实现主要是引入了NfcAdapter。界面布局只有一个textView
import androidx.appcompat.app.AppCompatActivity;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.nfc.tech.MifareClassic;
import android.nfc.tech.NfcA;
import android.nfc.tech.NfcF;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.socks.library.KLog;
import java.io.IOException;public class NfcActivity extends AppCompatActivity {NfcAdapter nfcAdapter;TextView text;private PendingIntent pi;private IntentFilter[] mFilters;private String[][] mTechLists;private String metaInfo;private Boolean auth=false;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_nfc);text = (TextView) findViewById(R.id.text);// 获取默认的NFC控制器nfcAdapter = NfcAdapter.getDefaultAdapter(this);pi = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);try {ndef.addDataType("*/*");} catch (IntentFilter.MalformedMimeTypeException e) {throw new RuntimeException("fail", e);}mFilters = new IntentFilter[]{ndef,};mTechLists = new String[][]{{IsoDep.class.getName()}, {NfcA.class.getName()},};KLog.d(" mTechLists", NfcF.class.getName() + mTechLists.length);if (nfcAdapter == null) {Toast.makeText(this, "手机不支持nfc", Toast.LENGTH_SHORT).show();finish();return;}if (!nfcAdapter.isEnabled()) {Toast.makeText(this, "设置没开NFC", Toast.LENGTH_SHORT).show();finish();return;}}@Overrideprotected void onNewIntent(Intent intent) {/*isoDep CPU卡(ISO 14443-4) NfcA或NfcBm1卡 NfcA */super.onNewIntent(intent);Toast.makeText(this, intent, Toast.LENGTH_SHORT).show();Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);KLog.e("tagFromIntent", "tagFromIntent" + tagFromIntent);if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {processIntent(intent);//处理响应}}//页面获取焦点@Overrideprotected void onResume() {super.onResume();nfcAdapter.enableForegroundDispatch(this, pi, null, null);}//页面失去焦点@Overrideprotected void onPause() {super.onPause();if (nfcAdapter != null) {nfcAdapter.disableForegroundDispatch(this);}//字符序列转换为16进制字符串private String bytesToHexString(byte[] src) {StringBuilder stringBuilder = new StringBuilder("0x");if (src == null || src.length <= 0) {return null;}char[] buffer = new char[2];for (int i = 0; i < src.length; i++) {buffer[0] = Character.forDigit((src[i] >>> 4) & 0x0F, 16);buffer[1] = Character.forDigit(src[i] & 0x0F, 16);System.out.println(buffer);stringBuilder.append(buffer);}return stringBuilder.toString();}private String ByteArrayToHexString(byte[] inarray) {int i, j, in;String[] hex = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A","B", "C", "D", "E", "F"};String out = "";for (j = 0; j < inarray.length; ++j) {in = (int) inarray[j] & 0xff;i = (in >> 4) & 0x0f;out += hex[i];i = in & 0x0f;out += hex[i];}return out;}private byte[] Hex2Bytes(String hexString) {byte[] arrB = hexString.getBytes();int iLen = arrB.length;byte[] arrOut = new byte[iLen / 2];String strTmp = null;for (int i = 0; i < iLen; i += 2) {strTmp = new String(arrB, i, 2);arrOut[(i / 2)] = ((byte) Integer.parseInt(strTmp, 16));}return arrOut;}/*** Parses the NDEF Message from the intent and prints to the TextView*/private void processIntent(Intent intent) {//取出封装在intent中的TAGTag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);String CardId = ByteArrayToHexString(tagFromIntent.getId());metaInfo = "卡片ID:" + CardId+"\n";KLog.e(metaInfo);boolean auth = false;String tagString=tagFromIntent.toString();//读取TAGif (tagString.contains("MifareClassic")){metaInfo+="MifareClassic\n";readMiCard(tagFromIntent);}else{readIsoDepTag(tagFromIntent);}}private void readIsoDepTag(Tag tagFromIntent) {IsoDep isoDep = IsoDep.get(tagFromIntent);try {if (!isoDep.isConnected()) {isoDep.connect();}byte[] SELECT = { //APDU查询语句(byte) 0x00, // CLA = 00 (first interindustry command set)(byte) 0xA4, // INS = A4 (SELECT)(byte) 0x00, // P1 = 00 (select file by DF name)(byte) 0x0C, // P2 = 0C (first or only file; no FCI)(byte) 0x06, // Lc = 6 (data/AID has 6 bytes)(byte) 0x31, (byte) 0x35, (byte) 0x38, (byte) 0x34, (byte) 0x35, (byte) 0x46 // AID 应用表示,用于系统区分nfc卡片和启动对应服务};byte[] result = isoDep.transceive(SELECT); //尝试请求一次KLog.d(result[0]+" "+result[1]); //基本都是错误返回,因为没有nfc的厂家协议说明,啥都做不了isoDep.close();} catch (Exception e) {e.printStackTrace();}}private void readMiCard(Tag tagFromIntent) {MifareClassic mfc = MifareClassic.get(tagFromIntent);try {mfc.connect();int type = mfc.getType();//获取TAG的类型int sectorCount = mfc.getSectorCount();//获取TAG中包含的扇区数String typeS = "";switch (type) {case MifareClassic.TYPE_CLASSIC:typeS = "TYPE_CLASSIC";break;case MifareClassic.TYPE_PLUS:typeS = "TYPE_PLUS";break;case MifareClassic.TYPE_PRO:typeS = "TYPE_PRO";break;case MifareClassic.TYPE_UNKNOWN:typeS = "TYPE_UNKNOWN";break;}metaInfo += "\n卡片类型:" + typeS + "\n共" + sectorCount + "个扇区\n共"+ mfc.getBlockCount() + "个块\n存储空间: " + mfc.getSize() + "B\n";for (int j = 0; j < sectorCount; j++) {//Authenticate a sector with key A.auth = mfc.authenticateSectorWithKeyA(j,MifareClassic.KEY_DEFAULT);if (!auth){if(mfc.authenticateSectorWithKeyA(j,MifareClassic.KEY_MIFARE_APPLICATION_DIRECTORY)){auth= mfc.authenticateSectorWithKeyA(j,MifareClassic.KEY_MIFARE_APPLICATION_DIRECTORY);}if(mfc.authenticateSectorWithKeyA(j,MifareClassic.KEY_NFC_FORUM)){auth= mfc.authenticateSectorWithKeyA(j,MifareClassic.KEY_NFC_FORUM);}}int bCount;int bIndex;if (auth) {metaInfo += "Sector " + j + ":验证成功\n";// 读取扇区中的块bCount = mfc.getBlockCountInSector(j);bIndex = mfc.sectorToBlock(j);for (int i = 0; i < bCount; i++) {byte[] data = mfc.readBlock(bIndex);metaInfo += "Block " + bIndex + " : "+ bytesToHexString(data) + "\n";bIndex++;}} else {metaInfo += "Sector " + j + ":验证失败\n";}}text.setText(metaInfo);//Toast.makeText(this, metaInfo, Toast.LENGTH_SHORT).show();} catch (Exception e){e.printStackTrace();}
}}
这个是做了一个nfc读取页面,声明过滤器后,如果手机识别到nfc卡片响应,会弹出应用选择器,这个时候,我们开发的应用就在列表中。
选中后跳转到这个activity中,读取nfc卡片后,界面会显示这个nfc的卡片类型。后续逻辑,如果是芯片卡(IsoDep),还会尝试交互一波读取一下数据。如果是MifareClassic,就把每个64个扇区块数据读取出来,打印到页面上。
发现内容有点多,一篇还没讲完。
后续还有关于手上几个nfc卡的试验结果,分析,结论,留给下篇再继续介绍