概要
カードリーダー使ってのICカード読書にすこし触れる機会があった
忘れないように議事録作成しとこう的な
MifareはID取得~読書を
FeliCaはID取得までを
ICカード周りの理解が浅すぎてまだ何もわかってない・・・
他の有識者の方が残されてるもののキメラ的な感じにはなってますので
そちらを参考にされる方が有意義かもです。
だいぶ端折ったりするので
そのままコピペすると動作しない場合があるのはご愛嬌
普通に読みづらかったら素直にごめんない。
環境とか
| 機器 | 備考 |
|---|---|
| OS | windows 11 Pro |
| 言語 | C# .NET8 |
| 開発環境 | VisualStudio |
| リーダー | Sony PaSoRi RC-S300/S |
| カード | Mifare1K FeliCa Lite-S |
| ライブラリ | PCSC-sharp |
カードリーダーの接続確認
今回は使用リーダーが決まっているので
接続されているデバイス一覧から対象デバイスを指定して確認をしておきます。
/// <summary>
/// Context
/// </summary>
private ISCardContext? context;
string CardReaderName="SONY FeliCa Port/PaSoRi 4.0 0";
this.context = ContextFactory.Instance.Establish(SCardScope.System);
var readerNames = context.GetReaders();
if (readerNames == null || readerNames.Length == 0 ||
!readerNames.Any(v => v == CardReaderName)){
throw new Exception("対象のICカードリーダーが見つかりません。");
カードリーダーを読取待機させる
監視イベントについては[StatusChanged]か[CardInserted]
今回は[StatusChanged]を使ったのでそれ
/// <summary>
/// Monitor
/// </summary>
private ISCardMonitor? monitor;
//インスタンスの作成
this.monitor = MonitorFactory.Instance.Create(SCardScope.System);
// イベント登録
this.monitor.StatusChanged += Monitor_StatusChanged;
// 開始
this.monitor.Start(CardReaderName);
public void Monitor_StatusChanged(object sender, StatusChangeEventArgs e)
{
if (e != null && e.NewState == SCRState.Present && e.LastState == SCRState.Empty)
{
//読取処理
~~~
}
}
組み合わせてIDの読取する
読取処理の中身もつけてIDm/UIDを取得してきます。
ID取得に関してはMifare/FeliCaで同じ
読取部なんかはライブラリのサンプルを引用
イベント付与以降にカードが置かれると反応するやつ
/// <summary>
/// Context
/// </summary>
private ISCardContext? context;
/// <summary>
/// Monitor
/// </summary>
private ISCardMonitor? monitor;
/// <summary>
/// Load
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_Load(object sender, EventArgs e)
{
string CardReaderName="SONY FeliCa Port/PaSoRi 4.0 0";
try
{
this.context = ContextFactory.Instance.Establish(SCardScope.System);
var readerNames = context.GetReaders();
if (readerNames == null || readerNames.Length == 0 ||!readerNames.Any(v => v == CardReaderName))
throw new Exception("対象のICカードリーダーが見つかりません。");
}
catch (NoServiceException nsex)
{
Debug.WriteLine($"カードエラー : {nsex.InnerException}");
throw;
}
//-----------------------------------------------------------
//対象リーダーが見つかればモニター開始する
//-----------------------------------------------------------
//インスタンスの作成
this.monitor = MonitorFactory.Instance.Create(SCardScope.System);
// イベント登録
this.monitor.StatusChanged += Monitor_StatusChanged;
// モニター開始
this.monitor.Start(CardReaderName);
}
/// <summary>
/// カードの読取り状態変更イベント
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Monitor_StatusChanged(object sender, StatusChangeEventArgs e)
{
if (e != null && e.NewState == SCRState.Present && e.LastState == SCRState.Empty)
{
// ID取得
var cardId = this.ReadCardId();
if(!string.IsNullOrEmpty(cardId))
Debug.WriteLine($"カードID : {cardId}");
else
Debug.WriteLine($"カード接続不良");
}
}
/// <summary>
/// 実際のカード読取り処理
/// </summary>
/// <returns></returns>
private string ReadCardId()
{
string cardId = string.Empty;
ICardReader? reader = null;
try
{
reader = context!.ConnectReader(CardReaderName, SCardShareMode.Shared, SCardProtocol.Any);
}
catch (Exception)
{
// 下記で返るのでとりあえずExceptionで例外は握りつぶす
}
//カードのと接続ができなかったら空で返る
if (reader == null)
{
return cardId;
}
// APDUコマンドの作成
var apdu = new CommandApdu(IsoCase.Case2Short, reader.Protocol)
{
CLA = 0xFF, //(固定)
Instruction = InstructionCode.GetData, //命令
P1 = 0x00,
P2 = 0x00,
Le = 0 // We don't know the ID tag size
};
// 読み取りコマンド送信
using (reader.Transaction(SCardReaderDisposition.Leave))
{
var sendPci = SCardPCI.GetPci(reader.Protocol);
var receivePci = new SCardPCI(); // IO returned protocol control information.
var receiveBuffer = new byte[256];
var command = apdu.ToArray();
var bytesReceived = reader.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, reader.Protocol);
if (responseApdu.HasData)
{
// バイナリ文字列の整形
StringBuilder id = new StringBuilder(BitConverter.ToString(responseApdu.GetData()));
cardId = id.ToString();
}
}
return cardId;
}
一応ここまででID取得がそれなりにできるはず
一旦区切り。
参考させていただいたサイト
C# PCSC Sharpを使ってICカードのIDを読み取る
C#でNFC(Felica/Mifare)の読み取り
Mifareの読書とかとか
ライブラリも対応しないらしく
普通に効率悪かったり間違ってる部分もあったりするかもしれませんが、
一応動作はしてるので問題ないとは思います。
ただし、引用等していただく場合は、自己責任でお願いいたしますい
一旦寄り道..カード情報の取得
特に活用方があるかはわかりませんが
命令の[CA]で取得できるものが何個かあるうちの一つでカード種が取得可能なのでそれを取得
システム側でカード種別を切り分けたいときとかに使用可能かも?
APDU投げるのはだいたい同じなので割愛
apdu = new CommandApdu(IsoCase.Case2Short, reader.Protocol)
{
CLA = 0xFF,
Instruction = InstructionCode.GetData,
P1 = 0xF1,
P2 = 0x00,
Le = 0
};
//割愛
//返ってきたデータを格納
var ResData = responseApdu.GetData();
//UTF-8でエンコードと末尾に[\0]入ってるので削除
var CardType = Encoding.GetEncoding("UTF-8").GetString(ResData).Replace("\0", "");
// Mifare1Kなら"Mifare 1K"とかが入ってきます。
// カードの種類によって何が入っているかは要検証ください。
Mifareの仕様について
Mifareは標準で認証鍵がないとアクセスできない。
データ部については
1Kではセクタが0~15
各セクタに0~3ブロック それぞれ16バイト
が存在します。
ただし、各セクタの3ブロック目には自セクタの鍵A,Bと各鍵のブロックへのアクセス権限が
0セクタの0ブロックにはUID(ReedOnly)が記載されてますので
基本的にはそれ以外を読み書きになります。
がわかっていればいいんじゃないかなと思ってます。
鍵についての注意点
仕様書上のデフォルト鍵はA,Bともに[FF-FF-FF-FF-FF-FF]ですが
鍵Aについては基本WriteOnlyのため読込を行っても[00-00-00-00-00-00]が返ってきます。
ただ普通に書込ができるので間違えて書き換えてしまうと以降
何もできないセクタを作成してしまう可能性があります。
とういうかしました。一枚無駄に
それでも鍵を書き換える場合は
参考サイトにあるアクセス権限計算サイトを活用するのをおすすめします。
で読書の手順としては
1.一時メモリへ鍵を記憶
2.その後ブロックに対してメモリに保存している鍵でアクセス(認証)
3.データの読書
の段階を踏まないと行けないらしい
1,2なんかはライブラリにて対応してないらしいので
それらしく投げてるって感じです。
というわけで
一時メモリに鍵を記憶
メモリ領域がどれくらいあるかとかはちょっと確認してないですが、
書いていきます..
//第一引数が[Case4Short]に変わってます。
apdu = new CommandApdu(IsoCase.Case4Short, reader.Protocol)
{
CLA = 0xFF, //FF固定
Instruction = InstructionCode.ExternalAuthenticate, //命令種 0x82
P1 = 0x00, //00固定
P2 = 0x00, //メモリ番号(いくつまであるかは不明とりあえず00)
Le = 0x06, //データの長さ(鍵のバイト数)
Data = [FF,FF,FF,FF,FF,FF]//鍵の中身(Aの初期値)
};
//割愛
var responseApdu = new ResponseApdu(receiveBuffer, bytesReceived, IsoCase.Case4Short, reader.Protocol);
//Responseが10進数で返ってきてるはずなのでこんな感じで成否のチェック
if (responseApdu.SW1 != 144 || responseApdu.SW2 != 0)
Debug.WriteLine($"失敗");
else
Debug.WriteLine($"成功");
メモリに記憶している鍵でブロックの認証
こちらはデータ部が少しややこしやかも
・0,1要素目
固定らしい
・2要素目
ロック番号の指定は[セクタ番号 × 4 + ブロック番号]で指定をします。
0セクタの0ブロックなら0x00
1セクタの0ブロックなら0x04
3セクタの2ブロックなら0x14
・3要素目
メモリに覚えてる鍵がAなのかBなのかをしていします。
Aの場合は0x60Bの場合は0x61を指定
・4要素目
メモリ番号
前記の[P2]に指定したメモリ番号をそのまま書けば大丈夫です。
apdu = new CommandApdu(IsoCase.Case4Short, reader.Protocol)
{
CLA = 0xFF, //命令クラス FF固定
Instruction = InstructionCode.InternalAuthenticate, //認証_0x86
P1 = 0x00, //00固定
P2 = 0x00, //00固定
Le = 0x05, //データ部の長さ
Data = [0x01, 0x00, 0x04, 0x60, 0x00]
//[01固定,00固定,認証先ブロック番号,(60 or 61),使用メモリ番号]
};
ブロックの読書
読取と書込をそれぞれ
//読取のAPDUコマンド
apdu = new CommandApdu(IsoCase.Case2Short, reader.Protocol)
{
CLA = 0xFF, //命令クラス FF固定
Instruction = InstructionCode.ReadBinary, //Read_0xB0
P1 = 0x00,//00固定
P2 = 0x04, //読取ブロック番号(0~127)
Le = 0x10, //返ってくるデータ長(16バイトなので0x10)
};
var responseApdu = new ResponseApdu(receiveBuffer, bytesReceived, IsoCase.Case4Short, reader.Protocol);
//responseApdu.HasDataでデータが返ってきたか判断できる
//取り出す場合はresponseApdu.GetData()
//-------------------------------------------------------------------------------------------------------------------
//書込のAPDUコマンド
var apdu = new CommandApdu(IsoCase.Case4Short, reader.Protocol)
{
CLA = 0xFF, //命令クラス FF固定
Instruction = InstructionCode.UpdateBinary, //Read_0xB0
P1 = 0x00,//00固定
P2 = 0x04, //書込ブロック番号
Le = 0x10, //書込データ長(16バイトなので0x10)
Data = [0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF],
};
var responseApdu = new ResponseApdu(receiveBuffer, bytesReceived, IsoCase.Case4Short, reader.Protocol);
//0x90が返ってきたかで成否判断
//if (responseApdu.SW1 != 144 || responseApdu.SW2 != 0)
参考させていただいたサイトPart2
MiFare関係
公式ドキュメントデータシート
アクセス権限計算
Mifareカードの構造
最後に
鍵の書込は普通にデータ書込と同じ感じでいけます。
読書後は一時メモリをいつまで覚えているのかわからないので
最後にメモリの中身を書きえておくとかも必要かもしれないです。
めちゃ走り書きのようになってるので
至らぬ部分もあるかと。。。
もしここまで読んでいただけたら嬉しいかもです。
気が向いたら編集しようかなとは思ってます
まぁしないだろうな。
では、ありがとうございました。