大学の学生証が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を読み込みたいと思います。
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);
}
また忘れずにマニフェストに以下の記述を書いておきましょう。
...
<!-- 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は認証が必要のないデータにアクセスするとき使用するものであり、認証が必要なデータにアクセスする場合は別のコマンドも必要になります。
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