読者です 読者をやめる 読者になる 読者になる

uchan note

プログラミングや電子工作の話題を書きます

nfcpy で複数の System Code を持つ NFC タグを扱う方法

Linux 電子工作

Linux (Ubuntu 16.04) で nfcpy を使ってみました。落とし穴が多い気がしたので、ここにまとめます。 特に、複数の system code を持つような NFC タグの扱いが複雑ですので、詳しく説明します。

概要

LinuxNFC タグを読み書きするには 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.connectNFC リーダーを起動し、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 個のサービスを指定したとき、BlockCodeservice は 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