一、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权限


<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.radio.www.m355.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.util.Log;

import androidx.annotation.RequiresApi;

import com.radio.www.m355.ui.M355App;
import com.radio.www.m355.utils.Aes128EcbUtils;

import com.radio.www.m355.utils.HexDump;

import java.util.Arrays;

import javax.crypto.Cipher;



/**
* Created by Roy.lee
* On 2022/6/27
* 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 static Handler mHandler;
private static StringBuffer mSb;
private static final int INIT_CARD = 0;
private static final int READ_CARD = 1;
private static int MODE = READ_CARD;
private byte[] RANDOM_NUMBER = new byte[8];
private static final String str = "6f15840e315041592e5359532e4444463031a503880101";
public static Intent newHCEServiceIntent(Context context, Handler handler, StringBuffer mBuf){
Intent hceIntent = new Intent(context, CardEmulationService.class);
mHandler = handler;
mSb = mBuf;
return hceIntent;
}

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

private void logAppend(String log){
if (mSb != null)
mSb.append(log + "\r\n");

if (mHandler != null)
mHandler.sendEmptyMessage(1);
}


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

case "00A4"://选择文件
if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_INIT_AID))){//
MODE = INIT_CARD;
logAppend(TAG + " : ... write card ...");
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_READ_AID))){//
MODE = READ_CARD;
logAppend(TAG + " : ... read card ...");
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_AID)) || Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_AIDS))){//
MODE = READ_CARD;
logAppend(TAG + " : ... read Aid ...");
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_FILE_DIRECTORY))){//

logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_FILE_01))){//
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_FILE_02))){//
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}

else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_FILE_3F00))){ //3F00
logAppend(TAG + SEND + HexDump.toHexString(HexDump.concatenate(HexDump.hexStringToByteArray(str),ApduCommands.SW_9000)));
return HexDump.concatenate(HexDump.hexStringToByteArray(str),ApduCommands.SW_9000);
}

else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.SELECT_FILE_DF99))){ //3F00
logAppend(TAG + SEND + HexDump.toHexString(HexDump.concatenate(HexDump.hexStringToByteArray(M355App.mmkv.decodeString("AppID")),ApduCommands.SW_9000)));
return HexDump.concatenate(HexDump.hexStringToByteArray(M355App.mmkv.decodeString("AppID")),ApduCommands.SW_9000);
}
break;

case "00B0"://读取数据
if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.READ_UID))){//
Long intUserCarcNmu = M355App.mmkv.decodeLong("UID",0);
// TODO 判断卡号
byte[] cardNum = new byte[5];
byte[] bytes = HexDump.longTo4Bytes(intUserCarcNmu);
System.arraycopy(bytes,0,cardNum,0,bytes.length);
logAppend(TAG + SEND + HexDump.toHexString(HexDump.concatenate(cardNum,ApduCommands.SW_9000)));
return HexDump.concatenate(cardNum,ApduCommands.SW_9000);
}
else if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.READ_CUID))){//
logAppend(TAG + SEND + HexDump.toHexString(HexDump.concatenate(HexDump.hexStringToByteArray(M355App.mmkv.decodeString("CUID","0000000000000000")),ApduCommands.SW_9000)));
return HexDump.concatenate(HexDump.hexStringToByteArray(M355App.mmkv.decodeString("CUID","0000000000000000")),ApduCommands.SW_9000);
}
break;

case "0084"://获取随机数
if (Arrays.equals(commandApdu, HexDump.hexStringToByteArray(ApduCommands.GET_CHALLENGE))){//
RANDOM_NUMBER = HexDump.getRand(8);
//logAppend(TAG + SEND + HexDump.toHexString(RANDOM_NUMBER));
logAppend(TAG + SEND + HexDump.toHexString(HexDump.concatenate(RANDOM_NUMBER,ApduCommands.SW_9000)));
return HexDump.concatenate(RANDOM_NUMBER,ApduCommands.SW_9000);
}
break;

case "0082"://外部认证
if (commandApdu.length == 13 && Arrays.equals(HexDump.getSubArray(commandApdu,0,5),HexDump.hexStringToByteArray(ApduCommands.EXTERNAL_AUTH))){
byte[] subArray = HexDump.getSubArray(commandApdu, 5, 8);
byte[] desDecrypt = new byte[8];

String external_auth_key = M355App.mmkv.decodeString("EXTERNAL_AUTH_KEY","");
if (external_auth_key == null || external_auth_key.equals("") ){
logAppend(TAG + SEND + "KEY 文件未找到");
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_6A82));
return ApduCommands.SW_6A82;
}

desDecrypt = Aes128EcbUtils.DESede(subArray,
HexDump.hexStringToByteArray(external_auth_key),
Cipher.DECRYPT_MODE);


//logAppend(TAG + RECE + HexDump.toHexString(desDecrypt));

if (Arrays.equals(RANDOM_NUMBER,desDecrypt)){
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}else {
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_63CF));
return ApduCommands.SW_63CF;
}
}
break;

case "0088"://内部认证
if (commandApdu.length == 13 && Arrays.equals(HexDump.getSubArray(commandApdu,0,5),HexDump.hexStringToByteArray(ApduCommands.INTERNAL_AUTH))){
byte[] randArray = HexDump.getSubArray(commandApdu, 5, 8);
logAppend(TAG + RECE + HexDump.toHexString(randArray));
byte[] desEncrypt = Aes128EcbUtils.Des(randArray,
HexDump.hexStringToByteArray(M355App.mmkv.decodeString("INTERNAL_AUTH_KEY")),
Cipher.ENCRYPT_MODE);
logAppend(TAG + SEND + HexDump.toHexString(HexDump.concatenate(desEncrypt,ApduCommands.SW_9000)));
return HexDump.concatenate(desEncrypt,ApduCommands.SW_9000);
}
break;

case "800E"://擦除当前目录文件
if (commandApdu.length == 5 && Arrays.equals(HexDump.getSubArray(commandApdu,0,5),HexDump.hexStringToByteArray(ApduCommands.ERASE_DF))){
// TODO 清除数据
clearCardData();
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
break;
case "80E0"://创建文件
if (commandApdu.length == 17 && Arrays.equals(commandApdu,HexDump.hexStringToByteArray(ApduCommands.CREATE_EC01_FILE))){
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
else if (commandApdu.length == 12 && Arrays.equals(commandApdu,HexDump.hexStringToByteArray(ApduCommands.CREATE_SECRET_KEY_FILE))){
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
else if (commandApdu.length == 12 && Arrays.equals(commandApdu,HexDump.hexStringToByteArray(ApduCommands.CREATE_FILE_01))){
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
else if (commandApdu.length == 12 && Arrays.equals(commandApdu,HexDump.hexStringToByteArray(ApduCommands.CREATE_FILE_02))){
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
break;
case "80D4"://写KEY
if (commandApdu.length == 26 && Arrays.equals(HexDump.getSubArray(commandApdu,0,10),HexDump.hexStringToByteArray(ApduCommands.WRITE_EXTERNAL_AUTH_KEY))){

//TODO 保存外部认证秘钥
byte[] exAuthKeyBytes = HexDump.getSubArray(commandApdu,10,16);
M355App.mmkv.encode("EXTERNAL_AUTH_KEY",HexDump.toHexString(exAuthKeyBytes));
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
else if (commandApdu.length == 18 && Arrays.equals(HexDump.getSubArray(commandApdu,0,10),HexDump.hexStringToByteArray(ApduCommands.WRITE_INTERNAL_AUTH_KEY))){
//TODO 保存内部认证秘钥
byte[] inAuthKeyBytes = HexDump.getSubArray(commandApdu,10,8);
M355App.mmkv.encode("INTERNAL_AUTH_KEY",HexDump.toHexString(inAuthKeyBytes));
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
break;

case "00D6"://写uid
if (commandApdu.length == 9 && Arrays.equals(HexDump.getSubArray(commandApdu,0,5),HexDump.hexStringToByteArray(ApduCommands.WRITE_UID))){

byte[] uidBytes = HexDump.getSubArray(commandApdu,5,4);
//TODO 保存UID
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
M355App.mmkv.encode("UID",HexDump.toHexString(uidBytes));
return ApduCommands.SW_9000;
}
else if (commandApdu.length == 13 && Arrays.equals(HexDump.getSubArray(commandApdu,0,5),HexDump.hexStringToByteArray(ApduCommands.WRITE_CUID))){
//TODO 保存CUID
byte[] cuidBytes = HexDump.getSubArray(commandApdu,5,8);
M355App.mmkv.encode("CUID",HexDump.toHexString(cuidBytes));
if (mHandler != null)
mHandler.sendEmptyMessage(2);
logAppend(TAG + SEND + HexDump.toHexString(ApduCommands.SW_9000));
return ApduCommands.SW_9000;
}
break;
}

return ApduCommands.SW_6300;
}

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

@Override
public void onDeactivated(int reason) {
Log.i(TAG, "onDeactivated(). Reason: " + reason);
logAppend(TAG + "onDeactivated(). Reason: " + reason);
}


}

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.radio.www.m355.ui;

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.radio.www.m355.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
* 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);
}


}



作者:Roy88

链接:https://www.jianshu.com/p/f0698ba1a06f

来源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。