两篇文章,合到一起了,写的通俗易懂,怕丢失,转载过来的。
下面是原文,未做修改。另附一篇我的总结。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指令,可以参考官方文档
完~