109
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

AndroidでFelica(NFC)のブロックデータの取得

大学の学生証がFelicaになっておりそのデータを取得したかったのでAndroidで実装しようと思いました。
しかしいろいろ調べてみてもIDmの取得くらいまでしか書いてない記事や本が多かったのでブロックデータの取得のメモという感じで残しとこうかと思います。

NFCとかFelicaがわからない人は各自でぐぐってみて欲しいです。
また、FeliCaカード ユーザーズマニュアル 抜粋版も読んでみると詳しく理解できると思います。

取得したいFelicaの構造

Felicaはシステム・エリア・サービス・ブロックデータから構成されています。
今回はこのサービス部分にあるブロックデータを取得したいです。
また、Felicaはシステムを分割することができ、複数のシステムを作成することが可能です。それぞれのシステムは、機能的・セキュリティ的に分離されており、相互に干渉することはありません。
参考: FeliCaカード ユーザーズマニュアル 抜粋版 p.20 3章 ファイルシステム

今回取得したいFelicaは以下の様な構造になっていました。

- System 0: (System Code -> 0x8CDA)
    - IDm
    - Area
    - Area
        - Service
        - Service
        - ...
- System 1: (System Code -> 0xFE00)
    - IDm
    - Area
    - Area
    - ...
    - Area
        - Service
        - Service: (Service Code -> 0x1A8B) ← ここにアクセスしたい
        - ...

System Code, Service Code は学生証が変わっても同一のものでした。

AndroidでFelicaの読み込み

以下のコード様な手順でFelicaを読み込みます。ちなみに、FelicaはNFC-TypeFに属しています。
今回はenableForegroundDispatchを使用してアプリが起動している時のみFelicaを読み込みたいと思います。

NFCReaderActivity.java
private IntentFilter[] intentFiltersArray;
private String[][] techListsArray;
private NfcAdapter mAdapter;
private PendingIntent pendingIntent;
private NfcReader nfcReader = new NfcReader();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_nfcreader);

    pendingIntent = PendingIntent.getActivity(
            this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);

    IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);

    try {
        ndef.addDataType("text/plain");
    }
    catch (IntentFilter.MalformedMimeTypeException e) {
        throw new RuntimeException("fail", e);
    }
    intentFiltersArray = new IntentFilter[] {ndef};

    // FelicaはNFC-TypeFなのでNfcFのみ指定でOK
    techListsArray = new String[][] {
        new String[] { NfcF.class.getName() }
    };

    // NfcAdapterを取得
    mAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());
}

@Override
protected void onResume() {
    super.onResume();
    // NFCの読み込みを有効化
    mAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray);
}

@Override
protected void onNewIntent(Intent intent) {
// IntentにTagの基本データが入ってくるので取得。
    Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
    if (tag == null) {
        return;
    }

    // ここで取得したTagを使ってデータの読み書きを行う。
}

@Override
protected void onPause() {
    super.onPause();
    mAdapter.disableForegroundDispatch(this);
}

また忘れずにマニフェストに以下の記述を書いておきましょう。

AndroidManifest.xml
...

<!-- NFC -->
<uses-permission android:name="android.permission.NFC" />
<uses-feature
    android:name="android.hardware.nfc"
    android:required="true" />

...

これでアプリを起動しNFCをかざすとonNewIntentにデータがきます。
Intentから上記のようにTagを取得すると、取得したTagからかざしているFelicaにアクセスできるようになります。

サービス部分のデータ取得

まずFelicaにアクセスするためにはFelicaコマンドを使用します。
※ 参考: FeliCaカード ユーザーズマニュアル 抜粋版 p.42 4章 コマンド

AndroidでFelicaにアクセスするにはSDKに含まれているandroid.nfc.NfcFパッケージのNfcF.connect()で接続しNfcF.tranceive(byte[] data)でコマンドを送りNfcF.close()で閉じればOKです。
送るコマンドはパケットデータに整形して送ります。

コマンドには取得したいサービスが属するシステム領域のIDmが必要になります。
System 0領域にあるサービスにアクセスしたい場合は、上で取得したタグからtag.getId()を使うことによってSystem 0領域のIDmを取得することができます。
しかし、今回はSystem 1領域にあるServiceにアクセスしたいため、先にポーリング処理を行いSystem 1のIDmを取得する必要があります。その後、取得したIDmを使用して目的のブロックデータを取得します。
また、今回必要となるコマンドはPollingコマンドとRead Without Rncryptionコマンドになり、だいたいの仕様は下の方に書いておきます。
※ Read Without Rncryptionは認証が必要のないデータにアクセスするとき使用するものであり、認証が必要なデータにアクセスする場合は別のコマンドも必要になります。

NfcReader.java
import android.nfc.Tag;
import android.nfc.tech.NfcF;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class NfcReader {

    public byte[][] readTag(Tag tag) {
        NfcF nfc = NfcF.get(tag);
        try {
            nfc.connect();
            // System 1のシステムコード -> 0xFE00
            byte[] targetSystemCode = new byte[]{(byte) 0xfe,(byte) 0x00};

            // polling コマンドを作成
            byte[] polling = polling(targetSystemCode);

            // コマンドを送信して結果を取得
            byte[] pollingRes = nfc.transceive(polling);

            // System 0 のIDmを取得(1バイト目はデータサイズ、2バイト目はレスポンスコード、IDmのサイズは8バイト)
            byte[] targetIDm = Arrays.copyOfRange(pollingRes, 2, 10);

            // サービスに含まれているデータのサイズ(今回は4だった)
            int size = 4;

            // 対象のサービスコード -> 0x1A8B
            byte[] targetServiceCode = new byte[]{(byte) 0x1A, (byte) 0x8B};

            // Read Without Encryption コマンドを作成
            byte[] req = readWithoutEncryption(targetIDm, size, targetServiceCode);

            // コマンドを送信して結果を取得
            byte[] res = nfc.transceive(req);

            nfc.close();

            // 結果をパースしてデータだけ取得
            return parse(res);
        } catch (Exception e) {
            Log.e(TAG, e.getMessage() , e);
        }

        return null;
    }

    /**
     * Pollingコマンドの取得。
     * @param systemCode byte[] 指定するシステムコード
     * @return Pollingコマンド
     * @throws IOException
     */
    private byte[] polling(byte[] systemCode) {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(100);

        bout.write(0x00);           // データ長バイトのダミー
        bout.write(0x00);           // コマンドコード
        bout.write(systemCode[0]);  // systemCode
        bout.write(systemCode[1]);  // systemCode
        bout.write(0x01);           // リクエストコード
        bout.write(0x0f);           // タイムスロット

        byte[] msg = bout.toByteArray();
        msg[0] = (byte) msg.length; // 先頭1バイトはデータ長
        return msg;
    }

    /**
     * Read Without Encryptionコマンドの取得。
     * @param IDm 指定するシステムのID
     * @param size 取得するデータの数
     * @return Read Without Encryptionコマンド
     * @throws IOException
     */
    private byte[] readWithoutEncryption(byte[] idm, int size, byte[] serviceCode) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(100);

        bout.write(0);              // データ長バイトのダミー
        bout.write(0x06);           // コマンドコード
        bout.write(idm);            // IDm 8byte
        bout.write(1);              // サービス数の長さ(以下2バイトがこの数分繰り返す)

        // サービスコードの指定はリトルエンディアンなので、下位バイトから指定します。
        bout.write(serviceCode[1]); // サービスコード下位バイト
        bout.write(serviceCode[0]); // サービスコード上位バイト
        bout.write(size);           // ブロック数

        // ブロック番号の指定
        for (int i = 0; i < size; i++) {
            bout.write(0x80);       // ブロックエレメント上位バイト 「Felicaユーザマニュアル抜粋」の4.3項参照
            bout.write(i);          // ブロック番号
        }

        byte[] msg = bout.toByteArray();
        msg[0] = (byte) msg.length; // 先頭1バイトはデータ長
        return msg;
    }

    /**
     * Read Without Encryption応答の解析。
     * @param res byte[]
     * @return 文字列表現
     * @throws Exception
     */
    private byte[][] parse(byte[] res) throws Exception {
        // res[10] エラーコード。0x00の場合が正常
        if (res[10] != 0x00) 
            throw new RuntimeException("Read Without Encryption Command Error");

        // res[12] 応答ブロック数
        // res[13 + n * 16] 実データ 16(byte/ブロック)の繰り返し
        int size = res[12];
        byte[][] data = new byte[size][16];
        String str = "";
        for (int i = 0; i < size; i++) {
            byte[] tmp = new byte[16];
            int offset = 13 + i * 16;
            for (int j = 0; j < 16; j++) {
                tmp[j] = res[offset + j];
            }

            data[i] = tmp;
        }
        return data;
    }
}

注意

  • NfcF.tranceive(byte[] data)で送るパケットデータは、1バイト目に必ずデータサイズを付け加える必要があります。また、このデータサイズには1バイト目につけるデータサイズを含んだサイズになります。
  • Felicaは不適切なコマンドを受け取ると、エラーを返さず、無反応になります。だいたいはNfcF.tranceive(byte[] data)時に無反応になり、android.nfc.TagLostExceptionがスローされます。

コマンド

各コマンドには特記事項などがあり、それらの詳しい仕様はFeliCaカード ユーザーズマニュアル 抜粋版から確認をお願いします。

Polling コマンド

システムコードで指定するシステムの製造ID(IDm)と製造パラメータ(PMm)を取得するコマンドです。
リクエストコードの指定により、システムのシステムコードもしくは通信性能を、
タイムスロットの指定により、応答可能な最大タイムスロット数が指定可能です。

サイズの表記は全て16進数です。

コマンドパケットデータ

パラメータ名 サイズ データ 備考
コマンドコード 1 0x00 0x00
システムコード 2 システムコードの指定
特記事項を参照
0xFE00
リクエストコード 1 リクエストデータの指定
0x00: 要求なし
0x01: システムコード要求
0x02: 通信性能要求
その他: 予約
0x01
タイムスロット 1 応答可能な最大スロット数の指定 0x0F

レスポンスパケットデータ

パラメータ名 サイズ データ 備考
レスポンスコード 1 0x00 0x00
システムコード 2 システムコードの指定
特記事項を参照
0xFE00
リクエストコード 1 リクエストデータの指定
0x00: 要求なし
0x01: システムコード要求
0x02: 通信性能要求
その他: 予約
0x01
タイムスロット 1 応答可能な最大スロット数の指定 0x0F

参考: FeliCaカード ユーザーズマニュアル 抜粋版 p.57 4.4.2章 Polling

Read Without Encryption

認証を必要としないサービスからブロックデータを読みだすためのコマンドです。

コマンドパケットデータ

パラメータ名 サイズ データ 備考
コマンドコード 1 0x06 0x06
IDm 8 targetIDm
サービス数 1 m 1 <= m <= 16 1
サービスコードリスト 2m サービスコードは
リトルエンディアンで指定
0x01
ブロック数 1 n 特記事項を参照 4
ブロックリスト N 2n <= N <= 3n

レスポンスパケットデータ

パラメータ名 サイズ データ 備考
レスポンスコード 1 0x07 0x07
IDm 8 targetIDm
ステータスフラグ1 1 ユーザーズマニュアル 抜粋版
4.5 ステータスフラグ 参照
0xFE00
ステータスフラグ2 1 ユーザーズマニュアル 抜粋版
4.5 ステータスフラグ 参照
0x01
ブロック数 1 n ステータスフラグ1が0x00
の場合のみ付与されます。
ブロックデータ 16n ステータスフラグ1が0x00
の場合のみ付与されます。

参考: FeliCaカード ユーザーズマニュアル 抜粋版 p.63 4.4.5章 Read Without Encryption

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
109
Help us understand the problem. What are the problem?