💳

NFCリーダー+Python+nfcpyで学生証の情報を読み取る

2023/08/19に公開

準備

NFCリーダー

学生証の読み取りに使用します。後述するnfcpyに対応しているデバイスを用意しましょう。例えば、以下のソースコードのusb_device_mapに存在しないSonyのRC-S300はnfcpyに対応していないので、デバイスを新規購入する際は注意してください。

https://github.com/nfcpy/nfcpy/blob/master/src/nfc/clf/device.py#L42-L52

nfcpy

PythonでNFCリーダーを扱うライブラリです。libusbに依存しています。

https://github.com/nfcpy/nfcpy

pip install nfcpy

Windowsの場合はさらにWinUSBとlibusbの手動インストールが必要なので、以下の公式のドキュメントに従ってインストールします。

https://nfcpy.readthedocs.io/en/latest/topics/get-started.html

WinUSBの導入

公式ドキュメントでも紹介されていますが、Windowsの場合はZadigを用いてWinUSBドライバを入れる方法が簡単でおすすめです。

https://zadig.akeo.ie/

OptionsのList All Devicesにチェックを入れ該当デバイスを選択し、DriverをWinUSBに変更してReplace Driverをクリックします。

libusbの導入

https://libusb.info/

Linuxの場合は大抵のディストリビューションで最初から入っているので、多くの場合は何もしなくて良いです。もし手動で入れる必要がある場合は古いバージョン(0.x)を入れないよう注意してください。

Windowsの場合はlibusb.infoのDownloads>Latest Windows Binariesからバイナリをダウンロードし、VS2015-x64\dll\libusb-1.0.dllC:\Windows\System32に、VS2015-Win32\dll\libusb-1.0.dllC:\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)といいます。

https://www.sony.co.jp/Products/felica/business/tech-support/

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やダンプするアドレスが異なるはずなので適宜書き換えてください。

https://github.com/3w36zj6/tus-card-reader

脚注
  1. FeliCaカード ユーザーズマニュアル 抜粋版 ↩︎

Discussion