WindowsでNFCタグを読み取る

## 【目的】
Windows端末で社員証などのNFCタグからタグの固有ID(IDm)を取得して、アプリケーションで利用します。
先に公開した[「Visual Studio 2019 によるExcelアドインの作成」](https://qiita.com/rhene/items/dd2cf0667f7ca0fd15a8)と[「Visual Studio 2019 によるExcelアドインの作成 - VBAからアドイン内のメソッドを呼び出す」](https://qiita.com/rhene/items/853efe91a96df93e1693)と併用することで、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】](https://docs.microsoft.com/ja-jp/windows/win32/api/winscard/)

[【Advanced Card Systems Ltd.の仕様書】](https://www.acs.com.hk/download-manual/4414/API-ACR1251U-1.08.pdf)


### PCSC-sharp
上記のPCSC(winscard.dll)を.Net環境から使えるようにするラッパー。
ライセンス形態は[こちら](https://github.com/danm-de/pcsc-sharp/blob/master/COPYING)。
PCSC.Iso7816と組み合わせて使う。

[【PCSC - Nuget】](https://www.nuget.org/packages/PCSC/)
[【PCSC.Iso7816 - Nuget】](https://www.nuget.org/packages/PCSC.Iso7816/)

[【PCSC-sharp - Github】](https://github.com/danm-de/pcsc-sharp)


### Windows.Devices.SmartCards
UWPアプリ用の標準ライブラリ (無償)

[【Windows.Devices.SmartCards Namespace - MSDN】](https://docs.microsoft.com/ja-jp/uwp/api/windows.devices.smartcards)


### SDK for NFC Starter Kit
SONYが提供しているSDK。
商用利用は有償。

[【SDK for NFC Starter Kit - SONY】](https://www.sony.co.jp/Products/felica/business/products/ICS-D004.html)


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

[【nfcpy - PyPi】](https://pypi.org/project/nfcpy/)
[【nfcpy - Github】](https://github.com/nfcpy/nfcpy)


## 【PCSCによる処理フロー】
基本的には以下の処理フローを踏襲します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454012/9eca0e78-3a7e-f6fe-65c5-0aba1fcd35ed.png)


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

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

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

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

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


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


### プログラム本体
```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定義
```c#: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の定数定義
```c#: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を取得するコマンドになっていますが、それ以外のやり取りを行いたい場合、各サービスの仕様書を確認する必要があります。