Android NFCとNexusSで MifareClassic を読み書きする(後編)

こんにちは、ちきんです。今回も引き続きMifareClassicの話です。
今回は、入手した真っさらな MifareClassic(Mifare Standard)のカードに NdefFormatable#format() をしたところ、
NDEF_DISCOVERED で intentが発生したり、Ndef の instanceが使えるようになりましたのでその辺りのお話と、
MifareClassicシリーズ最終回ということでMifareClassicに関するまとめをしてみたいと思います。

(1) MifareClassicのカードをNDEF Formatする

まず、早速MifareClassicの初期状態を見てみると、以下のようになっていました。

○初期状態
========== Sector 1 ===============
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx  // ID Blockなので伏せ
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 0000FF07 8069FFFF FFFFFFFF
========== Sector 2-15 ============
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 0000FF07 8069FFFF FFFFFFFF
========== Sector 16 ==============
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 0000FF07 80BCFFFF FFFFFFFF  // 何故かちょっと違う
===================================

前回のカードとほぼ同じアクセス権情報ですが、最終セクタの1byteだけ違いました。
この部分は前回の資料には特に用途が書いてなかったので、どういう意味か不明ですが、気にしないことにします。

それでは、前回と同じように、format()を実行してみると、以下のようになりました。

○ ndefFormatable.format(ndefMessage) の実行後
========== Sector 1 ===============
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx  // ID Blockなので伏せ
140103E1 03E103E1 03E103E1 03E103E1  // 更新
03E103E1 03E103E1 03E103E1 03E103E1  // 更新
00000000 00007877 88C10000 00000000  // 更新
========== Sector 2 ===============
00000319 D20A0C74 6578742F 706C6169  // 更新
6E48656C 6C6F2C20 4E444546 21FE0000  // 更新
00000000 00000000 00000000 00000000
00000000 00007F07 88400000 00000000  // 更新
========== Sector 3-16 ============
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00007F07 88400000 00000000  // 更新
===================================
Sector 1 の鍵は、
KeyA: KEY_MIFARE_APPLICATION_DIRECTORY
KeyB: KEY_DEFAULT
Sector 2-16 の鍵は、
KeyA: KEY_NFC_FORUM
KeyB: KEY_DEFAULT
赤字は ndefMessage.toByteArray() と一致する部分。

主な変更点は、

  • 全セクタのSector Trailer(最終ブロック)が更新された
  • Sector Trailerの変化は前回と同様で、セクタ1のKeyAは KEY_MIFARE_APPLICATION_DIRECTORYに、他のセクタのKeyAは KEY_NFC_FORUM になった。 KeyBは読み取り不可に。
  • Sector1のDataBlockは、 Indexっぽい情報になっている(?)
  • 実際のNdefMessage Dataは、 Sector2以降に書き込まれる
  • より大きいDataを書きこむと、Sector3以降にも書込みが行われる

というところでした。

(2) NDEF formatしたカードの振る舞い

以下のように振る舞いが変わりました。

  • NDEF formatしたカードを 再度認識させると TECH_DISCOVERED ではなく NDEF_DISCOVERED で通知が来るようになりました。
  • プリインストールされている Tagアプリでも読み取れるようになりました(まあ、当然ですか、、)。
  • getTechList()の結果が変わり、 tech.NdefFormatable が無くなり tech.Ndef になりました。
  • しかし、
intent.getParcelableExtra(NfcAdapter.EXTRA_ID)
intent.getParcelableExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);

は null のままでした

  • NDEF_DISCOVEREDのときの Intent Filterを

<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"></action>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plainX" />   <---- plainX にした
</intent-filter>

とすると、 NDEF_DISCOVERED ではなく TECH_DISCOVERED になりました。 mimeTypeは一致しないといけないようです。

  • TECH_DISCOVEREDでも、 getTechList() に Ndef は残っていたので、 Ndef APIは使えるようです

(3) NDEF formatReadOnlyしたらどうなるのか?

今度は、 format() ではなく、 formatReadOnly() をしてみました。しかし、正常終了しませんでした。
しかし、NDEF formatにはなっていましたので、 format() -> makeReadOnly() という順番で実行され、最後で失敗したような感じでした。

試しに、 tech.Ndef の以下の methodを呼び出すと

canMakeReadOnly(): false
getMaxSize(): 716  // (16byte * 3block * 15sector = 720) - 4
isWritable(): true

という応答でした。 MifareClassicを ReadOnly にすることは可能なはずですが、 canMakeReadOnly() の応答は false ということでした。
ちょっと意外だったので、どういう条件で判定しているかを調べてみると、

android/nfc/tech/Ndef.java

public boolean canMakeReadOnly() {
if (mNdefType == TYPE_1 || mNdefType == TYPE_2) {
return true;
} else {
return false;
}
}

ということなので、 Ndef.NFC_FORUM_TYPE_1 Ndef.NFC_FORUM_TYPE_2 というTypeのカードしか、trueにならないようです(少なくとも現時点では)。
このTypeというのは、 NFC tag type definitionsにおけるTypeと思われます。
MifareClassic は、Type1かType2に当てはまりそうですが、Androidでは別な区分にされています(理由はわかりませんが)。
API Documentでは、

* NFC Forum Type 1 Tag (NFC_FORUM_TYPE_1), such as the Innovision Topaz
* NFC Forum Type 2 Tag (NFC_FORUM_TYPE_2), such as the NXP MIFARE Ultralight
* NFC Forum Type 3 Tag (NFC_FORUM_TYPE_3), such as Sony Felica
* NFC Forum Type 4 Tag (NFC_FORUM_TYPE_4), such as NXP MIFARE Desfire

となっているので、 MifareUltralight は Type2 だけど、 MifareClassicはどうもこの規格には入らないみたいですね。
NDEFを扱いたいなら、 MifareClassicではなくてMifareUltralightを使うほうが良いのかもしれませんね(今更…–;)。

(4) MifareClassicのまとめ

最後に Android NFC API と MifareClassic のポイントをまとめます。

NDEF Format済みか否かを調べるには

Tag#getTechList() に “Ndef” が入っていればFormat済み、 “NdefFormatable” が入っていればFormatされていない、ということができそうです。
判定には、例えば以下のような判定ロジックを作って、


public class NFCUtil {
static public boolean hasTech(Tag tag, String klassName) {
for (String tech : tag.getTechList()) {
if (tech.equals(klassName)) {
return true;
}
}
return false;
}
static public boolean hasTech(Tag tag, Class< ? extends TagTechnology> tech) {
return hasTech(tag, tech.getCanonicalName());
}
}

で、

Tag tag = (Tag)intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (NFCUtil.hasTech(tag, Ndef.class)) {
// NDEF Format済み
}
// とか
if (NFCUtil.hasTech(tag, NdefFormatable.class)) {
// NDEF Format ではない
}

というように判定できます。

Format済みの場合

  • NDEF_DISCOVERED の Intentが発生する(ことがある。 発生するかしないかはNdefMessageの最初のNdefRecordに依存すると仕様ではなっている)。
    例えば、MimeType,text/plain のNDEFなら、以下のようなIntentFilterを AndroidManifest.xml の activityタグの中に書いておくと良いです。

<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"></action>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
  • 上記、android:mimeType の部分を変更したときのIntentの変化をいくつか調べてみると以下のようになります。
    タグの内容 Intent種別
    タグを省略する TECH_DISCOVERED
    mimeType=”text/plainX” TECH_DISCOVERED
    mimeType=”*” アプリがInstallできない
    mimeType=”*/*” NDEF_DISCOVERED
    mimeType=”text/*” NDEF_DISCOVERED
    mimeType=”te*/*” TECH_DISCOVERED
    “text/*” と “image/*” を2つ記述 NDEF_DISCOVERED

    *でマッチングさせたり、複数を列挙してマッチングさせたりできるようです。

  • tech.NdefのAPIが利用可能です
Ndef ndef = Ndef.get(tag); // で instance化
  • 鍵を考える必要はなく、NdefMessageの Read/Write を行うことができます。但し、データサイズだけは気にする必要があります。
if (!ndef.isConnected()) {
ndef.connect(); // connect() 忘れると例外がでます。
}
ndef.writeNdefMessage(ndefMessage);
  • makeReadOnly() は使えないようです

書き込めるSizeや makeReadOnly などは、カードに依存しますが、後はだいぶ抽象化されている感じですね。

Formatしていない場合

  • NDEF_DISCOVERED は発生せず、 TECH_DISCOVERED の Intent が発生します。
    捕捉するには、AndroidManifest.xml に下記を追加して、

<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED"></action>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_filter"/>

res/xml/nfc_filter.xmlを以下のようにします。

<?xml version="1.0" encoding="utf-8"?><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- MifareClassic の 未Formatの時のみ捕捉する場合 -->
<tech-list>
<tech>android.nfc.tech.NdefFormatable</tech>
<tech>android.nfc.tech.MifareClassic</tech>
</tech-list>
</resources>
  • (おそらく)全セクタのKeyAが KEY_DEFAULT で、KeyAで任意の書込みができるならば、 NdefFormatable#format() が可能です。
NdefFormatable ndefFormatable = NdefFormatable.get(tag); // tag は android.nfc.Tag
if (!ndefFormatable.isConnected()) {
ndefFormatable.connect();
}
ndefFormatable.format(ndefMessage);
  • (もちろん)Ndef Formatしなくても、Read/Writeすることはできます(当然 KeyA, KeyB や アクセス権次第ですが)
MifareClassic mc = MifareClassic.get(tag); // tag は android.nfc.Tag
if (!mc.isConnected()) { mc.connect(); }
if (mc.authenticateSectorWithKeyA(sectorIndex, MifareClassic.KEY_DEFAULT)) { // KEY_DEFAULT を使う場合
// for read
mc.readBlock(targetBlock);
// for write
mc.writeBlock(targetBlock, writeData);
}

(5) さいごに

MifareClassic は ステルス・ネットワークスさんのサイトで10枚で3650円(送料、消費税込)でした。
前日の夕方(一応営業時間内)に注文したら、翌日の午前中に届けてくれました。
皆さんもちょっと実験に如何でしょうか(^^;)。
それにしても、手元の10枚のカードを何に使おうか、、何か軍人将棋みたいなカードゲームでも作れないかな、、と思ってしまうのでした。


Comments are closed.