Java
NFC
AndroidStudio

アプリ開発にあたって、NFCに関する概要をまとめた(その4:最後の鍵)

突破できそうで突破できなかった

Pasmoの通信と、歩行計の通信のUSBパケットデータを見比べて
明らかに歩行計の通信では、毎回Write Without Encryptionが行われていました。

差といえばこれくらいしかありません。
しかも、その際のコマンドコードが各データが格納されたブロックらしきコマンドとなっています。

ここでカスは仮説を立てました。
「このWrite Without Encryption実行時に指定しているブロックの情報を、そのまま書き出し時に引き継いでいるのではないか」
「実際Write Without Encryptionで情報更新をしていなくても、データを引き出すためにその工程が必要になるのではないか」

ということでやってみました。

3.Write Without Encryption (データを読み込む作業)

これまた不思議なもので、意外とWrite Without Encryptionには参考となるWebサイトがなかったのですが
もうすでにRead Without Encryptionは突破している(はず)と信じ、同じ要領で作成してみました。
(とはいっても、実際に何かを上書きするコマンドではなく、仕様書に基づき、既存のデータを上書きするだけの内容です。)

しかし、ここで重要なのは取り出したいデータの内容によって
Write Without Encryptionのコマンドも変更しなければいけないということがありました。
でも、とりあえず一旦は手作業で。

Activity.Main.writeWithoutEncryption
    private byte[] readWithoutEncryption(byte[] idm)
            throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(100);

        bout.write(0);           // データ長バイトのダミー
        bout.write(0x08);        // Felicaコマンド「Write Without Encryption」 ※1
        bout.write(idm);         // カードID 8byte 
        bout.write(1);           // サービス数
        bout.write(0x0f);        // サービスコード下位バイト ※2
        bout.write(0x00);        // サービスコード上位バイト
        bout.write(1);           // ブロック数
        bout.write(0x80);        //ブロックエレメント下位バイト?
        bout.write(0x00);     //ブロックエレメント上位バイト?

        bout.write(0x00);     //ブロックデータ 01 ※3
        bout.write(0x01);     //ブロックデータ 02 ※3
        bout.write(0x02);     //ブロックデータ 03 ※3
        bout.write(0x03);     //ブロックデータ 04 ※3
        bout.write(0x04);     //ブロックデータ 05 ※3
        bout.write(0x05);     //ブロックデータ 06 ※3
        bout.write(0x06);     //ブロックデータ 07 ※3
        bout.write(0x07);     //ブロックデータ 08 ※3
        bout.write(0x08);     //ブロックデータ 09 ※3
        bout.write(0x09);     //ブロックデータ 10 ※3
        bout.write(0x0A);     //ブロックデータ 11 ※3
        bout.write(0x0B);     //ブロックデータ 12 ※3
        bout.write(0x0C);     //ブロックデータ 13 ※3
        bout.write(0x0D);     //ブロックデータ 14 ※3
        bout.write(0x0E);     //ブロックデータ 15 ※3
        bout.write(0x0F);     //ブロックデータ 16 ※3 全16byte

        byte[] msg = bout.toByteArray();
        msg[0] = (byte) msg.length; // 先頭1バイトはデータ長
        return msg;
    }

仕様書の通りに入れると、このような仕上がりになります。
※1はWrite Without Encryptionは8なので、(0x08)になります。
※2、※3の値は大人の都合で、サンプルの値を使用しています。

ここはRead Without Encryptionとはブロックデータの送信内容も異なるため
Forを使っておらず、上から順につなげたものがreturn msg;に代入されます。

これらをつなげると
0x08/idm/0x01/0x0f/0x00/0x01/0x80/0x00/0x00/0x01/0x02/0x03/
0x04/0x05/0x06/0x07/0x08/0x09/0x0A/0x0B/0x0C/0x0D/0x0E/0x0F となり
仮にidmを0x00/0x01/0x02/0x03/0x04/0x05/0x06/0x07の8byteとして、16進の表記(0x)を取ると
080001020304050607010f00018000000102030405060708090A0B0C0D0E0Fとなります。

これで3つ目の鍵が完成なはずです。
ようやくここまでこぎつけたはずです。
あとはこれをnfc.transceiveで送信すればOKかもしれないはずです。

さて、いざ実行してみると…

Write Without Encryptionの箇所は成功しているようです。レスポンスコードが返ってきました。
ですが、その次のRead Without Encryptionでエラー値が返ってきました。
あぼ~ん(゚q。)
つまり、成功して失敗しているのです。
書き込みデータの内容は特に変更や操作する必要もないので、どうでもいいのですが
欲しいのはそのブロック内に入ったデータです。

知らない間に、Read Without Encryption内のソースを触ってしまったのかと
見直しても特に変化した箇所等は見当たりません。
Write Without Encryptionをコメント化して前の状態に戻して実行してみると、
期待値ではないが、ちゃんとレスが返ってきます。

改めて、メモしておいたUSBのパケットデータを見直しても
Write Without EncryptionとRead Without Encryptionの間で
特別なコマンドを挟んでいる痕跡もありません。
Write/Readともにコマンドコードを確認してみますが間違いもなく
手を変え品を変えてみましたが、
やはりエラー、もしくは期待値ではない値の返却の繰り返しでした。

ここで、完全に行き詰ってしまいます。
もう、打つ手がありません。
こうなったら、もう最終手段です。

神様に力を借りて、ようやく…

これは本当に最後まで使いたくなかった奥の手なのですが
PC用のソフトを制作した会社の担当者(神様)に聞いてみる事にしました。

ただし、これはあくまでPC用ソフト内でどのように動作しているかを聞くだけです。
そして、神様の回答はこうでした。

「Write Without Encryptionを実行した後、
データの書き込みが終わるまで少し時間を空ける必要があって
その後にRead Without Encryptionでデータを取り出しています。
約150msほど空ければいいと思います。」

その時間、わずか150ms(0.15秒)
つまり、たった1行、150msスレッドを停止させるソースを書き込めば
いいだけのことでした。

神様にお礼を言って、さっそくそのSleepメソッドを1行を追加し実行したところ、
ようやく期待値の結果がなだれ込んできました!

うれしくて、大声を出して飛び上がりそうになりました。
プログラミングをしていて、一番幸せを感じる瞬間です。

さて、あまりはしゃいでもいられなく、
まだ、1つのコマンドに成功しただけです。
全部で4つのコマンドを実施しデータを取り出さなければいけません。

同じ要領でデータ取出しの検証を行い、
2パケットに分かれているデータの場合は、同じWrite/Readを2回繰り返せば
前半、後半てな感じで値が返ってきます。
これで、一番最初の目的だった、【歩行計からデータを取り出す】という目的を
果たすことができました。
そうです、ここからアプリ制作のためのスタートラインです。
カスはようやくスタートラインに立つことができました。

めでたしめでたし。

つまり、まとめるとこうなります。

1.【IDm】を取得するためにPollingを実施する。
2.データの読み取りだけだったら、Read Without Encryptionでデータを取り出す
3.しかし、そのブロックが書き込みをして、読み込みをするブロックであれば
 Write Without Encryptionをして場所を特定させて、
 その後そのままRead Without Encryptionでデータを取り出す。

TagInfoアプリでPasmoを読み込んでFULL SCANのタブを開いて、
データが入っているサービスコードの部分を見てみてください。
R/WもしくはR/Oなどあるかと思います。
これがブロックに対しての書き込み、読み込みの制約の部分ではないでしょうか(多分)
R/W→一旦Writeで書き込みをしてReadで読み取る
R/O→Readで読み取るだけ

そのあたり、まだ未検証ですが
余裕があれば、AMAZONでNFCタグのカードを購入して
色々と操作して検証してみてもいいかもしれません。

おわりに…

今回私が行ったNFC通信ができる歩行計からデータを取り出し
Androidアプリで表示させるということは
かなり偏りが大きな内容になると思います。

それでも、ある一つの事例としてどなたかの参考になれば幸いです。

たった数文字のコマンドを作成し、送信し、受信し、結果を受け取り、その結果を表示させる。
とても簡単な作業なのですが、その工程を理解し、実行出来るようになるには
大変な思いもありました。

キャッシュレス化の波が大きくなってくる昨今、
この先さらにNFCという技術はそれだけにとどまらない範囲に波及してくると思います。
私の場合、今は必要に応じて技術と知識をつけているにすぎませんが
1つの経験とスキルを得ることが出来ました。

こんな感じで終わってしまっていいものか迷いますが
一旦ここで区切らせていただきます。
長文記事を最後まで読んでいただき本当にありがとうございます。

【関連記事】
アプリ開発にあたって、NFCに関する概要をまとめた(その1:入口)
アプリ開発にあたって、NFCに関する概要をまとめた(その2:鍵を探す)
アプリ開発にあたって、NFCに関する概要をまとめた(その3:鍵はいくつあるのか)