業務では使っていない Kotlin にはまだどうにも馴染めないでいるのですが、使わないままだとずっと馴染むことができません。ということで、今回 Kotlin を練習する目的で書いてみたのは、普段から Android での取り扱いが心地よくない APDU コマンドです。
APDU コマンドをクラス化
Kotlin のプロパティの UML での適切な表現方法がわからなくて詰めの甘さは否めませんが、ざっくり下図のようなイメージです。ETSI 102 221 と ISO/IEC 7816 では記述が少し異なりますが、Android OMAPI の実装と同様に ISO/IEC 7816 寄りの仕様で描いています。
SIM カードに APDU コマンドを送信する方法は、Android では下記の二種類が用意されています。
- Telephony Manager API (android.telephony.TelephonyManager) を使う
- Open Mobile API (android.se.omapi) を使う
Android Secure Element アプリの最近の git ログから察するに、その方法を Open Mobile API に一本化する流れで動いているようです。しかし、Telephony Manager API を使って APDU コマンドを送信できなくなるまでには、まだ数年の猶予はあるでしょう。現時点では、どちらの API を利用する場合でも使えるクラスにしておくのが無難です。
上記の理由から、APDU コマンドを構成する情報を個別に取り出せるように、また一連のコマンドバイト列を作ることができるようにもします。そんな APDU コマンドクラスを Kotlin で書いてみたコードがこちら。ポイントは「TPDU とは混同させない」ところと「利用者に CLA 値を直値で指定させない」ところです。
2020/03/01 追記 : 上記のコミットをプッシュして一息ついた後、data フィールドを String 型で持つのは Telephony Manager 側のインターフェース仕様に寄り過ぎだと思い、下記のコミットを追加しました。data フィールドを ByteArray 型で指定することもできるので、AOSP/OMAPI 側のインターフェース仕様にも都合が良くなったはずです。
Kotlin に慣れた人が書けば、もっと Kotlin らしく書けるような気がします。なまじ Java っぽくて、Kotlin の文法をあまり知らなくてもそれなりに書けてしまうというのも善し悪しです。
Telephony Manager API はここが嫌い
別の API で獲得した論理チャネルを通してコマンドを送信する API の書式が、こちらです。P3 とか出てきちゃいますし、Case 4 コマンドの場合に Le (length field for coding number Ne) をどう指定すれば良いのかわからないですし、これきっとたぶん(どっち)もう APDU じゃなくて TPDU ですよね。端末ベンダによって期待動作の解釈が分かれそうな仕様なのが気持ち悪いです。
/** * Exchange APDUs with the SIM on a logical channel. * * Input parameters equivalent to TS 27.007 AT+CGLA command. * * @param channel Channel id of the channel to use for communication. Has to * be greater than zero. * @param cla Class of the APDU command. * @param instruction Instruction of the APDU command. * @param p1 P1 value of the APDU command. * @param p2 P2 value of the APDU command. * @param p3 P3 value of the APDU command. If p3 is negative a 4 byte APDU * is sent to the SIM. * @param data Data to be sent with the APDU. * @param response Callback message. response.obj.userObj will be * an IccIoResult on success. */ public void iccTransmitApduLogicalChannel(int channel, int cla, int instruction, int p1, int p2, int p3, String data, Message response);
AOSP/OMAPI (Android の Open Mobile API) の前身の SEEK for Android が、この API を使って APDU コマンドを送信しています。実装を見たところ、APDU コマンドバイト列の頭から順に P1 / P2 / P3 .. と取っているようですので、Case 4 コマンドの Le は data の末尾にくっついて RIL に引き渡されるように見えます。
@Override public byte[] internalTransmit(byte[] command, SmartcardError error) throws RemoteException { try { Log.d(TAG, "internalTransmit > " + ByteArrayConverter.byteArrayToHexString(command)); int cla = Util.clearChannelNumber(command[0]) & 0xFF; int ins = command[1] & 0xff; int p1 = command[2] & 0xff; int p2 = command[3] & 0xff; int p3 = -1; if (command.length > 4) { p3 = command[4] & 0xff; } String data = null; if (command.length > 5) { data = ByteArrayConverter.byteArrayToHexString(command, 5, command.length - 5); } ... response = mTelephonyManager.iccTransmitApduLogicalChannel( mChannelIds.get(channelNumber), cla, ins, p1, p2, p3, data); ...
また、チャネル番号と CLA が別の独立した引数になっているのもどうかと思います。もしチャネル番号とは矛盾するビットパターンで CLA を書いて渡したら、どのように処理してくれるんでしょうか。拡張論理チャネルの場合の挙動に不安を感じます。
Open Mobile API にもまた別の気持ち悪さが
一方、Open Mobile API で APDU コマンドを送信する API は、Channel クラスのこちら。引数はとてもシンプルで、APDU コマンドのバイト列を引き渡すだけです。一見これで良さそうに見えますが、こちらのインターフェース仕様にも CLA 値に危うさを感じます。
/** * Transmit an APDU command (as per ISO/IEC 7816-4) to the Secure Element. ... * @param command the APDU command to be transmitted, as a byte array. ... */ public @NonNull byte[] transmit(@NonNull byte[] command) throws IOException {
Open Mobile API では、論理チャネル番号を利用者が気にしないで良い設計になっています。標準論理チャネルまたは拡張論理チャネルに適した CLA 値を利用者側で選択することができないことから、こちらも拡張論理チャネルの場合の挙動が期待しないものになることがあるかもしれません。