NFCリーダー+Python+nfcpyで学生証の情報を読み取る
準備
NFCリーダー
学生証の読み取りに使用します。後述するnfcpyに対応しているデバイスを用意しましょう。例えば、以下のソースコードのusb_device_map
に存在しないSonyのRC-S300はnfcpyに対応していないので、デバイスを新規購入する際は注意してください。
nfcpy
PythonでNFCリーダーを扱うライブラリです。libusbに依存しています。
pip install nfcpy
Windowsの場合はさらにWinUSBとlibusbの手動インストールが必要なので、以下の公式のドキュメントに従ってインストールします。
WinUSBの導入
公式ドキュメントでも紹介されていますが、Windowsの場合はZadigを用いてWinUSBドライバを入れる方法が簡単でおすすめです。
OptionsのList All Devicesにチェックを入れ該当デバイスを選択し、DriverをWinUSBに変更してReplace Driverをクリックします。
libusbの導入
Linuxの場合は大抵のディストリビューションで最初から入っているので、多くの場合は何もしなくて良いです。もし手動で入れる必要がある場合は古いバージョン(0.x)を入れないよう注意してください。
Windowsの場合はlibusb.infoのDownloads>Latest Windows Binariesからバイナリをダウンロードし、VS2015-x64\dll\libusb-1.0.dll
をC:\Windows\System32
に、VS2015-Win32\dll\libusb-1.0.dll
をC:\Windows\SysWOW64
にコピーします。
カードを読み取る
NFCリーダーをUSB接続した状態で、以下のコードを実行してみましょう。
import nfc
def on_connect(tag: nfc.tag.Tag) -> bool:
print("connected")
print("\n".join(tag.dump()))
return True # Trueを返しておくとタグが存在しなくなるまで待機され、離すとon_releaseが発火する
def on_release(tag: nfc.tag.Tag) -> None:
print("released")
with nfc.ContactlessFrontend("usb") as clf:
while True:
clf.connect(rdwr={"on-connect": on_connect, "on-release": on_release})
tag.dump()
をすると、以下のように読み取ったタグのメモリ構造が文字列で得られます。
System XXXX (unknown)
Area 0000--FFFE
Area XXX0--XXXF
Random Service XXX: write with key & read w/o key (0xXXXX 0xXXXX)
0000: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX | |
0001: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX | |
0002: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX | |
0003: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX | |
System FE00 (Common Area)
Area 0000--FFFE
Area XXX0--XXXF
Area XXX1--XXXF
Random Service XXX: write with key & read w/o key (0xXXXX 0xXXXX)
0000: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX | |
0001: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX | |
0002: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX | |
0003: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX | |
Felicaについて
ダンプしたデータを理解するためにFelicaの概要について紹介します。
まず近距離無線通信の世界共通規格をNFC(Near Field Communication)といい、さらにSonyが開発したNFC準拠の非接触型ICカードの技術方式および登録商標をFelica(Felicity Card)といいます。
System
SonyがFeliCaの使用されているシステムを管理をするために、組織やアプリケーション毎に発行する論理的な単位およびその識別のための2byteのコードをSystemといいます。
0xFE00
は共通領域となっています。またカードはSystem Codeを複数持つことができます。
例えば、Suica/PASMO/ICOCAなどの交通系電子マネーには0x0003
が割り当てられています。
Service
System内の各機能に対して任意に割り当てられる論理的な単位およびその識別のための2byteのコードをServiceといいます。Service Codeはブロックデータの集合およびアクセス方式を提供します。
またService Codeの上位10bitはサービスの種類を表し、下位6bitはサービスの属性を表します。
例えば、交通系電子マネーでは属性情報には0x008B
、利用履歴には0x090F
が割り当てられています。これらを2進数に直すと(区切りの部分に/
を入れています)、属性情報は0b0000000010/001011
、利用履歴は0b0000100100/001111
となり、属性部分を取り出すとそれぞれ0b001011
, 0b001111
となります。
以下はサービス属性の値の一覧表です[1]。
サービス属性 | 値 | |
---|---|---|
ランダムサービス | リード/ライトアクセス : 認証必要 | 0b001000 |
リード/ライトアクセス : 認証不要 | 0b001001 |
|
リードオンリーアクセス : 認証必要 | 0b001010 |
|
リードオンリーアクセス : 認証不要 | 0b001011 |
|
サイクリックサービス | リード/ライトアクセス : 認証必要 | 0b001100 |
リード/ライトアクセス : 認証不要 | 0b001101 |
|
リードオンリーアクセス : 認証必要 | 0b001110 |
|
リードオンリーアクセス : 認証不要 | 0b001111 |
|
パースサービス | ダイレクトアクセス : 認証必要 | 0b010000 |
ダイレクトアクセス : 認証不要 | 0b010001 |
|
キャッシュバック/デクリメントアクセス : 認証必要 | 0b010010 |
|
キャッシュバック/デクリメントアクセス : 認証不要 | 0b010011 |
|
デクリメントアクセス : 認証必要 | 0b010100 |
|
デクリメントアクセス : 認証不要 | 0b010101 |
|
リードオンリーアクセス : 認証必要 | 0b010110 |
|
リードオンリーアクセス : 認証不要 | 0b010111 |
Random Service
Random Serviceとは、ユーザーが自由にブロックを指定してアクセス可能なサービスです。
Cyclic Service
Cyclic Serviceとは、書き込みが常に一番古いブロックに対して行われる機能をもつサービスです。ログの記録などに使われます。
Purse Service
Purse Serviceとは、ブロックデータの一部を正の数値とみなし、その値を減算する機能を持つサービスです。料金徴収などに使われます。
製造IDと製造パラメータ
通信相手のカードを識別するための情報を製造ID(IDm)といい、製造者コードとカード識別番号から構成されています。カードに複数のシステムが存在する場合は各システムごとにIDmが設定されるので、Pollingで明示的に読み込み先のシステムコードを指定します。
通信相手の性能を識別するための情報を製造パラメータ(PMm)といいます。製品を特定するための情報であるICコードと最大応答時間パラメータから構成されています。
目当てのデータを取得する
ダンプで得られた情報から欲しいデータを探しましょう。学生証の場合は、学籍番号と本名の半角カナ表記がShift JISでエンコードされて格納されている場合が多いようです。
目当てのデータが格納されているシステムコード及びサービスコードとブロックコードを確認したら、以下のソースコード内の該当箇所を書き換えて実行してみましょう。
from typing import cast
import nfc
from nfc.tag import Tag
from nfc.tag.tt3 import BlockCode, ServiceCode, Type3Tag
from nfc.tag.tt3_sony import FelicaStandard
SYSTEM_CODE = カードの種類判定用のシステムコード
def read_data_block(tag: Type3Tag, service_code_number: int, block_code_number: int) -> bytearray:
service_code = ServiceCode(service_code_number, サービス属性)
block_code = BlockCode(block_code_number)
read_bytearray = cast(bytearray, tag.read_without_encryption([service_code], [block_code]))
return read_bytearray
def get_student_id(tag: Type3Tag) -> str:
student_id_bytearray = read_data_block(tag, サービス番号, ブロックコード)
return student_id_bytearray.decode("shift_jis") # スライスで必要な部分だけ切り出す
def get_student_name(tag: Type3Tag) -> str:
student_name_bytearray = read_data_block(tag, サービス番号, ブロックコード)
return student_name_bytearray.decode("shift_jis") # スライスで必要な部分だけ切り出す
def on_connect(tag: Tag) -> bool:
print("connected")
if isinstance(tag, FelicaStandard) and SYSTEM_CODE in tag.request_system_code(): # カードがFeliCaでかつシステムコードが存在する場合
tag.idm, tag.pmm, *_ = tag.polling(データの読み込み先のシステムコード)
print(get_student_id(tag))
print(get_student_name(tag))
return True # Trueを返しておくとタグが存在しなくなるまで待機される
def on_release(tag: Tag) -> None:
print("released")
with nfc.ContactlessFrontend("usb") as clf:
while True:
clf.connect(rdwr={"on-connect": on_connect, "on-release": on_release})
Pollingで読み込み先のシステムコードを指定してから、目当てのデータの格納場所をサービス番号とブロックコードで指定します。
できたもの
以上の内容を踏まえた上で、Richで出力を整えたり、読み取った情報を外部にPOSTリクエストする機能を付けたものが以下のリポジトリになります。組織や用途によってSYSTEM_CODE
やダンプするアドレスが異なるはずなので適宜書き換えてください。
Discussion