利用PCSC協定透過讀卡機讀取全民健保卡
最近COVID-19疫情肆虐,全民健保卡突然搖身一變成為口罩領取的重要憑證,剛好有空研究一下相關的操作方式,發現其實還蠻好寫的(感謝前人包裝的PCSC wrapper class),加上網路上也有許多先進已經把APDU指令集整個sniffer完成,因此不到30分鐘就把雛型寫好了。
PS/CS相關知識
- PC/SC的全名是Personal Computer / SmartCard,是微軟創建的一個標準的通訊協定,意圖統一所有的智慧晶片卡之存取。
- 支援許多通訊界面,例如:USB、RS232、PCMCIA...
- 所有的讀卡機廠商若想要在Windows上面玩,就得自己寫PC/SC驅動程式。
- PC/SC於USB設備之調用,基本上是透過CCID(Intergrated Circuts Card Interface Device)協定來進行,CCID是USB組織制定的一個標準。
- CCID跟最終端的智慧晶片卡(ICC;Intergrated Chips Card),則是透過ISO 7816 標準協定來進行。
- APDU全名為Smart card Application Protocol Data Unit,簡單的說就是從讀卡機送資料給智慧晶片卡的封包協定,最必要的輸出格式包含了四個Bytes的標頭(CLA, INS, P1, P2)與至多65535 bytes的資料。
結論:Application > Windows PC/SC > USB > CCID > ISO 7816,大致上存取制度如上。
感謝前輩封裝好的PS/CS類別
上面講了這麼多的標準,如果用Unmanaged寫法直接去呼叫Win32 API一定會很想死,所以第一時間在nuget發現了Daniel Mueller前輩已經幫我們封裝成.NET版本的PCSC wrapper類別庫,以下是相關連結:
我在這邊把PCSC Library 5.0.0.zip放在雲端硬碟,有需要的人可自行下載。
台灣全民健保卡顯性資料汲取程式
有了上面的觀念與前輩的類別後,操作PCSC協定簡直易如反掌,以下是我所撰寫的範例程式碼,想要少寫一些namespace的人記得using一下PCSC跟PCSC.Iso7816喔。
using System;
using System.Linq;
namespace Console_Simply
{
class Program
{
public static void Main()
{ //讀卡機名稱
string cReader;
//尋找本機讀卡機設備
using (var oReaders = PCSC.ContextFactory.Instance.Establish(PCSC.SCardScope.User))
{
cReader = oReaders.GetReaders().FirstOrDefault();
if (string.IsNullOrEmpty(cReader))
{ //找不到任何PSCS讀卡機就跳走
Console.WriteLine("# 系統找不到任何讀卡機,請重新檢查硬體。");
return;
}
else
{
Console.Clear();
Console.WriteLine($"# 歡迎使用台灣全民健保卡檢視程式 / by slashview.com");
Console.WriteLine($"# 共找到「{oReaders.GetReaders().Count()} 部設備」並使用「{cReader}」讀卡機,程式運行期間請勿任意移除設備。");
}
}
//建立事件監控
using (var oMonitor = PCSC.Monitoring.MonitorFactory.Instance.Create(PCSC.SCardScope.System))
{
oMonitor.CardRemoved += (oSender, oArgs) => { Console.WriteLine("# 偵測到晶片卡移除。"); };
oMonitor.CardInserted += (oSender, oArgs) =>
{
Console.WriteLine("# 偵測到晶片卡插入。");
GetCardInfo(cReader); //讀取健保卡顯性資料
};
oMonitor.MonitorException += (oSender, oArgs) =>
{
Console.WriteLine("# 讀卡機被移除或是讀取晶片卡出現異常,請重新啟動程式。");
System.Environment.Exit(0); //強制退出
};
oMonitor.Start(cReader);
//有可能執行程式前讀卡機與卡片就都已經準備好,如此一來並不會觸發事件,因此先強制執行一次讀取看看
try
{ GetCardInfo(cReader); }
catch
{ } //若有讀取出錯就直接跳過(可能是未插卡)
//設定離開程序
System.ConsoleKeyInfo oKey;
do
{
Console.WriteLine("# 若需要離開程式,請按下ESC按鈕。");
oKey = Console.ReadKey(true);
} while (oKey.Key != System.ConsoleKey.Escape);
}
//程式結束
Console.WriteLine("# 程式結束。");
}
/// <summary>
/// 讀取台灣全民健保卡顯性資料
/// </summary>
public static void GetCardInfo(string cReader)
{
using (var oContext = PCSC.ContextFactory.Instance.Establish(PCSC.SCardScope.User))
using (var oReader = new PCSC.Iso7816.IsoReader(
context: oContext,
readerName: cReader,
mode: PCSC.SCardShareMode.Shared,
protocol: PCSC.SCardProtocol.Any
))
{
Console.WriteLine("-----");
//初始化健保卡
var oAdpuInit = new PCSC.Iso7816.CommandApdu(PCSC.Iso7816.IsoCase.Case4Short, oReader.ActiveProtocol)
{
CLA = 0x00,
INS = 0xA4,
P1 = 0x04,
P2 = 0x00,
Data = new byte[] { 0xD1, 0x58, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11 }
};
Console.WriteLine($"@ APDU InitCard: {System.BitConverter.ToString(oAdpuInit.ToArray())}");
//取得初始化健保卡回應
var oAdpuInitResponse = oReader.Transmit(oAdpuInit);
Console.WriteLine($"@ Response: SW1={oAdpuInitResponse.SW1.ToString("X")}|SW2={oAdpuInitResponse.SW2.ToString("X")}");
//檢查回應是否正確(144;00)
if (!(oAdpuInitResponse.SW1.Equals(144) && oAdpuInitResponse.SW2.Equals(0)))
{
Console.WriteLine("-----");
Console.WriteLine("# 晶片卡並非健保卡,請換張卡片試看看。");
return;
}
//讀取健保顯性資訊
var oAdpuProfile = new PCSC.Iso7816.CommandApdu(PCSC.Iso7816.IsoCase.Case4Short, oReader.ActiveProtocol)
{
CLA = 0x00,
INS = 0xCA,
P1 = 0x11,
P2 = 0x00,
Data = new byte[] { 0x00, 0x00 }
};
Console.WriteLine($"@ APDU GetProfile: {System.BitConverter.ToString(oAdpuProfile.ToArray())}");
//取得讀取健保卡顯性資訊回應
var oAdpuProfileResponse = oReader.Transmit(oAdpuProfile);
Console.WriteLine($"@ Response: SW1={oAdpuProfileResponse.SW1.ToString("X")}|SW2={oAdpuProfileResponse.SW2.ToString("X")}");
//檢查回應是否正確(144;00)
if (!(oAdpuInitResponse.SW1.Equals(144) && oAdpuInitResponse.SW2.Equals(0)))
{
Console.WriteLine("-----");
Console.WriteLine("# 健保卡讀取錯誤,請換張卡片試看看。");
return;
}
//如果有回應且具備資料的話,就將其輸出到畫面上
if (oAdpuProfileResponse.HasData)
{ //播放提示音
Console.Beep();
//位元組資料
byte[] aryData = oAdpuProfileResponse.GetData();
//文字編碼解碼器
var oUTF8 = System.Text.Encoding.UTF8;
var oBIG5 = System.Text.Encoding.GetEncoding("big5");
//建立使用者匿名物件
var oUser = new
{
cCardNumber = oUTF8.GetString(aryData.Take(12).ToArray()),
cName = oBIG5.GetString(aryData.Skip(12).Take(20).ToArray()),
cID = oUTF8.GetString(aryData.Skip(32).Take(10).ToArray()),
cBirthday = oUTF8.GetString(aryData.Skip(42).Take(7).ToArray()),
cGender = oUTF8.GetString(aryData.Skip(49).Take(1).ToArray()) == "M" ? "男" : "女",
cCardPublish = oUTF8.GetString(aryData.Skip(50).Take(7).ToArray())
};
//輸出資料
Console.WriteLine("-----");
Console.WriteLine($"卡號 :{oUser.cCardNumber}");
Console.WriteLine($"姓名 :{oUser.cName}");
Console.WriteLine($"身分證號:{oUser.cID}");
Console.WriteLine($"生日 :{oUser.cBirthday}");
Console.WriteLine($"性別 :{oUser.cGender}");
Console.WriteLine($"發卡日期:{oUser.cCardPublish}");
Console.WriteLine("-----");
}
}
}
}
}
大家可以發現這個PCSC wrapper類別竟然還貼心地附上事件監控Monitor的類別,簡直超級佛心,讓我們這些晚輩可以快快樂樂運用上事件監控的寫法,最後利用ILMerge合併PCSC.dll與PCSC.Iso7816.dll這兩個類別庫後,附上編譯好的單一可執行檔案讓大家可以測試看看:
台灣全民健保卡測試程式(.Net Framework 4.8)
程式運行畫面: