Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9521

WindowsでNFCタグを読み取る

$
0
0

【目的】

Windows端末で社員証などのNFCタグからタグの固有ID(IDm)を取得して、アプリケーションで利用します。
先に公開した「Visual Studio 2019 によるExcelアドインの作成」「Visual Studio 2019 によるExcelアドインの作成 - VBAからアドイン内のメソッドを呼び出す」と併用することで、ExcelからAddIn経由でNFC情報を扱うことも可能になります。

【使用機器】

機器 備考
パソコン Windows 10 Pro
NFCリーダ Sony PaSoRi RC-S380/S
開発環境 VisualStudio 2019/2017

【各種ライブラリなど】

Windows環境で利用できるNFC開発環境を以下にリストアップします。
比較的容易に実装できて、無償で利用できるPSCS(またはPCSC-sharp)の利用をお勧めします。

Personal Computer/Smart Card (winscard.dll)

Windows7以降ではOSに標準で添付 (無償)
DLLImportすれば.Net開発環境(C#、VB)から利用可能 (← 以下にサンプルコードあり)

【winscard.h header - MSDN】

【Advanced Card Systems Ltd.の仕様書】

PCSC-sharp

上記のPCSC(winscard.dll)を.Net環境から使えるようにするラッパー。
ライセンス形態はこちら
PCSC.Iso7816と組み合わせて使う。

【PCSC - Nuget】
【PCSC.Iso7816 - Nuget】

【PCSC-sharp - Github】

Windows.Devices.SmartCards

UWPアプリ用の標準ライブラリ (無償)

【Windows.Devices.SmartCards Namespace - MSDN】

SDK for NFC Starter Kit

SONYが提供しているSDK。
商用利用は有償。

【SDK for NFC Starter Kit - SONY】

nfcpy

Python用のNFCライブラリ。
以前はPython2にしか対応していなかったが、最近Python3に対応したらしい。
Windowsでは別途ドライバの導入作業が必要。(要管理者権限)
ライセンス形態はEUPL1.1

【nfcpy - PyPi】
【nfcpy - Github】

【PCSCによる処理フロー】

基本的には以下の処理フローを踏襲します。

image.png

1. SCardEstablishContext

リソースマネージャに接続してハンドルを取得します。

2. SCardListReaders

PCに接続されているNFCリーダを取得します。(複数可)
取得できなかった場合は接続されるまでループするか、エラーで処理を中止します。

3. SCardConnect

接続されているNFCリーダを指定して、カード(NFCタグ)に接続します。
カードと接続できなかった場合、接続されるまでループするか、エラーで処理を中止します。
なお、NFCリーダ上に読み取れるカードがなかった場合はエラーとなるため、基本的にはループしてカードと接続できるまで待ちます。

4. SCardTransmit

接続したカードにコマンドを送信し、結果を受信します。
ここでIDmを取得したり、カードに保存されている情報を読取り/書込みをお行います。
コマンドや受信データは取得したい情報によって異なるため、該当の仕様書を確認する必要があります。

5. SCardDisconnect

カードから切断します。
これで一連の処理は完了です。

winscard.dllによるサンプル実装 (C# コンソールアプリ)

プログラム本体

Program.cs
using PCSC;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PCSC_Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            IntPtr hContext = IntPtr.Zero;

            // ##################################################
            // 1. SCardEstablishContext
            // ##################################################
            Console.WriteLine("***** 1. SCardEstablishContext *****");
            uint ret = Api.SCardEstablishContext(Constant.SCARD_SCOPE_USER, IntPtr.Zero, IntPtr.Zero, out hContext);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                string message;
                switch (ret)
                {
                    case Constant.SCARD_E_NO_SERVICE:
                        message = "サービスが起動されていません。";
                        break;
                    default:
                        message = "サービスに接続できません。code = " + ret;
                        break;
                }
                throw new ApplicationException(message);
            }

            if (hContext == IntPtr.Zero)
            {
                throw new ApplicationException("コンテキストの取得に失敗しました。");
            }
            Console.WriteLine(" サービスに接続しました。");


            // ##################################################
            // 2. SCardListReaders
            // ##################################################
            Console.WriteLine("***** 2. SCardListReaders *****");
            uint pcchReaders = 0;

            // NFCリーダの文字列バッファのサイズを取得
            ret = Api.SCardListReaders(hContext, null, null, ref pcchReaders);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                // 検出失敗
                throw new ApplicationException("NFCリーダを確認できません。");
            }

            // NFCリーダの文字列を取得
            byte[] mszReaders = new byte[pcchReaders * 2]; // 1文字2byte
            ret = Api.SCardListReaders(hContext, null, mszReaders, ref pcchReaders);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                // 検出失敗
                throw new ApplicationException("NFCリーダの取得に失敗しました。");
            }


            UnicodeEncoding unicodeEncoding = new UnicodeEncoding();
            string readerNameMultiString = unicodeEncoding.GetString(mszReaders);

            // 認識したNDCリーダの最初の1台を使用
            int nullindex = readerNameMultiString.IndexOf((char)0);
            var readerName = readerNameMultiString.Substring(0, nullindex);
            Console.WriteLine(" NFCリーダを検出しました。 " + readerName);



            // ##################################################
            // 3. SCardConnect
            // ##################################################
            Console.WriteLine("***** 3. SCardConnect *****");
            IntPtr hCard = IntPtr.Zero;
            IntPtr activeProtocol = IntPtr.Zero;
            ret = Api.SCardConnect(hContext, readerName, Constant.SCARD_SHARE_SHARED, Constant.SCARD_PROTOCOL_T1, ref hCard, ref activeProtocol);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                throw new ApplicationException("カードに接続できません。code = " + ret);
            }
            Console.WriteLine(" カードに接続しました。");



            // ##################################################
            // 4. SCardTransmit
            // ##################################################
            Console.WriteLine("***** 4. SCardTransmit *****");
            uint maxRecvDataLen = 256;
            var recvBuffer = new byte[maxRecvDataLen + 2];
            var sendBuffer = new byte[] { 0xff, 0xca, 0x00, 0x00, 0x00 };  // ← IDmを取得するコマンド

            Api.SCARD_IO_REQUEST ioRecv = new Api.SCARD_IO_REQUEST();
            ioRecv.cbPciLength = 255;

            int pcbRecvLength = recvBuffer.Length;
            int cbSendLength = sendBuffer.Length;

            IntPtr handle = Api.LoadLibrary("Winscard.dll");
            IntPtr pci = Api.GetProcAddress(handle, "g_rgSCardT1Pci");
            Api.FreeLibrary(handle);

            ret = Api.SCardTransmit(hCard, pci, sendBuffer, cbSendLength, ioRecv, recvBuffer, ref pcbRecvLength);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                throw new ApplicationException("NFCカードへの送信に失敗しました。code = " + ret);
            }

            // 受信データからIDmを抽出する
            // recvBuffer = IDm + SW1 + SW2 (SW = StatusWord)
            // SW1 = 0x90 (144) SW1 = 0x00 (0) で正常だが、ここでは見ていない
            string cardId = BitConverter.ToString(recvBuffer, 0, pcbRecvLength - 2);
            Console.WriteLine(" カードからデータを取得しました。");
            Console.WriteLine(" 【IDm】:" + cardId);


            // ##################################################
            // 5. SCardDisconnect
            // ##################################################
            Console.WriteLine("***** 5. SCardDisconnect *****");
            ret = Api.SCardDisconnect(hCard, Constant.SCARD_LEAVE_CARD);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                throw new ApplicationException("NFCカードとの切断に失敗しました。code = " + ret);
            }
            Console.WriteLine(" カードを切断しました。");
        }
    }
}

API定義

nfcapi.cs
using System;
using System.Runtime.InteropServices;

namespace PCSC
{
    class Api
    {
        [DllImport("winscard.dll")]
        public static extern uint SCardEstablishContext(uint dwScope, IntPtr pvReserved1, IntPtr pvReserved2, out IntPtr phContext);

        [DllImport("winscard.dll", EntryPoint = "SCardListReadersW", CharSet = CharSet.Unicode)]
        public static extern uint SCardListReaders(
          IntPtr hContext, byte[] mszGroups, byte[] mszReaders, ref UInt32 pcchReaders);

        [DllImport("winscard.dll")]
        public static extern uint SCardReleaseContext(IntPtr phContext);

        [DllImport("winscard.dll", EntryPoint = "SCardConnectW", CharSet = CharSet.Unicode)]
        public static extern uint SCardConnect(IntPtr hContext, string szReader,
             uint dwShareMode, uint dwPreferredProtocols, ref IntPtr phCard,
             ref IntPtr pdwActiveProtocol);

        [DllImport("winscard.dll")]
        public static extern uint SCardDisconnect(IntPtr hCard, int Disposition);

        [StructLayout(LayoutKind.Sequential)]
        internal class SCARD_IO_REQUEST
        {
            internal uint dwProtocol;
            internal int cbPciLength;
            public SCARD_IO_REQUEST()
            {
                dwProtocol = 0;
            }
        }

        [DllImport("winscard.dll")]
        public static extern uint SCardTransmit(IntPtr hCard, IntPtr pioSendRequest, byte[] SendBuff, int SendBuffLen, SCARD_IO_REQUEST pioRecvRequest,
                byte[] RecvBuff, ref int RecvBuffLen);

        [DllImport("winscard.dll")]
        public static extern uint SCardControl(IntPtr hCard, int controlCode, byte[] inBuffer, int inBufferLen, byte[] outBuffer, int outBufferLen, ref int bytesReturned);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct SCARD_READERSTATE
        {
            internal string szReader;
            internal IntPtr pvUserData;
            internal UInt32 dwCurrentState;
            internal UInt32 dwEventState;
            internal UInt32 cbAtr;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 36)]
            internal byte[] rgbAtr;
        }

        [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")]
        public static extern int SCardStatus(IntPtr hCard, string szReader, ref int cch, ref int state, ref int protocol, ref byte[] bAttr, ref int cByte);


        [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);

    }
}

APIの定数定義

pcsc_const.cs
using System;

namespace PCSC
{
    class Constant
    {
        public const uint SCARD_S_SUCCESS = 0;
        public const uint SCARD_E_NO_SERVICE = 0x8010001D;
        public const uint SCARD_E_TIMEOUT = 0x8010000A;

        public const uint SCARD_SCOPE_USER = 0;
        public const uint SCARD_SCOPE_TERMINAL = 1;
        public const uint SCARD_SCOPE_SYSTEM = 2;

        public const int SCARD_STATE_UNAWARE = 0x0000;
        public const int SCARD_STATE_CHANGED = 0x00000002;
        public const int SCARD_STATE_PRESENT = 0x00000020;
        public const UInt32 SCARD_STATE_EMPTY = 0x00000010;
        public const int SCARD_SHARE_SHARED = 0x00000002;
        public const int SCARD_SHARE_EXCLUSIVE = 0x00000001;
        public const int SCARD_SHARE_DIRECT = 0x00000003;

        public const int SCARD_PROTOCOL_T0 = 1;
        public const int SCARD_PROTOCOL_T1 = 2;
        public const int SCARD_PROTOCOL_RAW = 4;

        public const int SCARD_LEAVE_CARD = 0;
        public const int SCARD_RESET_CARD = 1;
        public const int SCARD_UNPOWER_CARD = 2;
        public const int SCARD_EJECT_CARD = 3;

        // SCardStatus status values
        public const int SCARD_UNKNOWN = 0x00000000;
        public const int SCARD_ABSENT = 0x00000001;
        public const int SCARD_PRESENT = 0x00000002;
        public const int SCARD_SWALLOWED = 0x00000003;
        public const int SCARD_POWERED = 0x00000004;
        public const int SCARD_NEGOTIABLE = 0x00000005;
        public const int SCARD_SPECIFICMODE = 0x00000006;
    }
}

実行結果

本プログラムでは非常に基本的なことしか実装していません。
パソコンにNFCリーダを接続し、事前にNFCリーダ上にNFCタグ(カード、タグ、スマホなど)を置いた上で実行すると、コンソール上に以下のように表示されます。

***** 1. SCardEstablishContext *****
 サービスに接続しました。
***** 2. SCardListReaders *****
 NFCリーダを検出しました。 Sony FeliCa Port/PaSoRi 3.0 0
***** 3. SCardConnect *****
 カードに接続しました。
***** 4. SCardTransmit *****
 カードからデータを取得しました。
 【IDm】:04-D6-0E-42-**-**-**
***** 5. SCardDisconnect *****
 カードを切断しました。

4. SCardTransmit で表示している 【IDm】: 以降がNFCタグから取得した固有IDの値になります。
NFCリーダが認識できなかったり、NFCタグが存在しない/繋がらない場合など、異常時には例外を吐きます。

なお、NFCタグがリーダにかざされるまで待つには 3. SCardConnect でループさせるか、 SCardGetStatusChange メソッドを使ってNFCリーダの状態変化を検出するまで待つ必要があります。

また、ソースコード中の var sendBuffer = new byte[] { 0xff, 0xca, 0x00, 0x00, 0x00 }; の部分がNFCタグに送信するコマンドになります。
サンプルではIDmを取得するコマンドになっていますが、それ以外のやり取りを行いたい場合、各サービスの仕様書を確認する必要があります。


Viewing all articles
Browse latest Browse all 9521

Trending Articles