Android使用NFC实现模拟卡

一、NFC概述

NFC(Near Field Communication)也叫近距离无线通信,是一项无线技术。 NFC由非接触式射频识别(RFID)及互联互通技术整合演变而来,在单一芯片上结合感应式读卡器、感应式卡片和点对点的功能,利用移动终端能在短距离内与兼容设备进行识别和数据交换。

NFC具有距离近、带宽高、能耗低等特点。适用于一些敏感信息或个人数据的传输等,在安全性上具有优势,NFC与现有非接触智能卡技术兼容,已经成为得到越来越多主要厂商支持的正式标准。利用NFC功能,可以实现消费、门禁等多种应用,代替卡包里的多种卡片,如移动支付、电子票务、门禁、移动身份识别、防伪等应用。

参考《深入理解Android:Wi-Fi、NFC和GPS卷》一书:该技术最早由Philips和Sony两家公司于2002年年末联合推出,从原理上说,NFC和WiFi类似,二者都利用无线射频技术来实现设备之间的通信。但是区别是,NFC的工作频率为13.56MHz,有效距离为<4cm。

所以这在很大程度上要求使用NFC的双方设备具备相当高的信任程度,不然不可能使其靠近自己的设备,这在一定程度上表明NFC技术的安全性。
接下来说一下RFID即无线射频识别技术,而NFC技术起源于RFID技术,RFID有低频,高频(13.56MHz)和超高频工作频率,.在应用领域:RFID更多的应用在生产,物流,跟踪和资产管理上,而NFC则工作在门禁,公交卡,手机支付等领域。在工作模式:NFC同时支持读写模式和卡模式。而在RFID中,读卡器和非接触卡是独立的两个实体,不能切换。

二、NFC应用

NFC设备可以用作非接触式智能卡、智能卡的读写器终端以及设备对设备的数据传输链路。NFC应用可以分为四个基本类型:

1.接触、完成。诸如门禁、活动检票之类的应用,用户只需将储存有票证或门禁代码的设备靠近阅读器即可。还可用于简单的数据撷取应用。

2.接触、确认。移动付费之类的应用,如食堂消费、交通工具支付,用户必须输入密码确认交易,或者仅接受交易。

3.接触、连接。将两台支持NFC的设备链接,即可进行点对点网络数据传输,例如下载音乐、交换图像或同步处理通信录等。

4.接触、探索。NFC设备可能提供不止一种功能,消费者可以探索了解设备的功能,找出NFC设备潜在的功能与服务。

三、NFC工作模式

NFC支持如下3种工作模式:读卡器模式(Reader/writer mode)、仿真卡模式(Card Emulation Mode)、点对点模式(P2P mode)。

下来分别看一下这三种模式:

1、读卡器模式:

数据在NFC芯片中,可以简单理解成“刷标签”。本质上就是通过支持NFC的手机或其它电子设备从带有NFC芯片的标签、贴纸、名片等媒介中读写信息。通常NFC标签是不需要外部供电的。当支持NFC的外设向NFC读写数据时,它会发送某种磁场,而这个磁场会自动的向NFC标签供电。

2、仿真卡模式:

数据在支持NFC的手机或其它电子设备中,可以简单理解成“刷手机”。本质上就是将支持NFC的手机或其它电子设备当成借记卡、公交卡、门禁卡等IC卡使用。基本原理是将相应IC卡中的信息凭证封装成数据包存储在支持NFC的外设中 。
在使用时还需要一个NFC射频器(相当于刷卡器)。将手机靠近NFC射频器,手机就会接收到NFC射频器发过来的信号,在通过一系列复杂的验证后,将IC卡的相应信息传入NFC射频器,最后这些IC卡数据会传入NFC射频器连接的电脑,并进行相应的处理(如电子转帐、开门等操作)。

3、点对点模式:

该模式与蓝牙、红外差不多,用于不同NFC设备之间进行数据交换,不过这个模式已经没有有“刷”的感觉了。其有效距离一般不能超过4厘米,但传输建立速度要比红外和蓝牙技术快很多,传输速度比红外块得多,如过双方都使用Android4.2,NFC会直接利用蓝牙传输。这种技术被称为Android Beam。所以使用Android Beam传输数据的两部设备不再限于4厘米之内。
点对点模式的典型应用是两部支持NFC的手机或平板电脑实现数据的点对点传输,例如,交换图片或同步设备联系人。因此,通过NFC,多个设备如数字相机,计算机,手机之间,都可以快速连接,并交换资料或者服务。

下面对比一下NFC、蓝牙和红外之间的差异:
对比项 NFC 蓝牙 红外 网络类型 点对点 单点对多点 点对点 有效距离 <=0.1m <=10m,最新的蓝牙4.0有效距离可达100m 一般在1m以内,热技术连接,不稳定 传输速度 最大424kbps 最大24Mbps 慢速115.2kbps,快速4Mbps 建立时间 <0.1s 6s 0.5s 安全性 安全,硬件实现 安全,软件实现 不安全,使用IRFM时除外 通信模式 主动-主动/被动 主动-主动 主动-主动 成本 低 中 低

四、Android实现仿真卡模式(Card Emulation Mode)

这里通过NFC的仿真卡模式(Card Emulation Mode)实现了公司所有读头产品相关配置和固件升级功能;
1:通过手机NFC模拟配置卡功能来配置读头相关参数;
2:通过手机NFC模拟电子工牌,实现刷手机开门功能;
3:通过手机NFC模拟卡片升级,升级读头固件等功能;
NFC具体细节可参阅博文这里不做过多赘述,直接上代码:

1:NFC权限
 <uses-permission android:name="android.permission.NFC" />

    <!--API 9 设备可以使用近场通信(NFC)进行通信。-->
    <uses-feature
        android:name="android.hardware.nfc"
        android:required="true" />
    <!--API 19 该设备支持基于主机的NFC卡仿真。-->
    <uses-feature
        android:name="android.hardware.nfc.hcef"
        android:required="true" />
    <!--API 24 该设备支持基于主机的NFC-F卡仿真。-->
    <uses-feature
        android:name="android.hardware.nfc.hce"
        android:required="true" />

2:Service implementation

Android 4.4带有一个便利Service类,可以作为实现HCE服务的基础:HostApduService类。

processCommandApdu() 只要NFC阅读器向您的服务发送应用程序协议数据单元(APDU),就会调用此方法。APDU也在ISO / IEC 7816-4规范中定义。APDU是NFC读取器和HCE服务之间交换的应用程序级数据包。该应用程序级协议是半双工的:NFC读取器将向您发送命令APDU,它将等待您发送响应APDU作为回报。

注: ISO / IEC 7816-4规范还定义了多个逻辑信道的概念,您可以在不同的逻辑信道上进行多个并行APDU交换。然而,Android的HCE实现仅支持单个逻辑通道,因此只有APDU的单线程交换。

如前所述,Android使用AID来确定读者想要与之通信的HCE服务。通常,NFC读取器发送到您的设备的第一个APDU是“SELECT AID”APDU; 此APDU包含读者想要与之交谈的AID。Android从APDU中提取该AID,将其解析为HCE服务,然后将该APDU转发到已解析的服务。

onDeactivated() 卡片移走或断开连接时调用,并带有一个参数,指示两者中的哪一个发生了。

package com.roy.www.nfc_configcard.service;

import android.content.Context;
import android.content.Intent;
import android.nfc.cardemulation.HostApduService;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

import androidx.annotation.RequiresApi;


import com.roy.www.nfc_configcard.ui.activity.ContentActivity;
import com.roy.www.nfc_configcard.ui.activity.NFCUpdateMcuActivity;
import com.roy.www.nfc_configcard.utils.ActivityUtil;
import com.roy.www.nfc_configcard.utils.Aes128EcbUtils;
import com.roy.www.nfc_configcard.utils.ByteUtils;
import com.roy.www.nfc_configcard.utils.HexDump;
import com.roy.www.nfc_configcard.utils.MmkvUtils;

import java.util.Arrays;

import javax.crypto.Cipher;


/**
 * Created by Roy.lee
 * On 2022/6/27
 * Email: 631934797@qq.com
 * Description: 仿真卡服务
 */

public class CardEmulationService extends HostApduService {
    private static final String TAG = CardEmulationService.class.getSimpleName();
    private static final String SEND = " : ==>  ";
    private static final String RECE = " : <==  ";

    private long stopTime;

    private static Handler mHandler;
    private static StringBuilder mStrBuilder;

    private static int SELECT_FILE = 0;
    private byte[] RANDOM_NUMBER ;
   

    private boolean IS_EX_AUTH = false;
    private boolean IS_IN_AUTH = false;


    public static final int MESSAGE_UPDATE_PROGRESS = 0;
    public static final int READ_CARD = 1;
    public static final int DISCONNECT = 2;

    public static boolean isUpdate = false;
    public static byte[] MCU_BUF;
    public static byte[] MCU_INFO = new byte[59];
    public static byte[] VERSION_BYTES = new byte[48];
    public static byte[] MCU_SIZE = new byte[4] ;
    public static byte[] SUM = new byte[4];
    public static byte EOR;
    public static int cnot = 0;

    public static Intent newHCEServiceIntent(Context context){
        Intent hceIntent = new Intent(context, CardEmulationService.class);
        return hceIntent;
    }

    public static Intent newHCEServiceIntent(Context context, Handler handler, StringBuffer mBuf){
        Intent hceIntent = new Intent(context, CardEmulationService.class);
        mHandler = handler;
        mSb = mBuf;
        return hceIntent;
    }

    public static void setHandler(Handler handler, StringBuilder builder){
        mHandler = handler;
        mStrBuilder = builder;
    }

    @Override
    public void onCreate() {
        Log.i(TAG, "... CardEmulationService on create ...");
        logAppend(TAG + " : ... CardEmulationService on create ...");

        super.onCreate();
    }



    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
        Log.i(TAG, RECE + ByteUtils.toHexString(commandApdu));
        logAppend(TAG + RECE + ByteUtils.toHexString(commandApdu));
        String cmdApdu = ByteUtils.toHexString(ByteUtils.getSubArray(commandApdu,0,2));
        Log.i(TAG, "cmdApdu  : " + cmdApdu);

        if (cmdApdu.equals(ApduCommands.CMD_00A4)){//选择文件
            return selectFileAndAid(commandApdu);
        }
        else if (cmdApdu.equals(ApduCommands.CMD_00B0)){//读取数据
            return readBinary(commandApdu);
        }
        else if (cmdApdu.equals(ApduCommands.CMD_0084)){//获取随机数
            return getChallenge(commandApdu);
        }
        else if (cmdApdu.equals(ApduCommands.CMD_0082)){//外部认证
            return externalAuth(commandApdu);
        }
        else if (cmdApdu.equals(ApduCommands.CMD_0088)){//内部认证
            return internalAuth(commandApdu);
        }
        else if (cmdApdu.equals(ApduCommands.CMD_800E)){//擦除当前目录文件
            return eraseDF(commandApdu);
        }
        else if (cmdApdu.equals(ApduCommands.CMD_80E0)){//创建文件
            return createFile(commandApdu);
        }
        else if (cmdApdu.equals(ApduCommands.CMD_80D4)){//写KEY
            return writeKey(commandApdu);
        }
        else if (cmdApdu.equals(ApduCommands.CMD_00D6)){//写二进制文件
            return updateBinary(commandApdu);
        }else{
            return ApduCommands.SW_6300;
        }
    }

    @Override
    public void onDeactivated(int reason) {
        Log.i(TAG, "onDeactivated(). Reason: " + reason);
        logAppend(TAG + "onDeactivated(). Reason: " + reason);
        SELECT_FILE = 0;
        IS_EX_AUTH = false;
        IS_IN_AUTH = false;
        stopTime = System.currentTimeMillis();
        mHandler.sendEmptyMessage(DISCONNECT);
    }



    /**
     * 选文件和AID
     * @param commandApdu
     */
    private byte[] selectFileAndAid(byte[] commandApdu) {
        if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_MCU_AID))
                && ActivityUtil.isForeground(this, NFCUpdateMcuActivity.class.getName())
                && isUpdate){//
            sendUpdateMessage(0);
            logAppend(TAG + " : ... MCU AID ...");
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
            cnot++;
            return ApduCommands.SW_9000;
        }
        else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_FILE_FFFF))){
            //TODO 返回固件信息
            logAppend(TAG + SEND + HexDump.toHexString(MCU_INFO));
            return MCU_INFO;
        }
        else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_FILE_0000))){
            sendUpdateMessage(100);
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
            logAppend(TAG + " : 升级成功");
            return ApduCommands.SW_9000;
        }
        else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_FILE_1111))){
            logAppend(TAG + " : 校验失败");
            return ApduCommands.SW_9000;
        }

        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_CONFIG_AID))
                && ActivityUtil.isForeground(this,ContentActivity.class.getName())){
            logAppend(TAG + " : ... Config Aid ...");
            byte[] rand = HexDump.getRand(4);
            ApduCommands.initDesKey(rand);
            logAppend(TAG + SEND + ByteUtils.toHexString(ByteUtils.concatenate( rand,ApduCommands.SW_9000)));
            return ByteUtils.concatenate(rand,ApduCommands.SW_9000);
        }

        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_CF01))){
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
       
        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF01))){
            SELECT_FILE = 1;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
        
        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF02))){
            SELECT_FILE = 2;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
      
        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF03))){
            SELECT_FILE = 3;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF04))){
            SELECT_FILE = 4;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
       
        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF05))){
            SELECT_FILE = 5;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }

        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF06))){
            SELECT_FILE = 6;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
      
        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF10))){
            SELECT_FILE = 10;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
      
        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF11))){
            SELECT_FILE = 11;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF12))){
            SELECT_FILE = 12;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_EF13))){
            SELECT_FILE = 13;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
        else if (Arrays.equals(commandApdu, ByteUtils.toByteArray(ApduCommands.SELECT_FILE_3F00))){
            SELECT_FILE = 14;
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
        else {
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_6A82));
            return ApduCommands.SW_6A82;
        }

    }


    /**
     * 读取数据
     * @param commandApdu
     * @return
     */
    private byte[] readBinary(byte[] commandApdu) {
        if (commandApdu.length == 6) {
            int offset = HexDump.bigBytesToInt(HexDump.getSubArray(commandApdu,2,2));
            int len = HexDump.bigBytesToInt(HexDump.getSubArray(commandApdu,4,2));
            if ((offset+len ) > CardEmulationService.MCU_BUF.length){
                logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6982));
                return ApduCommands.SW_6982;
            }

            sendUpdateMessage((offset * 100) / CardEmulationService.MCU_BUF.length);
            byte[] tempBytes = HexDump.getSubArray(CardEmulationService.MCU_BUF,offset,len);
            logAppend(TAG + SEND + HexDump.toHexString(HexDump.concatenate(tempBytes,ApduCommands.SW_9000)));
            return HexDump.concatenate(tempBytes,ApduCommands.SW_9000);
        }

        else if (commandApdu.length == 5 && Arrays.equals(HexDump.getSubArray(commandApdu,0,2), ByteUtils.toByteArray(ApduCommands.READ_BINARY))){
            int len = commandApdu[commandApdu.length-1]&0xFF;
            int offset = HexDump.bigBytesToInt(HexDump.getSubArray(commandApdu,2,2));
            byte[] tempBytes = new byte[len];
            if (SELECT_FILE == 1){
                if (len <= sEF01File.toFileStream(sEF01File).length){
                    tempBytes = HexDump.getSubArray(sEF01File.toFileStream(sEF01File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }
            }
            else if (SELECT_FILE == 2){
                if (len <= sEF02File.toFileStream(sEF02File).length){
                    tempBytes = HexDump.getSubArray(sEF02File.toFileStream(sEF02File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }

            }
            else if (SELECT_FILE == 3){
                if (len <= sEF03File.toFileStream(sEF03File).length){
                    tempBytes = HexDump.getSubArray(sEF03File.toFileStream(sEF03File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }
            }
            else if (SELECT_FILE == 4){
                if (len <= sEF04File.toFileStream(sEF04File).length){
                    tempBytes = HexDump.getSubArray(sEF04File.toFileStream(sEF04File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }
            }
            else if (SELECT_FILE == 5){
                if (len <= sEF05File.toFileStream(sEF05File).length){
                    tempBytes = HexDump.getSubArray(sEF05File.toFileStream(sEF05File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }
            }
            else if (SELECT_FILE == 6){
                if (len <= sEF06File.toFileStream(sEF06File).length){
                    tempBytes = HexDump.getSubArray(sEF06File.toFileStream(sEF06File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }
            }
            else if (SELECT_FILE == 10){
                if (len <= sEF10File.toFileStream(sEF10File).length){
                    tempBytes = HexDump.getSubArray(sEF10File.toFileStream(sEF10File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }
            }
            else if (SELECT_FILE == 11){
                if (len <= sEF11File.toFileStream(sEF11File).length){
                    tempBytes = HexDump.getSubArray(sEF11File.toFileStream(sEF11File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }
            }
            else if (SELECT_FILE == 12){
                if (len <= sEF12File.toFileStream(sEF12File).length){
                    tempBytes = HexDump.getSubArray(sEF12File.toFileStream(sEF12File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }
            }
            else if (SELECT_FILE == 13){
                if (len <= sEF13File.toFileStream(sEF13File).length){
                    tempBytes = HexDump.getSubArray(sEF13File.toFileStream(sEF13File),offset,len);
                }else {
                    logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                    logAppend(TAG + " : 读取长度错误");
                    return ApduCommands.SW_6700;
                }
            }


            logAppend(TAG + SEND + ByteUtils.toHexString(ByteUtils.concatenate(tempBytes,ApduCommands.SW_9000)));
            return ByteUtils.concatenate(tempBytes,ApduCommands.SW_9000);

        } else {
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6A81));
            return ApduCommands.SW_6A81;
        }
    }


    /**
     * 获取随机数
     * @param commandApdu
     * @return
     */
    private byte[] getChallenge(byte[] commandApdu) {
        if (commandApdu.length == 5){
            int len = commandApdu[4] & 0xFF;
            if (len == 4 || len == 8){
                RANDOM_NUMBER = HexDump.getRand(len);
                logAppend(TAG + SEND + HexDump.toHexString(HexDump.concatenate(RANDOM_NUMBER,ApduCommands.SW_9000)));
                return HexDump.concatenate(RANDOM_NUMBER,ApduCommands.SW_9000);
            }else {
                logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6700));
                return ApduCommands.SW_6700;
            }
        } else {
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6A81));
            return ApduCommands.SW_6A81;
        }
    }


    /**
     * 外部认证
     * @param commandApdu
     * @return
     */
    private byte[] externalAuth(byte[] commandApdu) {
        int len = commandApdu[4]&0xFF;
        if (commandApdu.length == (len+5) && Arrays.equals(ByteUtils.getSubArray(commandApdu,0,4),ByteUtils.toByteArray(ApduCommands.EXTERNAL_AUTH))){
            byte[] subArray = ByteUtils.getSubArray(commandApdu, 5, len);

            byte[] desDecrypt = Aes128EcbUtils.DESede(subArray,
                    ApduCommands.EX_AUTH_KEY,
                    Cipher.DECRYPT_MODE);

            if (Arrays.equals(RANDOM_NUMBER,desDecrypt)){
                IS_EX_AUTH = true;
                logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
                return ApduCommands.SW_9000;
            }else {
                IS_EX_AUTH = false;
                logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_63CF));
                return ApduCommands.SW_63CF;
            }
        } else {
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9302));
            return ApduCommands.SW_9302;
        }

    }

    /**
     * 内部认证
     * @param commandApdu
     * @return
     */
    private byte[] internalAuth(byte[] commandApdu) {
        if (commandApdu.length == 13 && Arrays.equals(ByteUtils.getSubArray(commandApdu,0,5),ByteUtils.toByteArray(ApduCommands.INTERNAL_AUTH))){
            byte[] randArray = ByteUtils.getSubArray(commandApdu, 5, 8);
            logAppend(TAG + " : 88随机数 <--- " + ByteUtils.toHexString(randArray));
            byte[] desEncrypt = Aes128EcbUtils.DESede(randArray,
                    ByteUtils.toByteArray(MmkvUtils.decodeString("INTERNAL_AUTH_KEY")),
                    Cipher.ENCRYPT_MODE);
            logAppend(TAG + SEND + ByteUtils.toHexString(ByteUtils.concatenate(desEncrypt,ApduCommands.SW_9000)));
            return ByteUtils.concatenate(desEncrypt,ApduCommands.SW_9000);
        }
        else {
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6A82));
            return ApduCommands.SW_6A82;
        }

    }


    /**
     * 擦除目录文件
     * @param commandApdu
     * @return
     */
    private byte[] eraseDF(byte[] commandApdu) {
        if (commandApdu.length == 5 && Arrays.equals(HexDump.getSubArray(commandApdu,0,5),HexDump.hexStringToByteArray(ApduCommands.ERASE_DF))){
     
            clearCardData();
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }else {
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6982));
            return ApduCommands.SW_6982;
        }
    }

    /**
     * 创建文件
     * @param commandApdu
     * @return
     */
    private byte[] createFile(byte[] commandApdu) {
        if (commandApdu.length == 17 && Arrays.equals(commandApdu,ByteUtils.toByteArray(ApduCommands.CREATE_EC01_FILE))){
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
        else if (commandApdu.length == 12 && Arrays.equals(commandApdu,ByteUtils.toByteArray(ApduCommands.CREATE_SECRET_KEY_FILE))){
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
        else if (commandApdu.length == 12 && Arrays.equals(commandApdu,ByteUtils.toByteArray(ApduCommands.CREATE_FILE_01))){
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
        else if (commandApdu.length == 12 && Arrays.equals(commandApdu,ByteUtils.toByteArray(ApduCommands.CREATE_FILE_02))){
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }

        else {
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6982));
            return ApduCommands.SW_6982;
        }
    }

    /**
     * 写KEY
     * @param commandApdu
     * @return
     */
    private byte[] writeKey(byte[] commandApdu) {
        if (commandApdu.length == 26 && Arrays.equals(ByteUtils.getSubArray(commandApdu,0,10),ByteUtils.toByteArray(ApduCommands.WRITE_EXTERNAL_AUTH_KEY))){
  
            byte[] exAuthKeyBytes = ByteUtils.getSubArray(commandApdu,10,16);
            MmkvUtils.encode("EXTERNAL_AUTH_KEY",ByteUtils.toHexString(exAuthKeyBytes));
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }
        else if (commandApdu.length == 18 && Arrays.equals(ByteUtils.getSubArray(commandApdu,0,10),ByteUtils.toByteArray(ApduCommands.WRITE_INTERNAL_AUTH_KEY))){
         
            byte[] inAuthKeyBytes = ByteUtils.getSubArray(commandApdu,10,8);
            MmkvUtils.encode("INTERNAL_AUTH_KEY",ByteUtils.toHexString(inAuthKeyBytes));
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }

        else {
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6982));
            return ApduCommands.SW_6982;
        }

    }


    /**
     * 写二进制文件
     * @param commandApdu
     * @return
     */
    private byte[] updateBinary(byte[] commandApdu) {
        if (commandApdu.length == 9 && Arrays.equals(ByteUtils.getSubArray(commandApdu,0,5),ByteUtils.toByteArray(ApduCommands.WRITE_UID))){
            byte[] uidBytes = ByteUtils.getSubArray(commandApdu,5,4);
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            MmkvUtils.encode("UID",ByteUtils.toHexString(uidBytes));
            return ApduCommands.SW_9000;
        }
        else if (commandApdu.length == 13 && Arrays.equals(ByteUtils.getSubArray(commandApdu,0,5),ByteUtils.toByteArray(ApduCommands.WRITE_CUID))){
            byte[] cuidBytes = ByteUtils.getSubArray(commandApdu,5,8);
            MmkvUtils.encode("CUID",ByteUtils.toHexString(cuidBytes));
            if (mHandler != null)
                mHandler.sendEmptyMessage(2);
            logAppend(TAG + SEND + ByteUtils.toHexString(ApduCommands.SW_9000));
            return ApduCommands.SW_9000;
        }else {
            logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6A81));
            return ApduCommands.SW_6A81;
        }
    }



    private void clearCardData() {
        MmkvUtils.encode("UID","");
        MmkvUtils.encode("CUID","");
        MmkvUtils.encode("EXTERNAL_AUTH_KEY","");
        MmkvUtils.encode("INTERNAL_AUTH_KEY","");
    }


    private void logAppend(String log){
    
        if (mStrBuilder != null)
            mStrBuilder.insert(0,log +  "\r\n");
        if (mHandler != null)
            mHandler.sendEmptyMessage(1);
    }


    private void sendUpdateMessage(int pos) {
        Message message = Message.obtain();
        message.what = MESSAGE_UPDATE_PROGRESS;
        message.arg1 = pos;
        mHandler.sendMessage(message);
    }

}


3:注册CardEmulationService
 <service
            android:name="com.radio.www.service.CardEmulationService"
            android:exported="true"
            android:permission="android.permission.BIND_NFC_SERVICE">
            <intent-filter>
                <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
            </intent-filter>

            <meta-data
                android:name="android.nfc.cardemulation.host_apdu_service"
                android:resource="@xml/hceservice" />
   </service>

4:配置hceservice.xml

hceservice.xml中配置AID,可以配置一个或多个AID,AID是这个service标识,通过AID来找到对应的service;这里也可以不配置AID,可以通过代码动态注册AID。

<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/hce_service_descr"
    android:requireDeviceUnlock="false">
    <aid-group android:description="@string/hce_aid_descr"
        android:category="other">
        <!--    TODO: change ID to F...-->
        <!--    see: https://stackoverflow.com/questions/27533193/android-hce-are-there-rules-for-aid-->
        <!--        以“A”开头的AID:国际注册的AID-->
        <!--        以“D”开头的AID:国家注册的AID-->
        <!--        以“F”开头的AIDs:专有AIDs(无需注册)-->
        <aid-filter android:name ="444639395f3030303030303030303030"/>
    </aid-group>
</host-apdu-service>

5:动态注册AID

使用CardEmulation类实现代码动态注册AID:

package com.roy.www.nfc_configcard.ui.activity.

import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.nfc.NfcAdapter;
import android.nfc.cardemulation.CardEmulation;
import android.os.Build;
import android.os.Bundle;


import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import com.roy.www.nfc_configcard.R;


import java.util.ArrayList;
import java.util.List;

import javax.crypto.Cipher;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;

/**
 * Created by Roy.lee
 * On 2022/6/27
 * Email: 631934797@qq.com
 * Description:
 */
public class MainActivity extends AppCompatActivity {

    private CardEmulation mCardEmulation;
    private ComponentName mService;
    private static final List<String> AIDS = new ArrayList<>();

    static {
        AIDS.add("444639395f3030303030303030303030");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        NfcAdapter mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        mCardEmulation = CardEmulation.getInstance(mNfcAdapter);
        mService = new ComponentName(this, CardEmulationService.class);

    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onResume() {
        super.onResume();

        mCardEmulation.setPreferredService(this, mService);
        mCardEmulation.registerAidsForService(mService, "other", AIDS);

        startService(CardEmulationService.newHCEServiceIntent(MainActivity.this, mHandler, mStrBuf));
      
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
   protected void onPause() {
        super.onPause();
        Log.d("CardEmulation", "removeAidsForService");
        mCardEmulation.removeAidsForService(mService, "other");
        mCardEmulation.unsetPreferredService(this);
    }


}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容