今回の到達点
- PCSC-SharpとPSCS.ISO7816を使用して、以下のことができるようになる。
- Felica(Suica/ICOCA)のIDmを読み込めるようになる。
- Mifare?(T-money)のUIDを読み込めるようになる。
- ATRを取得できようになる。
- Felica(Suica/ICOCA)のCard Typeの情報を取得できるようになる。
PCSC,PCSC.Iso7816のインストール
- 適当にプロジェクトを作成して、Visual Studio → プロジェクト → Nugetパッケージの管理 → 参照タブ → 検索欄に”PCSC”を入力してください。
- 右側のチェックボックスを入れて、インストールボタンを押してください。
同様にPCSC.Iso7816もインストールしてください。
カードリーダ状態とATRの読み取りコード
あ、もちろんSony Pasori も用意してくださいね。
C#のWinFormで作成していきます。下記のGithubのリンクを参考にして、変更していきます。比較をしながら、読み進めてもらえると幸いです。
- リーダとPCの接続確認とATR取得のためのサンプルとして、pcsc-sharp/Examples/ConnectedReaderStatus/Program.cs を参考にしています。
- IDm/UIDとCard Typeの取得のためのサンプルとして、pcsc-sharp/Examples/ISO7816-4/Transmit/Program.cs を参考にしています。
- C#のWinFormで、次のように、ボタン・ドロップダウンリスト・テキストボックスを作成してください。
- 下のように名前を変更してください。
- ソース先頭のusing パートに以下のusingを二つ追加してください。
- buttonCheckReaderのボタンのクリック動作(buttonCheckReader_Click関数)に以下のソースコードを張り付けてください。ConnectedReaderStatusのサンプルをTextBoxへ内容の書き込みとComboBoxでReaderを選択できるようにするためのコードが追加されています。①~⑥を参照してください。
using PCSC; using PCSC.Iso7816;
textBox_Log.Clear(); //① comboBox_CardReader.Items.Clear(); //② using (var ctx = ContextFactory.Instance.Establish(SCardScope.User)) { var firstReader = ctx .GetReaders() .FirstOrDefault(); if (firstReader == null) { textBox_Log.Text += "No reader connected."; //③ return; } using (var reader = ctx.ConnectReader(firstReader, SCardShareMode.Direct, SCardProtocol.Unset)) { var status = reader.GetStatus(); textBox_Log.Text += @"Reader names: {" + string.Join(", ", status.GetReaderNames()) + "}\r\n"; //④以下4行 textBox_Log.Text += @"Protocol: {" + status.Protocol + "}\r\n"; textBox_Log.Text += @"State: {" + status.State + "}\r\n"; textBox_Log.Text += @"ATR: { " + BitConverter.ToString(status.GetAtr() ?? new byte[0]) + "}\r\n"; textBox_Log.Text += @"ATR: { " + NoBarATR(status.GetAtr()) + "}\r\n"; comboBox_CardReader.Items.AddRange(status.GetReaderNames());//⑤ } } textBox_Log.Text += "-----------------------------------------------\r\n"; //⑥
- NoBarATR()関数とコンボボックスの関数SelectedIndexChangedを作成、変数ををFormの.csに張り付けてください。
/// <summary> /// ハイフンがない形のATRを出力します。 /// </summary> /// <param name="arr"></param> /// <returns></returns> private string NoBarATR(byte[] arr) { if (arr == null) return string.Empty; string result = string.Empty; foreach( byte oneByte in arr) { result += String.Format("{0:X2}", oneByte) + " "; } return result; } private int _selectedIndex = -1; private void comboBox_CardReader_SelectedIndexChanged(object sender, EventArgs e) { _selectedIndex = comboBox_CardReader.SelectedIndex; }
- USBでPasoriを接続して、読み取りたいカードをPasoriの上に置いてください。私の場合はICOCAを使用します。
- 一度この時点で、ビルドして、実行してください。
- 一番上のボタンを押すと以下のように、データが出力されます。(ICOCA)
- 一番上のボタンを押すと以下のように、データが出力されます。(韓国T-moneyカード使用・出張時に使用するもの)
- 適当にカード外したりしながら、テストしてみてください。一応、複数個カードリーダが接続されてもいけるようにはなってるようです。(多数のリーダを用意して、テストはしてません。悪しからず。)
ATRについて
現時点までのコードで、PCに接続されたカードリーダの状態と、カードのATRが取得できることがわかります。じゃあ、そのわかったATRで何がわかるのというお話なのです。
まずは用語、“Answer to reset”の略語らしいのです。Attributeの略語かと勝手に思ってました。
リンク先にかかれてますが、要約すると、SIMカードや、銀行カード、FelicaなどのSmart Cardで使用されるもので、ざっくりと伝達パラメータや、カードの性質・状態を示す情報をもっているようです。
The ATR conveys information about the communication parameters proposed by the card, and the card’s nature and state.
Wikipediaより
さらに上記Wikipediaのリンクをみているとわかるのですが、TA,TB,TDとかいうパラメタの中に、電圧やら、伝送周波数(MHz)やらの情報が入っているようです。このなかに、Felicaとか、Mifareとかの情報が入っているのかと思ったら違っていました。
ATR自身のパースのC#ライブラリは特に見つけられなかったのと、見つける意味が見当たらなかったので、今回は探しませんでした。代わりに、ATRをパースしてくれる ruimtools:ATR utilityサイトがあったり、Smart card ATR parsingでは.pyのコードが公開されており、各カードのATRをため込んだ.txtもダウンロードできますので、この.txtを使用すれば、ATRからある程度、カードの種類までは特定可能かと思います。
ということで先ほど取得できるようになった2段目のATRコード(ハイフンなし)のほうをパースしてくれるサイトに入れると、パースしてくれて、なおかつ、種類も表示してくれます。
1.ICOCAカードの例。
2.T-moneyカードの例。
思惑は外れたのですが、ひとつ勉強になりました。
IDm/UID, カードタイプの読み取りのコード
ここからは、pcsc-sharp/Examples/ISO7816-4/Transmit/Program.cs を参考にしています。PCSC側だけでも実はデータの送受信可能だと思いますが、PCSC.Iso7816のクラスを使うほうが楽?なので、使用させてもらいました。
IDm/UIDの読み取りのコード
- button_ReadID_Click()関数に以下のコードを挿入してください。
var contextFactory = ContextFactory.Instance; using (var context = contextFactory.Establish(SCardScope.System)) { var readerNames = context.GetReaders(); if (NoReaderFound(readerNames)) { textBox_Log.Text += "You need at least one reader in order to run this example." + "\r\n"; return; } var readerName = ChooseRfidReader(readerNames); if (readerName == null) { return; } // 'using' statement to make sure the reader will be disposed (disconnected) on exit using (var rfidReader = context.ConnectReader(readerName, SCardShareMode.Shared, SCardProtocol.Any)) { var apdu = new CommandApdu(IsoCase.Case2Short, rfidReader.Protocol) { CLA = 0xFF, Instruction = InstructionCode.GetData, P1 = 0x00, P2 = 0x00, Le = 0 // We don't know the ID tag size }; using (rfidReader.Transaction(SCardReaderDisposition.Leave)) { textBox_Log.Text += "Retrieving the UID .... " + "\r\n"; var sendPci = SCardPCI.GetPci(rfidReader.Protocol); var receivePci = new SCardPCI(); // IO returned protocol control information. var receiveBuffer = new byte[256]; var command = apdu.ToArray(); var bytesReceived = rfidReader.Transmit( sendPci, // Protocol Control Information (T0, T1 or Raw) command, // command APDU command.Length, receivePci, // returning Protocol Control Information receiveBuffer, receiveBuffer.Length); // data buffer var responseApdu = new ResponseApdu(receiveBuffer, bytesReceived, IsoCase.Case2Short, rfidReader.Protocol); textBox_Log.Text += "SW1: " + responseApdu.SW1.ToString() + ", SW2: " + responseApdu.SW2.ToString() + "\r\n"; if (responseApdu.HasData) { textBox_Log.Text += "Uid: " + BitConverter.ToString(responseApdu.GetData()) + "\r\n"; } else { textBox_Log.Text += "Uid: No uid received" + "\r\n"; } } } } textBox_Log.Text += "-----------------------------------------------\r\n";
- NoReaderFound()関数とChooseRfidReader()関数をFormの.csコードを挿入してください。
/// <summary> /// Returns <c>true</c> if the supplied collection <paramref name="readerNames"/> does not contain any reader. /// </summary> /// <param name="readerNames">Collection of smartcard reader names</param> /// <returns><c>true</c> if no reader found</returns> private bool NoReaderFound(ICollection<string> readerNames) { if (readerNames == null) return true; if (readerNames.Count == 0) return true; return false; } public string ChooseRfidReader(IList<string> readerNames) { // Show available readers. // Console.WriteLine("Available readers: "); textBox_Log.Text += "Available readers: "; for (var i = 0; i < readerNames.Count; i++) { textBox_Log.Text += "ReaderID: " + i.ToString() + " ,Reader Name:" + readerNames[i] + "\r\n"; } // Ask the user which one to choose. textBox_Log.Text += "Which reader is an RFID reader? \r\n"; //var line = "0"; int choice = _selectedIndex; //if (int.TryParse(line, out choice)) { if (choice >= 0 && (choice <= readerNames.Count)) { return readerNames[choice]; } } textBox_Log.Text += "An invalid number has been entered.\r\n"; //Console.ReadKey(); return null; }
- 特にこれといって特殊なことはしていませんが、ISO7816側を使用したことでコマンドの意味が明確になったと思います。
- スマートカード / コマンドとレスポンスを参照すると、以下の部分の送信コマンドがわかりやすくなると思います。このコマンドをIsoCase.Case4などとしたりして、正しいコマンド形式にして送信すると正しく結果が得られます。私的にはISO7816を使用して、上記リンクにたどりつくまではコマンドの意味がさっぱり分かりませんでした。
- スマートカード / コマンドとレスポンスの返答部分のステータスワードがSW1,SW2に分かれており、0x90,0x00 (10進 144,0)で返答がかえって来ることが重要です。
- 実際にコードを実行してみください。実行する際はCheckReaderConectionボタン→コンボボックスでPasori選択→Read IDボタンを押すです。
var apdu = new CommandApdu(IsoCase.Case2Short, rfidReader.Protocol) { CLA = 0xFF, Instruction = InstructionCode.GetData, P1 = 0x00, P2 = 0x00, Le = 0 // We don't know the ID tag size };
実際にICOCAのIDm(UID)を読み取ってみたケース。実行画面の後ろに見えているエクセルはSony Pasoriの SFCard Viewer 2をダウンロードして、CSVで保存したものです。それと一致していましたので、問題はなさそうです。ホッと一息。
実際にT-moneyのUIDを読み取ってみたケース。とりあえず見よ取れてますし、いけてはいそう。
かえって来たコードもSW1,SW2が144(0x90),0なので、良いと思います。
カードタイプの読み取りのコード
- button_ReadID_Click()関数に張り付けたのと同じコードをbutton_ReadCardTypeのクリック関数に、コピー&ペーストしてください。
- 追加した関数内の次のコードを変更します。変更前のコード
- 次のコード部分を変更します。変更後のコードP1 = 0xF3になっています。
- リビルドして、実行してみください。実行方法は先ほど同じです。
- 実際にカードタイプを読み取ってみたケースです。(一部表示のための文字列を変えていますが、cart typeとなっているのはご愛敬。)
- 実際にT-moneyカードを読み取ってみたケース。
- カードタイプ Felica: 04, T-money: 08として結果が出ています。
- PC/SCでFelica LiteにC言語でアクセスするにて、下記のように書かれていますので、”Felica: 04, T-money: 08″は結果として、正しそうですね。
var apdu = new CommandApdu(IsoCase.Case2Short, rfidReader.Protocol) { CLA = 0xFF, Instruction = InstructionCode.GetData, P1 = 0x00, P2 = 0x00, Le = 0 // We don't know the ID tag size };
var apdu = new CommandApdu(IsoCase.Case2Short, rfidReader.Protocol) { CLA = 0xFF, Instruction = InstructionCode.GetData, P1 = 0xF3, // ここを変えています。 P2 = 0x00, Le = 0 // We don't know the ID tag size };
#define CARD_TYPE_UNKNOWN 0x00 #define CARD_TYPE_ISO14443A 0x01 #define CARD_TYPE_ISO14443B 0x02 #define CARD_TYPE_PICOPASSB 0x03 #define CARD_TYPE_FELICA 0x04 #define CARD_TYPE_NFC_TYPE_1 0x05 #define CARD_TYPE_MIFARE_EC 0x06 #define CARD_TYPE_ISO14443A_4A 0x07 #define CARD_TYPE_ISO14443B_4B 0x08 #define CARD_TYPE_TYPE_A_NFC_DEP 0x09 #define CARD_TYPE_FELICA_NFC_DEP 0x0A
ちなみに、上記の定義は以下から得られる.pdfに書かれています。(バージョンは適宜その時の最新を参照ください。ファイル名等も同じ)
ICS-D010/30J SDK for NFC Starter Kitをリンクよりダウンロード・解凍して、doc¥PCSC ¥M579_PC_SC_2.41j.pdf を参照すると
SDK for NFC ユーザーズマニュアル PC/SC編(Lite) (Version 2.41 No. M579-J02-41)が表示されますので、
P24を開いてください。
『3.2.6. 独自定義(拡張)コマンド』独自コマンド定義により、APDU で FeliCa カードにアクセスすることが可能です。
【構成詳細】 名称:Data P1:F3 説明:カード種別
を参照してください。上記の”Felica: 04, T-money: 08″の定義と同じ数字が書かれています。またこの章の記述によりますと、カード種別名称も取得できます。
感想
とりあえず、これで値がとれているので、ATR, IDm/UID, カードタイプが取得できているので、当初の目標は達成。かなり詰め込みました。
今後の宿題としてます。また、かなり殴り書きなので、また整理する部分があれば、整理したいと思います。今回分は書いて燃え尽きた。
参考にさせてもらったサイト
PCSC
EternalWindows セキュリティ / スマートカード
PC/SCでFelica LiteにC言語でアクセスする
の中のint readIDm(SCARDHANDLE hCard) のコマンド
LazyPCSCFelicaLite (C++でPC/SCを使ってFelica Liteにアクセスするライブラリ)
[nfc]PC/SCを試す (5)
[nfc]PC/SCを試す (4)
[nfc]PC/SCを試す (1)
VB.netでPC/SC(NFC通信)してFelicaやMifareを読み取るサンプル
コメントで『Felica IDm取得コマンド』,『Mifare UID取得コマンド』と書かれた部分
【C#】Windows7 以下で FeliCa を読んでみる