PCSC-sharpライブラリでNFCタグの読み取りソフトを作ってみた

従業員の入退室管理のため、300枚はあろうかというMifareカードのUID（カード固有の製造番号）を読み取る必要があった。
良さげなフリーソフトが見つからなかったので、自分でコード書いてみたという話。

# PCSC-sharpとは
Windows7以降に標準添付されている`winscard.dll`を **.NET** から簡単に使えるようにするラッパーライブラリ。
:point_right_tone2: 【入手元】 https://github.com/danm-de/pcsc-sharp

`winscard.dll`はスマートカード（ISO7816規格）をWindowsから操作する**PC/SC**というAPIの実装である。

PCSC-sharpに頼らず自分でDLLを叩きたいんだ！という人は、
@rhene さんの記事 https://qiita.com/rhene/items/725dfe4a6b6307731cbf
がとても参考になると思う。

# NFCとは
**RFID**と呼ばれる無線通信方式のひとつで、下表の通り複数の規格がある。

|規格            |日本における用途       |
|:---------------|:---------|
|Type-A (Mifare) |Taspoなど |
|Type-B          |マイナンバーカード、住民基本台帳カード、運転免許証など|
|Type-F (FeliCa) |Suica、楽天Edy、nanaco、iD、WAONなどの電子マネー |
|ISO/IEC 15693   |物流の商品タグなど   |

Type-AとType-Bは事実上の世界標準で、**NFC Pay**（クレジットカードのタッチ決済）にも使われている。
日本ではSuicaの自動改札で通信速度が必要だったという背景からFeliCaが採用され、その他電子マネーもこぞって追随したことからFeliCaが普及した。

入退室管理に使うカードではそこまでの性能は必要ないので、安価なMifareが広く使われている。

> FeliCaはSONYが開発したものだが、技術力の高さゆえの過剰性能が災いしガラパゴス化した。
> 4K、8Kテレビは国内メーカーを助けるための開発ではあるが、同じ道を歩まないことを願う。

# ソフトウェアの作成手順
## 開発環境
### ソフトウェア
- Visual Studio 2019（C#）
- Windows 10 Pro 1903

### ハードウェア
- SONY ICカードリーダ/ライタ PaSoRi RC-S380（非接触型）
- DELL ノートパソコン Latitude 7390 内蔵NFCセンサ（非接触型）
- DELL ノートパソコン Latitude 7390 内蔵スマートカードスロット（接触型）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/4a4b4061-bf72-4753-790b-3cfa271765eb.png)

## NuGetパッケージ
次のNuGetパッケージをプロジェクトに追加する。

- https://www.nuget.org/packages/PCSC/
- https://www.nuget.org/packages/PCSC.Iso7816/

## 画面構成
UIコンポーネントを下図のように配置した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/01b62078-13c1-7251-7b5a-c1288c7c4288.png)

## C# コード
冒頭に`using`ディレクティブを追加する。

```c#
using PCSC;
using PCSC.Iso7816;
using PCSC.Exceptions;
```

アプリケーション起動時、接続されているカードリーダの名前をすべてコンボボックスに追加する。

カードリーダがひとつも接続されていなければアプリケーションを終了する。
`PCSC.Exceptions.NoServiceException` がスローされたら、ドライバがインストールされていないので、やはり終了する。

```c#:FormMain_Load
private void FormMain_Load(object sender, EventArgs e) {
    // リーダーの機器情報を取得
    try {
        using (var context = ContextFactory.Instance.Establish(SCardScope.System)) {
            var readerNames = context.GetReaders();
            if (readerNames == null || readerNames.Length == 0) {
                MessageBox.Show("スマートカードリーダが見つかりません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Hand);
                Application.Exit();  // スマートカードリーダが無ければ終了する
            }
            foreach (var readerName in readerNames) {
                comboBoxDevice.Items.Add(readerName);  // カードリーダの名前をコンボボックスに追加
            }
        }
    } catch (NoServiceException) {
        MessageBox.Show("スマートカードリソースマネージャが稼働していません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Hand);
        Application.Exit();
    }
    comboBoxDevice.SelectedIndex = 0;
    radioButtonNone.Checked = true;

    // データグリッドビューの初期設定
    dataGridView.AllowUserToResizeColumns = true;
    dataGridView.AllowUserToResizeRows = false;
    dataGridView.RowHeadersVisible = false;
    dataGridView.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText;
    dataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;

    dataGridView.Columns.Add("seq", "連番");
    dataGridView.Columns.Add("datetime", "読み取り日時");
    dataGridView.Columns.Add("uid", "UID/IDm");

    dataGridView.Columns["seq"].Width = 80;
    dataGridView.Columns["datetime"].Width = 200;
    dataGridView.Columns["uid"].Width = 250;
}
```

ボタンがクリックされたら、選択したカードリーダからNFCタグのUIDを読み取り、データグリッドに追加する。
読み取り用のAPDUコマンドを組み立て、送信し、結果を受信するという動きになる。

[pcsc-sharp/Examples/ISO7816-4/Transmit/Program.cs](https://github.com/danm-de/pcsc-sharp/blob/master/Examples/ISO7816-4/Transmit/Program.cs) を参考にしている。

```c#
private void buttonRead_Click(object sender, EventArgs e) {
    var readerName = comboBoxDevice.Text;  // 選択中のカードリーダ名を得る

    using (var context = ContextFactory.Instance.Establish(SCardScope.System)) {
        try {
            using (var rfidReader = context.ConnectReader(readerName, SCardShareMode.Shared, SCardProtocol.Any)) {
                // APDUコマンドの作成
                var apdu = new CommandApdu(IsoCase.Case2Short, rfidReader.Protocol) {
                    CLA = 0xFF,
                    Instruction = InstructionCode.GetData,
                    P1 = 0x00,
                    P2 = 0x00,
                    Le = 0 // We don't know the ID tag size
                };
                // 読み取りコマンド送信
                using (rfidReader.Transaction(SCardReaderDisposition.Leave)) {
                    var sendPci = SCardPCI.GetPci(rfidReader.Protocol);
                    var receivePci = new SCardPCI(); // IO returned protocol control information.

                    var receiveBuffer = new byte[256];
                    var command = apdu.ToArray();

                    var bytesReceived = rfidReader.Transmit(
                        sendPci, // Protocol Control Information (T0, T1 or Raw)
                        command, // command APDU
                        command.Length,
                        receivePci, // returning Protocol Control Information
                        receiveBuffer,
                        receiveBuffer.Length); // data buffer

                    var responseApdu = new ResponseApdu(receiveBuffer, bytesReceived, IsoCase.Case2Short, rfidReader.Protocol);
                    if (responseApdu.HasData) {
                        // バイナリ文字列の整形
                        StringBuilder id = new StringBuilder(BitConverter.ToString(responseApdu.GetData()));
                        if (radioButtonNone.Checked) {
                            id.Replace("-", string.Empty);
                        } else if (radioButtonSpace.Checked) {
                            id.Replace("-", " ");
                        }
                        // データグリッドビューに結果を追加し、連番をカウントアップする
                        int seq = (int)numericUpDown.Value;
                        DateTime dt = DateTime.Now;
                        dataGridView.Rows.Add(seq, dt.ToString("yyyy/MM/dd HH:mm:ss"), id.ToString());
                        numericUpDown.Value = ++seq;
                    } else {
                        MessageBox.Show("このカードではIDを取得できません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    }
                }
            }
        } catch (RemovedCardException) {
            MessageBox.Show("スマートカードが取り外されたため、通信できません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Hand);
        } catch (PCSCException ex) {
            MessageBox.Show(ex.Message, "スマートカードエラー", MessageBoxButtons.OK, MessageBoxIcon.Hand);
        }
    }
}
```

## 実行結果サンプル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/a80593fd-c85a-a1ff-0ec2-315d2be196c9.png)