关于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卡的试验结果,分析,结论,留给下篇再继续介绍