
第5回 HCEで小さな決済システムを作ってみた!(後編)
今回でNFC HCEの連載も最終回です。最後の締めくくりとして、第4回で紹介した決済ポイントカードシステム「トンガロイドペイ」を実際に作成し、動かしてみました。
動作紹介
どうでしょう?この素敵な動き。現在はNFC HCEの登録とポイントの加算機能までです。さて、ではこれをどのように作り上げたか、詳しく説明します。
システムの詳細
まずは登録のシーケンスです。コマンドとしてSELECT_FILE、INTERNAL AUTHENTICATE、WRITE_BINARYの3つのコマンドを使います。
はじめにNFC HCEのアカウントページ(と想定する)F222222222を指定して、SELECT_FILEを実行します。次にNFC HCEに対し、Googleアカウント情報(ハッシュ値)を要求するために、INTERNAL AUTHENTICATEを実行します。ここでデータフィールドに7788を指定していますが、ここは特に意味を持ちません。最後に、入手したGoogleアカウント情報(ハッシュ値)をもとに、トークン情報を生成します。このトークン情報を、WRITE_BINARYを使用してNFC HCEに対して書き込みます。これでNFC HCEにはトークン情報、NFC R/Wにはトークン情報とGoogleアカウント情報(ハッシュ値)のデータベースを持つこととなり、使用する準備が整いました。
次に使用のシーケンスです。コマンドはSELECT_FILEとWRITE_BINARYの2つのみです。
はじめにNFC HCEのポイントページ(と想定する)F222222222を指定して、SELECT_FILEを実行します。次にポイント残高を、WRITE_BINARYを使用してNFC HCEに対して書き込みます。
さて、ここで皆さんお気づきかと思いますが、実は登録や使用におけるデータに対する全てのページをF222222222としており、
- INTERNAL_AUTHENTICATEを実行した場合にはGoogleアカウント情報(ハッシュ値)の返却
- WRITE_BINARYを実行した場合には、データプレフィックス次第で以下の挙動
- AA:ハッシュ書き込み
- BB:ポイント残高書き込み
のように使い分けました。本当であれば各々、ページを分けて管理してあげたかったところですが、この辺りはご愛嬌、エンハンスのネタという事にしておきます。
ソースコード解説(NFC HCE編)
さて、皆さんが待ちに待ったソースコードの解説です。まずはNFC HCE側のコードから紹介します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) { ... if(byteEquals(commandApdu, HexStringToByteArray(SELECT_APDU_HEADER), HexStringToByteArray(SELECT_APDU_HEADER).length)) { //SELECT_PAGE受信時の処理 ... } else if(byteEquals(commandApdu, HexStringToByteArray(INT_AUTH_HEADER), HexStringToByteArray(INT_AUTH_HEADER).length)) { //INTERNAL_AUTHENTICATE受信時の処理 ... } else if(byteEquals(commandApdu, HexStringToByteArray(WRITE_BIN_HEADER), HexStringToByteArray(WRITE_BIN_HEADER).length)) { //WRITE_BINARY受信時の処理 ... } ... } |
NFC HCEではこのprocessCommandApdu()というメソッドが、NFC R/Wとの通信を司る根幹となります。大まかには、SELECT_FILE、INTERNAL AUTHENTICATE、WRITE_BINARYのデータを受け取った場合の条件分岐をそれぞれ記載しています。それでは各々のコマンド受信時の処理を見ていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
if(byteEquals(commandApdu, HexStringToByteArray(SELECT_APDU_HEADER), HexStringToByteArray(SELECT_APDU_HEADER).length)) { if(Arrays.equals(SELECT_APDU, commandApdu)) { token = AccountStorage.GetAccount(this); if(token != null){ Log.i(TAG, "2-1st sequence : select aid"); return ConcatArrays(HexStringToByteArray(token), SELECT_OK_SW); }else{ Log.i(TAG, "1-1st sequence : select aid"); return SELECT_OK_SW; } } else { Log.w(TAG, "unknown command."); return UNKNOWN_CMD_SW; } } |
APDUの中身がSELECT_FILEコマンドだった場合は、まず、ページがSELECT_APDU(F222222222)である事を確認します。その後、自分自身がトークン情報を持っているかどうか確認し、応答にトークンを加えるか否か、処理を分岐させています。これにより、SELECT_FILEの応答を受け取ったNFC R/Wが登録シーケンスに走るか使用シーケンスに走るか変化していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
} else if(byteEquals(commandApdu, HexStringToByteArray(INT_AUTH_HEADER), HexStringToByteArray(INT_AUTH_HEADER).length)) { Log.i(TAG, "1-2nd sequence : internal authenticate"); ... Account googleAcount =getGoogleAccount(); if(googleAcount ==null){ return UNKNOWN_CMD_SW; }else { String hashCode = String.valueOf(googleAcount.hashCode()); Log.d(TAG, "hashCode = " + hashCode + ", length = " + hashCode.length()); if((hashCode.length() % 2) ==1){ hashCode = hashCode.concat("0"); } return ConcatArrays( HexStringToByteArray(hashCode), SELECT_OK_SW); } } |
ここは登録シーケンスでのみ使用する部分ですね。事前に受信データ長などから異常処理を経た後、getGoogleAccount()というプライベートメソッドを使用して、Googleアカウント情報を取得し、そこからハッシュを取り出した上で返送します。getGoogleAccount()の中身については、第4回でも紹介しましたので割愛します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
} else if(byteEquals(commandApdu, HexStringToByteArray(WRITE_BIN_HEADER), HexStringToByteArray(WRITE_BIN_HEADER).length)) { String header =WRITE_BIN_HEADER; int startlen = header.length(); String kind = ByteArrayToHexString(commandApdu).substring(startlen, startlen + 2); if(kind.equals("AA")){ Log.i(TAG, "1-3rd sequence : write binary (Token)"); //トークン格納処理 ... return SELECT_OK_SW; }else if(kind.equals("BB")){ Log.i(TAG, "2-2rd sequence : write binary (POINTs)"); //ポイント表示処理 ... return SELECT_OK_SW; }else{ ; } return UNKNOWN_CMD_SW; } |
最後のWRITE_BINARY受信時の処理です。ここではデータ領域に「AA」が格納されているか「BB」が格納されているかで、トークン格納かポイント表示か、処理を分岐させています。
と、APDUのデータ構成に慣れてさえくれば、結構単純に実装する事が出来ました。
ソースコード解説(NFC R/W編)
次はNFC R/W側のコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public void onTagDiscovered(Tag tag) { ... IsoDep isoDep = IsoDep.get(tag); ... if (isoDep != null) { try { // Connect to the remote NFC device isoDep.connect(); //コマンド送受信処理 ... } catch (IOException e) { ... } } } |
NFC R/Wでは、NFC HCEなどのカードを検知した際にコールされるonTagDiscovered()が処理の根幹であり、ここでSELECT_PAGE、INTERNAL_AUTHENTICATE、WRITE_BINARYのコマンドを送受信していく形となります。それぞれの処理の内容を見ていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//---------------1st seq start--------------- ... // SELECT_PAGEコマンド生成 byte[] command = BuildSelectApdu(SAMPLE_LOYALTY_CARD_AID); ... // SELECT_PAGEコマンド送信 byte[] result = isoDep.transceive(command); ... if (Arrays.equals(SELECT_OK_SW, statusWord)) { ... //tokenがDB存在するか、検索 Cursor cursor = mPointDB.query(PointDB.TABLE_TITLE, null, PointDB.COLUMN_TOKEN+"=?", new String[]{receiveNumber}); //DBに存在する(既に登録済みユーザであれば、ポイント更新) if(cursor.moveToFirst()) { ... //ポイント10加算 ... //データベース更新 ... //ポイント残高送信 ... byte[] result_bin = isoDep.transceive(command_bin); ... if (Arrays.equals(SELECT_OK_SW, statusWord_bin)) { //ポイント残高表示+効果音 mAccountCallback.get().onAccountReceived("Point : "+point, 2); mMp = MediaPlayer.create(mContext, R.raw.tongaroidpay); handler.postDelayed(delayFunc, 150); } return; } } else { Log.e(TAG, "SelectApdu Received: UNKNOWN"); return; } |
ここはSELECT_PAGEの送信、またその結果が「使用シーケンス」だった場合の処理となります。ポイントはisoDep.transceive()メソッドを使用することで、NFCを使用したデータの送受信を簡単に出来るところですね。SELECT_PAGEの応答にトークンが含まれていた場合、ポイントの加算を実施したり、データベースを更新したり、ポイント残高をisoDep.transceive()メソッドを使用してNFC HCEへ送信したり、と、使用シーケンスが走ります。ちなみにここで流れる効果音は、「アンドロイドペイ~」ではなく「トンガロイドペイ~」と音声が流れますので、是非聴いてみてください。
1 2 3 4 5 6 7 8 9 10 11 |
//---------------2nd seq start.--------------- //INTERNAL_AUTHENTICATEコマンド生成 byte[] command_auth = BuildIntAuthApdu(INT_AUTH_KEY); ... //INTERNAL_AUTHENTICATEコマンド送信 byte[] result_auth = isoDep.transceive(command_auth); ... if (Arrays.equals(SELECT_OK_SW, statusWord_auth)) { //トークン生成 token = createToken(hash); } |
ここはSELECT_PAGEの結果、登録シーケンスだった場合のINTERNAL_AUTHENTICATE送信処理です。ここでもisoDep.transceive()メソッドを使って、データ通信を実施しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
private String createToken(String hash_org) { int token; String hash_ini, hash_end, hash_token; int randLength = 6; //先頭と末尾の3文字を除いた中間の数字をトークンに置き換える hash_ini = hash_org.substring(0,3); hash_end = hash_org.substring(hash_org.length()-3); ... String strRand = new String(); Random rnd = new Random(); for(int i=0; i |
トークンはこんな感じで、先頭/末尾3byte以外をランダム値に置き換える感じで実装しました。本当は色々と規格があるみたいですが、今回は簡素に、という感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//---------------3rd seq start.--------------- //WRITE_BINARYコマンド生成 byte[] command_bin = BuildWriteBinaryAdpu(WRITE_BINARY_SIZE, token, WRITE_TOKEN); ... //WRITE_BINARYコマンド送信 byte[] result_bin = isoDep.transceive(command_bin); ... if (Arrays.equals(SELECT_OK_SW, statusWord_bin)) { ... // DBにアカウント情報を格納 mPointDB.insert(values); values.clear(); } //登録完了表示+効果音 mAccountCallback.get().onAccountReceived("Completed.", 1); mMp = MediaPlayer.create(mContext, R.raw.touroku); handler.postDelayed(delayFunc, 150); |
ここでは生成したトークンをWRITE_BINARYでNFC HCEへ送信する処理です。お決まりのようにisoDep.transceive()ですね。なお、ここで流れる効果音は、とっておきの秘密です。是非聴いてみてください。
以上がコードの簡単な説明です。実際のソースコードはGitHubに登録してありますので、皆さん、是非ダウンロードしてみてください。
最後に
さて、計5回にわたりNFC HCEについて記事を書きましたが、いかがだったでしょうか?5回とは短いもので、本当は結構遣り残した事がありました。
- きちんとページ構成を作り上げたかった
- きちんとREAD_BINARYなどを使って、Googleアカウント情報とか吸い上げたかった
- もっとセキュリティ面を高めたかった
- ポイント加算だけでなく、減算など、NFC R/W側に機能を持たせたかった
他にも数え切れません。ただ、「NFC R/Wからのデータ送受信方法」「ISO7816に基づいた(?)データの扱い方」「NFC HCEでのデータ受信方法」辺りが分かれば、簡単にエンハンス出来そうです。もし機会があれば、番外編で発信しますね。
それでは皆さん、よいNFCライフを!!今までご愛読、ありがとうございます!!
zozrmb