本記事はNTTコミュニケーションズ Advent Calendar 2020 6日目の記事です。
昨日は@khrdさんのKindでVirtualClusterを試そうでした。
はじめに
フルMVNOを提供している事業者の説明の中には「SIMアプレットを活用して機能を拡張する」というような文言が書いてあります。
本記事ではこのSIMアプレットに着目し、概要とSIMアプレット開発について見ていきます。
SIM Toolkitとは
SIM ToolkitはSIMカード上で動作するアプリケーション(アプレット)に対して端末やネットワークと対話するための仕組みを提供しています。
対話とは具体的に言うと、
- SIMが持つ情報を端末に表示する
- ユーザーがSIMに情報を入力する
- SIMがネットワーク経由でSMSを送受信する
などが挙げられます。
このSIM Toolkitを使って作られたプログラムをSIMアプレットと呼びます。
SIMアプレットの特徴
SIMアプレットにはざっくりと以下のような2つの特徴があります。
1. 能動的にコマンドを実行する仕組みがある
アプレットは基本的に端末側からカード側へコマンド(APDU)を送り、カード側がそれに応答するという仕組みになっています。つまり、カード側を起点にして端末側に対してなにかデータを送ることはできません。
SIMアプレットではコマンドの応答コード(Status Word. HTTPでいう200, 404みたいなもの)にひと工夫あり、端末側からのコマンドに対して「送りたいデータがXXバイトある!」という応答コードを返すことでカード側から能動的にコマンドを発行することが可能になっています。この能動的なコマンドをProactive Command
と呼びます。
これにより受動的な仕組みの上で能動的に振る舞えるようになっています。
2. OTAを使ってSIMにコマンドの発行ができる
SIMアプレットは端末が接続している通信回線を利用して外部のサーバと通信することが可能です。例えばサーバから端末にSMSを送信すると、その内容をもとに端末がSIMに対してコマンドを発行します。その実行結果をSMSやインターネット経由で外部のサーバに送信することもできます。
テキストを表示するSIMアプレットを触ってみよう
ここからは実際にSIMアプレットをスマートフォン上で動かしていきます。
なお検証で使用するSIMカードは研究開発用のもののため、自前で用意しない限り基地局と接続できません。そのため自端末の中で完結するアプレットを作っていきます。
前提
- OS: Ubuntu 20.04
- 使用したSIMカード: sysmoUSIM-SJS1
- with ADM keys
- SIMカードにアプレットを書き込むために必要な鍵情報です。
- 現在は販売終了しているらしく、後継モデルが販売されています。
- with ADM keys
- カードリーダー
- Alcor Micro社 AU9540
- (ThinkPad T470sに内蔵されているもの)
- Alcor Micro社 AU9540
- 検証に使用したスマートフォン
- LG isai(au LGL22)
- Android 4.4.2
- (iPhone 5cではアクティベーションを求められて先に進めず断念)
- LG isai(au LGL22)
環境構築
SIMアプレットをコンパイルしSIMカードに書き込むために、JDKとSIM用ツールを準備します。
OpenJDK8のインストール
Java Cardを扱うためにまずJDKをインストールします。
$ sudo apt install openjdk-8-jdk
筆者の環境では以下のバージョンがインストールされました。
$ java -version
openjdk version "1.8.0_275"
OpenJDK Runtime Environment (build 1.8.0_275-8u275-b01-0ubuntu1~20.04-b01)
OpenJDK 64-Bit Server VM (build 25.275-b01, mixed mode)
sysmoUSIM-SJS1向けツールの用意
使用するSIMカード(sysmoUSIM-SJS1)用にツールが提供されていますのでcloneします。
なお、sysmocomから提供されている本ツールの動作にはPython 2が必要です。
$ git clone git://git.osmocom.org/sim/sim-tools
この中に含まれるshadysim.py
を使用します。
カードリーダーにSIMカードを挿した状態でスクリプトを実行するとICCID(SIMカード毎に固有な識別番号)が表示されます。
sim-tools/shadysim$ python2 shadysim.py --pcsc
ICCID: 8988211000000364102f
またこのリポジトリにはJava Card用のコンバーターやJava Card 2.1.2のAPI、SIM APIが含まれており、アプレットのソースコードがあればすぐにビルドができるようになっています。
サンプルアプレットをインストールする
環境の準備ができたところで、テキストを表示するサンプルアプレットをSIMカードに書き込んでみましょう。
サンプルアプレットの取得
サンプルアプレットが含まれるリポジトリをcloneします。
このとき、取得したhello-stk
ディレクトリは上で取得したsim-tools
ディレクトリと同じ階層に置いてください。
$ git clone git://git.osmocom.org/sim/hello-stk
$ ls
hello-stk
sim-tools
アプレットのビルド
サンプルのjavaファイルからJava Card向けのcapファイルを作っていきます。
エラーが出なければビルド成功です。
$ cd hello-stk/hello-stk
hello-stk/hello-stk$ make
mkdir -p ./build/classes
mkdir -p ./build/javacard
javac -target 1.1 -source 1.3 -g -d ./build/classes -classpath "../../sim-tools/javacard/lib/api21.jar:../../sim-tools/javacard/lib/sim.jar" src/org/toorcamp/HelloSTK/HelloSTK.java
警告: [options] ブートストラップ・クラスパスが-source 1.3と一緒に設定されていません
警告: [options] ソース値1.3は廃止されていて、今後のリリースで削除される予定です
警告: [options] ターゲット値1.1は廃止されていて、今後のリリースで削除される予定です
警告: [options] 廃止されたオプションについての警告を表示しないようにするには、-Xlint:オプションを使用します。
警告4個
java -jar ../../sim-tools/javacard/bin/converter.jar \
-d ./build/javacard \
-classdir ./build/classes \
-exportpath ../../sim-tools/javacard/api21_export_files \
-applet 0xd0:0x70:0x02:0xca:0x44:0x90:0x01:0x01 org.toorcamp.HelloSTK.HelloSTK \
org.toorcamp.HelloSTK 0xd0:0x70:0x02:0xCA:0x44:0x90:0x01 1.0
Java Card 2.1.2 Class File Converter (version 1.2)
Copyright (c) 2001 Sun Microsystems, Inc. All rights reserved.
conversion completed with 0 errors and 0 warnings.
ビルドが完了するとbuild
ディレクトリ以下に生成物が格納されます。そのうちHelloSTK.cap
をSIMカードに書き込んでいきます。
hello-stk/hello-stk$ ls build/javacard/org/toorcamp/HelloSTK/javacard
HelloSTK.cap HelloSTK.exp
SIMカードへ書き込み
アプレットのSIMカードへの書き込みにはshadysim.py
を使用します。
SIMカードに書き込むためにはアプレット領域に書き込むための認証鍵が2種類(KIC, KID)が必要です(sysmocomでSIMを購入するとカジュアルにメールで認証鍵情報が送られてきます)。
以下のコマンドを実行すると書き込みが行われます。$KIC
,$KID
は各自のものに置き換えてください。
書き込みに成功すればICCIDのみが出力されて実行が終了します。
$ python2 shadysim.py \
--pcsc \
-l ../../hello-stk/hello-stk/build/javacard/org/toorcamp/HelloSTK/javacard/HelloSTK.cap \
-i ../../hello-stk/hello-stk/build/javacard/org/toorcamp/HelloSTK/javacard/HelloSTK.cap \
--module-aid D07002CA44900101 \
--instance-aid D07002CA44900101 \
--nonvolatile-memory-required 0100 \
--volatile-memory-for-install 0100 \
--max-menu-entry-text 15 \
--max-menu-entries 05 \
--enable-sim-toolkit \
--kic $KIC \
--kid $KID
ICCID: 8988211000000364102f
スマホでの動作確認
それではお待ちかねの実機検証です。SIMカードをカードリーダーから取り出し、スマホに挿したら電源ON or 再起動しましょう。
Androidではアプリ一覧に「SIMツールキット」というアプリが表示されます。
メニューにあるHello, STK
をタップするとメッセージが表示されます。
これにてサンプルアプレットの動作確認は完了です!
アプレットはSIMカード上で動作しているので、基本的にはSIMカードを他のスマートフォンに移し替えても同じアプレットを使うことができます。
ソースコードを見てみよう
SIMアプレットはJavaのサブセットであるJava Cardで書かれています。
Java Cardではchar
やfloat
型や多次元配列が扱えなかったり、インスタンス変数はカードが持つEEPROMに書き込まれるなどの制限があります。
HelloSTKの冒頭部分を見ていきましょう。
public class HelloSTK extends Applet implements ToolkitInterface, ToolkitConstants {
// DON'T DECLARE USELESS INSTANCE VARIABLES! They get saved to the EEPROM,
// which has a limited number of write cycles.
private byte helloMenuItem;
static byte[] welcomeMsg = new byte[] { 'W', 'e', 'l', 'c', 'o', 'm', 'e', ' ',
't', 'o', ' ', 'T', 'o', 'o', 'r', 'C',
'a', 'm', 'p', ' ', '2', '0', '1', '2' };
static byte[] menuItemText = new byte[] { 'H', 'e', 'l', 'l', 'o', ',', ' ', 'S', 'T', 'K'};
最初に、メニューやダイアログに表示するテキストを定義しています。char型が扱えないため、byte型の配列に1文字ずつ格納しています。
コメントにもあるように、インスタンス変数はEEPROMに書き込まれるため、変数をたくさん作ると書き込み可能回数がどんどん減っていくため注意です(RAMに書き込む方法もあります)。
次の部分を見ていきましょう。
private HelloSTK() {
// This is the interface to the STK applet registry (which is separate
// from the JavaCard applet registry!)
ToolkitRegistry reg = ToolkitRegistry.getEntry();
// Define the applet Menu Entry
helloMenuItem = reg.initMenuEntry(menuItemText, (short)0, (short)menuItemText.length,
PRO_CMD_SELECT_ITEM, false, (byte)0, (short)0);
}
// This method is called by the card when the applet is installed. You must
// instantiate your applet and register it here.
public static void install(byte[] bArray, short bOffset, byte bLength) {
HelloSTK applet = new HelloSTK();
applet.register();
}
ここではアプレットをカードに登録する処理を行っています。
コンストラクタではメニューの項目について、なにを表示するか、選択されたらどんなProactive Commandを発行するか、などを定義しています。項目を複数出したい場合は項目数だけinitMenuEntry
を呼び出す必要があります。
install()
はアプレットがカードに書き込まれたときにカードから呼び出されるメソッドです。アプレットからregister()
を実行するとアプレットのインスタンスがカードに登録されます。
(参考:An Introduction to Java Card Technology - Part 1)
次の部分を見ていきましょう。
// This processes APDUs sent directly to the applet. For STK applets, this
// interface isn't really used.
public void process(APDU arg0) throws ISOException {
// ignore the applet select command dispached to the process
if (selectingApplet())
return;
}
// This processes STK events.
public void processToolkit(byte event) throws ToolkitException {
EnvelopeHandler envHdlr = EnvelopeHandler.getTheHandler();
if (event == EVENT_MENU_SELECTION) {
byte selectedItemId = envHdlr.getItemIdentifier();
if (selectedItemId == helloMenuItem) {
showHello();
}
}
}
process()
はSIM Toolkit(STK)を経由せず直接アプレットにAPDUが送られたときに呼び出されます。インターフェースとして必要ですが、今回は何も処理しないようにしています。
STKを経由するとprocessToolkit()
が呼び出されます。
Envelope
とは端末からSIMに対して送られるデータです。端末で発生したイベント情報などが含まれます。EnvelopeHandler
を使ってアプレットからこのデータを扱えるようにしています。
今回のケースでは以下のような判断をしています。
- 「メニューが選択された」というイベントか?
- 選択されたメニュー項目は
helloMenuItem
か?
判断から外れた場合はなにもせず処理が終了します。
では最後にshowHello()
の中身を見ていきましょう。
private void showHello() {
ProactiveHandler proHdlr = ProactiveHandler.getTheHandler();
proHdlr.initDisplayText((byte)0, DCS_8_BIT_DATA, welcomeMsg, (short)0,
(short)(welcomeMsg.length));
proHdlr.send();
return;
}
ProactiveHandler
を用意してProactive Commandを扱えるようにしています。
次にinitDisplayText()
を呼び出し、端末に送るテキスト表示のコマンドを作成します。
コマンドを作成したらsend()
で端末側にProactive Commandを発行します。これにより端末側のディスプレイ上にテキストが表示されます。
(おまけ)音を出してみよう
ETSI TS 102 223には様々なコマンドが記載されています。
その中で"PLAY TONE"というのが個人的に気になったのでトーン音を出すアプレットを作ってみました。
ソースコードはgistにアップしました。
https://gist.github.com/staybuzz/5a76d15da57e6b1a31e1e6efa0c71192
サンプルアプレットから追加した部分として、
- メニュー項目
- メニューが選択されたときの処理
- PLAY TONE用のProactive Commandの作成、送信
を実装しています。
音鳴らしてる pic.twitter.com/uFL2ye8Xf8
— すていばず (@staybuzz) December 6, 2020
おわりに
本記事ではSIM Toolkitを使ったSIMアプレットについて、アプレットのビルドから書き込み、動作確認まで行いました。
SIMアプレットを活用することで、遠隔から端末の状態を取得できたりSIMから事業者に通知するなど、さまざまなことが出来そうです。
明日は@suzusuzuさんです!