GNU/Linux上でSony RC-S300を使ってICカードの情報を読み取る（Rust）

Sony RC-S300をヨドバシカメラで購入したので, 家にあるICカード（交通系IC）の情報を読み取って遊んでみます。


# 環境
- ArchLinux x86_64
    - kernel: 6.12.1-arch1-1
    - DE: GNOME 47.2


# パッケージのインストールと有効化

```bash
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という形で実装されています。


# スキャンしてみる

適当なコンソールで

```bash
pcsc_scan
```
を実行した後、スマートカードをリーダーに近づけるとカードのATRを得ることができます。またIDmも表示されます。

> IDmはスマートカードに紐付けされた一意の文字列で、基本的には他のスマートカードと被らないように設定してある...らしいです。
> 
> ATRはカードリーダがスマートカードから得られた情報そのもので、どんなカードなのかを簡易的に知ることができます（例えば、Felicaに対応しているのか等）

# Rustのコード上で扱う

コードのリポジトリはここ

https://github.com/donabe8898/rc-s300-rust

重要な部分だけ解説していきます。


## APDUコマンドを送信してIDmを取得

Application Protocol Data Unit（以下APDU）はICカードを通信するための国際規格です。策定されているAPDUコマンドを用いてカードとの通信を行い、データの読み取りや書き込みを行います。

`imp.rs`
```rust
    // カードから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の残高を取得する
```rust
 // 試しに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);
    }
```

https://pcscworkgroup.com/Download/Specifications/pcsc3_v2.01.09.pdf

これも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の読み取りであれば、カードの残高だけでなく利用履歴やカードの発行場所なども知ることができるので面白そうです。


# 参考

https://wiki.archlinux.jp/index.php/%E3%82%B9%E3%83%9E%E3%83%BC%E3%83%88%E3%82%AB%E3%83%BC%E3%83%89

https://zenn.dev/compass/articles/39bb050bdaeaaa

ICOCA等のサービスコード

http://jennychan.web.fc2.com/format/suica.html

https://qiita.com/Yasuaki23/items/3109df682af2a7032f8d#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89

https://www.sony.co.jp/Products/felica/business/tech-support/data/card_usersmanual_2.0.pdf

https://tex2e.github.io/blog/protocol/jpki-mynumbercard-with-apdu