关于IC卡的基本介绍
先对相关的基础知识进行一下讲解。
RFID: 叫射频识别技术,分为接触式(需要插卡)与非接触式(只需刷卡),NFC就是从这个技术发展而来的,包含多个频段,915MHz,125KHz,13.56MHz,2.4GHz等。
ID卡: 主要工作在125KHz,只有一个身份识别码,判断方式就是卡身有一串卡号,使用时需要联网进行操作。
IC卡: 主要工作在13.56MHz,里面有存储空间,可以进行读写,脱机工作(公交卡,门禁卡等)。
NFC: 近场通讯技术,只能工作在13.56MHz,所以能读取全部工作在这个频段的卡,是属于RFID技术的,但是又有新的功能,可以理解为RFID的子类。
日常较多接触的是13.56频段的IC卡,由于不同的厂家生产的不同的芯片,数据格式与通信协议是不同的,所以需要对应,Android在这方面是有完整的底层支持的,你看到的NfcA、NfcB、NfcF、NfcV、IsoDep、Ndef这些就是对应不同的数据格式或者通讯协议的。举个例子,nxp公司的MIFARE Classic数据格式就是NfcA,MIFARE DESFire数据格式是IsoDep,二代身份证用的就是NfcB,Sony生产的Felica用的就是NfcF,德州仪器的VicinityCard卡用的是NfcV。
MIFARE Classic S50内部结构
公司使用的是MIFARE Classic S50的IC卡,也叫M1卡,是市面上较常见的类型。
存储大小1k,分为16个扇区,每个扇区分四个块,每块可以储存十六个字节的数据。第0扇区的第0块为厂家信息,无法修改,IC卡的卡号便是读取的这里。扇区最后一块,也就是第三块是密码块。
扇区密码块数据结构详解
每个扇区的最后一块为密码块,每个扇区的密码独立,所以十六个扇区可以有十六个不同的密码,读取扇区数据前需要核对对应密码,校验成功才能读取。密码块可以分为三部分来理解。
密码块十六个字节,前六个字节是密码A,中间四个字节是控制位,后六个字节是密码B。一般新买的卡片,密码A和密码B都是ff ff ff ff ff ff(16进制),所以新卡可以直接通过这个密码读取到卡号。新卡控制位默认是ff 07 80 69,密码块总结就是:6个字节的密码A + 4个字节密钥控制位 + 6个字节的密码B。
关于控制位的算法与逻辑较复杂,有兴趣的可以看这个:控制位解读 与 IC卡详解
我这里可以初略总结一下:
1.默认方式
控制位为“FF 07 80 69”,这种方式下密钥A或密钥B都可以读写数据区,密钥A可写密钥区,优点是密钥控制字无需重新计算,读写方便,缺点是安全性能差,密钥A容易泄露。
2.密钥B写方式
控制位为“7F 07 88 69”,这种方式下密钥A或密钥B都可以读写数据区,而对于密钥区只能由密钥B来写。优点是密钥B权限最高,只要知道密钥B,无论密钥A写成什么都可以改写,由最高管理员掌握密钥B,可下发多种密钥A的一般管理员,一般不会废卡的。缺点是密钥B很重要,一旦忘记,卡就不能再改写密钥了。
3.A读B写方式
控制位为“08 77 8F 69”,这种方式下由密钥A读密钥B来写,可以说是上面一种方式的变体,对于密钥B有更强的保护。
4.只读不写方式
控制位为“FF 00 F0 69”,这种方式下密钥A或密钥B都可以读数据区,但都不能写数据区(数值可减少,不能增加),密钥A可以改写密钥区。这种方式对于数据是极大的保护,尤其是定额卡,里面的钱只能减少而不能增加。
需求分析
公司使用S50的IC卡,需要实现读取卡号,写入与修改数据,对写入的数据加密。保密等级要求不高,所以使用默认的存取控制就行。
代码实现
下面展示Android代码的实现。对了,记得在AndroidManifest里增加权限:
<uses-permission android:name="android.permission.NFC" />
读取数据
/**
* @author wy
* 读取工具类
*/
public class NfcReadHelper {
private Tag tag;
private NFCCallback callback;
private static NfcReadHelper helper;
/**
* 默认初始密码
*/
private byte[] bytes = {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff};
public NfcReadHelper(Intent intent) {
this.tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
}
/**
* 单例初始化
*
* @param intent
* @return
*/
public static NfcReadHelper getInstence(Intent intent) {
if (helper == null) {
helper = new NfcReadHelper(intent);
}
return helper;
}
/**
* 设置NFC卡的密码
*
* @param str
* @return
*/
public NfcReadHelper setPassword(String str) {
if (null != str && (str.length() <= 6)) {
for (int i = 0; i < str.length(); i++) {
bytes[i] = (byte) str.charAt(i);
}
}
return helper;
}
/**
* 读取NFC卡的全部信息
*
* @param callback
*/
public void getAllData(final NFCCallback callback) {
ThreadPoolManager.getInstance().execute(() -> {
Map<String, List<String>> map = new HashMap<>();
MifareClassic mfc = MifareClassic.get(tag);
if (null != mfc) {
try {
//链接NFC
mfc.connect();
//获取扇区数量
int count = mfc.getSectorCount();
//用于判断时候有内容读取出来
boolean flag = false;
for (int i = 0; i < count; i++) {
List<String> list = new ArrayList<>();
//验证扇区密码,否则会报错(链接失败错误)
boolean isOpen = mfc.authenticateSectorWithKeyA(i, bytes);
if (isOpen) {
//获取扇区里面块的数量
int bCount = mfc.getBlockCountInSector(i);
//获取扇区第一个块对应芯片存储器的位置
int bIndex = mfc.sectorToBlock(i);
//String data1 = "";
for (int j = 0; j < bCount; j++) {
//读取数据
byte[] data = mfc.readBlock(bIndex);
bIndex++;
list.add(byteToString(data));
}
flag = true;
}
map.put(i + "", list);
}
if (flag) {
callback.callBack(map);
} else {
callback.error();
}
} catch (Exception e) {
callback.error();
e.printStackTrace();
} finally {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
/**
* 读取NFC卡的特定扇区信息
*
* @param a 扇区
* @param b 块
* @param callback
*/
public void getData(final int a, final int b, final NFCCallback callback) {
ThreadPoolManager.getInstance().execute(() -> {
Map<String, List<String>> map = new HashMap<>();
MifareClassic mfc = MifareClassic.get(tag);
if (null != mfc) {
try {
mfc.connect();
int count = mfc.getSectorCount();
if (a < 0 || a > count - 1) {
callback.error();
return;
}
int bCount = mfc.getBlockCountInSector(a);
if (b < 0 || b > bCount - 1) {
callback.error();
return;
}
boolean isOpen = mfc.authenticateSectorWithKeyA(a, bytes);
if (isOpen) {
int bIndex = mfc.sectorToBlock(a);
byte[] data = mfc.readBlock(bIndex + b);
callback.callBack(byteToString(data));
} else {
callback.error();
}
} catch (Exception e) {
callback.error();
e.printStackTrace();
} finally {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
callback.error();
}
});
}
/**
* 返回监听类
*/
public interface NFCCallback {
/**
* 返回读取nfc卡的全部信息
*
* @param data 前面代表扇区 四个块的数据用#号隔开
*/
default void callBack(Map<String, List<String>> data){
};
void callBack(String data);
void error();
}
/**
* 将byte数组转化为字符串(带字母的)
*
* @param src
* @return
*/
public static String byteToString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder();
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();
}
/**
* 将byte数组转化为字符串(纯数字,十位数)
*
* @param src
* @return
*/
public static String byteToString_num(byte[] src) {
byte[] bytes2 = new byte[src.length];
for (int i = 0; i < src.length; i++) {
bytes2[i] = src[src.length - 1 - i];
}
String carstr = new BigInteger(AppUtils.bytesToHexString(bytes2, bytes2.length), 16).toString();
StringBuffer carsb = new StringBuffer(carstr);
if (!TextUtils.isEmpty(carstr)) {
int te = 10 - carstr.length();
for (int i = 0; i < te; i++) {
carsb.insert(0, "0");
}
}
return carsb.toString();
}
}
写入数据
/**
* @author wy
* 写入工具类
*/
public class NFCWriteHelper {
private Tag tag;
private NFCCallback callback;
private static NFCWriteHelper helper;
/**
* 默认初始密码
*/
private byte[] bytes = {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff};
private static int PASSWORD_LENTH = 6;
public NFCWriteHelper(Intent intent) {
this.tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
}
/**
* 单例初始化
*
* @param intent
* @return
*/
public static NFCWriteHelper getInstence(Intent intent) {
if (helper == null) {
helper = new NFCWriteHelper(intent);
}
return helper;
}
/**
* 设置NFC卡的读取密码
*
* @return
*/
public NFCWriteHelper setReadPassword(byte[] bytes) {
this.bytes = bytes;
return helper;
}
/**
* 设置NFC卡的读取密码
*
* @param str
* @return
*/
public NFCWriteHelper setReadPassword(String str) {
if (null != str && (str.length() <= PASSWORD_LENTH)) {
for (int i = 0; i < str.length(); i++) {
bytes[i] = (byte) str.charAt(i);
}
}
return helper;
}
/**
* 写卡
*
* @param str 书写内容,16个字节
* @param a 书写的扇区 (从0开始数)
* @param b 书写的块(从0开始数)
* @param callback 返回监听
*/
public void writeData(String str, int a, int b, NFCCallback callback) {
MifareClassic mfc = MifareClassic.get(tag);
byte[] data = new byte[16];
if (null != mfc) {
try {
//连接NFC
mfc.connect();
//获取扇区数量
int count = mfc.getSectorCount();
//如果传进来的扇区大了或者小了直接退出方法
if (a > count - 1 || a < 0) {
callback.isSusses(false);
return;
}
//获取写的扇区的块的数量
int bCount = mfc.getBlockCountInSector(a);
//如果输入的块大了或者小了也是直接退出
if (b > bCount - 1 || b < 0) {
callback.isSusses(false);
return;
}
//将字符转换为字节数组
for (int i = 0; i < 16; i++) {
if (i < str.length()) {
data[i] = (byte) str.charAt(i);
} else {
data[i] = (byte) 'f';
}
}
//验证扇区密码
boolean isOpen = mfc.authenticateSectorWithKeyA(a, bytes);
if (isOpen) {
int bIndex = mfc.sectorToBlock(a);
//写卡
mfc.writeBlock(bIndex + b, data);
callback.isSusses(true);
} else {
callback.isSusses(false);
}
} catch (Exception e) {
e.printStackTrace();
callback.isSusses(false);
} finally {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 修改密码
*
* @param password 书写密码,16个字节
* @param a 书写的扇区
* @param callback 返回监听
*/
public void changePasword(String password, int a, final NFCCallback callback) {
MifareClassic mfc = MifareClassic.get(tag);
byte[] data = new byte[16];
if (null != mfc) {
try {
mfc.connect();
if (password.length() != PASSWORD_LENTH) {
callback.isSusses(false);
return;
}
int count = mfc.getSectorCount();
if (a > count - 1 || a < 0) {
callback.isSusses(false);
return;
}
//将密码转换为keyA
for (int i = 0; i < password.length(); i++) {
data[i] = (byte) password.charAt(i);
}
//将密码转换为KeyB
for (int i = 0; i < password.length(); i++) {
data[i + password.length() + 4] = (byte) password.charAt(i);
}
//输入控制位
data[password.length()] = (byte) 0xff;
data[password.length() + 1] = (byte) 0x07;
data[password.length() + 2] = (byte) 0x80;
data[password.length() + 3] = (byte) 0x69;
//验证密码
boolean isOpen = mfc.authenticateSectorWithKeyA(a, bytes);
if (isOpen) {
int bIndex = mfc.sectorToBlock(a);
int bCount = mfc.getBlockCountInSector(a);
//写到扇区的最后一个块
mfc.writeBlock(bIndex + bCount - 1, data);
callback.isSusses(true);
} else {
callback.isSusses(false);
}
} catch (Exception e) {
e.printStackTrace();
callback.isSusses(false);
} finally {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 返回监听类
*/
public interface NFCCallback {
/**
* 返回是否成功
*
* @param flag
*/
void isSusses(boolean flag);
}
}
写入这里包含了密码修改的实现。
NFC工具软件
读写IC卡有个好使的软件推荐推荐,在开发的时候可以使用这个软件对比数据:
NFC Reader Tool
总结
每一个新需求都是一次新的学习机会,这又是一个我未踏足过的知识面,希望自己无限进步,永远不会失去对新事物的好奇和热情。如果这篇博客对你有帮助,请点个赞。