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


概要

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

('A`) ンモー

その結果

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

色々調査

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

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

おおよそのところ

  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) で定義されている。

コマンド体型としては

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

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

・・・。

そ う い う こ と が し た い ん じ ゃ な い ん だ よ !!!

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

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

SmartCard だけに

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

onovotny/MiFare: MiFare Classic support for Windows Phone 8.1, Windows Store 8.1 and Windows Desktop apps

サンプルも完備 MiFare/MainWindow.xaml.cs at master · onovotny/MiFare

//カードが接続されたら
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);
    }
}

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

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

AN1304 NFC Type MIFARE Classic Tag Operation

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

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

権限ブロックは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

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

バグっちぃコードを何箇所か直したのがこちら。
qyen/MiFare

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

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

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()に渡してやればいいと。

ということで

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() でいける。

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)がもう最高で手放せないので使ってみると良いよ。



  1. ちなみに0xFFB0000510で 0x05番目のセクタを0x10バイト分読み込む(0xB0)という意味。わかるかこんなもん。 

  2. どうやらこの辺の仕組みは既に割られていてあんまり意味ないという話もあるみたいだけど。