关于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" />
<activity
android: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-data
android: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;
@Override
public 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;
}
}
@Override
protected void onNewIntent(Intent intent) {
/*
isoDep CPU卡(ISO 14443-4) NfcA或NfcB
m1卡 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);//处理响应
}
}
//页面获取焦点
@Override
protected void onResume() {
super.onResume();
nfcAdapter.enableForegroundDispatch(this, pi, null, null);
}
//页面失去焦点
@Override
protected 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中的TAG
Tag 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();
//读取TAG
if (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卡的试验结果,分析,结论,留给下篇再继续介绍