使用Java驱动ACR122U对IC卡进行读写

时间:2024-02-24 08:11:05

转载的: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指令,可以参考官方文档
完~