1. 程式人生 > >使用Java驅動ACR122U對IC卡進行讀寫

使用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指令,可以參考官方文件
完~