WindowsでMiFareカード読み書きしてあれこれした話

# 概要

唐突に発生したプロジェクトでMiFareという規格のスマートカード使って別の製品とお話しなきゃいけなくなった。
用意されたリーダーライターは[ntt-comのACR1251CL](http://amzn.to/2opIDM6)
前提知識はゼロでレディーファイッ!

('A`) ﾝﾓｰ

その結果

- Windowsで読み書きしようとしても情報が少なくて大変
- 用意されてるI/Fはどえらい低レベルなAPIですげー面倒
- githubには大抵のものはある(誇張
- onobotnyに足向けて寝れなくなった

## 色々調査

![スクリーンショット (45).png](https://qiita-image-store.s3.amazonaws.com/0/199750/2a706d22-539a-d64e-3273-902f346b7d54.png)

Windows10のPCにACR1251CLを接続して(カードを載せて)デバイスマネージャで見るとこんな感じに見える。

WindowsからはWinSCardというインターフェースでやり取りすればいいらしいというのはわかったので調べてみる。

- [Smart Card API \(Windows\)](https://msdn.microsoft.com/en-us/library/dd627646(v=vs.85).aspx)
- [スマートカード:eternalwindows](http://eternalwindows.jp/security/scard/scard00.html)

おおよそのところ

1. SCardEstablishContext でSmartCardプロバイダに接続して
2. SCardListReaders でリーダーに接続して
3. SCardConnect でカードそのものに接続して
4. SCardTransmit でAPDUデータを飛ばして色々処理
5. SCardDisconnect/ SCardFreeMemory/SCardReleaseContext で後始末。

という流れのようだ。
そしてカードに対する処理そのものであるAPDUデータは[ISO7816 part 4 section 6 with Basic Interindustry Commands \(APDU level\)](http://cardwerk.com/smart-card-standard-iso7816-4-section-6-basic-interindustry-commands/) で定義されている。

コマンド体型としては

|  | 説明 | 長さ(byte) |
|:-:|:-:|:-:|
|CLA|クラス|1|
|INS|命令|1|
|P1|パラメータ1|1|
|P2|パラメータ2|1|
|Lc|データ長|1か3|
|Data|データ|Lc(可変)|
|Le|受信バッファ長|1か3|

というバイト配列になっていて `0xFFB0000510`[^1]みたいなコマンドを送るとコマンドに応じた返答が返ってくるようだ。

･･･。



**そ う い う こ と が し た い ん じ ゃ な い ん だ よ !!!**
![スクリーンショット (46).png](https://qiita-image-store.s3.amazonaws.com/0/199750/267f3b09-3b1a-639f-2622-4f3842d6ac74.png)

スマートカードの中のデータにアクセスして、目的の場所を書き換えれらればそれでいいんだよ。


## もうちょっとスマートにアクセスできる仕組みは無いのか
~~SmartCard だけに~~

とか思ったらさすがのgithub。なんでもあるぜ。

[onovotny/MiFare: MiFare Classic support for Windows Phone 8\.1, Windows Store 8\.1 and Windows Desktop apps](https://github.com/onovotny/MiFare)

サンプルも完備 [MiFare/MainWindow\.xaml\.cs at master · onovotny/MiFare](https://github.com/onovotny/MiFare/blob/master/samples/MiFareReader.Desktop/MainWindow.xaml.cs)

```c#
//カードが接続されたら
private async Task HandleCard(CardEventArgs args) {
    try {
        //(前のカードが残ってたら消しておく)
        card?.Dispose();

        //カードのインスタンス作って
        card = args.SmartCard.CreateMiFareCard();
        var localCard = card;

        //カード情報引いたり
        var cardIdentification = await localCard.GetCardInfo();
        DisplayText("Connected to card\r\nPC/SC device class: " + cardIdentification.PcscDeviceClass.ToString() + "\r\nCard name: " + cardIdentification.PcscCardName.ToString());

        // MiFareカードなら
        if (cardIdentification.PcscDeviceClass == MiFare.PcSc.DeviceClass.StorageClass
             && (cardIdentification.PcscCardName == CardName.MifareStandard1K || cardIdentification.PcscCardName == CardName.MifareStandard4K)) {

            // 2セクタ0ブロックを読み込み
            var data = await localCard.GetData(2, 0, 48);
            // ダンプしたり何なりする

            // 2セクタ1ブロックを0フィル
            await localCard.SetData(2,1, Enumerable.Range(0,16).Select<byte>(i=>0x00).ToArray());

            //更新を適用
            await localCard.Flush();
        }
    } catch (Exception e) {
        PopupMessage("HandleCard Exception: " + e.Message);
    }
}
```
![ほー、いいじゃないか こういうのでいいんだよ こういうので](http://xn--u9j9e1eqds75s687ao4iq05b49x.com/wp-content/uploads/2017/02/SnapCrab_No-0061-5.jpg)


## データ読むにも書くにも必要なMiFareカードの仕様のはなし(雑)

onovotny先生のお陰でデータアクセスできるようになったとはいえ、データ読み書きするためには内部のデータ構造がある程度分かってないとつらいのでまとめてみる。

[AN1304 NFC Type MIFARE Classic Tag Operation](https://www.nxp.com/docs/en/application-note/AN1304.pdf)

Mifareの内部はセクターと呼ばれる64バイトごとの領域が並んでいる。

![Mifare 1K memory organization](https://qiita-image-store.s3.amazonaws.com/0/199750/7430918a-2fa3-bcdc-2fc8-bf5440dfdf6b.png)

各セクターは16バイトごとの3つのデータブロックと、16バイトの権限ブロックからなる。

![あるカードのダンプ](https://qiita-image-store.s3.amazonaws.com/0/199750/82b244ce-93fc-69af-6316-0ad96ee0da5e.png)

権限ブロックは6バイトの**KeyA**と3バイトのパーミッションマスク、1バイトのユーザーエリア、6バイトの**KeyB** で成り立っている。
この例ではblock 0-2まではKeyA KeyBともに読み書き可能で、権限ブロックのBlock 3はKeyAでだいたい読み書き可能になっているようだ。

MiFare Classic 1K/4K ではデータの読み書きをする前に対象のセクターに対しログインをする必要があり、その際に対象のキー(KeyAまたはKeyB)と一致したバイト列を指定する必要がある。
ログインしたキーとキーの権限で読み書き可能になっていなければオペレーションが失敗する、というセキュリティ構成になっている。[^2]

要するにMiFareカードのデータを読み書きするためには

1. 読み書きするアドレス(セクター+ブロック)
2. 読み書きするためのキー情報

が揃っていればOKなわけだ。(ライトな暴論)

アクセス権限はこのへんで計算しよう。 
[MIFARE Classic 1K Access Bits Calculator](http://calc.gmss.ru/Mifare1k/)


## そうと決まればコーディングだ。

バグっちぃコードを何箇所か直したのがこちら。
[qyen/MiFare](https://github.com/qyen/MiFare)

### ログインするキーを指定してカードを開く

ちょうどいいサンプルが入ってた。

```c#:MiFare/Classic/FactoryMethod.cs
public static MiFareCard CreateMiFareCard(this SmartCard card) {
    if (card == null)
        throw new ArgumentNullException(nameof(card));
    var keys = from sector in Enumerable.Range(0, 40)
                select new SectorKeySet {
                    Sector = sector,
                    KeyType = KeyType.KeyA,
                    Key = Defaults.KeyA
                };

    return CreateMiFareCard(card, keys.ToList());
}
```
セクターに対するキーの配列を作ってCreateMiFareCard()に渡してやればいいと。

ということで

```c#:sample.cs
private async Task HandleCard(CardEventArgs args) {
    try {
        //(前のカードが残ってたら消しておく)
        card?.Dispose();
        //こうして
        var keySet = new List<SectorKeySet>(){
            new SectorKeySet(){Sector=1,KeyType=KeyType.KeyA,"FFFFFFFFFFFF"),
            new SectorKeySet(){Sector=1,KeyType=KeyType.KeyB,"A0A1A2A3A4A5"),
            new SectorKeySet(){Sector=2,KeyType=KeyType.KeyA,"000000000000"),
            new SectorKeySet(){Sector=2,KeyType=KeyType.KeyB,"BBBBBBAAAAAA"),
               // : 以下略
        }
        //こうじゃ
        card = args.SmartCard.CreateMiFareCard(keySet);
```

みたいな。

### KeyA KeyB 権限マスクを書き換える

Sector#FlushTrailer() でいける。

```c#:sample.cs

//セクターを読んで
var sector = card.GetSector(2);
//(必要なら)データブロックごとの権限を設定して
foreach (var area in sector.Access.DataAreas) {
    area.Read = DataAreaAccessCondition.ConditionEnum.KeyAOrB;
    area.Write = DataAreaAccessCondition.ConditionEnum.KeyB;
    area.Increment = DataAreaAccessCondition.ConditionEnum.KeyB;
    area.Decrement = DataAreaAccessCondition.ConditionEnum.KeyB;
}
//こうじゃ
await sector.FlushTrailer("FFFFFFFFFFFF", "FF00FF00FF00");

```


## こんな感じで

サンプル見ながら組んだらいけた。onovotny先生ありがとう。


## 参考

カードの中身を参照するのにちょうどいいツールが見当たらなくてすげぇ苦労した。
やっとのことで探し当てたこれ([Cardpeek](http://pannetrat.com/Cardpeek/))がもう最高で手放せないので使ってみると良いよ。




-----
[^1]: ちなみに`0xFFB0000510`で 0x05番目のセクタを0x10バイト分読み込む(0xB0)という意味。わかるかこんなもん。
[^2]: どうやらこの辺の仕組みは既に割られていてあんまり意味ないという話もあるみたいだけど。