RC-S300をLinuxで使う
16日目 ← 17日目 → 18日目
これはKCSアドベントカレンダー2022の記事です。
はじめまして、Compassです。
ものすごく投稿遅れて大変申し訳ございません…
言い訳をすると、前の金曜日にノートPCのSDDMがナニモシテナイのに壊れたのと、内容の調査に色々と時間がかかってしまったのが理由です。
「もっと前からやればよかったんですよね?」「はい……」
この記事ではRC-S300をLinuxで使うことができたのでNFCカードの読み取りをしようと思います。前置きは手短にしようと思いますので本編に入ります。
はじめに
RC-S300とはSonyが発売しているカードリーダーです(リンク)。いわゆるPaSoRiです。RC-S300の前身にはRC-S380というモデルがありますが、こちらの情報は私が解説するまでもなく、情報は山ほど出てきます。が、RC-S300に関する情報はほとんどなく、こちらのQiita記事くらいです(私も参考にしました)。[1]というのも有名なnfcpyというライブラリが非対応なため、あまり購入を推奨されていないような印象です。が、解説のたくさんあるRC-S380はSonyのホームページには生産終了モデルの中に入っており、「今から買いたくないな…」と思っていたところ、上の記事を見つけ、購入しました。結果、使うことができましたのでここに記します。
ちなみにほとんど情報のないRC-S300ですが、Twitterで検索するとnfcpyで使えない嘆きの声ならいくつか出てきます。
以下の内容にしたがって起きた不利益等について私は責任を負いません
まあ大丈夫だと思いますが、マイナンバーカードは無理やり読み出そうとするとチップが壊れるらしいので…(無理やり読み出すってどういうことか知りませんが)
環境
OS: Manjaro Linux x86_64
Kernel: 5.10.157-1-MANJARO
入れるパッケージの詳細
pcsclite
PC/SCはWindows環境でスマートカードを扱うためのライブラリだそうです。PCSCliteはそのLinux実装です。
ccid
スマートカードのためのUSBプロトコルだそうです。よくわかってないのでぼかしています。詳しいことはWikipediaに任せます。
opensc
OpenSCはICカードを読み込むためのツール、ライブラリです。暗号化機能に焦点を置いているそうです。
とりあえずスキャンするまで
基本上のQiita記事をベースにやっていきます。
必要なパッケージのインストール
私はManjaro Linux(Arch系)のため、pacman
を使用します。他のディストロの方は適宜読み替えるなり、上のQiitaの記事を読むなりしてください。他のディストロだと結構パッケージ名が違うかもしれません。
sudo pacman -S libusb pcsclite ccid opensc pcsc-tools
サービスの有効化
systemctl enable pcscd
systemctl start pcscd
とりあえず読み込んで見る
ここまでくればとりあえず読み込めるはずです。以下のコマンドを実行してください。
pcsc_scan
ここで一番下でクルクルしていれば成功です!うまく行っていない方はsystemctl restart pcscd
をすると治るかもしれません。なんかlibccidを読み込むためらしいです。
クルクルした状態で何らかのカードを近づけてみるとカードの情報が出力されます!(離すとまた何かが出ます)
あと、Chromium系ならSFCard Viewer Web版も残高読み取りとかできましたし、パソリ メンテナンス Web版あたりでアップデートもできました(もちろん動作保証はありませんし、私も責任は負いかねます)。FireFoxはやってみましたができませんでした。ArchWikiのこのページを参考にすればできるかもしれません(未検証)。
プログラムから読み取る
まずlibnfcで書こうと思ったんですが、pacmanで入れてみましたがうまく行かず、ソースコードからビルドして使おうとしましたがうまくビルドできませんでした。
結局PC/SCを直叩きすることにしました。PC/SCはいろいろな言語から扱えるらしいです。
そして、Pythonで書こうと思ったんですがpip install
しようとしたらエラーが出て、
note: This is an issue with the package mentioned above, not pip.
とか言われたので諦めてRustで書きました。
libnfcもPythonも早々に諦めたので色々やればできるかもしれません。できた方いれば教えてください…。
Rust
これをベースに改造します。
まずCargo.toml
の[dependencies]に
[dependencies]
pcsc = "2.7.0"
dialoguer = "0.10.2"
hex-literal = "0.3.4"
を追加します。dialoguer
は別に使わなくても大丈夫です。
そして、以下のようなコードを書きます。
use std::{ffi::CString};
use dialoguer::Select;
use hex_literal::hex;
use pcsc::{Context, Scope, ShareMode, Protocols, MAX_BUFFER_SIZE};
fn main() {
let context = Context::establish(Scope::User).unwrap();
let mut readers_buf = [0; 2048];
let mut readers = context.list_readers(&mut readers_buf).unwrap();
let readers_string:Vec<_> = readers.clone().map(|r| {let cs = CString::from(r); cs.to_str().unwrap().to_string()}).collect();
let selection = Select::new()
.default(0)
.items(&readers_string)
.interact();
let Ok(index) = selection else {return};
let reader = readers.nth(index).unwrap();
println!("使用するカードリーダ: {reader:?}");
let card = context.connect(reader, ShareMode::Shared, Protocols::ANY).unwrap();
let apdu = hex!("FF CA 00 00 00");
let mut rapdu_buf = [0; MAX_BUFFER_SIZE];
let rapdu = card.transmit(&apdu, &mut rapdu_buf).unwrap();
let len = rapdu.len();
let result = &rapdu[len-2..len];
if !(*result.get(0).unwrap() == 0x90 && *result.get(1).unwrap() == 0x00){
println!("IDmの読み出しに失敗しました。")
}else {
let idm = &rapdu[..len-2];
println!("{idm:02X?}", );
}
}
このコードを実行し、FeliCaカード(Suicaとか)を読み取るとIDmが、Type-Aカード(社員証がこれだったりするらしいです)を読み取るとUIDが表示されるはずです(どちらも固定、一意)。Type-Bカード(免許証、マイナンバーカードとか)を読み取るとPUPIという値が読み取られますが、これは固定値ではないため、読み取るたびに値が変わります。このプログラム実行上の注意点として、予めカードを置いておくようにしてください。
私もやってみたところ、NFC Toolsのシリアルナンバーと一致したので成功っぽいです!
途中のapduという変数で突然16進数が出てきますが、これはAPDU(Application Protocol Data Unit)というISO/IEC 7816-4で定義されたスマートカードとの間の通信規格です。詳細は他のサイトに譲ります。下の方の参考サイトを参照してください。
カードリーダと通信をするには
上のコードでは、カードをおいてからカードと通信を行っています。では、SWITCH PROTOCOLとかTRANSPARENT EXCHANGEみたいなAPDUをR/Wに送るためにはどうすればいいのでしょうか。まず、context.connect()
の第二引数に、ShareMode::Direct
を指定します。こうすると、カードリーダーを一つのプログラムで独占し、カードリーダーと直接通信をすることができます。ここまでくれば、あとはCard::control()
で通信をするだけ…といったような解説も目にしますがこれではうまく行きません! (4時間くらい溶かした) この場合はCard::transmit()
で通信をしなければなりません。が、pcsc-rustはShareMode::Directを指定した場合にはエラーが出力され、うまく通信をすることができません。そのため、私がライブラリを改造しました。まあコミットログ見れば分かるとおり1行しか変えてないので改造っていうほどではないんですけどね。
ということでCargo.toml
の下に以下を追加してください。
[patch.crates-io]
pcsc = { git = "https://github.com/CompassSN/pcsc-rust", package = "pcsc"}
pcsc-sys = { git = "https://github.com/CompassSN/pcsc-rust", package = "pcsc-sys" }
これでCard::transmit()
を呼んでもエラーが出ないようになります。
終わりに
一応カードリーダと通信できましたし、キリのいいところまで行けたかなと思います。最初の方はLinuxで使う方法でしたが後半はLinux関係なくなりましたね…。あと情報が少なくてかなりつらかったです。
(おまけ)良さそうなクレート
カードリーダとの通信のAPDUでよく出てくるTLV(Tag, Length, Value)ですが、BER-TLVという形式らしいです。RustにはBER-TLV用のクレートがあったので貼っておきます。
(求有識者)Pollingできない問題
PollingしようとするとTLV形式が違うとか言われてPollingできない問題
(解決策)
Switch Protocolしてください。
あとはここを参考にコマンドを組み立てればいけます。が、"Data to be transmitted"の先頭、Polling Commandの前に0x06をつけている理由がよくわかりません…。
誰かご存知の方いればご教示いただきたいです…。
参考サイト
APDU関連
元リンク] ↑検索では全く引っかかりませんが神サイトです(実際"Eternal Windows"で検索しても全く引っかかりません)。Win32APIを使うときにも非常に参考になるので是非。 ↑PC/SCの拡張APDUの仕様書。コマンド関連は"Pcsc3 V2 01 09"、"Pcsc V2 01 09 Sup2"を見ればいいと思います。 ↑日本語でよくまとまっています。
↑日本語 ↑なんか消えてたのでWayback Machineです。元リンクも貼っておきます。[FeliCaについて
↑ユーザーズマニュアルとか読んだほうがいいです ↑あんまり使ってないけど良さそう
PC/SC関連
↑公式 Rustで書く場合も参考になります
ブログ等々
↑とても詳しく載っています。他のページもさせていただきました ↑コマンドの具体例について参考にしました ↑搬送波がどうのこうのとかわかりやすい ↑上のスライドの人のサイト
-
この記事のタイトルでRC-S300/Pとなっていたり、ターミナルでRC-S300/Pと記載されている理由は不明です…。ただ、私も"RC-S300"を購入しましたが"RC-S300/P"と表示されましたし、ヨドバシカメラでは"RC-S300 P"という名称で販売されていたりします。 ↩︎
Discussion
コメントありがとうございます。これはManjaro Linuxで動作動作確認をしているため、Arch系でなければパッケージ名が異なるかもしれません。お使いのディストリビューションは何でしょうか?