zoukankan      html  css  js  c++  java
  • 使用Java驱动ACR122U对IC卡进行读写

    转载的:https://www.ruitz.cn/?p=74 和 https://www.ruitz.cn/?p=82 

    两篇文章,合到一起了,写的通俗易懂,怕丢失,转载过来的。

    下面是原文,未做修改。另附一篇我的总结。https://blog.csdn.net/guolongpu/article/details/83341025

    0x00 起因

    rtz手头有一个智能IC读卡器ACR122U,常年来使用的都是别人的软件
    终于有一天,rtz按耐不住想要自己写一个驱动软件的冲动~
    rtz的想法很简单,自己写一个能读/写IC卡的程序玩玩即可~

    0x01 资料查找

    查资料的过程是痛并快乐着的~
    经过小半个下午的资料查找,rtz大致了解了以下情况:
    1、微软写了个叫PCSC的读卡器规范,ACR122U支持这个规范
    2、Java有个类库叫javax.smartcardio,作用是操作PCSC规范的读卡器
    这个时候rtz一拍大腿!就用Java写咯(不过据说Java写硬件驱动不太优雅~)

    0x02 连接读卡器

    jdoc(点介里~)告诉rtz一个简单的范例~
    于是rtz根据范例稍加改写,形成了v1.0 查找插在电脑上的读卡器~

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    public static void main(String[] args) {

        TerminalFactory factory = TerminalFactory.getDefault();//得到一个默认的读卡器工厂(迷。。)

        List<CardTerminal> terminals;//创建一个List用来放读卡器(谁没事会在电脑上插三四个读卡器。。)

        try {

            terminals = factory.terminals().list();//从工厂获得插在电脑上的读卡器列表

            terminals.stream().forEach(s->System.out.println(s));//打印获取到的读卡器名称

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    运行一下~程序返回了一串PC/SC terminal ACS ACR122 0
    唔。。看起来读卡器连接成功了。

    0x03 Utils

    因为数据返回是一个byte[]数组,文档和API使用的是16进制数,
    所以需要一个将byte[]转为十六进制数的小方法
    可以更直观的看到结果~

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    private static final char[] HEX_CHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    public static String bytesToHexString(byte[] bytes) {

        StringBuilder sb = new StringBuilder();

        int a = 0;

        for (byte b : bytes) { // 使用除与取余进行转换

            if (b < 0) {

                a = 256 + b;

            } else {

                a = b;

            }

            //sb.append("0x");

            sb.append(HEX_CHAR[a / 16]);

            sb.append(HEX_CHAR[a % 16]);

            //sb.append(" ");

        }

        return sb.toString().toUpperCase();

    }

    0x04 读取卡片序列号

    IC卡的0扇区0区块放着这张卡的序列号~一般是出厂时就固化不可更改的~
    而且!读取序列号不需要验证密码哟。。先读一个出来玩玩
    根据龙杰公司提供的API文档接口文档
    读取序列号需要发送FF CA 00 00 le 其中le是期望返回的数据长度
    一般序列号都是4byte的嘛。。就全部读出来好了~le填上0x04表示期望得到4byte数据~

    1

    CommandAPDU getUID = new CommandAPDU(0xFF, 0xCA, 0x00, 0x00,0x04);//构造一个APDU指令,期望得到4byte序列号

    完整main方法:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    public static void main(String[] args) {

        TerminalFactory factory = TerminalFactory.getDefault();

        List<CardTerminal> terminals;

        try {

            terminals = factory.terminals().list();//get读卡器列表

            CardTerminal a = terminals.get(0);//使用第0个读卡器[暂且不考虑同时插N个读卡器的情况了]

            a.waitForCardPresent(0L);//等待放置卡片

            Card card = a.connect("T=1");//连接卡片,协议T=1 块读写(T=0貌似不支持,一用就报错)

            CardChannel channel = card.getBasicChannel();//打开通道

            CommandAPDU getUID = new CommandAPDU(0xFF, 0xCA, 0x00, 0x00,0x04);//中文API第12页

            ResponseAPDU r = channel.transmit(getUID);//发送getUID指令

            System.out.println("UID: " + bytesToHexString(r.getData()));

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    运行程序,找一张白卡放在读卡器上~
    哔的一声,出现了UID: D7B5B535 !
    序列号get完成~
    (呼呼。。写的有点累,,歇一会写下半部分╮(╯▽╰)╭)

    0x05 加载认证密钥

    根据官方文档介绍,密钥必须先预存进读卡器
    然后才可以对卡片进行认证。

    1

    2

    3

    4

    5

    byte[] pwd = {(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff};//先用一个数组把密钥存起来~

    CommandAPDU loadPWD = new CommandAPDU(0xFF, 0x82, 0x00, 0x00, pwd,0,6);//然后构造一个加载密钥APDU指令~

    ResponseAPDU r = channel.transmit(loadPWD);//发送loadPWD指令

    System.out.println("result: " + Utils.handleUID(r.getBytes()));

    根据文档,返回0x90 0x00 即为操作成功。

    0x06 认证密钥

    根据文档,rtz所使用的1KB容量的卡片
    共有16个扇区,每个扇区4个区块
    区块地址从00向上递增。
    其中,每个扇区的第三区块是密码和控制字存储的区块,不能作为数据存储使用。
    还有一个特例,就是0扇区的0区块,存储的是卡片的序列号,不可更改。
    每个扇区只需认证一次密钥即可对三个数据块随意读写。
    出厂默认的控制字FF078069表示KEYA 或者KEYB都可以随意读写。
    为了方(tou)便(lan) rtz使用了KEYA来进行认证.
    在上一小节,rtz已经将密钥加载进读卡器,密钥存储地址为00H(密钥号)

    1

    2

    3

    4

    byte[] check = {(byte)0x01,(byte)0x00,(byte)0x08,(byte)0x60,(byte)0x00};//认证数据字节,包含了需要认证的区块号、密钥类型和密钥存储的地址(密钥号)

    CommandAPDU authPWD = new CommandAPDU(0xFF, 0x86, 0x00, 0x00, check,0,5);//加上指令头部,构造出完整的认证APDU指令.

    ResponseAPDU r = channel.transmit(authPWD);//发送认证指令

    System.out.println("result: " + Utils.handleUID(r.getBytes()));//打印返回值

    根据文档,返回0x90 0x00即为认证成功。

    0x07 读区块

    读区块前必须完成密钥认证

    1

    2

    3

    CommandAPDU getData = new CommandAPDU(0xFF, 0xB0, 0x00, 0x08,0x10);//构造读区块APDU指令,读第八个区块(2扇区0区块)值

    ResponseAPDU r = channel.transmit(getData);//发送读区块指令

    System.out.println("data: " + Utils.handleUID(r.getBytes()));//打印返回值

    0x08 写区块

    写区块前必须完成密钥认证
    读写同一扇区不同区块只需验证一次密码~

    1

    2

    3

    4

    byte[] up = {(byte)0x00,(byte)0x01,(byte)0x02,(byte)0x03,(byte)0x04,(byte)0x05,(byte)0x06,(byte)0x07,(byte)0x08,(byte)0x09,(byte)0x0A,(byte)0x0B,(byte)0x0C,(byte)0x0D,(byte)0x0E,(byte)0x0F};

    CommandAPDU updateData = new CommandAPDU(0xFF, 0xD6, 0x00, 0x08,up,0,16);

    ResponseAPDU r = channel.transmit(updateData);//发送写块指令

    System.out.println("response: " + Utils.bytesToHexString(r.getBytes()));//打印返回值

    第二篇代码只精简出关键部分..主要是rtz太懒了
    关于如何构造APDU指令,可以参考官方文档
    完~

  • 相关阅读:
    Windows 8.1 序列化与反序列化
    window store app 附件读取
    Window 8.1 计时器功能及图片切换
    c#多层嵌套Json
    isNotNull与isNotEmpty的区别
    商务用语
    国家气象局三天天气WebService接口
    WebServise
    EF架构基础代码
    接口定义
  • 原文地址:https://www.cnblogs.com/ncepu/p/13694961.html
Copyright © 2011-2022 走看看