Android 关于IC卡的读写和加密

关于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

总结

每一个新需求都是一次新的学习机会,这又是一个我未踏足过的知识面,希望自己无限进步,永远不会失去对新事物的好奇和热情。如果这篇博客对你有帮助,请点个赞。

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值