クマは森で用を足しますか?

アウトプットは重要です。

eSIM が無ければ Pixel 4 を潰せばいいじゃない

リムーバブルな eSIM が手に入らない

eSIM にコマンドを送りたくなること、ありますよね。4FF/nano カードタイプのいわゆるリムーバブルな eSIM を入手し、PC/SC インターフェースを通して ES10x のコマンドを送ることができればと考えたんですけど、そもそもそんな eSIM を簡単に入手できそうにありません。

f:id:cheerio-the-bear:20210127230729p:plain

SGP.26 のテスト証明書を入れた商品は見つけましたが、それでは今後やれることが限られてしまいそうです。海外の eSIM 事業者さんにもお尋ねしてみましたが、門前払いみたいな感じでした。まぁ、そりゃそうでしょう。

リムーバブルじゃなくてもいいか(諦めた)

全く eSIM が手に入らないのかというと、そういうわけではありません。リムーバブルなカードタイプではなく、更には既に商品に組み込まれてしまっているだけのことです。所望のものが手に入らないのはもう仕方がないということで、Pixel 4 に組み込まれている eSIM を利用することにしました。

f:id:cheerio-the-bear:20210127230744p:plain

我ながら適当なワークアラウンドを考えたものです。adb shell content コマンドを使って ES10x コマンド部を Android 端末に渡し、端末内に追加する Content Provider を通して eSIM に送り込みます。アプリから見れば、ES10x コマンドの送信をお願いした先の仕組みが違うだけのことで、何ら不都合はありません。不都合として何か挙げるとするならば、Pixel 4 をルート化しないといけないことくらいでしょうか。

今回作成した Content Provider はこちら

クライアントから ES10x コマンドを受け取り、APDU ヘッダを付けて STORE DATA コマンドにして、テレフォニー API を使って eSIM に送りこむだけの Content Provider です。論理チャネルのオープン・クローズや ISD-R の選択もやりますが、どうあれ大した処理ではないのでコード量は極小です。

github.com

ProfileInfoListRequest 'BF2D' の実行例をご紹介します。Content URI の最後のスラッシュ以降が ES10b コマンド部で、そこで指定したコマンドデータが STORE DATA コマンドに載せられて eSIM まで運ばれます。要求するデータオブジェクトは profileState '9F70' と profileName '92'、profileClass '95'、notificationConfigurationInfo 'B6' の四つ。

$ adb shell content query --uri content://com.github.cheeriotb.isdrap.provider/store/BF2D075C059F709295B6
Row: 0 response=BF2D7DA07BE31D9F7001009214416E726974737520546573742050726F66696C65950100E35A9F700100920349494A950102B64C302480020410811E534D2D56342D3033332D412D47544D2E50522E474F2D4553494D2E434F4D302480020780811E534D2D56342D3033332D412D47544D2E50522E474F2D4553494D2E434F4D9000

Android 端末からは、ProfileInfoListResponse 'BF2D' とステータスワード '9000' が返っています。テストプロファイルがひとつ、そして IIJ の商用プロファイルがひとつ入っていることが読み取れます。

ProfileInfo profileState disabled (0)
profileName Anritsu Test Profile
profileClass test (0)
ProfileInfo profileState disabled (0)
profileName IIJ
profileClass operational (2)
notificationConfigurationInfo profileManagementOperation notificationDelete (3)
notificationAddress SM-V4-033-A-GTM.PR.GO-ESIM.COM
notificationConfigurationInfo profileManagementOperation notificationInstall (0)
notificationAddress SM-V4-033-A-GTM.PR.GO-ESIM.COM

端末から返ってくるデータサイズが大きい場合の動作をまだ確認できていないので、うまく動かないケースがあれば都度修正しようと思います。また、現状は Pixel 端末を想定したコードになっていますが、ほんの少し書き換えるだけで Rakuten Mini 等にも転用できるはずです。

久しぶりにリフレクションしました

今回利用した API の中には @hide なものも含まれていたので、久しぶりにリフレクションを使いました。Kotlin だと、他にもっと綺麗に書く方法もあるんでしょうか。

    transmit = TelephonyManager::class.java.getDeclaredMethod(
        "iccTransmitApduLogicalChannelBySlot", Int::class.java,
        Int::class.java, Int::class.java, Int::class.java, Int::class.java,
        Int::class.java, Int::class.java, String::class.java)

    val tm = context!!.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?
        ?: throw UnsupportedOperationException("Telephony Manager is unavailable")

    transmitRsp = Response(transmit.invoke(tm, PHYSICAL_SLOT_NUM, openRsp.channel,
            STORE_DATA_CLA, STORE_DATA_INS, p1, seqNumber, block.length / 2, block)
            as String?)

あと、サービスだけどうして Java で書いちゃったのかは謎です。適当に試してみたときに、勝手に手がそう動いちゃったんだと思います。