「ソフテックだより」では、ソフトウェア開発に関する情報や開発現場における社員の取り組みなどを定期的にお知らせしています。
さまざまなテーマを取り上げていますので、他のソフテックだよりも、ぜひご覧下さい。
ソフテックだより(発行日順)のページへ
ソフテックだより 技術レポート(技術分野別)のページへ
ソフテックだより 現場の声(シーン別)のページへ
私は入社2年目の社員で、主にWindowsアプリケーション開発を担当しています。
とある開発案件でHF帯対応のRFIDタグの読み取り処理を実装する機会がありましたので、
今回は対象を一般化し、カードリーダーを用いたスマートカードの読み取り処理の実装方法についてご紹介します。
過去のソフテックだよりでも「非接触ICカード技術"FeliCa(フェリカ)"のIDm読み取り方法」と題しまして、 スマートカードからデータを読み取る方法について解説させていただきましたが、今回は開発言語をC#とした場合の実装方法とサンプルコードを示します。
スマートカードと一口に言っても、実は大きく分けて2種類があり「接触型」と「非接触型」があります。
接触型はカード表面に接点が露出していることが特徴的で、これを直接カードリーダーと接触させる必要があります。
後述する非接触型と比較すると一見不便そうに思えますが、接触させなければならないという特性は逆にセキュリティ面で優れており、クレジットカードやキャッシュカードでよく使用されています。
非接触型は接触型と異なり直接的な接点を必要とせず、カードリーダーにカードを近づけるだけで読み取りが可能ですが、近づける距離によって「密着型」「近接型」「近傍型」「遠隔型」4つに分類されます。
ここで、より離れた位置にある非接触型のカードを読み取るには、より大きな電磁波強度を備えたカードリーダーが必要となるため、注意が必要です。
上記で様々なカードの種類について触れましたが、後述するAPDU(カード間で受け渡すコマンド)の送受信以外の処理についてはカードごとの違いを意識せずに実装することができます。
ちなみに、HF帯対応のRFIDタグとして扱われる製品でも、スマートカードと同様に読み取ることができます。
ただし、実際には求められる機能や性能に応じて、例えば読み取り時間が不安定な場合にはタイムアウトを設定しカード接続からやり直せるようにするなどといった工夫しつつ実装する必要があります。
読み取りにはwinscard.dllを使用します。
winscard.dllはwindowsに標準で搭載されているライブラリで、カードリーダーを使用してカード情報を読み書きするための様々な機能を搭載しています。
C#を使用しつつwinscard.dllの機能を利用する場合、方法としては下記の2通りがあります。
本稿では(1)の方法について解説します。
(2)のパッケージとしては、代表的なものに「PCSC」があります。
NuGetを使用してパッケージを取得することで簡単に利用でき、日本語の解説記事も多い印象です。
Microsoft公式ドキュメントのうち、winscard.hに関する解説およびそれに続く種々の関数の解説を参考に、
次のように関数呼び出し用のクラスを作成し、C#コード上で利用できるようにします。
あわせて、必要となる定数(エラーコードなど)をあらかじめ定義しておきます。
ちなみに、いくつかの関数は関数名の末尾が「A」と「W」の2通り用意されています。
これは「関数プロトタイプの規則」に基づくもので、
関数名の末尾が「A」である関数はANSI、「W」である関数はUnicodeに対応しています。APIを定義する際には、文字コードの一致を意識しなければなりません。
static public class NfcAPI
{
[StructLayout(LayoutKind.Sequential)]
public struct SCARD_IO_REQUEST
{
public int dwProtocol;
public int cbPciLength;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SCARD_READERSTATE
{
public string szReader;
public IntPtr pvUserData;
public UInt32 dwCurrentState;
public UInt32 dwEventState;
public UInt32 cbAtr;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 36)]
public byte[] rgbAtr;
}
[DllImport("winscard.dll")]
public static extern uint SCardEstablishContext(int dwScope, int pvReserved1, int pvReserved2, ref IntPtr phContext);
[DllImport("winscard.dll")]
public static extern uint SCardReleaseContext(IntPtr hContext);
[DllImport("winscard.dll", EntryPoint = "SCardListReadersW", CharSet = CharSet.Unicode)]
public static extern uint SCardListReaders(IntPtr hContext, byte[] mszGroups, byte[] mszReaders, ref int pcchReaders);
[DllImport("winscard.dll", EntryPoint = "SCardGetStatusChangeW", CharSet = CharSet.Unicode)]
public static extern uint SCardGetStatusChange(IntPtr hContext, int dwTimeout, [In, Out] SCARD_READERSTATE[] rgReaderStates, int cReaders);
[DllImport("winscard.dll", EntryPoint = "SCardConnectW", CharSet = CharSet.Unicode)]
public static extern uint SCardConnect(IntPtr hContext, string szReader, int dwShareMode, int dwPreferredProtocols, ref IntPtr phCard, ref IntPtr pdwActiveProtocol);
[DllImport("winscard.dll")]
public static extern uint SCardDisconnect(IntPtr hCard, uint dwDisposition);
[DllImport("winscard.dll")]
public static extern uint SCardTransmit(IntPtr hCard, IntPtr pioSendRequest, byte[] SendBuff, int SendBuffLen, ref SCARD_IO_REQUEST pioRecvRequest,
byte[] RecvBuff, ref int RecvBuffLen);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll")]
public static extern void FreeLibrary(IntPtr handle);
[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr handle, string procName);
//--------------------------------------------------------------------------
// 定数
//--------------------------------------------------------------------------
public const uint SCARD_S_SUCCESS = 0;
public const uint SCARD_E_NO_SMARTCARD = 0x8010000C;
public const uint SCARD_E_CANT_DISPOSE = 0x8010000E;
public const int SCARD_SCOPE_USER = 0;
public const int SCARD_SCOPE_TERMINAL = 1;
public const int SCARD_SCOPE_SYSTEM = 2;
public const int SCARD_STATE_UNAWARE = 0x00;
public const int SCARD_STATE_IGNORE = 0x01;
public const int SCARD_STATE_CHANGED = 0x02;
public const int SCARD_STATE_UNKNOWN = 0x04;
public const int SCARD_STATE_UNAVAILABLE = 0x08;
public const int SCARD_STATE_EMPTY = 0x10;
public const int SCARD_STATE_PRESENT = 0x20;
public const int SCARD_STATE_ATRMATCH = 0x40;
public const int SCARD_STATE_EXCLUSIVE = 0x80;
public const int SCARD_STATE_INUSE = 0x100;
public const int SCARD_STATE_MUTE = 0x200;
public const int SCARD_STATE_UNPOWERED = 0x400;
public const int SCARD_SHARE_EXCLUSIVE = 1;
public const int SCARD_SHARE_SHARED = 2;
public const int SCARD_SHARE_DIRECT = 3;
public const int SCARD_PROTOCOL_UNDEFINED = 0x00;
public const int SCARD_PROTOCOL_T0 = 0x01;
public const int SCARD_PROTOCOL_T1 = 0x02;
public const int SCARD_PROTOCOL_RAW = 0x10000;
}
下図の順序で読み取りを進めていきます。
図1. 読み取り処理の順序
カードリーダーをPCに接続した状態でプログラムを起動すると、2.まで処理が進みます。
その後カードが読み取り可能になるまで待機し、カードが読み取り可能になり次第カードの読み取りを行い、プログラムが終了します。
実際には1度だけの読み取りではなく何度も繰り返し読み取りができるようにしたり、
カードの読み取りを非同期的に実行しつつ別スレッドから読み取りを中止できるようにしたりするなどといった工夫をしつつ実装を行うことになると思いますが、
本稿の例では単純に1度だけの読み取りとします。
データ取り出し処理の実装(サンプルコード中のReadCard()関数およびTransmitReadCommand内の処理)は、取り扱うカードによって変更する必要があります。
とくに、APDUと呼ばれるカードに対して送信するコマンドは、対象のカードが用意しているコマンドに基づいて作成する必要があります。
static void Main()
{
var reader = new NfcReader();
reader.Run();
}
class NfcReader
{
private string _readerName = "";
private IntPtr _hContext = IntPtr.Zero;
private IntPtr _hCard = IntPtr.Zero;
private IntPtr _activeProtocol = IntPtr.Zero;
// カードリーダーは1つのみ使用するため配列のサイズは1
private readonly NfcAPI.SCARD_READERSTATE[] readerStates = new NfcAPI.SCARD_READERSTATE[1];
private NfcAPI.SCARD_IO_REQUEST ioRequest;
public void Run()
{
try
{
// 1. SCardEstablishContextを使用してリソースマネージャに接続する
if (!EstablishContext())
{
throw new Exception("Failed : Establishing Context");
}
Debug.WriteLine("Succeeded : Establishing Context");
// 2. SCardListReadersを使用してカードリーダーを得る
if (!SelectReader())
{
throw new Exception("Failed : Selecting Device");
}
Debug.WriteLine("Succeeded : Selecting Device");
// 3. SCardGetStatusChangeを使用して読み取り可能になるまで待機する
if (!WaitStatusChange())
{
throw new Exception("Failed : Waiting Status Change");
}
Debug.WriteLine("Succeeded : Waiting Status Change");
// 4. カードに接続する
if (!ConnectCard())
{
throw new Exception("Failed : Connecting NfcAPI");
}
Debug.WriteLine("Succeeded : Connecting NfcAPI");
// 5. SCardTransmitを使用してデータを読み取る
if (ReadCard(out string dataText))
{
Debug.WriteLine("Succeeded : Reading Tag ... Content: " + dataText);
}
else
{
Debug.WriteLine("Failed : Reading Tag");
}
// 6. SCardDisconectを使用してカードとの接続を断つ
DisconnectCard();
// 7. リソースマネージャを解放する
ReleaseContext();
}
catch(Exception e)
{
Debug.WriteLine(e.Message);
ReleaseContext();
}
}
private bool EstablishContext()
{
// リソースマネージャに接続しハンドルを取得する
var retCode = NfcAPI.SCardEstablishContext(NfcAPI.SCARD_SCOPE_SYSTEM, 0, 0, ref _hContext);
if (retCode != NfcAPI.SCARD_S_SUCCESS)
{
_hContext = IntPtr.Zero;
return false;
}
return true;
}
private bool SelectReader()
{
// 使用可能なカードリーダーの数を得る
int readerCount = 0;
{
uint retCode = NfcAPI.SCardListReaders(_hContext, null, null, ref readerCount);
if (retCode != NfcAPI.SCARD_S_SUCCESS)
{
return false;
}
}
// カードリーダーの一覧を得る
byte[] readerData = new byte[readerCount * 2];
{
uint retCode = NfcAPI.SCardListReaders(_hContext, null, readerData, ref readerCount);
if (retCode != NfcAPI.SCARD_S_SUCCESS)
{
return false;
}
}
// カードリーダーの一覧のうち先頭のカードリーダーのみを取り出す
string readersDataText = Encoding.Unicode.GetString(readerData);
List readerNames = readersDataText.Split('\0').ToList();
_readerName = readerNames[0];
// カードリーダーの状態の初期化を行う
{
readerStates[0].dwCurrentState = NfcAPI.SCARD_STATE_UNAWARE;
readerStates[0].szReader = _readerName;
uint retCode = NfcAPI.SCardGetStatusChange(_hContext, 100, readerStates, readerStates.Length);
if (retCode != NfcAPI.SCARD_S_SUCCESS)
{
return false;
}
}
return true;
}
private bool WaitStatusChange()
{
while (true)
{
//
uint code = NfcAPI.SCardGetStatusChange(_hContext, int.MaxValue, readerStates, readerStates.Length);
if (code == NfcAPI.SCARD_S_SUCCESS)
{
if ((readerStates[0].dwEventState & NfcAPI.SCARD_STATE_PRESENT) == NfcAPI.SCARD_STATE_PRESENT)
{
return true;
}
}
else
{
return false;
}
}
}
public bool ConnectCard()
{
return ConnectCard(false);
}
private bool ConnectCard(bool isRetry)
{
uint retCode = NfcAPI.SCardConnect(_hContext, _readerName, NfcAPI.SCARD_SHARE_SHARED,
NfcAPI.SCARD_PROTOCOL_T0 | NfcAPI.SCARD_PROTOCOL_T1, ref _hCard, ref _activeProtocol);
// 接続に成功
if (retCode == NfcAPI.SCARD_S_SUCCESS)
{
return true;
}
// カードリーダーなし
else if (retCode == NfcAPI.SCARD_E_NO_SMARTCARD && !isRetry)
{
SelectReader();
return ConnectCard(true);
}
else
{
return false;
}
}
public bool DisconnectCard()
{
uint retCode = NfcAPI.SCardDisconnect(_hCard, NfcAPI.SCARD_E_CANT_DISPOSE);
return retCode == NfcAPI.SCARD_S_SUCCESS;
}
private bool ReleaseContext()
{
uint retCode = NfcAPI.SCardReleaseContext(_hContext);
return retCode == NfcAPI.SCARD_S_SUCCESS;
}
public bool ReadCard(out string dataText)
{
if (TransmitReadCommand(out byte[] recvBuffer))
{
dataText = Encoding.Unicode.GetString(recvBuffer);
return true;
}
else
{
dataText = "";
return false;
}
}
public bool TransmitReadCommand(out byte[] recvBuffer)
{
// 取り扱うカードのAPDUコマンド定義に基づく送信データの作成。
// ここでは、例としてマイナンバーカードの「公的個人認証AP」を選択するための送信データを示す。
var sendBuffer = new byte[15]
{
0x00, // CLA
0xA4, // INS
0x04, // P1
0x0C, // P2
0x0A, // データ長(Lc)
0xD3, 0x92, 0xF0, 0x00, 0x26, 0x01, 0x00, 0x00, 0x00, 0x01 // データ
};
ioRequest.dwProtocol = 0;
ioRequest.cbPciLength = 256;
recvBuffer = new byte[256];
int recvLen = recvBuffer.Length;
// 読み取り実行
{
IntPtr SCARD_PCI_T1 = GetPciT1();
uint retCode = NfcAPI.SCardTransmit(_hCard, SCARD_PCI_T1, sendBuffer, sendBuffer.Length,
ref ioRequest, recvBuffer, ref recvLen);
if (retCode != NfcAPI.SCARD_S_SUCCESS)
{
return false;
}
}
return true;
}
private IntPtr GetPciT1()
{
IntPtr handle = NfcAPI.LoadLibrary("Winscard.dll");
IntPtr pci = NfcAPI.GetProcAddress(handle, "g_rgSCardT1Pci");
NfcAPI.FreeLibrary(handle);
return pci;
}
}
今回はカードリーダーを用いたスマートカードの読み取りについて紹介させていただきましたが、先述した通りスマートカードと一口に言っても多くの種類・規格が存在します。
また、当然カードリーダーにも多くの製品が存在します。
しかし、ライブラリを使用することでカードの読み取り処理以外の部分は共通化させることができますので、
スマートカードを使用したアプリケーションの開発を検討されている方はぜひ本稿を参考にしていただければと思います。
最後までお読みいただきありがとうございました。
(H.K.)
関連ページへのリンク
関連するソフテックだより