最近一直比较忙,然后好不容易闲下来就想着刷手机,埋的这个坑给忘了,最近一次上线看到有部分网友再催下篇,今天终于决定把这个坑给补了。
在上篇的最后,贴上了activity的代码,可以看到主要分成几个部分:
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
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;
}
NfcAdapter就是nfc的控制器,这个主要是控制nfc开启关闭监听状态的。
然后对控制器对象进行状态识别,做一些异常处理和提示
@Override
protected void onNewIntent(Intent intent) {
/*
isoDep CPU卡(ISO 14443-4) 基于NfcA或NfcB
NFCA m1卡
*/
super.onNewIntent(intent);
Toast.makeText(this, "Discovered tag " + ++mCount + " with intent: " + intent, Toast.LENGTH_SHORT).show();
if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
processIntent(intent);
}
}
当手机识别到有nfc接触时,此时就会响应有intent,那么系统就会调用onNewIntent回调方法,将intent传送过来,我们只需要在这里检验这个intent是否与NfcAdapter.ACTION_TAG_DISCOVERED匹配后,才正式进入nfc的处理流程。
在响应的intent中,获取到nfc的标签类型。传递给Tag的对象。
Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
KLog.e("tagFromIntent", "tagFromIntent" + tagFromIntent);
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);
}
第一个从intent获取的对象,nfc的tag
package android.nfc
public final class Tag implements Parcelable {
....
//Represents an NFC tag that has been discovered.
//Tag is an immutable object that represents the state of a NFC tag at the time of discovery. It can be used as a handle to TagTechnology classes to perform advanced operations, or directly queried for its ID via getId and the set of technologies it contains via getTechList. Arrays passed to and returned by this class are not cloned, so be careful not to modify them.
这是该Tag类里面的注释介绍:
大概意思是,Tag类通常是用作表示已发现的NFC标签。识别到的标签对象,是一个不可变的对象,表示NFC标签在发现时的状态。它可以用作TagTechnology类的handle来执行高级操作,也可以通过getId直接查询其ID,以及通过getTechList直接查询其包含的getTechList。
然后我们就调用getId获取到目前响应的nfc卡片的id。
将然后我将nfc对象进行了简单的分类,属于MifareClassic的作为一类处理。其他的作为另一类处理。
此处为什么把MifareClassic单独处理
此处为什么把MifareClassic单独处理,因为这个玩意是属于常见,但是又特殊的一种。
有多常见呢,市面上大部分门禁卡,水卡,一卡通好像都是这个类别。我手上的门禁卡就是识别被分到到这个类别。
MifareClassic又称为M1卡,M1卡分为16个扇区,每个扇区对应4块(块0-块3),共64块,编号为0-63.第0扇区的第0块用于存放厂商代码,已经固化无法更改。其余区的第0-2块用于存放数据,块3为控制块用于存放密码A、存取控制、密码B。并且每个扇区块3中的密码和存储控制全部都是独立的,独立控制本扇区的各种操作,每个扇区都能实现不同的功能,所以广泛用作一卡通。卡中每个块包含16字节,所以M1卡的容量=16扇区4块16字节=1024字节=1k (M1卡的由来,照搬一下网上大神的描述)
来看一个我拿一张m1卡实测一下,下图是nfc读取的结果:
可以看到这张卡16个扇区64块数据是全部读取出来了,说明这张m1卡是没有加密的,没啥问题。
但是为什么说m1卡特殊呢?看一下上面这64行数据,理论上我们直接复制一下这些数据,然后本地复制一份。需要模拟的时候,响应一下本地已存储好的数据。大概思路是这样没错吧,我也是理所当然是这么想的。结果。。。。因为Android提供的方法不支持模拟这样的数据!!!真的是GG了。。为什么呢?
android的模拟只支持ISO 7816-4协议下APDU命令,也就是说,android模拟的NFC,实际上是通过APDU命令与读卡机进行数据交互,你的命令通过读卡机的“认可”,则代表刷卡成功了。
那很明显了,了解了M1卡的数据结构,就能很直接的发现:M1卡的交互模式根本不是什么命令数据交互,而是一种静态文件,等着读卡器完全读取,然后识别数据是否正确,正确的话完成刷卡步骤。
那有同学说不对了,我的门禁卡,一卡通我用过嘛,我手机上能模拟成功的,华为钱包,小米钱包什么的,都是能模拟的,不存在你说无法模拟的情况啊?是不是搞错了?
这个就得回到Android本身,能不能模拟,怎么模拟都是AndroidSdk的方法预置好的。而M1的这种交互模式,AndroidSdk压根没有这样的模拟方法,我们没办法模拟一个16扇区的数据结构封装给NFC,提供给刷卡机读取。
那为啥华为钱包,小米钱包就可以呢?因为他们是手机厂商的自家应用,厂商在系统ROM,为它自己的钱包app,封装好了可以存储M1的数据结构,和可支持调用api方法。
那这个具体怎么操作的?我们可以不可以突破一下,用上这些api方法呢?
我也不得而知。至少华为,小米等厂商的开发文档我都翻遍了,没有提供类似的nfc Api。
华为的开发者文档中,倒是提供了一整套华为钱包nfc及门禁等场景解决方案,有兴趣的网友可以自己上去看看,但是对个人开发者来说没用。m1的分析就到此结束吧,我们来看下一个。
啥是IsoDep类别呢?就是支持ISO 7816-4协议的nfc,android里面有对应的类。
你也看到我在代码中为此类别单独走向了一个交互分支。
这玩意是啥呢,一般是什么nfc会是这个类别呢?其实也常见,市面常见的大概有以下两类:银行卡、交通卡。这类卡也有些称呼,叫智能卡、芯片卡、IC卡等。
这玩意的nfc原理又有啥不一样呢?
和m1不一样,这类卡是交互式读取。卡与读卡器之前,是会有APDU指令数据传递的,数据一来一回。经过交互指令确认校验成功后,才会刷卡成功。
啥是APDU:
Application Protocol data unit,是智能卡与智能卡读卡器之间传送的信息单元, (向智能卡发送的命令)指令(ISO 7816-4规范有定义)。有感兴趣的网友可以自行百度学习。
那我们继续往下吧,看下代码吧
private void readIsoDepTag(Tag tagFromIntent) {
IsoDep isoDep = IsoDep.get(tagFromIntent);
try {
if (!isoDep.isConnected()) {
isoDep.connect();
}
byte[] SELECT = {
(byte) 0x00, // CLA = 00 (first interindustry command set)
(byte) 0xA4, // INS = A4 (SELECT)
(byte) 0x04, // P1 = 04 (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
};
byte[] result = isoDep.transceive(SELECT);
KLog.i(result[0]+" "+bytesToHexString(result));
if (!(result[0] == (byte) 0x90 && result[1] == (byte) 0x00))
KLog.e("could not select AID");
isoDep.close();
} catch (Exception e) {
e.printStackTrace();
}
}
这个方法内容大概就是,获取到了IsoDep对象,然后编辑了一个查询指令,去和卡片交互,尝试获取卡片的信息。然后将返回结果打印。其中针对返回result涉及到一个判断。判断result没有得到预定的返回结果,将会报错。
这个判断做什么的?那是因为这类卡,卡上是有芯片的,芯片里会有预定程序和逻辑,和读卡器做APDU交互。
作为读卡器,我们需要去读取卡中信息,那就得先按照芯片的id进行识别,这类id称为AID。
AID:应用标识 application identifier (AID),由注册的应用提供商标识(RID)以及专用应用标识符扩展(PIX)组成。
RID:应用提供者标识符,由ISO组织来分配,标识一个全球唯一的应用提供商,一般是分配给卡组织,比如分配给 Master、银联等。
PIX:专有应用标识符扩展编码,一般由厂商自行定义。
这样组装而成AID,就可以是用于识别对接芯片的service。AID的信息也只能找芯片厂商获取。
在代码中,我将AID放在了SELECT指令中,你要我问这些指令啥意思,我是在网上找大神给的范例抄的(大胆承认,这玩意咱真不会啊)。
然后。。。银行卡访问失败,走到了if里面,AID对不上。
然后。。。然后就没辙了。。没错,我们IsoDep道路就此结束了。
回归正题了,HCE模拟。
上篇提到:android4.4系统引入的基于主机模拟——HCE(Host-based Card Emulation),读卡设备的数据,直接传到android系统的应用上交流处理,然后由应用控制NFC进行模拟。
这也是Google官方Android文档上,唯一的应用层模拟方法。
我们来尝试写一写。首先创建一个Service继承HostApduService。
然后重写两个方法,processCommandApdu()和onDeactivated()
@RequiresApi(Build.VERSION_CODES.KITKAT)
class HCEService : HostApduService() {
override fun processCommandApdu(commandApdu: ByteArray?, extras: Bundle?): ByteArray {
TODO("Not yet implemented")
}
override fun onDeactivated(reason: Int) {
TODO("Not yet implemented")
}
}
通过AS的自动补全,我们很快能得到这样的代码。(怎么语法换成kotlin了?问就是在开始学习了,痛定思痛不再当个java保守派了)
里面需要实现的方法我们一个一个看。
见到刚刚探索过熟悉的APDU关键字了,也几乎可以确认,我们之前探索的结论是对的,Android只支持模拟APDU指令交互的芯片卡。我们来看看源码上的注释:
/**
* This method will be called when a command APDU has been received
* from a remote device. A response APDU can be provided directly
* by returning a byte-array in this method. Note that in general
* response APDUs must be sent as quickly as possible, given the fact
* that the user is likely holding their device over an NFC reader
* when this method is called.
*
*
If there are multiple services that have registered for the same
* AIDs in their meta-data entry, you will only get called if the user has
* explicitly selected your service, either as a default or just for the next tap.
*
*
This method is running on the main thread of your application.
* If you cannot return a response APDU immediately, return null
* and use the {@link #sendResponseApdu(byte[])} method later.
*
* @param commandApdu The APDU that was received from the remote device
* @param extras A bundle containing extra data. May be null.
* @return a byte-array containing the response APDU, or null if no
* response APDU can be sent at this point.
*/
/**
* 翻译
* 当从远程设备接收到命令APDU时,将调用此方法。在这种方法中,可以通过返回字节数组来直接提供响应APDU。请注意,在一般情况下,必须尽快发送响应APDU,因为当调用此方法时,用户可能会将其设备放在NFC读取器上。
* 如果有多个服务在其元数据条目中注册了相同的AID,只有当用户明确选择了您的服务时,您才会被调用,无论是作为默认服务还是仅用于下一次点击。
* 此方法正在应用程序的主线程上运行。如果不能立即返回响应APDU,请返回null,然后使用sendResponseApdu(byte[])方法。
*/
public abstract byte[] processCommandApdu(byte[] commandApdu, Bundle extras);
通过注释的翻译能看到,这个方法就是响应读卡器的APDU命令,然后进行处理。其中也提到了AID,和我们之前研究的一样,AID就是作为一个服务识别号。只有在用户明确选择了你的AID,系统才会唤起你的HCE服务,进行APDU交互。
APDU也在ISO / IEC 7816-4规范中定义。APDU是NFC读取器和HCE服务之间交换的应用程序级数据包。该应用程序级协议是半双工的:NFC读取器将向NFC卡片发送命令APDU,然后等待卡片响应APDU作为回报,也就是processCommandApdu方法。
最后一个要求,此方法在主线程运行,需要立即处理响应返回。
同样也去源码看看:
/**
* This method will be called in two possible scenarios:
* The NFC link has been deactivated or lost
* A different AID has been selected and was resolved to a different
* service component
* @param reason Either {@link #DEACTIVATION_LINK_LOSS} or {@link #DEACTIVATION_DESELECTED}
*/
/**
* 此方法将在两种可能的情况下调用:
* 1、NFC链接已停用或丢失
* 2、NFC选择更换AID,并将更换不同的Service响应
*/
public abstract void onDeactivated(int reason);
这个方法的响应场景有两个,一个是与NFC失去链接,第二个是NFC卡开始更换AID。总结来说就是,当前Service与NFC的交互结束了就会触发onDeactivated方法。
override fun processCommandApdu(commandApdu: ByteArray?, extras: Bundle?): ByteArray {
TODO("Not yet implemented")
Log.i( TAG, "收到: " + commandApdu?.let { ByteArrayToHexString(it) });
return byteArrayOf(
0x90.toByte(),
0x00.toByte())
}
override fun onDeactivated(reason: Int) {
TODO("Not yet implemented")
Log.i( TAG,"断开: " + reason.toString());
}
上面这段代码,逻辑很简短。
当卡收到读卡器的APDU指令时,对Byte数组指令16进制进行打印,然后返回一个长度为2的Byte数组对象指令。完成一次交互。
当然,实际的芯片卡指令肯定不会这么简单,也肯定不会只有一次交互。通常情况,是会有多次查询确认报文交互,才算一次刷卡完成。不过本人能力有限,实在不知道一次完整指令交互命令有哪些,分几次。所以就先写成这样吧,意思到了就行了。
补全xml里的service配置
<service
android:name=".service.HCEService"
android:enabled="true"
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/apduservice"/>
service>
新建apduservice.xml文件
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/apdu_service_description"
android:requireDeviceUnlock="false">
<aid-group
android:category="other"
android:description="@string/apdu_service_description">
<aid-filter android:name="15342F"/>
aid-group>
host-apdu-service>
AID是用来确定,NFC卡片指定与其通信的HCE服务。通常,NFC读取器发送到您的设备的第一个APDU是“SELECT AID”; 此APDU包含AID。Android从APDU中提取该AID,寻找注册此AID的Service,然后将该APDU转发到对应的Service处理。
nfc声明
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
<uses-feature
android:name="android.hardware.nfc.hcef"
android:required="true" />
<uses-feature
android:name="android.hardware.nfc.hce"
android:required="true" />
HCE模拟仅限于IsoDep的APDU交互,这玩意呢,如果没有厂商对接,开发者无从知晓APDU命令是啥,代码写不下去。。。
另一个无法模拟的M1,倒是可以很轻松读取了数据,但是无法模拟。。
一路走过来来看,很明显,IsoDep比M1卡更加安全。
M1没啥指令规则,只需要你到淘宝买一个读卡器,和几张白卡,想复制几张就复制几张。
IsoDep呢,你到现在可没怎么听说淘宝能支持复制银行卡吧!所以这玩意安全系数确实比较高。
那代码走不下去了,那我们还是回到理论上,最后总结一下,理解一下M1和IsoDep这两个类别的NFC吧。
1、是16扇区64块数据卡,可以轻松实现读取
2、Android系统目前的api无法模拟复制M1卡。
3、厂商的钱包APP,有厂商定制ROM后台作为支持,提供了复制手段和模拟手段(看到网上有大神说,root手机后,可以通过修改系统文件,完成M1的复制模拟。)
这类系列的校园卡,水卡,门禁卡。看了下目前的复制手段,都是需要专门的读卡器,将数据拷贝出来后,然后写入到另一张空白M1卡上,实现卡的复制。目前我暂时还没有见到过实操Android手机直接完成复制模拟的。
1、是基于芯片主控的NFC卡,采用APDU命令式交互,脱离厂商支持无法实现交互(猜不到正确指令)。
2、Android系统目前的HCE模式,可以支持此类NFC卡的交互,模拟。
3、目前各大银行类APP的nfc银行卡,各家手机的钱包app的公交卡,各家厂家支付类app绑定的银行卡,都以HCE进行模拟。
这类的卡呢,我们可以回想一下,我们再使用华为钱包、APPLE PAY等,再绑定银行卡的时候,他们是如何操作的。他们是直接复制的吗?
想起来了吗?好像不是复制,他们是下载的!每次绑定银行卡,公交卡,都是从发卡机构,下载了一张卡的数据,然后写入了手机NFC。
比如公交卡,以华为钱包为例,它支持的公交卡,并不是我们随便复制上去的。而是需要从华为钱包里面,去选择城市公交卡类别,然后交钱,完成开卡。对的,这是一个线上的开卡步骤,理论上和我们去线下地铁站买一张交通卡是一样的逻辑。
而华为支持哪些城市的公交卡,则是需要一家一家去合作开发适配的,也就是我们前面的AID,APDU指令适配。适配好了,那就可以模拟这张卡了。
那银行卡呢,一样的逻辑了。需要手机厂商与各家银行完成对接,然后绑定时,也是进行的发卡步骤。你在回忆回忆,是不是APPLE PAY,HUAWEI PAY,SAMSUNG PAY绑定银行卡后,各家银行会发短信告诉你,你开通了一张基于当前银行卡账户的虚拟子账户(子卡)。
那我们可以猜想一下,是不是等同于下载了一张银行卡的AID等相关数据,然后在用户使用时,进行调用HCE服务进行模拟。
1、基于HostApduService实现,可以模拟卡片,接受APDU命令,并与读卡器交互,达到模拟刷卡的效果。
2、所有此类的卡片,都需要有对应注册的AID,与读卡器进行识别。
最后,想说一下,这个NFC系列,拖了好久之后,总算补完了。
其实我们最后也没有完成HCE模拟出可用卡片的目标。。。也就扩展了一堆其他知识(算是任务脱离主线了。。)
这些基本都是自己一个人也是在网上边学习边试验,如果有哪里写的不对,做的不对的地方,望大佬们勿喷。
可以看到,基于HCE模拟的NFC场景,个人开发者想要做的话,真的很难。。这些基本都是需要企业与厂商对接的,很难折腾下去。。。
NFC篇终于结束了,上半年懒到头了,看看啥时候不偷懒了,再给自己开个新坑。。。。。。