0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#でカードリーダーを用いたICカード読取

Last updated at Posted at 2025-11-26

概要

カードリーダー使っての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で同じ
読取部なんかはライブラリのサンプルを引用

イベント付与以降にカードが置かれると反応するやつ

CardReedTest.cs

/// <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カードの構造

最後に

鍵の書込は普通にデータ書込と同じ感じでいけます。

読書後は一時メモリをいつまで覚えているのかわからないので
最後にメモリの中身を書きえておくとかも必要かもしれないです。

めちゃ走り書きのようになってるので
至らぬ部分もあるかと。。。
もしここまで読んでいただけたら嬉しいかもです。

気が向いたら編集しようかなとは思ってます
まぁしないだろうな。
では、ありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?