概要
唐突に発生したプロジェクトでMiFareという規格のスマートカード使って別の製品とお話しなきゃいけなくなった。
用意されたリーダーライターはntt-comのACR1251CL
前提知識はゼロでレディーファイッ!
('A`) ンモー
その結果
- Windowsで読み書きしようとしても情報が少なくて大変
- 用意されてるI/Fはどえらい低レベルなAPIですげー面倒
- githubには大抵のものはある(誇張
- onobotnyに足向けて寝れなくなった
色々調査
Windows10のPCにACR1251CLを接続して(カードを載せて)デバイスマネージャで見るとこんな感じに見える。
WindowsからはWinSCardというインターフェースでやり取りすればいいらしいというのはわかったので調べてみる。
おおよそのところ
- SCardEstablishContext でSmartCardプロバイダに接続して
- SCardListReaders でリーダーに接続して
- SCardConnect でカードそのものに接続して
- SCardTransmit でAPDUデータを飛ばして色々処理
- 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 |
というバイト配列になっていて 0xFFB0000510
1みたいなコマンドを送るとコマンドに応じた返答が返ってくるようだ。
・・・。
そ う い う こ と が し た い ん じ ゃ な い ん だ よ !!!
スマートカードの中のデータにアクセスして、目的の場所を書き換えれらればそれでいいんだよ。
もうちょっとスマートにアクセスできる仕組みは無いのか
SmartCard だけに
とか思ったらさすがのgithub。なんでもあるぜ。
サンプルも完備 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カードのデータを読み書きするためには
- 読み書きするアドレス(セクター+ブロック)
- 読み書きするためのキー情報
が揃っていればOKなわけだ。(ライトな暴論)
アクセス権限はこのへんで計算しよう。
MIFARE Classic 1K Access Bits Calculator
そうと決まればコーディングだ。
バグっちぃコードを何箇所か直したのがこちら。
qyen/MiFare
ログインするキーを指定してカードを開く
ちょうどいいサンプルが入ってた。
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()に渡してやればいいと。
ということで
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() でいける。
//セクターを読んで
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)がもう最高で手放せないので使ってみると良いよ。