一、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权限
android:name="android.hardware.nfc" android:required="true" /> android:name="android.hardware.nfc.hcef" android:required="true" /> 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 android:name="com.radio.www.service.CardEmulationService" android:exported="true" android:permission="android.permission.BIND_NFC_SERVICE"> android:name="android.nfc.cardemulation.host_apdu_service" android:resource="@xml/hceservice" /> 4:配置hceservice.xml hceservice.xml中配置AID,可以配置一个或多个AID,AID是这个service标识,通过AID来找到对应的service;这里也可以不配置AID,可以通过代码动态注册AID。 android:description="@string/hce_service_descr" android:requireDeviceUnlock="false"> android:category="other"> 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 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); } }