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