まえがき
ネット検索すると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()