C#でNFC(Felica/Mifare)の読み取り – 実践編 その2 – Suica/ICOCA履歴情報読み取り

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

今回の到達点

Suica/ICOCAの履歴情報を読み取れるようになる。
Felicaのどの領域がSuica/ICOCAの履歴情報に当たるかを知る。

基礎知識

Suica/ICOCA等の交通系のカードは、Felicaの技術が用いられています。それぞれの鉄道会社によって、使用されているカードの領域は詳細には異なるのですが、共通している部分もあり、また、共通部分には履歴情報としてアクセス可能な部分もあります。SonyのSFCard Viewer 2などもこの領域を用いています。

これらの規格(以降、サイバネ規格)を定めているのは、鉄道技術協会-サイバネティクス協議会です。ただし、これらの領域はアクセス可能な部分なのですが、路線コード等は公開されておりません。そのため、解析された方々の内容を参考させてもらいながら、以降の話を進めたいと思います。

下記リンクを、解析・16バイトの分割方法・駅コード参照にする際にいつも参考にさせてもらっています。

実際にやってみる

前回のC#でNFC(Felica/Mifare)の読み取り – 実践編 その1 – ATR, IDm/UID, カードタイプの取得で作成したコードをそのまま用いたいと思います。PCSC-Sharp, PCSC.Iso7816を同様にしますので、参考にしてください。

  1. 下記の画像のようにボタンを作成してください。
  2. ボタンのクリック関数に以下のコードを張り付けてください。
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))
    {

        //① SelectFileで指定 
        // Case4で設定します
        byte[] dataIn = { 0x0f, 0x09 };

        var apduSelectFile = new CommandApdu(IsoCase.Case4Short, rfidReader.Protocol)
        {
            CLA = 0xFF,
            Instruction = InstructionCode.SelectFile,
            P1 = 0x00,
            P2 = 0x01,
            // Lcは自動計算
            Data = dataIn,
            Le = 0 // 
        };


        using (rfidReader.Transaction(SCardReaderDisposition.Leave))
        {
            textBox_Log.Text += "SelectFile .... \r\n";

            var sendPci = SCardPCI.GetPci(rfidReader.Protocol);
            var receivePci = new SCardPCI(); // IO returned protocol control information.

            var receiveBuffer = new byte[256];
            var command = apduSelectFile.ToArray();

            var bytesReceivedSelectedFile = 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, bytesReceivedSelectedFile, IsoCase.Case2Short, rfidReader.Protocol);


            textBox_Log.Text += "SW1: " + responseApdu.SW1.ToString()
                                    + ", SW2: " + responseApdu.SW2.ToString()+ "\r\n"
                    + "Length: " + responseApdu.Length.ToString() + "\r\n";

            for (int i = 0; i < 20; ++i)
            {

                //② ReadBinaryとブロック指定
                //176 = 0xB0
                var apduReadBinary = new CommandApdu(IsoCase.Case2Short, rfidReader.Protocol)
                {
                    CLA = 0xFF,
                    Instruction = InstructionCode.ReadBinary,
                    P1 = 0x00,
                    P2 = (byte)i,
                    Le = 0 // 
                };

                textBox_Log.Text += "Read Binary .... \r\n";

                var commandReadBinary = apduReadBinary.ToArray();

                var bytesReceivedReadBinary2 = rfidReader.Transmit(
                    sendPci, // Protocol Control Information (T0, T1 or Raw)
                    commandReadBinary, // command APDU
                    commandReadBinary.Length,
                    receivePci, // returning Protocol Control Information
                    receiveBuffer,
                    receiveBuffer.Length); // data buffer

                var responseApdu2 =
                    new ResponseApdu(receiveBuffer, bytesReceivedReadBinary2, IsoCase.Case2Extended, rfidReader.Protocol);

                textBox_Log.Text += "SW1: " + responseApdu2.SW1.ToString()
                                        + ", SW2: " + responseApdu2.SW2.ToString()
                                        + "\r\n"
                                        + "Length: " + responseApdu2.Length.ToString() + "\r\n";

                parse_tag(receiveBuffer);

                // ③ここにデータ解析関数を実行

                textBox_Log.Text += "\r\n";

            }

        }

    }
}
textBox_Log.Text += "-----------------------------------------------\r\n";
  1. 下記のparse_tag関数も張り付けてください。
private void parse_tag(byte[] data)
{
   textBox_Log.Text += "Suica履歴データ:" + BitConverter.ToString(data, 0, 18) + "\r\n";
}
  1. コード上で肝になる部分は①のSelectFileで指定を行い。②のReadBinaryで読み取るブロックを指定・読み取る部分です。20個の履歴データを読み取れるようにループで回しています。
  2. ③の解析部分は特に今回長くなるのでコードとして開示はしませんが、サイバネ規格 (ICカード) – 通信用語の基礎知識
    の「090F 乗降履歴情報」の中にデータがどのように保持・記録されているかが示されています。
//① SelectFileで指定 
// Case4で設定します
byte[] dataIn = { 0x0f, 0x09 };

var apduSelectFile = new CommandApdu(IsoCase.Case4Short, rfidReader.Protocol)
{
    CLA = 0xFF,
    Instruction = InstructionCode.SelectFile,
    P1 = 0x00,
    P2 = 0x01,
    // Lcは自動計算
    Data = dataIn,
    Le = 0 // 
};
.........................
//② ReadBinaryとブロック指定
//176 = 0xB0
var apduReadBinary = new CommandApdu(IsoCase.Case2Short, rfidReader.Protocol)
{
    CLA = 0xFF,
    Instruction = InstructionCode.ReadBinary,
    P1 = 0x00,
    P2 = (byte)i,
    Le = 0 // 
};

解析を行った例

カードから読み取ったデータとしては、以下のようなかたちです。
Suica履歴データ:16-01-00-02-25-1A-99-24-99-16-38-03-00-0A-BC-00-90-00

それを解析(parse)しました。

  • 年月日:2018/08/26
  • 地域コード (入):0x00 入場駅コード (線区コード側):0x99 入場駅コード (駅コード側):0x24
  • 地域コード (出):0x00 出場駅コード (線区コード側):0x99 出場駅コード (駅コード側):0x16
  • 残額:0824

下記で引用する部分は、サイバネ規格 (ICカード) – 通信用語の基礎知識の「090F 乗降履歴情報」を参考にしてください。

年月日の解析

年月日:2018/08/26ですが、次の太字の部分にあたります。

+4〜+5 (2バイト): 年月日 [年/7ビット、月/4ビット、日/5ビット]

なので、

Suica履歴データ:16-01-00-02-25-1A-99-24-99-16-38-03-00-0A-BC-00-90-00

Hex: 25 1A
を各ビットに分けると、

7ビット 18
4ビット 8
5ビット 26

入場時の地域コード・線区コード・駅順コードの解析

こちらは次の太字の部分

Suica履歴データ:16-01-00-02-25-1A-99-24-99-16-38-03-00-0A-BC-00-90-00

線区コードと駅順こーどとしては、それぞれ1byteずつを割り当てます。

+6〜+9 (4バイト): 入出場駅コード(鉄道)、停留所コード(バス)、物販情報(物販)
鉄道
+6〜+7 (2バイト): 入場駅コード (窓口で新規発行の場合も駅コードあり)
+8〜+9 (2バイト): 出場駅コード (新規発行やチャージの場合は 0000)

また地区コードとしては、

F (1バイト): 地域コード (0=旧国鉄と関東私鉄/バス、1=中部私鉄/バス、2=関西および沖縄私鉄/バス、3=その他地域私鉄/バス)
7〜6 (2ビット): 入場地域コード
5〜4 (2ビット): 出場地域コード(新規やチャージでは0)
3〜0 (4ビット): 未使用 (0)

となり、併せて、入りの駅コード系としては、

地域コード (入):0x00
入場駅コード (線区コード側):0x99
入場駅コード (駅コード側):0x24

出場時の地域コード・線区コード・駅順コードの解析

こちらは次の太字の部分
Suica履歴データ:16-01-00-02-25-1A-99-24-99-16-38-03-00-0A-BC-00-90-00

入場と同様にして、出場も下記のように解析できます。

地域コード (出):0x00
出場駅コード (線区コード側):0x99
出場駅コード (駅コード側):0x16

残額の部分

こちらは次の太字の部分

Suica履歴データ:16-01-00-02-25-1A-99-24-99-16-38-03-00-0A-BC-00-9

+A〜+B (2バイト): 残額(LE)

LE(リトルエンディアン)なので、0x0338 = 824(10進)

となります。

解析の答え合わせ

答え合わせには、SFCard Viewer 2 を使用します。
SFCard Viewer 2で読み取り、保存したエクセルデータとして以下のようになります。色付けした部分のオレンジ色が入場駅。緑が出場駅です。

路線・駅コード一覧・登録の下のほうにある、Excelをダウンロードして、同じく横浜市営地下鉄の新横浜と桜木町を探して同じ色で塗りました。

これを読むと、新横浜は下記のようになります。

  • 地区コード:0
  • 線区コード:0x99
  • 駅順コード:0x24

桜木町は次のようになります。

  • 地区コード:0
  • 線区コード:0x99
  • 駅順コード:0x16

結論

ということで、
『自分でSuica読み取り・解析した値 = 路線・駅コード一覧・登録のエクセルの値』

『同じSuicaのカードの駅(SFCard Viwer2) = 路線・駅コード一覧・登録のエクセルの値』

となっており、上記のコードを用いて、自分でSuica読み取り・解析した値が正しいことがわかりました。

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