diff --git a/appmain/utils/mifare/ChameleonBatchAdapterImpl.java b/appmain/utils/mifare/ChameleonBatchAdapterImpl.java new file mode 100644 index 0000000..3e1a34d --- /dev/null +++ b/appmain/utils/mifare/ChameleonBatchAdapterImpl.java @@ -0,0 +1,179 @@ +package com.proxgrind.chameleon.utils.mifare; + +import com.proxgrind.chameleon.packets.DataPackets; +import com.proxgrind.chameleon.utils.system.LogUtils; +import com.proxgrind.chameleon.utils.tools.HexUtil; +import com.proxgrind.devices.BleSerialControl; + +import java.io.IOException; +import java.util.Arrays; + +public class ChameleonBatchAdapterImpl implements BatchAdapter { + // 持有一个原生通讯的句柄! + private BleSerialControl control = BleSerialControl.get(); + // 一个空的数组! + public static final byte[] EMPTY_DATA_ONE = new byte[]{0x00}; + + /** + * 读取函数,变色龙的读取实现,依赖起始块 + * + * @param startBlock 起始块,如果当前的模式是读取单块模式,则此参数是需要读取的块 + * @param isKeyA 当前是否是用keyA来验证并且读取的 + * @param key 当前被用来读取的秘钥 + * @param isReadSector 当前是否是读取全部扇区模式,注意,如果是读取扇区模式, + * 遇到4K的大扇区的情况下,需要进行每次4个块的读取,也就是 + * 从startBlock开始表示读取第一个大块 + * 从startBlock + 4 开始表示读取第二个大块 + * 从startBlock + 8 开始表示读取第三个大块 + * 从startBlock + 12 开始表示读取第四个大块 + */ + @Override + public byte[][] read(int startBlock, boolean isKeyA, byte[] key, boolean isReadSector) throws IOException { + // 组包! + byte[] dataOfParam = HexUtil.bytesMerge( + getType(isReadSector ? 2 : 1), // 置入类型,如果当前是读取扇区,则设置操作标志为2,否则设置为1 + EMPTY_DATA_ONE, // 置入状态码! + new byte[]{(byte) startBlock}, // 置入起始块! + isKeyA(isKeyA), // 置入当前验证的秘钥类型! + key, // 置入秘钥! + isReadSector ? new byte[16 * 4] : new byte[16] // 置入空字节 + ); + byte[] dataOfFinal = new DataPackets(0x72, dataOfParam).getData(); + LogUtils.d("发送的数据长度: " + dataOfParam.length); + // 发送数据! + byte[] respDatas = sendAndReadResponse(dataOfFinal, dataOfFinal.length, dataOfParam.length); + if (respDatas != null && respDatas.length == dataOfParam.length) { + // 得到最终的秘钥索引,并且进行下标 -1 的内容返回! + byte status = respDatas[1]; + checkTagStatus(status); + if (status == 0) { + LogUtils.d("读取失败"); + return null; + } + if (status == 1) { + return HexUtil.splitBytes( + Arrays.copyOfRange(respDatas, 10, respDatas.length), + 16 + ); + } + } + return null; + } + + @Override + public boolean write(int sector, boolean isKeyA, byte[] key, byte[] dataGroup) throws IOException { + // 组包! + byte[] dataOfParam = HexUtil.bytesMerge( + getType(3), // 置入类型! + EMPTY_DATA_ONE, // 置入状态码! + new byte[]{(byte) sector}, // 置入扇区! + isKeyA(isKeyA), // 置入当前验证的秘钥类型! + key, // 置入秘钥! + dataGroup // 置入数据! + ); + byte[] dataOfFinal = new DataPackets(0x72, dataOfParam).getData(); + // 发送数据! + byte[] respDatas = sendAndReadResponse(dataOfFinal, dataOfFinal.length, dataOfParam.length); + if (respDatas != null && respDatas.length == dataOfParam.length) { + // 得到最终的秘钥索引,并且进行下标 -1 的内容返回! + byte status = respDatas[1]; + checkTagStatus(status); + if (status == 0) { + LogUtils.d("写入失败"); + return false; + } + return status == 1; + } + return false; + } + + /** + * 经过测试,以35个秘钥为一组进行验证比较好! + */ + @Override + public byte[] verity(int sector, byte[][] keysGroup, boolean isKeyA) throws IOException { + // 组包! + byte[] dataOfParam = HexUtil.bytesMerge( + getType(0x04), // 置入类型! 04 + EMPTY_DATA_ONE, // 置入状态码! 00 + getBlock(sector), // 置入扇区! ~3F + isKeyA(isKeyA), // 置入当前验证的秘钥类型! 00 + getKeyCount(keysGroup), // 获得当前的秘钥总数! + HexUtil.bytesMerge(keysGroup) // 合并秘钥! + ); + byte[] dataOfFinal = new DataPackets(0x72, dataOfParam).getData(); + byte[] respDatas = sendAndReadResponse(dataOfFinal, dataOfFinal.length, 11); + if (respDatas != null && respDatas.length == 11) { + // 得到最终的秘钥索引,并且进行下标 -1 的内容返回! + byte index = respDatas[1]; + checkTagStatus(index); + // 只有当索引大于1的时候,才是真正的有应答,当应答FF的时候,则是卡片失联了 + if (index > 0 && index <= keysGroup.length) { + return keysGroup[index - 1]; + } + } + return null; + } + + public void checkTagStatus(int statusCode) throws IOException { + if (statusCode == 0xFF || statusCode == -1) { + throw new IOException("Tag lost."); + } + } + + public byte[] sendAndReadResponse(byte[] data, int dataLength, int acceptRespLength) throws IOException { + // 发送数据! + int timeout = getTimeout(); + // 刷新缓冲区且发送 + control.flush(); + int maxLen = BleSerialControl.MTU; + if (control.write(data, 0, dataLength, timeout) == dataLength) { + // 读取应答数据 + byte[] responseBuffer = new byte[acceptRespLength <= 0 ? maxLen : acceptRespLength]; + // 直接读取 + control.read( + responseBuffer, + 0, + Math.max(acceptRespLength, 0), + timeout + ); + return responseBuffer; + } + return null; + } + + /** + * 获取byte数组型的类型,分别可能是 + * 0 读取 + * 1 写入 + * 2 验证 + */ + public byte[] getType(int type) { + return new byte[]{(byte) type}; + } + + /** + * 将单个字节的sector转为数组! + */ + public byte[] getBlock(int sector) { + return new byte[]{(byte) MfDataUtils.get_trailer_block(MfDataUtils.sectorToBlock(sector))}; + } + + public byte[] isKeyA(boolean isKeyA) { + return new byte[]{(byte) (isKeyA ? 0 : 1)}; + } + + public byte[] getKeyCount(byte[][] keys) { + return new byte[]{(byte) keys.length}; + } + + @Override + public int getTimeout() { + return 2333; + } + + @Override + public void setTimeout(int timeout) { + + } +} diff --git a/appmain/utils/mifare/ChameleonMifareAdapter.java b/appmain/utils/mifare/ChameleonMifareAdapter.java new file mode 100644 index 0000000..f53b0ae --- /dev/null +++ b/appmain/utils/mifare/ChameleonMifareAdapter.java @@ -0,0 +1,266 @@ +package com.proxgrind.chameleon.utils.mifare; + +import android.nfc.tech.MifareClassic; + +import com.proxgrind.chameleon.packets.DataPackets; +import com.proxgrind.chameleon.utils.system.LogUtils; +import com.proxgrind.chameleon.utils.tools.HexUtil; + +import java.io.IOException; +import java.util.Arrays; + +public class ChameleonMifareAdapter implements MifareAdapter { + private ChameleonBatchAdapterImpl batchAdapter = new ChameleonBatchAdapterImpl(); + private byte[] uid; + private byte[] atqa; + private int sak = -1; + private byte[] ats; + + @Override + public boolean rescantag() throws IOException { + // 组包! + byte[] dataOfParam = HexUtil.bytesMerge( + batchAdapter.getType(0x00), // 置入类型! 04 + ChameleonBatchAdapterImpl.EMPTY_DATA_ONE, // 置入状态码! 00 + new byte[10], // 卡号 + new byte[1], // 卡号长度 + new byte[2], // ATQA + new byte[1], // SAK + new byte[1] // ATS长度 + ); + byte[] dataOfFinal = new DataPackets(0x72, dataOfParam).getData(); + byte[] respDatas = batchAdapter.sendAndReadResponse(dataOfFinal, dataOfFinal.length, 0); + // byte[] respDatas = HexUtil.hexStringToByteArray("0001505C8E04000000000000040400080B12b12b12b12b12b121b2121b12b12b1212b1bb32513b513513b5131b4141b1"); + if (respDatas != null && respDatas.length > 2) { + // 得到最终的秘钥索引,并且进行下标 -1 的内容返回! + byte status = respDatas[1]; + // 只有当索引大于1的时候,才是真正的有应答,当应答FF或者0的时候,则是卡片失联了 + batchAdapter.checkTagStatus(status); + if (status > 0) { + // 我们需要进行信息截取! + if (respDatas.length >= 13) { // 可以截取有效的UID! + uid = Arrays.copyOfRange(respDatas, 2, 12); + uid = Arrays.copyOf(uid, respDatas[12]); + LogUtils.d("UID: " + HexUtil.toHexString(uid)); + } + if (respDatas.length >= 15) { // 可以截取有效的ATQA + atqa = Arrays.copyOfRange(respDatas, 13, 15); + LogUtils.d("ATQA: " + HexUtil.toHexString(atqa)); + } + if (respDatas.length >= 16) { // 可以截取有效的SAK + sak = Arrays.copyOfRange(respDatas, 15, 16)[0]; + LogUtils.d("SAK: " + sak); + } + int atsLen = 0; + if (respDatas.length >= 17) { + atsLen = Arrays.copyOfRange(respDatas, 16, 17)[0]; + } + LogUtils.d("ATS长度: " + atsLen); + if (atsLen > 0 && respDatas.length - 17 >= atsLen) { + ats = Arrays.copyOfRange(respDatas, 17, 17 + atsLen); + LogUtils.d("ATS: " + HexUtil.toHexString(ats)); + } + return true; + } + } + return false; + } + + @Override + public boolean connect() throws IOException { + return rescantag(); + } + + @Override + public void close() throws IOException { + // 不需要做任何操作! + } + + @Override + public byte[] read(int block) throws IOException { + throw new IOException("Unsupported operation."); + } + + @Override + public boolean write(int blockIndex, byte[] data) throws IOException { + throw new IOException("Unsupported operation."); + } + + @Override + public boolean authA(int sectorIndex, byte[] key) throws IOException { + throw new IOException("Unsupported operation."); + } + + @Override + public boolean authB(int sectorIndex, byte[] key) throws IOException { + throw new IOException("Unsupported operation."); + } + + @Override + public void increment(int blockIndex, int value) throws IOException { + throw new IOException("Unsupported operation."); + } + + @Override + public void decrement(int blockIndex, int value) throws IOException { + throw new IOException("Unsupported operation."); + } + + @Override + public void restore(int blockIndex) throws IOException { + throw new IOException("Unsupported operation."); + } + + @Override + public void transfer(int blockIndex) throws IOException { + throw new IOException("Unsupported operation."); + } + + @Override + public byte[] getUid() { + try { + if (uid == null) + rescantag(); + } catch (IOException e) { + e.printStackTrace(); + } + return uid; + } + + @Override + public byte[] getAts() { + if (ats == null) { + try { + rescantag(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return ats; + } + + @Override + public byte[] getAtqa() { + if (atqa == null) { + try { + rescantag(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return atqa; + } + + @Override + public byte[] getSak() { + if (sak == -1) { + try { + rescantag(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return new byte[]{(byte) sak}; + } + + @Override + public int getType() { + try { + if (sak == -1) + rescantag(); + } catch (IOException e) { + e.printStackTrace(); + return -1; + } + switch (sak) { + case 0x01: + case 0x08: + case 0x09: + // Seclevel = SL2 + case 0x18: + case 0x19: + case 0x28: + //mIsEmulated = true; + case 0x38: + case 0x88: + return MifareClassic.TYPE_CLASSIC; + case 0x10: + // SecLevel = SL2 + case 0x11: + return MifareClassic.TYPE_PLUS; + // NXP-tag: false + case 0x98: + case 0xB8: + return MifareClassic.TYPE_PRO; + default: + // Stack incorrectly reported a MifareClassic. We cannot handle this + // gracefully - we have no idea of the memory layout. Bail. + return -1; + } + } + + @Override + public int getSectorCount() { + switch (getType()) { + case 0: + return 16; + case 1: + return 32; + case 2: + return 40; + } + return 0; + } + + @Override + public int getBlockCount() { + switch (getType()) { + case 0: + return MifareClassic.SIZE_1K / MifareClassic.BLOCK_SIZE; + case 1: + return MifareClassic.SIZE_2K / MifareClassic.BLOCK_SIZE; + case 2: + return MifareClassic.SIZE_4K / MifareClassic.BLOCK_SIZE; + } + return 0; + } + + @Override + public void setTimeout(int ms) { + } + + @Override + public int getTimeout() { + return 2333; + } + + @Override + public BatchAdapter getBatchImpl() { + return batchAdapter; + } + + @Override + public boolean isConnected() { + try { + return rescantag(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + @Override + public boolean isEmulated() { + return false; + } + + @Override + public boolean isSpecialTag() { + return false; + } + + @Override + public boolean isTestSupported() { + return false; + } +} diff --git a/appmain/utils/mifare/DumpUtils.java b/appmain/utils/mifare/DumpUtils.java new file mode 100644 index 0000000..bc8e42a --- /dev/null +++ b/appmain/utils/mifare/DumpUtils.java @@ -0,0 +1,992 @@ +package com.proxgrind.chameleon.utils.mifare; + +import android.net.Uri; +import android.nfc.tech.MifareClassic; + +import com.proxgrind.chameleon.javabean.MifareBean; +import com.proxgrind.chameleon.javabean.M1KeyBean; +import com.proxgrind.chameleon.utils.stream.FileUtils; +import com.proxgrind.chameleon.utils.system.LogUtils; +import com.proxgrind.chameleon.utils.tools.HexUtil; +import com.proxgrind.chameleon.utils.tools.MifareUtils; +import com.proxgrind.chameleon.utils.tools.RegexGroupUtils; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; + +/*Dump文件操作类封装*/ +public class DumpUtils { + + private static final String LOG_TAG = DumpUtils.class.getSimpleName(); + + //默认的空数据! + public static final String BLANK_DATA = "00000000000000000000000000000000"; + // 默认的秘钥 + public static final String BLANK_KEY = "FFFFFFFFFFFF"; + // 默认的尾部块 + public static final String BLANK_TRAIL = "FF078069"; + // 空的尾部块! + public static final String BLANK_TRAIL_BLOCK = BLANK_KEY + BLANK_TRAIL + BLANK_KEY; + //没有密钥时的填充 + public static final String NO_KEY = "*FFFFFFFFFFF"; + //没有数据时的填充 + public static final String NO_DAT = "*0000000000000000000000000000000"; + //没有尾部块的时候的填充 + public static final String NO_TRAIL = "*F078069"; + //默认的空尾部块! + public static final String NO_TRAIL_BLOCK = NO_KEY + NO_TRAIL + NO_KEY; + + // 默认秘钥! + public final static String[] KEY_DEFAULT = new String[]{ + "FFFFFFFFFFFF", + "A0A1A2A3A4A5", + "D3F7D3F7D3F7", + "000000000000", + "A0B0C0D0E0F0", + "A1B1C1D1E1F1", + "B0B1B2B3B4B5", + "4D3A99C351DD", + "1A982C7E459A", + "AABBCCDDEEFF" + }; + + public final static byte[][] KEY_DEFAULT_BYTE = mergeHexKeys(KEY_DEFAULT); + + public final static long MAX_DUMP_FILE_SIZE = FileUtils.Size.KB * 10; + + public final static int NEWPREFIXLENGTH = 56; + public final static int OLDPREFIXLENGTH = 48; + + /* + * 由于卡的种类问题,扇区数不能固定,块数量也不能固定 + * 因此,不可以写死扇区和数据块,并且,数据一定要完整!!! + * */ + + private DumpUtils() { + //no instance + } + + /* + * 目前已知的数据格式 + * S50的结构和S70的结构 + * 我们需要知道,块的数据格式总是16字节的长度 + * 但是,S50和S70的卡由于扇区数量不同,因此,不能单纯判断块的长度 + * S50: 64个块,64 * 16 = 1024b + * S70: 256个块,256 * 16 = 4096b + * */ + public static final int TYPE_TXT = 1; + public static final int TYPE_BIN = 2; + public static final int TYPE_NOT = -1; + + // 将秘钥字符串数组转换为秘钥字节数组! + public static byte[][] mergeHexKeys(String[] hexKeys) { + byte[][] ret = new byte[hexKeys.length][]; + for (int i = 0; i < ret.length; i++) { + // 将字符串转为字节! + ret[i] = HexUtil.hexStringToByteArray(hexKeys[i]); + } + return ret; + } + + //判断是否是注释行 + public static boolean isAnnotaion(String str) { + return str.trim().startsWith("#"); + } + + // 根据扇区号来获取相应的空扇区! + public static MifareBean getEmptyM1Bean(int sector) { + MifareBean ret = new MifareBean(); + int blockCount = MfDataUtils.getBlockCountInSector(sector); + String[] datas = new String[blockCount]; + for (int i = 0; i < blockCount; i++) { + datas[i] = i < (blockCount - 1) ? BLANK_DATA : BLANK_TRAIL_BLOCK; + } + ret.setDatas(datas); + ret.setSector(sector); + return ret; + } + + //判断是否是密钥格式 + public static boolean isKeyFormat(String key) { + return HexUtil.isHexString(key) && key.length() == 12; + } + + // 是否是正常的块数据! + public static boolean isBlocksValids(String[] datas) { + switch (datas.length) { + case MifareClassic.SIZE_1K / MifareClassic.BLOCK_SIZE: + case MifareClassic.SIZE_2K / MifareClassic.BLOCK_SIZE: + case MifareClassic.SIZE_4K / MifareClassic.BLOCK_SIZE: + return true; + } + // LogUtils.d("检测的长度: " + datas.length); + return false; + } + + //提取密钥 + public static String[] extractKeys(MifareBean[] datas) { + ArrayList ret = new ArrayList<>(); + //迭代当前的bean。 + for (MifareBean b : datas) { + //跳过无效的bean包! + if (b == null) continue; + //得到其中的数据封包 + String[] dataArr = b.getDatas(); + //跳过无效的数据封包! + if (dataArr == null) continue; + //判断块的长度,必须正确! + if (dataArr.length == 4 || dataArr.length == 16) { + //得到尾部的数据块! + String lastBlock = dataArr[dataArr.length - 1]; + //判断尾部块的数据是否有效! + if (lastBlock == null || (lastBlock.length() != 32)) continue; + //开始提取A密钥! + String keyA = lastBlock.substring(0, 12); + //开始提取密钥B! + String keyB = lastBlock.substring(20, 32); + //判断密钥有效性,酌情提取! + if (isKeyFormat(keyA)) { + ret.add(keyA); + } + if (isKeyFormat(keyB)) { + ret.add(keyB); + } + } + } + return ret.toArray(new String[0]); + } + + /* + * BCC获取 + * */ + public static byte calcBCC(byte[] uid) { + if (uid.length != 4) { + return -1; + } + byte bcc = uid[0]; + for (int i = 1; i < uid.length; i++) bcc = (byte) (bcc ^ uid[i]); + return bcc; + } + + /* + * BCC有效性判断! + * */ + public static boolean isBCCVaild(String uidAndBcc) { + if (uidAndBcc.length() != 10) return false; + String bccStr = uidAndBcc.substring(8, 10); + byte bcc = HexUtil.hexStringToByteArray(bccStr)[0]; + String uidStr = uidAndBcc.substring(0, 8); + byte[] uidBytes = HexUtil.hexStringToByteArray(uidStr); + return calcBCC(uidBytes) == bcc; + } + + /* + * 分离字符串,以换行符分割 + * */ + public static String[] splitDump(String dump) { + //判断dump来源 + if (isUnixLFFormat(dump)) { + //unix类系统 + return dump.split(getSystemLF("unix")); + } else { + return dump.split(getSystemLF("windows")); + } + } + + /* + * 判断是不是原生的数据文件 + * */ + public static boolean isRaw1K(File dump) { + return dump.length() == 1024; + } + + public static boolean isRaw1K(byte[] dumpByte) { + return dumpByte.length == 1024; + } + + /* + * 判断是否是2K的数据文件 + * */ + public static boolean isRaw2K(File dump) { + return dump.length() == 2048; + } + + public static boolean isRaw2K(byte[] dumpByte) { + return dumpByte.length == 2048; + } + + /* + * 判断是否是原生4k文件 + * */ + public static boolean isRaw4K(File dump) { + return dump.length() == 4096; + } + + public static boolean isRaw4K(byte[] dumpByte) { + return dumpByte.length == 4096; + } + + /* + * 将1kdump转为4k文件 + * */ + public static byte[] raw1Kto4k(byte[] raw1k) throws Exception { + if (isRaw1K(raw1k)) + throw new Exception("非1k字节文件!"); + //建立一个4096大小的数组 + byte[] ret = new byte[4096]; + for (int i = 0; i < raw1k.length; i++) { + //将1k数据合并至4k的数据 + ret[i] = raw1k[i]; + } + //填充剩余的字节为0 + for (int i = raw1k.length; i < ret.length; i++) { + ret[i] = 0x00; + } + return ret; + } + + /* + * 将4k文件转换为1k的文件 + * */ + public static byte[] raw4kto1k(byte[] raw4k) throws Exception { + if (isRaw4K(raw4k)) + throw new Exception("非4k字节文件!"); + byte[] ret = new byte[1024]; + for (int i = 0; i < ret.length; i++) { + ret[i] = raw4k[i]; + } + return ret; + } + + /* + * 判断是否是16进制字符 + * */ + public static boolean isDataChar(char c) { + if (c >= 'a' && c <= 'z') { + return true; + } + if (c >= 'A' && c <= 'Z') { + return true; + } + if (c >= '0' && c <= '9') { + return true; + } + if (c == '-') { + return true; + } + if (c == '?') + return true; + return c == '*'; + } + + /* + * 裁剪有效数据 + * */ + public static String cutVaildData(String str) { + String ret; + ret = RegexGroupUtils.matcherGroup(str, "([A-Fa-f0-9]{32})", 1, 0); + if (ret != null) { + return ret; + } + char[] cs = new char[str.length()]; + str.getChars(0, str.length(), cs, 0); + //搜索字符串数组,从尾部开始搜素! + int pos = 0; + for (int i = cs.length - 1; i >= 0; i--) { + if (!isDataChar(cs[i])) { + //如果当前不是数据字符,则移动指针到上一位(也是顺序下一位) + pos = i + 1; + // Log.d(LOG_TAG, "非数据字符,跳过"); + break; + } + } + //走常规的判断应当从尾部开始截取,直到遇见任何非16进制字符或者非注释字符时停止并获得定位 + if (pos == 0) { + // Log.d(LOG_TAG, "定位在0,直接返回原字符串: " + str); + return str; + } else { + ret = str.substring(pos); + // Log.d(LOG_TAG, "定位不为零,返回经过裁剪后的: " + ret); + } + // 修复非有效块依旧返回的问题! + return isValidBlockData(ret) ? ret : null; + } + + /* + * 判断是否是块数据 + * */ + public static boolean isBlockData(String data) { + return data.length() == 32 && data.matches("[0-9a-fA-F-?*]{32}"); + } + + public static boolean isValidBlockData(String data) { + return data.length() == 32 && data.matches("[0-9a-fA-F]{32}"); + } + + public static boolean isM1Data(byte[] data) { + return data.length == MifareClassic.SIZE_1K || + data.length == MifareClassic.SIZE_2K || + data.length == MifareClassic.SIZE_4K; + } + + public static boolean isM1Data(String[] data) { + for (String block : data) { + if (!isValidBlockData(block)) return false; + } + return true; + } + + public static boolean isULData(String[] data) { + for (String page : data) { + if (page.length() != 8 || !HexUtil.isHexString(page)) + return false; + } + return true; + } + + public static boolean isULData(byte[] data) { + return data.length == 64 || + data.length == 80 || + data.length == 164 || + data.length == 176 || + data.length == 192; + } + + /* + * 转换为bin数据流 + */ + public static byte[][] getBin(byte[] data) { + if (data == null) return null; + if (isM1Data(data)) { + return HexUtil.splitBytes(data, 16); + } else if (isULData(data)) { + return HexUtil.splitBytes(data, 4); + } else if (hasUltralightHeader(data)) { + data = Arrays.copyOfRange(data, OLDPREFIXLENGTH, data.length); + return isULData(data) ? HexUtil.splitBytes(data, 4) : null; + } else if (hasUltralightNewHeader(data)) { + data = Arrays.copyOfRange(data, NEWPREFIXLENGTH, data.length); + return isULData(data) ? HexUtil.splitBytes(data, 4) : null; + } else { + return null; + } + } + + /* + * 合并 + * */ + public static byte[] mergeBins(byte[][] datas) { + if (datas == null) return null; + ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); + for (byte[] data : datas) { + try { + bos.write(data); + } catch (IOException e) { + e.printStackTrace(); + } + } + return bos.toByteArray(); + } + + /* + * 转换为txt数据流 + * */ + public static String[] getTxt(byte[] data) { + if (data == null) return null; + StringBuilder sb = new StringBuilder(); + //转换为字符串 + String dataStr = new String(data); + //根据换行符进行切割字符串 + String[] dataLines = splitDump(dataStr); + //进行有效数据的判断切割 + for (int i = 0; i < dataLines.length; i++) { + //判断一下,如果数据有效,则追加进字符串构造器中 + //16进制的字符串必须要有至少HEX32个字符 + if (dataLines[i].length() < 32) continue; + //如果等于32个字符串则需要验证一下字符串是否是正确的16进制字符串! + if (dataLines[i].length() == 32) { + //添加结果集 + if (isBlockData(dataLines[i])) sb.append(dataLines[i]); + //判断是否需要换行! + if (i != (dataLines.length - 1)) sb.append(getSystemLF("unix")); + } else { + //如果大于32个字符则需要尝试截取一下 + String vaildData = cutVaildData(dataLines[i]); + // Log.d(LOG_TAG, "测试输出数据: " + vaildData); + //数据裁剪成功,添加进数据集中 + if (vaildData != null) sb.append(vaildData); + //判断是否需要换行 + if (i != dataLines.length - 1) sb.append(getSystemLF("unix")); + } + } + //结果集转换为数组 + String[] ret = sb.toString().split(getSystemLF("unix")); + //分别符合1K,2K,4K的规范! + //判断块数量,严格控制规范,使块数量在64或者256之间 + if (ret.length == 64 || ret.length == 128 || ret.length == 256) { + /*for (String b : ret) { + Log.d(LOG_TAG, "测试输出截取结果: " + b); + }*/ + return ret; + } else { + return null; + } + } + + /* + * 合并数组为字符串! + * */ + public static String mergeTxt(String[] txts, boolean needNewLine, String lineChar) { + if (txts == null) return null; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < txts.length; i++) { + if (needNewLine) { + if (i == txts.length - 1) { + sb.append(txts[i]); + } else { + sb.append(txts[i]).append(lineChar); + } + } else { + sb.append(txts[i]); + } + } + return sb.toString(); + } + + /* + * 判断数据类型 + * */ + public static int getType(byte[] data) { + //初步断定是原生的二进制文件 + if (getBin(data) != null) return TYPE_BIN; + //最后判断是txt文件 + if (getTxt(data) != null) return TYPE_TXT; + //然后尝试切割有效的数据 + return TYPE_NOT; + } + + /* + * txt转换为bin + * */ + public static byte[] txt2Bin(byte[] txt) { + //已经封装过将文本忽略修饰直接转换为纯hex文本的方法 + //直接调用这个方法进行获取,而后转换为bin格式 + String[] datas = getTxt(txt); + //输出流,把字节输出到buf,初始大小1024,4k时自动扩展! + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + //进行迭代 + for (String data : datas) { + //进行转换 + byte[] _tmps = HexUtil.hexStringToByteArray(data); + try { + if (_tmps != null) { + baos.write(_tmps); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + baos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return baos.toByteArray().length >= 1024 ? baos.toByteArray() : null; + } + + /* + * bin转换为txt + * */ + public static byte[] bin2Txt(byte[] bin) { + //得到经过解析的块数组 + byte[][] blocks = getBin(bin); + StringBuilder sb = new StringBuilder(); + //如果不为空,则是有效的数据 + if (blocks != null) { + //抽取出每一块的数据,将其转换为16进制的字符串 + for (int i = 0; i < blocks.length; ++i) { + //迭代这个数据,进行转换处理 + if (i == blocks.length - 1) { + //最后一个元素,无需添加换行 + sb.append(HexUtil.toHexString(blocks[i])); + } else { + sb.append(HexUtil.toHexString(blocks[i])).append("\n"); + } + } + } else { + return null; + } + return sb.toString().getBytes(); + } + + /* + * 进行修饰 + * */ + public static String decorate(byte[] bytes) { + /* + * 修饰为MCT支持的格式(为了兼容性) + * */ + //调用已经封装的字节数组转String数组的方法 + String[] blocks = getTxt(bytes); + return decorate(blocks); + } + + public static String decorate(String[] bytes) { + //根据规范,当块的数量是64时,他是1K卡(S50),有16个扇区 + //如果是256个块时,他是4K卡(S70) + if (bytes == null) { + // LogUtils.d("decorate()函数截取失败!"); + return null; + } + if (bytes.length != 64 && bytes.length != 128 && bytes.length != 256) + return null; + String label = "+Sector: "; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + //当前是每个扇区的起始块! + if (isHeader(i)) { + //添加扇区修饰,使数据可被MCT识别! + sb.append(label).append(toSector(i)).append('\n'); + //继而将这个起始扇区写在后面 + sb.append(bytes[i]).append('\n'); + } else { + if (i == bytes.length - 1) { + //已经在最结尾的一行,不需要换行 + sb.append(bytes[i]); + } else { + //需要换行 + sb.append(bytes[i]).append('\n'); + } + } + } + return sb.toString(); + } + + /* + * 块转扇区 + * */ + public static int toSector(int block) { + if (block < 32 * 4) { + return (block / 4); + } else { + return (32 + (block - 32 * 4) / 16); + } + } + + /* + * 扇区转块 + * */ + public static int toBlock(int sector) { + if (sector < 32) { + return (sector * 4); + } else { + return (32 * 4 + (sector - 32) * 16); + } + } + + /* + * 是否是在首行 + * */ + public static boolean isHeader(int block) { + if (block < 128) + return (block % 4 == 0); + else + return (block % 16 == 0); + } + + /* + * 是否在尾部 + * */ + public static boolean isFoolter(int block) { + if (block < 128) + return ((block + 1) % 4 == 0); + else + return ((block + 1) % 16 == 0); + } + + /* + * 去除修饰 + * */ + public static String[] undecorate(byte[] bytes) { + return getTxt(bytes); + } + + /* + * 将M1数据bean合并为数组 + * */ + public static String[] mergeDatas(ArrayList beans) { + StringBuilder sb = new StringBuilder(); + // 排序一下! + Collections.sort(beans, new Comparator() { + @Override + public int compare(MifareBean o1, MifareBean o2) { + return Integer.compare(o1.getSector(), o2.getSector()); + } + }); + for (int i = 0; i < beans.size(); i++) { + //取出bean + MifareBean bean = beans.get(i); + if (bean == null) return null; + //迭代bean中的信息,进行处理 + String[] _tmp = bean.getDatas(); + if (_tmp != null) { + for (int j = 0; j < _tmp.length; j++) { + if (i == beans.size() - 1 && j == (_tmp.length - 1)) { + //结尾,无需换行 + sb.append(_tmp[j]); + } else { + sb.append(_tmp[j]).append("\n"); + } + } + } else { + return null; + } + } + return sb.toString().split("\n"); + } + + /* + * 将数组分散为M1数据! + * */ + public static MifareBean[] mergeDatas(String[] datas) { + //块数量! + int blockCount = datas.length; + ArrayList rets = new ArrayList<>(16); + for (int i = 0; i < blockCount; ) { + MifareBean bean = new MifareBean(); + //获取当前的扇区 + int sector = MifareUtils.blockToSector(i); + bean.setSector(sector); + //获取当前的块数量统计! + int blockCountInSecrot = MifareUtils.getBlockCountInSector(sector); + String[] dataArray = new String[blockCountInSecrot]; + //进行迭代添加! + for (int j = i, k = 0; j < blockCountInSecrot; ++j, ++k) { + dataArray[i] = datas[j]; + } + //设置进去数据当中! + bean.setDatas(dataArray); + rets.add(bean); + //i值自增块数量! + i += blockCountInSecrot; + } + return rets.toArray(new MifareBean[0]); + } + + // 是否整个块都是零! + public static boolean isBlockAllZero(String block) { + if (block == null) return true; + return block.matches("[0]{32}"); + } + + // 是否有密钥可用! + public static boolean isAnyOneKeyAvailable(M1KeyBean[] keyBeans) { + for (M1KeyBean bean : keyBeans) { + return isKeyFormat(bean.getKeyA()) || + isKeyFormat(bean.getKeyB()); + } + return false; + } + + //实现合并读取结果 + public static MifareBean mergeBean(MifareBean aBean, MifareBean bBean) { + MifareBean ret; + //判断某个bean是否可用 + if (aBean == null || bBean == null) { + if (aBean != null) { + return aBean; + } + if (bBean != null) { + return bBean; + } + return getEmptyM1Bean(0); + } + //初始化结果bean + ret = new MifareBean(bBean.getSector()); + //否则将数据进行对比合并 + String[] aBeanDatas = aBean.getDatas(); + if (aBeanDatas == null) { + aBean = getEmptyM1Bean(aBean.getSector()); + aBeanDatas = aBean.getDatas(); + } + int last = aBeanDatas.length - 1; + //建立数据保存数组 + String[] datas = new String[aBean.getDatas().length]; + //取出块数据 + String[] tmpA = aBeanDatas; + String[] tmpB = bBean.getDatas() == null ? getEmptyM1Bean(bBean.getSector()).getDatas() : bBean.getDatas(); + for (int i = 0; i <= last; ++i) { + //先判断两组块数据是否相同 + if (tmpA[i].equals(tmpB[i])) { + //相同则取任意一个合并到ret中 + datas[i] = tmpA[i]; + } else { + // 修复某些块异常也被用来处理的问题 + if (!isValidBlockData(tmpA[i]) && isValidBlockData(tmpB[i])) { + datas[i] = tmpB[i]; + continue; + } + if (!isValidBlockData(tmpB[i]) && isValidBlockData(tmpA[i])) { + datas[i] = tmpA[i]; + continue; + } + // 判断数据有效性 + if (i != last) { + //否则是否全都是0,如果全部为零则优先考虑其他的情况! + boolean isADataAllZero = isBlockAllZero(tmpA[i]); + boolean isBDataAllZero = isBlockAllZero(tmpB[i]); + // 两者都是零,说明数据真的是 + if (isADataAllZero && isBDataAllZero) { + // 开始选择用正确的数据 + if (DumpUtils.isValidBlockData(tmpB[i])) { + datas[i] = tmpB[i]; + } else if (DumpUtils.isValidBlockData(tmpA[i])) { + datas[i] = tmpA[i]; + } else { + //不符合数据规范,跳过操作 + datas[i] = DumpUtils.BLANK_DATA; + } + } else { + // 开始选择用正确的数据 + // 如果AB数据有一个非0,则用那个 + if (DumpUtils.isValidBlockData(tmpB[i]) && isADataAllZero) { + datas[i] = tmpB[i]; + } else if (DumpUtils.isValidBlockData(tmpA[i]) && isBDataAllZero) { + datas[i] = tmpA[i]; + } else { + //不符合数据规范,跳过操作 + datas[i] = DumpUtils.BLANK_DATA; + } + } + } else { + //判断bBean的块数据非空 + //并且aBean中密钥B不可读的情况下 + //才能将bBean中的数据传入到ret中 + //得到控制位 + boolean isADataTrialerAllDefault = tmpA[i].equalsIgnoreCase(BLANK_TRAIL_BLOCK); + boolean isBDataTrialerAllDefault = tmpB[i].equalsIgnoreCase(BLANK_TRAIL_BLOCK); + // 两者皆为默认,暂且认定为真的数据就是这个! + if (isADataTrialerAllDefault && isBDataTrialerAllDefault) { + // 随便填充一个就行了! + datas[i] = tmpA[i]; + } else { + if (DumpUtils.isValidBlockData(tmpB[i]) && isADataTrialerAllDefault) { + //有效的尾部块 + datas[i] = tmpB[i]; + } else if (DumpUtils.isValidBlockData(tmpA[i]) && isBDataTrialerAllDefault) { + datas[i] = tmpA[i]; + } else { + //有效的尾部块 + // 置空尾部块! + datas[i] = DumpUtils.BLANK_TRAIL_BLOCK; + } + } + } + + } + } + ret.setDatas(datas); + return ret; + } + + //把密钥更新进数据组中 + public static void updateTrailer(MifareBean dB, M1KeyBean kB) { + if (dB == null || kB == null) return; + if (dB.getSector() != kB.getSector()) return; + String[] datas = dB.getDatas(); + if (datas == null) { + dB = getEmptyM1Bean(dB.getSector()); + datas = dB.getDatas(); + } + //有些时候,我们可以验证成功,但是无法读取操作数据块,此时,我们还是可以将密钥更新进数据集中 + int lastIndex = datas.length - 1; + String last = dB.getDatas()[lastIndex]; + if (last.length() != 32) { + //修复越界异常,我们进行判断并且跳过操作! + /*Log.d(LOG_TAG, "****************"); + Log.d(LOG_TAG, "Update position: " + dB.getSector()); + Log.d(LOG_TAG, "Invalid content: " + last); + Log.d(LOG_TAG, "Invalid length: " + last.length()); + Log.d(LOG_TAG, "****************");*/ + return; + } + //截取到最后一个块(尾部块),然后把有效密钥更新进去 + String _kA = kB.getKeyA().replaceAll("\\*", "F"); + String _kB = kB.getKeyB().replaceAll("\\*", "F"); + //更新密钥进去 + last = _kA + last.substring(12, 32); + last = last.substring(0, 20) + _kB; + dB.getDatas()[dB.getDatas().length - 1] = last; + } + + /* + * 判断是否是unix类系统 + * */ + public static boolean isUnixLFFormat(String str) { + return !str.contains("\r\n"); + } + + /* + * 获取对应的类系统的换行符 + * */ + public static String getSystemLF(String sysClz) { + String ret = "\r\n"; + switch (sysClz) { + case "windows": + break; + case "unix": + ret = "\n"; + break; + } + return ret; + } + + public static MifareBean[] getSectorFromArray(String[] contents) { + ArrayList retList = new ArrayList<>(); + if (contents != null) { + // 类型判断! + if (DumpUtils.isBlocksValids(contents)) { + //1K卡,2K卡! + MifareBean bean = null; + String[] _tmps = null; + for (int i = 0; i < contents.length; ) { + // 进行尾部的判断,如果是256的扇区的话我们需要进行偏移量的重新设置! + int count = MifareUtils.getBlockCountInSector(MifareUtils.blockToSector(i)); + if (DumpUtils.isHeader(i)) { + //在首部块! + bean = new MifareBean(); + _tmps = new String[count]; + } + //进行偏移拷贝! + if (_tmps != null) + System.arraycopy(contents, i, _tmps, 0, count); + if (bean != null) { + bean.setDatas(_tmps); + bean.setSector(DumpUtils.toSector(i)); + } + //结束(将结果添加至集合中) + retList.add(bean); + i += count; + } + } + boolean isUL = contents.length == 16 || + contents.length == 20 || + contents.length == 41 || + contents.length == 44 || + contents.length == 48; + if (isUL) { + for (int i = 0; i < contents.length; ++i) { + LogUtils.d("数据打印测试: " + contents[i]); + MifareBean mifareBean = new MifareBean(i, new String[]{contents[i]}); + retList.add(mifareBean); + } + } + } + return retList.toArray(new MifareBean[0]); + } + + public static boolean isDump(File file) { + if (file == null) return false; + // 大于50k的数据也认为不是正确的数据! + if (file.length() > MAX_DUMP_FILE_SIZE) return false; + if (file.exists() && file.isFile()) { + try { + return getType(FileUtils.readBytes(file)) != TYPE_NOT; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + return false; + } + + public static boolean isDump(byte[] data) { + if (data == null) return false; + // 大于50k的数据也认为不是正确的数据! + if (data.length > MAX_DUMP_FILE_SIZE) return false; + return getType(data) != TYPE_NOT; + } + + public static boolean isDump(Uri uri) { + byte[] data = readDump(uri); + if (data == null) return false; + return isDump(data); + } + + public static byte[] readDump(Uri uri) { + try { + return FileUtils.readBytes(uri, -1, MAX_DUMP_FILE_SIZE, -1); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public static MifareBean[] readDumpBeans(Uri uri) { + byte[] hexDat = readDump(uri); + if (hexDat == null) return null; + String decorate = decorate(hexDat); + if (decorate == null) return null; + return getSectorFromArray(splitDump(decorate)); + } + + public static boolean hasUltralightNewHeader(byte[] bytes) { + if (bytes == null) return false; + + if (bytes.length % 4 != 0 || bytes.length <= NEWPREFIXLENGTH) + return false; + + // tbo should be ZERO + if (bytes[8] != 0x00 || bytes[9] != 0x00) + return false; + + // tbo1 should be ZERO + if (bytes[10] != 0x00) + return false; + + // pages count must be equals to pages in header + int maxPage = (bytes.length - NEWPREFIXLENGTH) / 4 - 1; + return maxPage == bytes[11]; + } + + public static boolean hasUltralightHeader(byte[] bytes) { + if (bytes == null) return false; + // empty header + byte[] empty_header = new byte[] + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + + if (HexUtil.sequenceEqual(bytes, empty_header, empty_header.length)) { + return true; + } + + // detect mfu header. If probability of magic values is more than 50%, assume file has header. + double probability = 0d; + // first two bytes of version should be 0x00, 0x04 + if (bytes[0] == 0x00 && bytes[1] == 0x04) + probability += 0.25; + + // tbo should be ZERO + if (bytes[8] == 0x00 && bytes[9] == 0x00) + probability += 0.15; + + // tbo1 should be ZERO + if (bytes[15] == 0x00) + probability += 0.15; + + // tearing is normally 0xBD + if (bytes[10] == (byte) 0xBD || bytes[11] == (byte) 0xBD || bytes[12] == (byte) 0xBD) + probability += 0.35; + + return (probability >= 0.50); + } +} diff --git a/appmain/utils/mifare/MfDataUtils.java b/appmain/utils/mifare/MfDataUtils.java new file mode 100644 index 0000000..a512e9a --- /dev/null +++ b/appmain/utils/mifare/MfDataUtils.java @@ -0,0 +1,105 @@ +package com.proxgrind.chameleon.utils.mifare; + +/* + * MifareClassic标签用得到的工具! + * */ +public class MfDataUtils { + + public static boolean validateSector(int sector) { + // Do not be too strict on upper bounds checking, since some cards + // have more addressable memory than they report. For example, + // MIFARE Plus 2k cards will appear as MIFARE Classic 1k cards when in + // MIFARE Classic compatibility mode. + // Note that issuing a command to an out-of-bounds block is safe - the + // tag should report error causing IOException. This validation is a + // helper to guard against obvious programming mistakes. + + int NR_TRAILERS_4k = 40; + if (sector < 0 || sector >= NR_TRAILERS_4k) { + return false; + } + return true; + } + + public static boolean validateBlock(int block) { + // Just looking for obvious out of bounds... + int NR_BLOCKS_4k = 0xFF; + if (block < 0 || block >= NR_BLOCKS_4k) { + return false; + } + return true; + } + + public static boolean validateValueOperand(int value) { + if (value < 0) { + return false; + } + return true; + } + + public static int blockToSector(int blockIndex) { + if (!validateBlock(blockIndex)) return 0; + if (blockIndex < 32 * 4) { + return (blockIndex / 4); + } else { + return (32 + (blockIndex - 32 * 4) / 16); + } + } + + public static int sectorToBlock(int sectorIndex) { + if (!validateSector(sectorIndex)) { + return -1; + } + if (sectorIndex < 32) { + return (sectorIndex * 4); + } else { + return (32 * 4 + (sectorIndex - 32) * 16); + } + } + + public static boolean isFirstBlock(int uiBlock) { + // 测试我们是否处于小扇区或者大扇区? + if (uiBlock < 128) + return ((uiBlock) % 4 == 0); + else + return ((uiBlock) % 16 == 0); + } + + public static boolean isTrailerBlock(int uiBlock) { + // 测试我们处于小区块还是大扇区 + if (uiBlock < 128) + return ((uiBlock + 1) % 4 == 0); + else + return ((uiBlock + 1) % 16 == 0); + } + + public static int getBlockCountInSector(int sectorIndex) { + if (!validateSector(sectorIndex)) return -1; + if (sectorIndex < 32) { + return 4; + } else { + return 16; + } + } + + public static int get_trailer_block(int uiFirstBlock) { + // Test if we are in the small or big sectors + int trailer_block; + if (uiFirstBlock < 128) { + trailer_block = uiFirstBlock + (3 - (uiFirstBlock % 4)); + } else { + trailer_block = uiFirstBlock + (15 - (uiFirstBlock % 16)); + } + return trailer_block; + } + + public static int getIndexOnSector(int block, int sector) { + int index = 0; + //得到当前的块在扇区中的具体索引! + for (int i = 0; i < getBlockCountInSector(sector); i++) { //得到当前扇区的块总数! + if (block == (sectorToBlock(block) + i)) break; + ++index; + } + return index; + } +} diff --git a/appmain/utils/mifare/MifareAdapter.java b/appmain/utils/mifare/MifareAdapter.java new file mode 100644 index 0000000..7a8a174 --- /dev/null +++ b/appmain/utils/mifare/MifareAdapter.java @@ -0,0 +1,180 @@ +package com.proxgrind.chameleon.utils.mifare; + +import java.io.IOException; +import java.io.Serializable; + +/** + * MifareClassic定义 + */ +public interface MifareAdapter extends Serializable { + + /* + * 一个标准的MifareClassic + * 是可以被链接,被验证,被读取,被写入,被增值,被减值,被重置的! + * */ + + /** + * 重新查找获取标签! + * + * @return 查找结果! + */ + boolean rescantag() throws IOException; + + /** + * 链接标签 + * + * @return 链接结果! + */ + boolean connect() throws IOException; + + /** + * 断开标签 + */ + void close() throws IOException; + + /** + * 读取标签 + * + * @param block 读取的块 + * @return 读取结果, 可能为null + */ + byte[] read(int block) throws IOException; + + /** + * 写入标签 + * + * @param blockIndex 写入的块, + * @param data 将被写入的数据,必须是16字节长度的Hex字符串! + * @return 写入结果! + */ + boolean write(int blockIndex, byte[] data) throws IOException; + + /** + * 验证密钥A + * + * @param sectorIndex 被验证的块 + * @param key 用来验证的密钥 + * @return 验证结果! + */ + boolean authA(int sectorIndex, byte[] key) throws IOException; + + /** + * 验证密钥B + * + * @param sectorIndex 被验证的块 + * @param key 用来验证的密钥 + * @return 验证结果! + */ + boolean authB(int sectorIndex, byte[] key) throws IOException; + + /** + * 增值 + * + * @param blockIndex 被增值的块 + * @param value 非负递增的值 + */ + void increment(int blockIndex, int value) throws IOException; + + /** + * 增值 + * + * @param blockIndex 被增值的块 + * @param value 非负递减的值 + */ + void decrement(int blockIndex, int value) throws IOException; + + /** + * 恢复增值减值操作 + * + * @param blockIndex 被恢复的块 + */ + void restore(int blockIndex) throws IOException; + + /** + * 转移值数据到块 + * + * @param blockIndex 被转移的块 + */ + void transfer(int blockIndex) throws IOException; + + /** + * 获得UID + * + * @return UID字节数组 + */ + byte[] getUid(); + + byte[] getAts(); + + byte[] getAtqa(); + + byte[] getSak(); + + /** + * 获得类型 + * + * @return 1024 or 2048 or 4096 + */ + int getType(); + + /** + * 获得扇区数量 + * + * @return 卡片支持的扇区容量 + */ + int getSectorCount(); + + /** + * 获得块数量 + * + * @return 卡片支持的块容量 + */ + int getBlockCount(); + + /** + * 设置操作超时 + * + * @param ms 被设置的超时 + */ + void setTimeout(int ms); + + /** + * 获取操作超时 + * + * @return 超时参数 + */ + int getTimeout(); + + /** + * 获取批量操作的实现! + * 如果返回null,则不支持批量! + */ + BatchAdapter getBatchImpl(); + + /** + * 是否是链接状态! + * + * @return 如果卡片已经链接(在部分实现机制上) + * 否则你可以获取结果为true的返回值,否则为false + */ + boolean isConnected(); + + /** + * 是否是仿真卡 + * + * @return true为仿真 + */ + boolean isEmulated(); + + /** + * 是否是后门卡! + * + * @return 是否是后门卡 + */ + boolean isSpecialTag(); + + /** + * 是否是支持测试! + */ + boolean isTestSupported(); +} diff --git a/appmain/utils/mifare/StdMifareImpl.java b/appmain/utils/mifare/StdMifareImpl.java new file mode 100644 index 0000000..8229919 --- /dev/null +++ b/appmain/utils/mifare/StdMifareImpl.java @@ -0,0 +1,321 @@ +package com.proxgrind.chameleon.utils.mifare; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.nfc.NfcAdapter; +import android.nfc.NfcManager; +import android.nfc.Tag; +import android.nfc.tech.MifareClassic; +import android.os.Build; + +import com.proxgrind.chameleon.utils.stream.IOUtils; +import com.proxgrind.chameleon.utils.system.LogUtils; +import com.proxgrind.chameleon.utils.system.SystemUtils; +import com.proxgrind.chameleon.utils.tools.GlobalTag; + +import java.io.File; +import java.io.IOException; + +public class StdMifareImpl implements MifareAdapter, GlobalTag.OnNewTagListener { + private static MifareClassic mMfTag = null; + private final static StdMifareImpl stdMifareImpl = new StdMifareImpl(); + + /* + * 此方法需要传入一个TAG,这个TAG将被用来初始化一个MifareClassic标签! + * */ + private StdMifareImpl() { + mMfTag = getMfOfGlobalTag(); + GlobalTag.addListener(this); + } + + public static StdMifareImpl getInstance() { + synchronized (stdMifareImpl) { + mMfTag = getMfOfGlobalTag(); + } + return stdMifareImpl; + } + + private static MifareClassic getMfOfGlobalTag() { + if (mMfTag != null) IOUtils.close(mMfTag); + Tag tag = GlobalTag.getTag(); + if (tag != null) { + return MifareClassic.get(tag); + } + return null; + } + + /** + * Check if the device supports the MIFARE Classic technology. + * In order to do so, there is a first check ensure the device actually has a NFC hardware + * After this, this function will check if there are files + * like "/dev/bcm2079x-i2c" or "/system/lib/libnfc-bcrm*". Files like + * these are indicators for a NFC controller manufactured by Broadcom. + * Broadcom chips don't support MIFARE Classic. + * + * @return True if the device supports MIFARE Classic. False otherwise. + */ + public static boolean hasMifareClassicSupport(Context context) { + + // Check for the MifareClassic class. + // It is most likely there on all NFC enabled phones. + // Therefore this check is not needed. + /* + try { + Class.forName("android.nfc.tech.MifareClassic"); + } catch( ClassNotFoundException e ) { + // Class not found. Devices does not support MIFARE Classic. + return false; + } + */ + + // Check if ther is any NFC hardware at all. + if (NfcAdapter.getDefaultAdapter(context) == null) { + return false; + } + + // Check if there is the NFC device "bcm2079x-i2c". + // Chips by Broadcom don't support MIFARE Classic. + // This could fail because on a lot of devices apps don't have + // the sufficient permissions. + // Another exception: + // The Lenovo P2 has a device at "/dev/bcm2079x-i2c" but is still + // able of reading/writing MIFARE Classic tags. I don't know why... + // https://github.com/ikarus23/MifareClassicTool/issues/152 + boolean isLenovoP2 = Build.MANUFACTURER.equals("LENOVO") + && Build.MODEL.equals("Lenovo P2a42"); + File device = new File("/dev/bcm2079x-i2c"); + if (!isLenovoP2 && device.exists()) { + return false; + } + + // Check if there is the NFC device "pn544". + // The PN544 NFC chip is manufactured by NXP. + // Chips by NXP support MIFARE Classic. + device = new File("/dev/pn544"); + if (device.exists()) { + return true; + } + + // Check if there are NFC libs with "brcm" in their names. + // "brcm" libs are for devices with Broadcom chips. Broadcom chips + // don't support MIFARE Classic. + File libsFolder = new File("/system/lib"); + File[] libs = libsFolder.listFiles(); + if (libs != null) { + for (File lib : libs) { + if (lib.isFile() + && lib.getName().startsWith("libnfc") + && lib.getName().contains("brcm") + // Add here other non NXP NFC libraries. + ) { + return false; + } + } + } else { + return false; + } + return true; + } + + public static boolean isNfcOpened(Context context) { + if (hasMifareClassicSupport(context)) { + //判断设备是否支持NFC! + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(context); + // 可能出现adapter为空的情况! + if (adapter != null) { + return adapter.isEnabled(); + } else { + LogUtils.d("isNfcOpened:adapter is null. "); + } + } + LogUtils.d("isNfcOpened:MifareClassic unsupported. "); + return false; + } + + public MifareClassic getMf() { + return mMfTag; + } + + @Override + public boolean rescantag() throws IOException { + Tag tag = GlobalTag.getTag(); + if (mMfTag != null) mMfTag.close(); + if (tag != null) { + mMfTag = MifareClassic.get(tag); + } + return mMfTag != null; + } + + @Override + public boolean connect() throws IOException { + if (mMfTag.isConnected()) mMfTag.close(); + if (mMfTag != null && !isConnected()) { + mMfTag.connect(); + return true; + } + return false; + } + + @Override + public void close() throws IOException { + if (mMfTag != null) + if (isConnected()) + mMfTag.close(); + } + + @Override + public byte[] read(int block) throws IOException { + if (mMfTag != null) + return mMfTag.readBlock(block); + return null; + } + + @Override + public boolean write(int blockIndex, byte[] data) throws IOException { + if (mMfTag != null) { + mMfTag.writeBlock(blockIndex, data); + return true; + } + return false; + } + + @Override + public boolean authA(int sectorIndex, byte[] key) throws IOException { + if (mMfTag != null) { + return mMfTag.authenticateSectorWithKeyA(sectorIndex, key); + } + return false; + } + + @Override + public boolean authB(int sectorIndex, byte[] key) throws IOException { + if (mMfTag != null) + return mMfTag.authenticateSectorWithKeyB(sectorIndex, key); + return false; + } + + @Override + public void increment(int blockIndex, int value) throws IOException { + if (mMfTag != null) + mMfTag.increment(blockIndex, value); + } + + @Override + public void decrement(int blockIndex, int value) throws IOException { + if (mMfTag != null) + mMfTag.decrement(blockIndex, value); + } + + @Override + public void restore(int blockIndex) throws IOException { + if (mMfTag != null) + mMfTag.restore(blockIndex); + } + + @Override + public void transfer(int blockIndex) throws IOException { + if (mMfTag != null) + mMfTag.transfer(blockIndex); + } + + @Override + public byte[] getUid() { + if (mMfTag != null) + return mMfTag.getTag().getId(); + return null; + } + + @Override + public byte[] getAts() { + return new byte[0]; + } + + @Override + public byte[] getAtqa() { + return new byte[0]; + } + + @Override + public byte[] getSak() { + return new byte[0]; + } + + @Override + public int getType() { + if (mMfTag != null) + return mMfTag.getType(); + return -1; + } + + @Override + public int getSectorCount() { + if (mMfTag != null) + return mMfTag.getSectorCount(); + return -1; + } + + @Override + public int getBlockCount() { + if (mMfTag != null) + return mMfTag.getBlockCount(); + return -1; + } + + @Override + public void setTimeout(int ms) { + if (mMfTag != null) + mMfTag.setTimeout(ms); + } + + @Override + public int getTimeout() { + if (mMfTag != null) + return mMfTag.getTimeout(); + return -1; + } + + @Override + public BatchAdapter getBatchImpl() { + // 标准NFC不支持批量验证,所以需要返回null + return null; + } + + @Override + public boolean isConnected() { + synchronized (stdMifareImpl) { + if (mMfTag == null) { + LogUtils.d("MfTag对象为空,将会直接返回NULL"); + return false; + } + LogUtils.d("MfTag对象不为空,将会直接返回链接状态: " + mMfTag.isConnected()); + return mMfTag.isConnected(); + } + } + + @Override + public boolean isEmulated() { + return false; + } + + @Override + public boolean isSpecialTag() { + //自带的NFC不支持特殊的后门标签直接读写,因此直接返回false即可! + return false; + } + + @Override + public boolean isTestSupported() { + return false; + } + + @Override + public void onNewTag(Tag tag) { + synchronized (stdMifareImpl) { + if (mMfTag != null) { + if (mMfTag.isConnected()) IOUtils.close(mMfTag); + mMfTag = getMfOfGlobalTag(); + LogUtils.d("StdMifareImpl收到了卡片通知,将会进行卡片更新处理!"); + } + } + } +} diff --git a/appmain/utils/mifare/StdMifareIntent.java b/appmain/utils/mifare/StdMifareIntent.java new file mode 100644 index 0000000..21aa19d --- /dev/null +++ b/appmain/utils/mifare/StdMifareIntent.java @@ -0,0 +1,60 @@ +package com.proxgrind.chameleon.utils.mifare; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.nfc.NfcAdapter; +import android.nfc.NfcManager; +import android.nfc.tech.MifareClassic; + +public class StdMifareIntent { + + private NfcAdapter mAdapter = null; + + public StdMifareIntent(Context context) { + NfcManager manager = (NfcManager) context.getSystemService(Context.NFC_SERVICE); + //判断设备是否支持NFC! + if (manager == null) return; + mAdapter = manager.getDefaultAdapter(); + } + + /* + * 获取当前操作的适配器! + * */ + public NfcAdapter getAdapter() { + return mAdapter; + } + + /* + * 注册前台 + * */ + public void enableForegroundDispatch(Activity targetAct) { + if (mAdapter == null) return; + try { + //进行前台广播拦截! + Intent intent = new Intent(targetAct, + targetAct.getClass()).addFlags( + Intent.FLAG_ACTIVITY_SINGLE_TOP); + PendingIntent pendingIntent = PendingIntent.getActivity( + targetAct, 0, intent, 0); + mAdapter.enableForegroundDispatch(targetAct, pendingIntent, null, new String[][]{ + new String[]{MifareClassic.class.getName()}}); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /* + * 解注册前台 + * */ + public void disableForegroundDispatch(Activity activity) { + if (mAdapter == null) return; + try { + //解注册前台拦截 + mAdapter.disableForegroundDispatch(activity); + } catch (Exception e) { + e.printStackTrace(); + } + } +}