Sony RC-S300をヨドバシカメラで購入したので, 家にあるICカード(交通系IC)の情報を読み取って遊んでみます。
環境
- ArchLinux x86_64
- kernel: 6.12.1-arch1-1
- DE: GNOME 47.2
パッケージのインストールと有効化
sudo pacman -S pcsclite ccid opensc libusb pcsc-tools
sudo systemctl enbale --now pcscd
PC/SC
とは?
PC/SC
はPersonal Computer / Smart Cardの略でざっくり説明すると、Microsoft Windows上でスマートカードを扱うためのライブラリ及びAPI実装みたいです。
LinuxではPCSCLiteという形で実装されています。
スキャンしてみる
適当なコンソールで
pcsc_scan
を実行した後、スマートカードをリーダーに近づけるとカードのATRを得ることができます。またIDmも表示されます。
IDmはスマートカードに紐付けされた一意の文字列で、基本的には他のスマートカードと被らないように設定してある...らしいです。
ATRはカードリーダがスマートカードから得られた情報そのもので、どんなカードなのかを簡易的に知ることができます(例えば、Felicaに対応しているのか等)
Rustのコード上で扱う
コードのリポジトリはここ
重要な部分だけ解説していきます。
APDUコマンドを送信してIDmを取得
Application Protocol Data Unit(以下APDU)はICカードを通信するための国際規格です。策定されているAPDUコマンドを用いてカードとの通信を行い、データの読み取りや書き込みを行います。
imp.rs
// カードからIDmを取得する
pub fn get_idm(&mut self, card: &pcsc::Card) {
let idm_cmd = hex!("FF CA 00 00 00"); // どういったカードなのかを知るコマンド
let mut buf = [0; MAX_BUFFER_SIZE];
match card.transmit(&idm_cmd, &mut buf) {
Ok(res_apdu) => {
let res_len = res_apdu.len();
let result_code = &res_apdu[res_len - 2..res_len];
if !(*result_code.get(0).unwrap() == 0x90 && *result_code.get(1).unwrap() == 0x00) {
self.idm = Err(pcsc::Error::InvalidAtr); // 適当にエラーを返す(無効な値)
println!("> IDmの読み出しに失敗");
return;
} else {
self.idm = Ok(Vec::from(&res_apdu[0..8]))
}
}
Err(err) => {
eprintln!("APDUコマンドの送信(IDm読み取り)に失敗: {}", err);
self.idm = Err(pcsc::Error::CommError); // 適当にエラーを返す2(通信エラー)
return;
}
}
}
FF CA 00 00 00
という16進数の文字列がAPDUコマンドになります。
ICOCAの残高を取得する
// 試しにicocaの残高を読み取ってみる
pub fn icoca_bal(&mut self, card: &pcsc::Card) {
// ICOCA属性情報のサービスコードは0x008B
// http://jennychan.web.fc2.com/format/suica.html
let mut buf = [0; MAX_BUFFER_SIZE];
let select_file_cmd = hex!("FF A4 00 01 02 8B 00"); // サービスコードはリトルエディアンで指定
let read_binary_cmd = hex!("FF B0 00 00 00");
let r = match card.transmit(&select_file_cmd, &mut buf) {
Ok(responce) => responce,
Err(err) => {
eprintln!("APDUコマンドの送信に失敗: {}", err);
std::process::exit(1);
}
};
println!("select file cmd: {:02X?}", r);
let r = match card.transmit(&read_binary_cmd, &mut buf) {
Ok(responce) => responce,
Err(err) => {
eprintln!("APDUコマンドの送信に失敗: {}", err);
std::process::exit(1);
}
};
println!("read binary: {:02X?}", r);
// 残高表示
// 10進数へ変換
let comb = ((r[12] as u16) << 8) | (r[11] as u16);
println!("💰残高: {} 円", comb);
}
これもPC/SCのAPDU仕様書(PDF)を参考にコマンドを送信します。
簡単に説明するとFF A4 00 01 02 8B 00
で読み取る場所を選択し、FF B0 00 00 00
でバイナリデータを取得します。なお、AP選択の際に必要なサービスコードはリトルエディアンで記入します(今回だとICOCAのサービスコードは0x008B
なので8B 00
をFF A4...
の末尾につける。)
おわりに
ICOCAやSuicaの読み取りであれば、カードの残高だけでなく利用履歴やカードの発行場所なども知ることができるので面白そうです。
参考
ICOCA等のサービスコード