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