nfcpy で複数の System Code を持つ NFC タグを扱う方法
Linux (Ubuntu 16.04) で nfcpy を使ってみました。落とし穴が多い気がしたので、ここにまとめます。 特に、複数の system code を持つような NFC タグの扱いが複雑ですので、詳しく説明します。
概要
Linux で NFC タグを読み書きするには nfcpy という Python ライブラリを使うのが割とよく用いられる方法のようです。 この記事では、nfcpy で NFC タグに記録されたブロックを取得する方法を説明します。
nfcpy に関する記事はいくつもあるのですが、どれも複数の System Code を持つ NFC カードの扱い方は書いてないようでした。 また、対応する nfcpy のバージョンが古かったりするので、この記事を書こうと思いました。
この記事で対象とする nfcpy のバージョンは 0.11.1 です。 また、動作検証は Sony 製の NFC リーダー・ライター PaSoRi RC-S380 を使って行いました。
nfcpy のインストール
公式ドキュメントは http://nfcpy.readthedocs.io/en/stable-0.11/ にあります。 (ただし、このドキュメントは少し古いです。現在 nfcpy は GitHub に移行したのですが、ドキュメントは lauchpad 時代のままです。)
nfcpy は PyPI に登録されているので、pip でインストールできます。 (nfcpy は現時点でまだ Python 3 系に対応してないので、pip3 は使わないでください。)
$ sudo pip install -U nfcpy
pip でインストールするとサンプルコードが手に入らないので、別途 GitHub からダウンロードします。
$ git clone git@github.com:nfcpy/nfcpy.git
$ cd nfcpy
$ git checkout stable/0.11
ここまでできたら、サンプルコードを実行して PaSoRi でタグを読み取ることができるはずです。
$ sudo examples/tagtool.py
[nfc.clf] searching for reader on path usb
[nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:002:013
** waiting for a tag **
Type3Tag 'FeliCa Standard (RC-S915)' ID=xxxxxxxxxxxxxxxx PMM=yyyyyyyyyyyyyyyy SYS=8108
NFC タグをタッチすると検出され、製造ID(IDm)、製造パラメータ(PMm)、システムコードが表示されます。(最終行)
おまけ:sudo を使わないで作業する方法
環境によって、USB 接続の NFC リーダーに一般ユーザが接続することができない場合があります。 毎回 sudo を使えば問題ないのですが、それは嫌だという方には公式ドキュメントで説明されている通り、回避策があります。
まず、一般ユーザで NFC リーダーが扱えないことを確認します。
$ examples/tagtool.py --device usb
[nfc.clf] searching for reader on path usb
[nfc.clf] no reader available on path usb
[main] no contactless reader found on usb
[main] no contactless reader available
この時点でちゃんとリーダーを検出できていれば、何もする必要はありません。 検出できないことが確認できたら、lsusb コマンドでデバイス ID を調べます。
$ lsusb
...
Bus 002 Device 013: ID 054c:06c3 Sony Corp.
...
それっぽいデバイスを見つけたら 054c:06c3
のところの番号を使って tagtool.py を実行しなおします。
$ examples/tagtool.py --device usb:054c:06c3
[nfc.clf] searching for reader on path usb:054c:06c3
[main] access denied for device with path usb:054c:06c3
[main] first match for path usb:054c:06c3 is usb:002:013
[main] usb:002:013 is owned by root but you are xxxxx
[main] members of the root group may use usb:002:013
[main] you may want to add a udev rule to access this device
[main] sudo sh -c 'echo SUBSYSTEM==\"usb\", ACTION==\"add\", ATTRS{idVendor}==\"054c\", ATTRS{idProduct}==\"06c3\", GROUP=\"plugdev\" >> /etc/udev/rules.d/nfcdev.rules'
[main] no contactless reader available
「指定した ID のデバイスは root の所有なのでアクセスできないよ」てなことが書いてあります。 下から 2 行目に解決策が提示されていますので、そのコマンドを実行します。
$ sudo sh -c 'echo SUBSYSTEM==\"usb\", ACTION==\"add\", ATTRS{idVendor}==\"054c\", ATTRS{idProduct}==\"06c3\", GROUP=\"plugdev\" >> /etc/udev/rules.d/nfcdev.rules'
マシンを再起動すれば、一般ユーザで使えるようになっているはずです。
NFC タグの中身を見てみる
nfcpy をインストールしたことですし、NFC タグの中身を表示するプログラムを作りましょう。 以下、NFC といいつつ FeliCa Standard を対象としたプログラムになっています。 他の規格のカードではうまく動かない可能性があります。
dump_simple.py は NFC タグ(の先頭 System)が持つサービスをすべて列挙するプログラムです。
import nfc
def check_services(tag, start, n):
services = [nfc.tag.tt3.ServiceCode(i >> 6, i & 0x3f)
for i in xrange(start, start+n)]
versions = tag.request_service(services)
for i in xrange(n):
if versions[i] == 0xffff: continue
print services[i], versions[i]
def on_connect(tag):
print tag
n = 32
for i in xrange(0, 0x10000, n):
check_services(tag, i, n)
def main():
with nfc.ContactlessFrontend('usb') as clf:
clf.connect(rdwr={'on-connect': on_connect})
if __name__ == '__main__':
main()
取りあえず実行してみます。サービスの全列挙には実行には時間がかかりますのでしばらく待ちましょう。
$ python dump_simple.py
Type3Tag 'FeliCa Standard (RC-S915)' ID=xxxxxxxxxxxxxxxx PMM=yyyyyyyyyyyyyyyy SYS=8108
Service Code 0000h (Service 0 Type 000000b) 4097
Service Code 0100h (Service 4 Type 000000b) 4097
...
プログラムを説明します。
nfc.ContactlessFrontend('usb')
で USB 接続の NFC リーダーを開きます。
clf.connect
で NFC リーダーを起動し、NFC タグのタッチ検出を開始します。
connect
の引数で、NFC タグがタッチされたときに呼び出すコールバック関数 on_connect
を指定します。
ドキュメントにはコールバック関数を設定しない(clf.connect(rdwr={})
)場合、NFC タグをタッチするまでブロックするような記述がありますが、実験したところブロックしませんでした。
きちんと on-connect
コールバックを設定する必要があるようです。
NFC タグがタッチされると on_connect
にはそのタグを表すオブジェクト tag
が渡ってきます。
以降、tag
を使って NFC タグと通信し、様々なデータを読み取っていきます。
dump_simple.py プログラムは、タッチした NFC タグが持つすべてのサービスを列挙します。
tag.request_service
にサービスコード(ServiceCode
)を渡すと、タグがそのサービスを持つかどうかを調べられます。
タグが指定したサービスを持つなら 0xffff 以外の値が返ってきます。
サービスコードは 16 ビットの値で、上位 10 ビットがサービス番号、下位 6 ビットが属性値です。
16 ビット値ですから、0 から 0x10000 までを調べればすべてのサービスを網羅できます。
for i in xrange(0, 0x10000, n)
はそういう意味です。
nfc.tag.tt3.ServiceCode(i >> 6, i & 0x3f)
は、16 ビットの整数 i
から ServiceCode
オブジェクトを生成するやり方です。
i >> 6
で上位 10 ビットを取り出します。i & 0x3f
で下位 6 ビットを取り出します。
tag.request_service
は複数のサービスを一度に指定することができます。
実際に check_services
関数では n
個の ServiceCode
のリストを作り、request_service
関数に渡しています。
1 つずつ調べるよりまとめて調べる方が速いので、32 個ずつ調べることにしたのです。
複数の System を扱う
dump_simple.py では System を切り替える処理を入れていなかったため、暗黙的に NFC タグの先頭の System を扱うことになります。 複数の System を持つタグの場合、明示的に扱う System を指定する処理が必要です。
今度は、すべての System を網羅的に調べるように改造した dump_all_systems.py を示します。
import nfc
def check_services(tag, start, n):
services = [nfc.tag.tt3.ServiceCode(i >> 6, i & 0x3f)
for i in xrange(start, start+n)]
versions = tag.request_service(services)
for i in xrange(n):
if versions[i] == 0xffff: continue
print services[i], versions[i]
def check_system(tag, system_code):
idm, pmm = tag.polling(system_code=system_code)
tag.idm, tag.pmm, tag.sys = idm, pmm, system_code
print tag
n = 32
for i in xrange(0, 0x10000, n):
check_services(tag, i, n)
def on_connect(tag):
system_codes = tag.request_system_code()
for s in system_codes:
check_system(tag, s)
def main():
with nfc.ContactlessFrontend('usb') as clf:
clf.connect(rdwr={'on-connect': on_connect})
if __name__ == '__main__':
main()
実行するとこんな感じになります。複数の SYS=
を調べているのが分かりますね。
$ python dump_all_systems.py
Type3Tag 'FeliCa Standard (RC-S915)' ID=xxxxxxxxxxxxxxxx PMM=yyyyyyyyyyyyyyyy SYS=8108
Service Code 0000h (Service 0 Type 000000b) 4097
Service Code 0100h (Service 4 Type 000000b) 4097
...
Type3Tag 'FeliCa Standard (RC-S915)' ID=xxxxxxxxxxxxxxxx PMM=yyyyyyyyyyyyyyyy SYS=FE00
Service Code 1748h (Service 93 Random RW with key) 4097
Service Code 174Bh (Service 93 Random RO w/o key) 5963
...
プログラムの変更のポイントはこんな感じです。
tag.request_system_code
関数で NFC タグが持っている System の一覧を得る- 今まで
on_connect
でやっていた処理をcheck_system
関数に移して、polling
処理を加えた
polling
の処理がミソとなる部分ですので、少し詳しく説明します。
ポーリングは、NFC リーダーが NFC タグを探す処理のことです。 ポーリング時に System Code を指定することで衝突防止を実現しています。 指定したコードと同じコードを持つ NFC タグだけがポーリングに反応することで、複数の NFC タグが探索範囲に存在しても衝突しないようになっています。
さて、tag.polling
関数に System Code を渡すと、そのコードを持つ NFC タグの IDm, PMm を取得できます。
取得した IDm と PMm、それからポーリングに使った System Code を tag.idm
, tag.pmm
, tag.sys
に設定することで、以降のデータ通信をその System に対して行えるようになります。
データブロックの読み込み
NFC タグがもつすべての System とすべてのサービスが分かったので、次はいよいよ任意のサービスが持つデータブロックを読み込んでみます。
dump_block.py
import nfc
from binascii import hexlify
def on_connect(tag):
idm, pmm = tag.polling(system_code=0xfe00)
tag.idm, tag.pmm, tag.sys = idm, pmm, 0xfe00
sc = nfc.tag.tt3.ServiceCode(93, 0x0b) # 174B
bc = nfc.tag.tt3.BlockCode(1, service=0)
data = tag.read_without_encryption([sc], [bc])
print 'str:', data
print 'hex:', hexlify(data)
def main():
with nfc.ContactlessFrontend('usb') as clf:
clf.connect(rdwr={'on-connect': on_connect})
if __name__ == '__main__':
main()
このプログラムはシステム 0xfe00 内のサービス 0x174B が持つブロック 1 のデータを読み込みます。 システムコードとサービスコードは、先ほどの探索で得た値を使っています。
このあたりは read_without_encryption のドキュメント に載っているサンプルを参考にしています。
ドキュメントにもありますが、BlockCode
の生成時に渡す service=0
はサービスコードそのものではなく、read_without_encryption
の引数 service_list
内でのインデックスです。
read_without_encryption([sc1, sc2, ..., scN], [...])
などと N 個のサービスを指定したとき、BlockCode
の service
は 0 から N-1 までの値を取れます。
1 つのサービスは 16 バイトの大きさのブロックを複数個持ち、サービスの中で 0 から連番が付いています。
| Service X |
| ---------|
| | Block 0 |
| ---------|
| | Block 1 |
| ---------|
| | ... |
サービスコードの下位 6 ビットは属性値で、次のようになっています。 この中で、パスワードなしで読み書きできるのは "w/o key" (without key = キーなし)となっているサービスだけです。 例えば 2 つのサービス "1748h" と "174Bh" のうち、読み取れるのは "174Bh" の方だけです。
ビット | 意味 |
---|---|
001000 | Random RW with key |
001001 | Random RW w/o key |
001010 | Random RO with key |
001011 | Random RO w/o key |
001100 | Cyclic RW with key |
001101 | Cyclic RW w/o key |
001110 | Cyclic RO with key |
001111 | Cyclic RO w/o key |
010000 | Purse Direct with key |
010001 | Purse Direct w/o key |
010010 | Purse Cashback with key |
010011 | Purse Cashback w/o key |
010100 | Purse Decrement with key |
010101 | Purse Decrement w/o key |
010110 | Purse Read Only with key |
010111 | Purse Read Only w/o key |