macOSでPythonからNFCアクセスする(nfcpyは要らない)

まえがき

ネット検索するとnfcpy + pasori の解説記事がよく見つかるが python3 には対応していなそう。 python3でやりたいので、別の手段で実現する。
iPhoneにもFeliCaが乗ったので、 手元のMacでアクセスできるようになっておこうと思った。

要件

NFCというのは、 TypeA/TypeB/FeliCaという非接触ICカードの無線通信規格と やりとりするデータフォーマットまとめたようなもの。
なので、TypeA/TypeB/FeliCaに対応した非接触ICカードRWを使うとNFCアクセスできる。
日本においてはソニーのパソリが有名と思う。

非接触ICカードRWには、PCSCという制御用API仕様があり、 これに対応しているRWはPCSC-APIを使って制御できる。
つまり、MacでPCSCに対応しているRWを使えば、PythonからRWを制御できNFCアクセスが可能になる。 (パソリは、RC-S380であればPCSC対応しているが、Windows限定)

ということで、 探すと入手性の良いものが見つかった(ACR1252Uので、 今回はこれを使います。
一方のアクセス対象の媒体は、手元にあった自動車運転免許証(TypeBカード)を使う。

ACR1252U 利用準備

ドライバをダウンロードしてインストールする。
親切にプログラミングガイド(API Driver Manual)が用意されているので、ダウンロードして目を通す。

PythonからPCSCを使う

Sierraにおいてはシステムライブラリ(/System/Library/Frameworks/PCSC.framework/PCSC)としてPCSCが用意されており、 その実体はc言語の共有ライブラリなのでctypesを使って直接PCSC-APIを呼び出せる。
使うAPIと呼び出し順序は、API Driver Manualの5章に記載されているので、それに従う。

運転免許証へのアクセス

“ICカード免許証及び運転免許証作成システム等仕様書”という名前で仕様書が公開されており(仕様書名で検索すると見つかる)、 仕様書中のアクセス手順フローチャートの通りにアクセスできる。

MifareとかFeliCa Liteなど、他のカードも仕様書見て同様できる。

余談

対象のカードであるかどうか、というのは識別子(AIDやシステムコード)によって判断するのだけれど、 この識別子というのは厳密に管理されている。

AndroidがHCEに対応した時、どうするのかな?と思って調べたら、 自分で取得してねということだったのでめんどくささを感じ、 AndroidルートみたいなAIDをグーグルが取得して ”AndroidルートAID/ApplicationID” みたいな感じだったなら プッシュとかに気楽に使えるのにと思いました。 iPhoneはどうなるかな。

ソースコード

#!/usr/bin/env python3
# coding: utf-8

from ctypes import *
from binascii import a2b_hex
from binascii import b2a_hex

class PCSCException(Exception):
    pass

class PCSC(object):
    """PCSC Wrapper"""

    def __init__(self):
        # load pcsc library
        self.pcsclib = cdll.LoadLibrary('/System/Library/Frameworks/PCSC.framework/PCSC')

    def open(self):
        """SCardEstablishContext -> SCardListReaders"""
        # establish
        scope_system = c_uint32(2)
        scard_context = c_int32(0)
        ret = self.pcsclib.SCardEstablishContext(scope_system, None, None, byref(scard_context))
        if ret != 0:
            raise PCSCException('SCardEstablishContext')

        self.scard_context = scard_context

        # list readers
        readers_buffer_size = c_uint32(1024)
        readers = create_string_buffer(readers_buffer_size.value)
        ret = self.pcsclib.SCardListReaders(scard_context, None, readers, byref(readers_buffer_size))
        if ret != 0:
            raise PCSCException('SCardListReaders')

        # use first reader
        self.reader = create_string_buffer(readers.value)

    def close(self):
        """SCardReleaseContext"""
        ret = self.pcsclib.SCardReleaseContext(self.scard_context)
        # ignore result

    def _scard_connect_helper(self):
        """SCardConnect"""
        share_mode_share = c_uint32(2)
        # T=1 only
        preferred_protocol_t1 = c_uint32(2)
        activate_protocol = c_uint32(0)
        scard_handle = c_int32(0)
        ret = self.pcsclib.SCardConnect(self.scard_context,
                                        self.reader,
                                        share_mode_share,
                                        preferred_protocol_t1,
                                        byref(scard_handle),
                                        byref(activate_protocol))
        if ret != 0:
            return None

        return scard_handle

    def _scard_disconnect_helper(self, scard_handle):
        """SCardDisconnect"""
        disposition_leave = c_uint32(0)
        ret = self.pcsclib.SCardDisconnect(scard_handle, disposition_leave)
        if ret != 0:
            raise PCSCException('SCardDisconnect')

    def is_card_detect(self):
        """SCardConnect -> SCardDisconnect"""
        scard_handle = self._scard_connect_helper()
        if scard_handle is None:
            return False

        self._scard_disconnect_helper(scard_handle)

        return True

    def transceive(self, command):
        """SCardTransmit, transmit command and receive response"""
        scard_handle = self._scard_connect_helper()
        if scard_handle is None:
            raise PCSCException('SCardConnect')

        # command : hex string, ascii
        command_byte = a2b_hex(command.encode('ascii'))
        send_data = create_string_buffer(command_byte)
        send_data_length = c_uint32(len(command_byte))
        recv_buffer = create_string_buffer(1024)
        recv_buffer_length = c_uint32(1024)
        # T=1 only
        scard_pci_t1 = self.pcsclib.g_rgSCardT1Pci

        ret = self.pcsclib.SCardTransmit(scard_handle,
                                         scard_pci_t1,
                                         send_data,
                                         send_data_length,
                                         None,
                                         recv_buffer,
                                         byref(recv_buffer_length))
        if ret != 0:
            raise PCSCException('SCardTransmit')

        self._scard_disconnect_helper(scard_handle)

        return b2a_hex(recv_buffer[:recv_buffer_length.value]).upper().decode('ascii')

    def get_atr(self):
        """SCardStatus"""
        scard_handle = self._scard_connect_helper()
        if scard_handle is None:
            raise PCSCException('SCardConnect')

        atr = create_string_buffer(128)
        atr_length = c_uint32(128)
        ret = self.pcsclib.SCardStatus(scard_handle, None, None, None, None, atr, byref(atr_length))
        if ret != 0:
            raise PCSCException('SCardStatus')

        self._scard_disconnect_helper(scard_handle)

        return b2a_hex(atr[:atr_length.value]).upper().decode('ascii')

    def acr1252u_transceive_iso7816_apdu(self, command):
        """transmit and receive iso7816 apdu"""
        return self.transceive(command)

    def acr1252u_transceive_felica_apdu(self, command):
        """transmit and receive felica apdu"""
        return self.transceive('FF000000' + command[:2] + command)


def atr():
    """check ATR"""
    pcsc = PCSC()
    try:
        pcsc.open()
        while not pcsc.is_card_detect():
            pass

        print(pcsc.get_atr())

    except PCSCException as e:
        print(e)
    finally:
        pcsc.close()


def driver_license_ja():
    """typeb card test"""
    pcsc = PCSC()
    try:
        pcsc.open()
        while not pcsc.is_card_detect():
            pass

        # password
        pin1 = '1111'
        pin2 = '2222'
        # commands
        select_mf = '00A40000'
        select_ef = '00A4020C02'
        select_df = '00A4040C10'
        verify = '0020008004'
        read_binary = '00B00000000000'
        commands = [
            ## read test
            # select mf
            select_mf,
            # select mf/ef01
            select_ef + '2f01',
            # read binary
            read_binary,
            ## verify pin1, 2
            # select mf
            select_mf,
            # select mf/ief01
            select_ef + '0001',
            # verify
            verify + b2a_hex(pin1.encode('ascii')).decode('ascii'),
            # select mf/ief02
            select_ef + '0002',
            # verify
            verify + b2a_hex(pin2.encode('ascii')).decode('ascii'),
            ## read df data
            # select df1
            select_df + 'A00000023101' + '00' * 10,
            # select df1/ef01, read_binary
            select_ef + '0001',
            read_binary,
            # select df1/ef02, read_binary
            select_ef + '0002',
            read_binary,
        ]
        for command in commands:
            print('COMMAND  : {0}'.format(command))
            response = pcsc.acr1252u_transceive_iso7816_apdu(command)
            print('RESPONSE : {0}'.format(response))
            if len(response) < 4:
                raise PCSCException('Response APDU Length Error : {0}'.format(response))
            elif response[-4:] != '9000':
                raise PCSCException('Response APDU Error : {0}'.format(response))

    except PCSCException as e:
        print(e)
    finally:
        pcsc.close()


def main():
    #atr()
    driver_license_ja()


if __name__ == '__main__':
    main()

See also