WebUSBでFeliCaのIDmとMIFAREのIDも読み込む

# はじめに
[WebUSBでFeliCaの一意なIDであるIDmを読む / Qiita](https://qiita.com/saturday06/items/333fcdf5b3b8030c9b05)
こちらの記事を参考に、FeliCaだけではなく、MIFAREのIDも拾いたい。
いや、むしろ、どちらも拾えるものをということで、公開していただいているgithubをフォークして、書き加えてみましたので、
経緯、やったこと、調べたことをこちらにまとめておこうと思います。

結果のソースだけあればいいという人は、完成物をDLしてご利用ください。

## 新バージョンを公開しました
2023/06/16にRC-S300に対応しました。以下に別途記事を掲載しています。
合わせてご確認ください。

https://qiita.com/MarioninC/items/b5c59e78f3e23c06b83f

## 注意
それらしく動いてますが、pythonは読んだことないわ、仕様書は難しくて頭痛くなるわで、
サンプルのパケットを解析して、コピペするという作業をしてますので、
追加した内容については、どうして動いているのかということは分かっていません。
使う際は自己責任で。もしくはコメントを頂ければ。

## 完成物
https://github.com/marioninc/webusb-felica
元ネタと同様で、権利フリーです。

## 要件
Google Chrome最新（2020/04/01現在）
PaSori(RC-S380)
MIFARE Classicカード
Windows10(MacOSや、Linuxですと、ドライバーを書き換えず動くようです)　[（Windowsで）WebUSBでPasoriを扱ってみる / Qiita](https://qiita.com/frameair/items/abcaebbd654c304a0906)

## 元ソースとの変更箇所
####１．Google Chromeのバージョンアップでの仕様変更対応
>最近（2019/03/27）、自分で書いた方法に従って実験してみたのですが、Windowsで動作させる事が出来なくなっていました。
下記の様なエラーが発生して、正常に動作しません。
原因は不明なのですが、変わった事と言えば、ChromeのVersionが上がった位なので、それが何か影響しているのでしょうか…。
後は、Windows 10のUpdateが入ったこと、エディションがHomeからProに変わった事くらいですが、それは関係無いと思っています。
因みに解決策は下記の通り。
ソースコード（html）をローカルに保存して、185行目位にある、protocolCodeの代わりに、productIdを指定します。

引用元：[（Windowsで）WebUSBでPasoriを扱ってみる | 補足（2019/3/27追記） / Qiita](https://qiita.com/frameair/items/abcaebbd654c304a0906#%E8%A3%9C%E8%B6%B32019327%E8%BF%BD%E8%A8%98)
についての修正

#### ２．今回の本題
```javascript:MIFAREのID取得部分
await send(device,[0x00,0x00,0xff,0xff,0xff,0x03,0x00,0xfd,0xd6,0x06,0x00,0x24,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd707002200
  
  await send(device,[0x00,0x00,0xff,0xff,0xff,0x06,0x00,0xfa,0xd6,0x00,0x02,0x03,0x0f,0x03,0x13,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd701002800
  
  await send(device, [0x00, 0x00, 0xff, 0xff, 0xff, 0x28, 0x00, 0xd8, 0xd6, 0x02, 0x00, 0x18, 0x01, 0x01, 0x02, 0x01, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x08, 0x08, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0e, 0x04, 0x0f, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x06, 0x4b, 0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd703002600

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x0c,0x00,0xf4,0xd6,0x02,0x01,0x00,0x02,0x00,0x05,0x01,0x00,0x06,0x07,0x07,0x0b,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd703002600

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x05,0x00,0xfb,0xd6,0x04,0x36,0x01,0x26,0xc9,0x00]);
  await receive(device, 6);
  await receive(device, 20);
  // <<< 0000ffffff0900f7d705000000000804001800

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x06,0x00,0xfa,0xd6,0x02,0x04,0x01,0x07,0x08,0x14,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd703002600

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x06,0x00,0xfa,0xd6,0x02,0x01,0x00,0x02,0x00,0x25,0x00]);
  await receive(device, 6);
  await receive(device, 13);
  // <<< 0000ffffff0300fdd703002600

  await send(device, [0x00,0x00,0xff,0xff,0xff,0x06,0x00,0xfa,0xd6,0x04,0x36,0x01,0x93,0x20,0x3c,0x00]);
  await receive(device, 6);
  let idt = (await receive(device, 22)).slice(15, 19);
  // <<< 0000ffffff0c00f4d705000000000800000000490c00
  if (idt.length > 2) {
    let idtStr = '';
    for (let i = 0; i < idt.length; i++) {
      if (idt[i] < 16) {
        idtStr += '0';
      }
      idtStr += idt[i].toString(16);
    }
    idmMessage.innerText = "Card Type : MIFARE  カードのID: " + idtStr;
    idmMessage.style.display = 'block';
    waitingMessage.style.display = 'none';
    return;
  } else {
    idmMessage.style.display = 'none';
    waitingMessage.style.display = 'block';
  }
```

の2か所です。

# なぜ改造したのか
この度、WEB上で動く勤怠管理システム上で、ICカードタッチで出退勤をしたいという話をいただきました。
Google先生に聞いてみると[元ネタの記事](https://qiita.com/saturday06/items/333fcdf5b3b8030c9b05)が出てきたので、
これで簡単にできそうだぞと、とりあえずPaSoriを用意して、社員証をかざしても、あれ、IDが出ない……

このIDカードFeliCa対応のものではなく、MIFARE Classicと呼ばれる規格のもののようで、
使い捨てを前提として、IDが一意ではない（IDが被らないように管理する機関がない）カードらしいのです。
このことが原因で、IDを取得するためのリクエストコマンドも違えば、返ってくる応答も違うという具合で、FeliCa的にはIDmが取得できなかっただけでした。

## さて、どうしたものかという話
そもそもデバイス関係は専門外で、JIS規格の仕様書は入手しても、不慣れなので読むのが大変。
元ネタの元ネタになっているライブラリはpythonで書かれているようで、pythonも触ったことがない。
[nfcpy](https://github.com/nfcpy/nfcpy)は、[Windows で nfcpy のセットアップ / Qiita](https://qiita.com/mizutoki79/items/da903fe5a86fe4087429)を見ながら、
とりあえず、動くようにはなりましたが、[元ネタの記事](https://qiita.com/saturday06/items/333fcdf5b3b8030c9b05)みたいに、
ライブラリを読み解いて自分のものにはできず、うまく実装までこぎつけられませんでした。

## ライブラリが読めないなら、パケットを読めばいいじゃない
WEBUSBというものは、直接、パケットを叩くかなり低レイヤーな印象のものです。とどこかの記事で書かれていました。
つまり、意味はちゃんと分かっていなくても、通信さえマネできれば動くということではないか。
そう。ライブラリが読めないなら、直接パケットを読めばいいじゃない。
というわけで、パケットを盗み見るなら、おなじみ[Wireshark](https://www.wireshark.org/)
実は、標準の機能だけで、TCPの通信だけではなくUSBのパケットも読めるそうです。

そうとなれば、
1. [nfcpy](https://github.com/nfcpy/nfcpy)のサンプルにある、`tagtool.py`を実行して、
2. [Wireshark](https://www.wireshark.org/)で監視。
3. 手元にあったSuicaをタッチ。
4. もう一度、`tagtool.py`を実行して、
5. [Wireshark](https://www.wireshark.org/)で監視。
6. MEFAREの社員証をタッチ。
7. 適当にデータを加工して、比較する。

```text:読んだ結果(MIFARE)
out // FeliCaのIDmを取得するコマンド
0000ffffff0a00f6d6046e000600ffff0100b300
in // カードがない状態と同じ
0000ffffff0600fad70580000000a400

out
0000ffffff0300fdd606002400
in
0000ffffff0300fdd707002200

out
0000ffffff0600fad60002030f031300
in
0000ffffff0300fdd701002800

out
0000ffffff2800d8d6020018010102010300040005000600…
in
0000ffffff0300fdd703002600

out
0000ffffff0c00f4d602010002000501000607070b00
in
0000ffffff0300fdd703002600

out // カードを識別しているコードぽい
0000ffffff0500fbd604360126c900
in //MIFAREを置いているときの状態
0000ffffff0900f7d705000000000804001800

out
0000ffffff0600fad602040107081400
in
0000ffffff0300fdd703002600

out
0000ffffff0600fad602010002002500
in
0000ffffff0300fdd703002600

out // MIFAREのIDを取得するコード
0000ffffff0600fad604360193203c00
in // *がMIFAREのID
0000ffffff0c00f4d7050000000008**********0c00
```
こんな感じになっていました。
`out`がRC-380に送信しているパケット
`in`がnfcpyに返信されたパケットです。

…にっているところは、ログを書き出したときに、長すぎて省略されてしまったものです。
ソースコードに乗せてあります。

ここまでの部分では、待機状態でループ処理が走っています。

下から３番目の
`0000ffffff0600fad602040107081400`から３つは、Suicaや、タッチしていない状態では、一度も流れていないコマンドでした。
その上の`カードを識別しているコードぽい`の部分で、初めて返信パケットに変化があり、そこから分岐がされているようです。

`カードを識別しているコードぽい`
では、未検出時とSuicaの時は、`0000ffffff0600fad70580000000a400`でした。

## 実装
今回は、決済をしたいわけでも、なにかデータを書き込みたいわけでもないので、IDが読めれば十分でした。
ですので、せっかく分岐点を見つけたのですが、FeliCaの読み込みに失敗したら、MIFAREのIDを読み込んでみるという感じで実装しました。
FeliCaのIDmを読んだ直後にMIFAREのIDを読みに行っても、IDは返ってこなかったので、
上の結果の`FeliCaのIDmを取得するコマンド`のすぐあとから、コピペして、実装しました。

# 感想など
今回はかなり無理やりな感じがします。よくわかってないけど動いたはよろしくないですね。
実際問題、IDを見るだけならこれでもいいかもしれませんが、読み書きをする場合はちゃんと仕様書を読まないといけないなと。

pythonのライブラリを読み解こうとしたり、ICカードの仕様書を読み解こうとしたり、USBのパケットをのぞいたり……
色々なことがやって見れたので面白くはありましたが、ちゃんと作らないと規模が大きくなると使えないなあ。

解説など、いただける方がいれば、拝読させていただきます。

# 使用したもの
[python3](https://www.python.org/)
[nfcpy](https://github.com/nfcpy/nfcpy)
[libusb-1.0.dll](https://libusb.info/)
[Wireshark](https://www.wireshark.org/)
[Zadig](http://zadig.akeo.ie/)

# 参考文献
[WebUSBでFeliCaの一意なIDであるIDmを読む / Qiita](https://qiita.com/saturday06/items/333fcdf5b3b8030c9b05)
[（Windowsで）WebUSBでPasoriを扱ってみる / Qiita](https://qiita.com/frameair/items/abcaebbd654c304a0906)
[WebUSBことはじめ / Qiita](https://qiita.com/Aruneko/items/aebb75feca5bed12fe32#fnref6)
[WiresharkでUSBメモリのパケットキャプチャしてみた！ / LoT ラブオーティー](https://wireless-network.net/wireshark-usb-capture/)
[Windows で nfcpy のセットアップ / Qiita](https://qiita.com/mizutoki79/items/da903fe5a86fe4087429)