Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

概要

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

('A`) ンモー

その結果

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

色々調査

スクリーンショット (45).png

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みたいなコマンドを送るとコマンドに応じた返答が返ってくるようだ。

・・・。

そ う い う こ と が し た い ん じ ゃ な い ん だ よ !!!
スクリーンショット (46).png

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

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

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バイトごとの領域が並んでいる。

Mifare 1K memory organization

各セクターは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. どうやらこの辺の仕組みは既に割られていてあんまり意味ないという話もあるみたいだけど。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away