C#でNFC(Felica/Mifare)の読み取り – 実践編 その1 – ATR, IDm/UID, カードタイプの取得

NFC
スポンサーリンク
スポンサーリンク

今回の到達点

  • PCSC-SharpとPSCS.ISO7816を使用して、以下のことができるようになる。
  • Felica(Suica/ICOCA)のIDmを読み込めるようになる。
  • Mifare?(T-money)のUIDを読み込めるようになる。
  • ATRを取得できようになる。
  • Felica(Suica/ICOCA)のCard Typeの情報を取得できるようになる。

PCSC,PCSC.Iso7816のインストール

  1. 適当にプロジェクトを作成して、Visual Studio → プロジェクト → Nugetパッケージの管理 → 参照タブ → 検索欄に”PCSC”を入力してください。
  2. 右側のチェックボックスを入れて、インストールボタンを押してください。

同様にPCSC.Iso7816もインストールしてください。

カードリーダ状態とATRの読み取りコード

あ、もちろんSony Pasori も用意してくださいね。

C#のWinFormで作成していきます。下記のGithubのリンクを参考にして、変更していきます。比較をしながら、読み進めてもらえると幸いです。

  1. C#のWinFormで、次のように、ボタン・ドロップダウンリスト・テキストボックスを作成してください。
  2. 下のように名前を変更してください。
  3. ソース先頭のusing パートに以下のusingを二つ追加してください。
  4. using PCSC;
    using PCSC.Iso7816;
    
  5. buttonCheckReaderのボタンのクリック動作(buttonCheckReader_Click関数)に以下のソースコードを張り付けてください。ConnectedReaderStatusのサンプルをTextBoxへ内容の書き込みとComboBoxでReaderを選択できるようにするためのコードが追加されています。①~⑥を参照してください。
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"; //⑥
  1. 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;
}

  1. USBでPasoriを接続して、読み取りたいカードをPasoriの上に置いてください。私の場合はICOCAを使用します。
  2. 一度この時点で、ビルドして、実行してください。
  3. 一番上のボタンを押すと以下のように、データが出力されます。(ICOCA)
  4. 一番上のボタンを押すと以下のように、データが出力されます。(韓国T-moneyカード使用・出張時に使用するもの)
  5. 適当にカード外したりしながら、テストしてみてください。一応、複数個カードリーダが接続されてもいけるようにはなってるようです。(多数のリーダを用意して、テストはしてません。悪しからず。)

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の読み取りのコード

  1. 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";
  1. 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;
}
  1. 特にこれといって特殊なことはしていませんが、ISO7816側を使用したことでコマンドの意味が明確になったと思います。
  2. スマートカード / コマンドとレスポンスを参照すると、以下の部分の送信コマンドがわかりやすくなると思います。このコマンドをIsoCase.Case4などとしたりして、正しいコマンド形式にして送信すると正しく結果が得られます。私的にはISO7816を使用して、上記リンクにたどりつくまではコマンドの意味がさっぱり分かりませんでした。
  3. 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
    };
    
  4. スマートカード / コマンドとレスポンスの返答部分のステータスワードがSW1,SW2に分かれており、0x90,0x00 (10進 144,0)で返答がかえって来ることが重要です。
  5. 実際にコードを実行してみください。実行する際はCheckReaderConectionボタン→コンボボックスでPasori選択→Read IDボタンを押すです。

実際にICOCAのIDm(UID)を読み取ってみたケース。実行画面の後ろに見えているエクセルはSony Pasoriの SFCard Viewer 2をダウンロードして、CSVで保存したものです。それと一致していましたので、問題はなさそうです。ホッと一息。

実際にT-moneyのUIDを読み取ってみたケース。とりあえず見よ取れてますし、いけてはいそう。

かえって来たコードもSW1,SW2が144(0x90),0なので、良いと思います。

カードタイプの読み取りのコード

  1. button_ReadID_Click()関数に張り付けたのと同じコードをbutton_ReadCardTypeのクリック関数に、コピー&ペーストしてください。
  2. 追加した関数内の次のコードを変更します。変更前のコード
  3. 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
    };
    
  4. 次のコード部分を変更します。変更後のコードP1 = 0xF3になっています。
  5. 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
    };
    
  6. リビルドして、実行してみください。実行方法は先ほど同じです。
  7. 実際にカードタイプを読み取ってみたケースです。(一部表示のための文字列を変えていますが、cart typeとなっているのはご愛敬。)
  8. 実際にT-moneyカードを読み取ってみたケース。
  9. カードタイプ Felica: 04, T-money: 08として結果が出ています。
  10. PC/SCでFelica LiteにC言語でアクセスするにて、下記のように書かれていますので、”Felica: 04, T-money: 08″は結果として、正しそうですね。
  11. #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 v0.41の内容。

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 を読んでみる

ATR

[nfc][hce]ATRがFeliCaで返ってくる?

PC/SC Workgroup

Answer to reset – Wikipedia

規格等に関して

Felica 技術仕様

タイトルとURLをコピーしました