Visual Studio Code利用

Visual Studio Codeを用いてIMEI・IMSI・ICCIDを取得して applet_rst へ送信するアプレットの作成手順を示します

注釈


JCDK (Java Card Development Kit)の準備

  1. Java Card Development Kit Tools (Version 3.1)をインストールします
  2. Java Card Classic Platform Specification 3.0.5 をインストールします。
  3. 下図のようにJCDKがWindowsのホームパス (フォルダ C:\Users\ユーザ名 )に展開されていること、そのフォルダに java_card_kit-classic-3_0_5-ga-spec-doc-b33-03_jun_2015.zip があること、の2点を確認してください。
    ../../_images/1687879562.png

VS CODEのインストール

  1. こちら からファイルをダウンロード・インストールします。
    • 特別な選択・設定箇所はありません

環境変数の設定

  1. ctrl+shift+@ を押してコマンドプロンプトを開きます

  2. javacをUTF-8エンコーディングでビルドするため、ユーザー環境変数 JAVA_TOOL_OPTIONS-Dfile.encoding=UTF8 を設定します
    C:\Users\anyuser>setx JAVA_TOOL_OPTIONS -Dfile.encoding=UTF8
    
  3. 環境変数の状態を確認します
    C:\Users\anyuser>echo %JAVA_TOOL_OPTIONS%
    -Dfile.encoding=UTF8
    

プロジェクトの新規作成

  1. lib,tools ディレクトリを作成します
    ../../_images/1707738598.png
  2. APIパッケージ(USIM API・UICC API)をダウンロードします
    • SIMに搭載するアプレットから利用するAPIパッケージとJavaDocは、3GPPおよびETSIの規格書の付録として配布されていますので、ダウンロードして展開します。
  3. バッチファイル lib/download_apifiles.bat を配置します。
    ::
    :: download *.exp, *.jar, and JavaDoc from 3GPP and ETSI and extract them
    ::
    
    @echo off
    SETLOCAL
    
    cd %~dp0
    
    :: 3GPP TS 31.130 v13.0.0
    curl -fOL https://www.3gpp.org/ftp/Specs/archive/31_series/31.130/31130-d30.zip
    if ERRORLEVEL 1 exit /b
    
    
    :: ETSI TS 102 241 v13.0.0
    curl -fOL https://www.etsi.org/deliver/etsi_ts/102200_102299/102241/13.00.00_60/ts_102241v130000p0.zip
    if ERRORLEVEL 1 exit /b
    
    
    for %%F in (%~dp0\*.zip) do (
      powershell -command "Expand-Archive -Force '%%F'"
    )
    
    ENDLOCAL
    
  4. Terminalを起動します

  5. 作成したバッチファイルを実行します
    • 開発環境では以下の様に表示されます
      ../../_images/1707738742.png

アプレット書き込みツールの設定と機器動作確認

アプレットをSIMへインストールするためのツールを設定し、機器動作確認を行います。

  1. 以下のリンクからバッチファイルを取得して tools/ に保存します。

    ファイル 機能
    tools/scp03keys.bat SIM毎に異なるSCP03鍵 (OEMKIC, OEMKID, OEMKIK) を記載するファイル
    tools/install_applet.bat サンプルアプレットのインストールおよび、インストールパラメータの設定
    tools/uninstall_applet.bat サンプルアプレットのアンインストール
    tools/list_applet.bat SIMにインストールされているアプレット等の一覧を表示
    tools/find_card_readers.bat ICカードリーダおよびSIMの接続状況を表示
    tools/download_tools.bat GlobalPlatformPro, APDU4JをGitHubからダウンロードする
    tools/envpath.bat 環境変数JAVA_HOMEなどを一時的に設定する

    注釈

    上記のバッチファイルは GlobalPlatformPro (オープンソースソフトウェア)を利用してアプレットのインストール等を行います。

  2. Terminalを開き(vscodeのショートカット: ctrl+shift+@), バッチファイル tools/download_tools.bat を実行します
    • このバッチファイルは tools/gp.jar, tools/apdu4j.jar を GitHubからダウンロードします
  3. SIMをICカードリーダに装着し、バッチファイル tools/find_card_readers.bat を実行します。
    • [*] が表示される行が1行のみ存在することを確認してください。

    • [*] が表示されない場合
      • ICカードリーダおよびSIMの装着状況を確認してください。
    • [*] が複数行表示される場合
      • 他のSIMやICカード等がPCに装着されていますので 必ず取り外してください
      • 誤ってアプレットを書き込むことにより破損する恐れがあります。
  4. ファイル tools/scp03keys.bat を編集し、ICカードリーダに装着したSIMのSCP03鍵(OEMKIC, OEMKID, OEMKIK)の値を入力して保存します。

    ::
    :: アプレットをインストールするSIMカードの鍵情報を記載してください。
    ::
    
    set OEM_ENC=03030303030303030303030303030303
    set OEM_MAC=04040404040404040404040404040404
    set OEM_KEK=05050505050505050505050505050505
    
    set OEM_SSD=A0000001156000000000000000011001
    
    
  5. ファイル tools/list_applet.bat を実行します
    • AID値が表示されることを確認してください。
    • AID値が表示できない場合は、 tools/scp03keys.bat に記載した値を確認してください。
    • こちら から鍵の値を確認できます。
    • SCP03鍵の値が誤ったままで何度も操作を繰り返すと、SIMを破損する恐れがあります。

Antビルドファイルの作成

Apache Antを利用してアプレットをビルドする設定を行います。

  1. ビルドファイル build.xml を下記内容で作成します。

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    
    <project default="Verification" basedir=".">
        <property file="build.properties"/>
        <property environment="env"/>
    
        <!-- The Java Card Development Kit Tools (JCDK Tools) -->
        <property name="jc_home_tools"
                  location="${env.HOMEDRIVE}${env.HOMEPATH}/java_card_tools-win-bin-b_17-06_jul_2021"/>
    
        <!-- Project paths -->
        <property name="path.suncap" location="suncap"/>
        <property name="path.class" location="class"/>
        <property name="path.exports" location="exports"/>
        <property name="path.src" location="src"/>
    
        <!-- Tools paths -->
        <property name="converter.sun" location="${jc_home_tools}/bin/converter.bat"/>
        <property name="verify.sun" location="${jc_home_tools}/bin/verifycap.bat"/>
    
        <target name="extractExp" description="Extract *.exp from library jar">
            <unzip dest="${path.exports}">
                <patternset>
                    <exclude name="**/META-INF/*"/>
                </patternset>
                <fileset dir=".">
                    <include name="lib/**/*.jar"/>
                </fileset>
            </unzip>
        </target>
    
        <target name="Compiling" depends="extractExp" description="Compiling java source to class file...">
    
            <mkdir dir="${path.suncap}"/>
            <mkdir dir="${path.class}"/>
            <!-- Deletes all files and subdirectories of "class", without "class" itself -->
            <delete includeemptydirs="true" verbose="false">
                <fileset dir="${path.class}" includes="**/*"/>
            </delete>
    
            <!--Compile *.Java to *.Class-->
            <javac executable="${java.home}" verbose="false" debug="false"
                   destdir="${path.class}"
                   source="7" target="7" compiler="javac1.8"
                   failonerror="true" includeantruntime="false">
                <src path="${path.src}"/>
                <classpath>
                    <pathelement location="${jc_home_tools}/lib/api_classic-3.0.5.jar"/>
                    <pathelement path="${path.exports}"/>
                </classpath>
            </javac>
        </target>
    
        <target name="Conversion" depends="Compiling" description="Converting class file to cap file...">
            <!-- Deletes all files and subdirectories of "suncap", without "suncap" itself -->
            <delete includeemptydirs="true" verbose="false">
                <fileset dir="${path.suncap}" includes="**/*"/>
            </delete>
    
            <!--Convert *.Class to *.cap-->
            <exec executable="${converter.sun}" failonerror="true">
                <env key="JAVA_HOME" value="${java.home}"/>
                <arg line="-v "/>
                <arg line="-target 3.0.5"/>
                <arg line="-out CAP EXP"/>
                <arg line="-exportpath ${path.exports}"/>
                <arg line="-classdir ${path.class}"/>
                <arg line="-d ${path.suncap}"/>
                <arg line="-applet ${app.aid.class} ${app.name.package}.${app.name.class}"/>
                <arg line="${app.name.package}"/>
                <arg line="${app.aid.package}"/>
                <arg line="${app.ver.package}"/>
            </exec>
    
            <copy tofile="${path.suncap}/${app.name.cap}.cap"
                  file="${path.suncap}/${app.path.package}/javacard/${app.name.cap}.cap"/>
            <copy tofile="${path.suncap}/${app.name.cap}.exp"
                  file="${path.suncap}/${app.path.package}/javacard/${app.name.cap}.exp"/>
            <delete dir="${path.suncap}" verbose="false" includeemptydirs="true" excludes=" *.cap *.exp .gitkeep"/>
    
        </target>
    
        <target name="Verification" depends="Conversion" description="Verifying cap file...">
            <path id="path.jcexportfiles">
                <fileset dir="${path.exports}" includes="**/*.exp"/>
            </path>
            <pathconvert property="jcexportfiles" refid="path.jcexportfiles" pathsep=" "/>
    
            <exec executable="${verify.sun}" failonerror="true">
                <env key="JAVA_HOME" value="${java.home}"/>
                <arg line="-target 3.0.5"/>
                <arg line="${jcexportfiles}"/>
                <arg line="${path.suncap}/${app.name.cap}.exp"/>
                <arg line="${path.suncap}/${app.name.cap}.cap"/>
            </exec>
        </target>
    
    </project>
    
  2. ファイル build.properties を下記内容で作成します。

    app.path.package=internal\\sim\\sample_applet
    app.name.cap=sample_applet
    app.name.package=internal.sim.sample_applet
    app.name.class=SampleApplet
    app.aid.package=0xA0:0x00:0x00:0x01:0x15:0x70:0x00:0x00:0x00:0x00:0x00:0x00:0x44:0x45:0x56:0x01
    app.aid.class=0xA0:0x00:0x00:0x01:0x15:0x70:0x00:0x00:0x00:0x00:0x00:0x00:0x44:0x45:0x56:0x02
    app.ver.package=1.0
    
  3. 開発環境では以下の様に表示されます
    ../../_images/1707740095.png

最小構成のアプレットの作成とビルド

ここまでの操作でプロジェクト設定が完了したので、最小構成のアプレットを作成してビルドしてみます。

  1. ソースファイル src/internal/sim/sample_applet/SampleApplet.java を下記内容で作成します。

    package internal.sim.sample_applet;
    
    import javacard.framework.*;
    import uicc.toolkit.*;
    
    public class SampleApplet extends Applet implements ToolkitInterface, ToolkitConstants {
    
        public static void install(byte[] bArray, short bOffset, byte bLength) throws ISOException {
            SampleApplet sampleApplet = new SampleApplet();
            sampleApplet.register();
        }
    
        private SampleApplet() {
    
        }
    
        public Shareable getShareableInterfaceObject(AID clientAID, byte parameter) {
            if (clientAID == null) {
                return this;
            }
            return null;
        }
    
        @Override
        public void process(APDU apdu) throws ISOException {
    
        }
    
        @Override
        public void processToolkit(short event) throws ToolkitException {
    
        }
    }
    

    このソースファイルで実装しているコンストラクタと4個のメソッドが、SIMアプレットの最小限の構造になります。 各メソッドの機能は次の通りです。

    メソッド 機能
    install(bArray, ..)
    • アプレットのインストール時に1回だけ実行されるエントリポイントです。
    • Appletのインスタンスを作成して .register() を呼ぶことが必須機能です。
    • .register()の呼び出しが成功し、本メソッドが例外を発生させなければ、アプレットのインストールが完了します。
    • その他のインストール時処理を行います(各種オブジェクトインスタンスの作成、リソース確保、インストールパラメータの受け取りなど)
    processToolkit(event)
    • 端末(スマートフォンや通信モジュール等)上のSIM Toolkitソフトウェアから、SIM上のUSIM Application宛にEnvelope Commandなどのイベントを受け取ると呼び出されるメソッドです。
    • SIMアプレットの機能は、processToolkitメソッドに各種イベントを処理するハンドラを実装することによって構築します。
    process(apdu)
    • USIM Applicationを経由せずにアプレット宛のAPDUコマンドを受け取った場合に呼び出されます。
    • 本サンプルアプレットでは使用しません。
    getShareableInterfaceObject()
    • SIMカード上の別のアプレット(USIM Application)からこのアプレットの processToolkit メソッドを呼び出すために必要なインタフェースです。
    • 特に変更を加える必要はありません。
  2. コマンドプロンプトから ant と打つとビルドが始まり、Verification が正常終了すると BUILD SUCCESSFUL と出力されます
    PS C:\anyuser\tmp> ant
     :
    BUILD SUCCESSFUL
    Total time: 1 second
    PS C:\anyuser\tmp>
    
  3. ファイル suncap/sample_applet.cap が生成されたことを確認してください。
    ../../_images/1707740518.png

アプレットのデバッグ手段の実装

SIMは極めて機微な情報を容易に取り出せないように格納するデバイスとして設計されている関係上、アプレットの実機デバッグを行うための機構やデバッガが用意されていません。

以下では、ETSI 102 223 などの規格に規定されている「Card Application Toolkit」の機能を利用して スマートフォンの画面上にメモリダンプ等を表示する処理を実装し、いわゆるprintデバッグが行えるようにします。

  1. 下記のソースファイルをディレクトリ src/internal/sim/sample_applet/ に追加します。

    ソースファイル 主な実装機能
    ByteUtil.java byte配列を16進数文字列に変換する処理など
    DiagUtil.java UICC APIを使用してスマートフォン上で動作するSIM Toolkitアプリへ文字列を送り、ダイアログを表示させる
  2. 下記のように src/internal/sim/sample_applet/SampleApplet.java を変更します。

    src/internal/sim/sample_applet/SampleApplet.java (変更後のファイル)

    diff (src/internal/sim/sample_applet/SampleApplet.java)
    --- 1/SampleApplet.java	2023-06-29 14:22:30.218688600 +0900
    +++ 2/SampleApplet.java	2023-06-29 14:22:30.548822900 +0900
    @@ -1,17 +1,45 @@
     package internal.sim.sample_applet;
     
     import javacard.framework.*;
    +import javacardx.framework.math.BCDUtil;
     import uicc.toolkit.*;
     
     public class SampleApplet extends Applet implements ToolkitInterface, ToolkitConstants {
     
    +    private ToolkitRegistry toolkitRegistry;
    +    private byte menuItem1;
    +
    +    static final byte[] nvramText = {'N', 'V', 'R', 'A', 'M', ':', ' '};
    +    static final byte[] ramText = {'R', 'A', 'M', ':', ' '};
    +    static final byte[] menuItem1Text = {'D', 'e', 'b', 'u', 'g'};
    +
    +    private byte[] debugBuffer;
    +    private short[] debugMemBuffer;
    +    private DiagUtil diag;
    +
    +    private byte[] bcdBuffer;
    +    private byte[] tmpBuffer;
    +
         public static void install(byte[] bArray, short bOffset, byte bLength) throws ISOException {
             SampleApplet sampleApplet = new SampleApplet();
             sampleApplet.register();
    +
    +        sampleApplet.initUiccToolkit();
         }
     
    -    private SampleApplet() {
    +    private void initUiccToolkit() {
    +        toolkitRegistry = ToolkitRegistrySystem.getEntry();
     
    +        menuItem1 = toolkitRegistry.initMenuEntry(menuItem1Text, (short) 0, (short) menuItem1Text.length,
    +            PRO_CMD_SELECT_ITEM, false, (byte) 0, (short) 0);
    +    }
    +
    +    private SampleApplet() {
    +        diag = new DiagUtil();
    +        debugBuffer = JCSystem.makeTransientByteArray((short) 16, JCSystem.CLEAR_ON_RESET);
    +        debugMemBuffer = JCSystem.makeTransientShortArray((short) 2, JCSystem.CLEAR_ON_RESET);
    +        bcdBuffer = JCSystem.makeTransientByteArray((short) 10, JCSystem.CLEAR_ON_RESET);
    +        tmpBuffer = JCSystem.makeTransientByteArray((short) 64, JCSystem.CLEAR_ON_RESET);
         }
     
         public Shareable getShareableInterfaceObject(AID clientAID, byte parameter) {
    @@ -28,6 +56,53 @@
     
         @Override
         public void processToolkit(short event) throws ToolkitException {
    +        // 本メソッドが例外を発生させても外部から観測できないため、デバッグが困難になります。
    +        // 必ず全ての例外をキャッチして、デバッグ用に記録するようにします。
    +        try {
    +            processToolkitEvent(event);
    +        } catch (UserException e) {
    +            Util.setShort(debugBuffer, (short) 4, e.getReason());
    +        } catch (ToolkitException e) {
    +            Util.setShort(debugBuffer, (short) 6, e.getReason());
    +            throw e;
    +        } catch (Exception e) {
    +            //その他の例外の発生回数を記録
    +            short i = Util.getShort(debugBuffer, (short) 2);
    +            Util.setShort(debugBuffer, (short) 2, (short) (i + 1));
    +        }
    +    }
    +
    +    public void processToolkitEvent(short event) throws ToolkitException, UserException {
    +        if (event == EVENT_MENU_SELECTION) {
    +            // ユーザがSIM Toolkitアプリを端末上で開き、メニュー項目をタップすると呼ばれる
    +            EnvelopeHandler envHdlr = EnvelopeHandlerSystem.getTheHandler();
    +            byte selectedItemId = envHdlr.getItemIdentifier();
    +
    +            if (selectedItemId == menuItem1) {
    +                DiagUtil.text(menuItem1Text); //'Debug'
    +                diag.displayBytes(debugBuffer, (short) 0, (short) debugBuffer.length);
    +
    +                short pos, bcdBytes;
    +                //NVRAMの残量を表示
    +                JCSystem.getAvailableMemory(debugMemBuffer, (short) 0, JCSystem.MEMORY_TYPE_PERSISTENT);
    +                Util.setShort(bcdBuffer, (short) 0, debugMemBuffer[0]);
    +                Util.setShort(bcdBuffer, (short) 2, debugMemBuffer[1]);
    +                bcdBytes = BCDUtil.convertToBCD(bcdBuffer, (short) 0, (short) 4, bcdBuffer, (short) 0);
    +                pos = Util.arrayCopy(nvramText, (short) 0, tmpBuffer, (short) 0, (short) nvramText.length);
    +                pos = ByteUtil.bcdToCharArray(bcdBuffer, bcdBytes, tmpBuffer, pos);
    +                diag.text(tmpBuffer, (short) 0, pos);
    +
    +                //RAMの残量を表示
    +                JCSystem.getAvailableMemory(debugMemBuffer, (short) 0, JCSystem.MEMORY_TYPE_TRANSIENT_RESET);
    +                Util.setShort(bcdBuffer, (short) 0, debugMemBuffer[0]);
    +                Util.setShort(bcdBuffer, (short) 2, debugMemBuffer[1]);
    +                bcdBytes = BCDUtil.convertToBCD(bcdBuffer, (short) 0, (short) 4, bcdBuffer, (short) 0);
    +                pos = Util.arrayCopy(ramText, (short) 0, tmpBuffer, (short) 0, (short) ramText.length);
    +                pos = ByteUtil.bcdToCharArray(bcdBuffer, bcdBytes, tmpBuffer, pos);
    +                diag.text(tmpBuffer, (short) 0, pos);
    +            }
    +        }
     
         }
    +
     }
    

    変更箇所では以下の処理を実装しています。

    メソッド 処理内容
    SampleApplet() (コンストラクタ) テンポラリバッファとして使用するRAM領域を確保する (JCSystem.makeTransientByteArray())
    install(), initUiccToolkit() メニュー項目 Debug を登録する
    processToolkit(), processToolkitEvent() スマートフォン画面上でメニュー項目 Debug をタップすると イベント EVENT_MENU_SELECTION が発生するので、これを受け取って byte配列 debugBuffer の内容および、 NVRAM領域・RAM領域の残バイト数をダイアログ表示する
    processToolkit() 例外(Exception)をすべて捕捉し、例外発生回数などをbyte配列 debugBuffer に記録する
  3. 上記の変更を行ったら、アプレットがビルドできることを確認してください。 (View → Tool Windows → Antを開き、Verification ターゲットを実行する)

アプレットの書き込みと動作確認

以下では、前項でビルドしたアプレットをSIMにインストールして、動作確認を行います。

  1. コマンドプロンプトを起動(ショートカット: ctrl+shift+@)し、 バッチファイル tools\install_applet.bat を実行します。

    アプレットのインストールが正常完了すると、下記例のように CAP loaded , Installed OK が表示されます。

  2. SIMをスマートフォンに装着し電源を入れます。

  3. アプリ「SIM Toolkit」を起動します。

    注釈

    Androidの場合、スマートフォンの起動後数分待ってからSIM Toolkitアプリを起動してください。(機種によっては、起動直後のアプリ動作が不安定なものがあります)

    注釈

    アプレット側で toolkitRegistry.initMenuEntry() によりメニュー項目を登録している場合のみ、SIM Toolkitアプリを起動できるようにスマートフォンが構成されます。

  4. メニュー項目 Debug が表示されますので、タップします。

    下図のようにダイアログが数回表示され、byte配列 debugBuffer の内容、NVRAMの残容量、RAMの残容量が確認できます。

    この機能を利用・改変することにより、以下のようなデバッグを行うことができます。

    • 例外発生回数・発生有無の確認

    • いわゆるprintデバッグ
      • debugBufferに格納した数値や、DiagUtil.text()で表示させる文字列によって処理内容を確認します。
    • NVRAM/RAMの残容量や、メモリリーク有無の確認
      • 本サンプルアプレットと他のアプレットを同時にインストールしておくと、他のアプレットで発生したメモリリーク等の検出に使うこともできます。

    screenshot_1 screenshot_2 screenshot_3 screenshot_4

IMEIの取得(端末ーアプレット間のインタフェース方法)

UICC API・USIM APIを利用して、端末 (スマートフォンや通信モジュール等) からセルラー回線の状態に関連する情報を得ることができます。

以下では、APIの実装例として、端末からIMEIを取得する機能をサンプルアプレットに追加します。

  1. 下記のようにSampleApplet.javaを変更します。

    src/internal/sim/sample_applet/SampleApplet.java (変更後のファイル)

    diff (src/internal/sim/sample_applet/SampleApplet.java)
    --- 2/SampleApplet.java	2023-06-29 14:22:30.548822900 +0900
    +++ 3/SampleApplet.java	2023-06-29 14:22:30.908912600 +0900
    @@ -8,10 +8,12 @@
     
         private ToolkitRegistry toolkitRegistry;
         private byte menuItem1;
    +    private byte menuItem2;
     
         static final byte[] nvramText = {'N', 'V', 'R', 'A', 'M', ':', ' '};
         static final byte[] ramText = {'R', 'A', 'M', ':', ' '};
         static final byte[] menuItem1Text = {'D', 'e', 'b', 'u', 'g'};
    +    static final byte[] menuItem2Text = {'T', 'e', 's', 't'};
     
         private byte[] debugBuffer;
         private short[] debugMemBuffer;
    @@ -20,6 +22,9 @@
         private byte[] bcdBuffer;
         private byte[] tmpBuffer;
     
    +    private byte[] readBuffer;
    +    private byte[] imeiBuffer;
    +
         public static void install(byte[] bArray, short bOffset, byte bLength) throws ISOException {
             SampleApplet sampleApplet = new SampleApplet();
             sampleApplet.register();
    @@ -32,6 +37,8 @@
     
             menuItem1 = toolkitRegistry.initMenuEntry(menuItem1Text, (short) 0, (short) menuItem1Text.length,
                 PRO_CMD_SELECT_ITEM, false, (byte) 0, (short) 0);
    +        menuItem2 = toolkitRegistry.initMenuEntry(menuItem2Text, (short) 0, (short) menuItem2Text.length,
    +            PRO_CMD_SELECT_ITEM, false, (byte) 0, (short) 0);
         }
     
         private SampleApplet() {
    @@ -40,6 +47,9 @@
             debugMemBuffer = JCSystem.makeTransientShortArray((short) 2, JCSystem.CLEAR_ON_RESET);
             bcdBuffer = JCSystem.makeTransientByteArray((short) 10, JCSystem.CLEAR_ON_RESET);
             tmpBuffer = JCSystem.makeTransientByteArray((short) 64, JCSystem.CLEAR_ON_RESET);
    +
    +        readBuffer = JCSystem.makeTransientByteArray((short) 256, JCSystem.CLEAR_ON_RESET);
    +        imeiBuffer = JCSystem.makeTransientByteArray((short) 15, JCSystem.CLEAR_ON_RESET);
         }
     
         public Shareable getShareableInterfaceObject(AID clientAID, byte parameter) {
    @@ -101,8 +111,41 @@
                     pos = ByteUtil.bcdToCharArray(bcdBuffer, bcdBytes, tmpBuffer, pos);
                     diag.text(tmpBuffer, (short) 0, pos);
                 }
    +            if (selectedItemId == menuItem2) {
    +                loadIMEI();
    +                DiagUtil.text(imeiBuffer);
    +            }
             }
     
         }
     
    +    private void loadIMEI() throws ToolkitException, UserException {
    +        short length = 0;
    +        imeiBuffer[0] = 0;
    +
    +        // 端末に コマンド PROVIDE LOCAL INFORMATION を送り、IMEIを取得する
    +        // 参照規格: ETSI TS 102223 Clause 6.4.15, 6.6.15など
    +        ProactiveHandler ph = ProactiveHandlerSystem.getTheHandler();
    +        ProactiveResponseHandler rh = ProactiveResponseHandlerSystem.getTheHandler();
    +
    +        ph.init(PRO_CMD_PROVIDE_LOCAL_INFORMATION, (byte) 0x01, DEV_ID_TERMINAL);
    +        ph.send();
    +
    +        if (rh.getGeneralResult() == RES_CMD_PERF) {
    +            length = rh.findAndCopyValue(TAG_IMEI, readBuffer, (short) 0);
    +        } else {
    +            UserException.throwIt((short) 0x7001);
    +        }
    +        if (length != 8) {
    +            UserException.throwIt((short) 0x7002);
    +        }
    +        // IMEIのデータ形式を考慮して文字列に変換し、チェックデジットを計算する
    +        // 参照規格: ETSI TS 102223 Clause 8.20, ETSI TS 124008 (3GPP TS 24.008), ETSI TS 123003 など
    +        ByteUtil.nibbleSwap(readBuffer, (short) 0, length);
    +        ByteUtil.bytesToHex(readBuffer, (short) 0, length, tmpBuffer, (short) 0);
    +        short checkDigit = ByteUtil.calcCheckDigitByLuhn(tmpBuffer, (short) 1, (short) 14);
    +        tmpBuffer[15] = (byte) (checkDigit + '0');
    +        Util.arrayCopyNonAtomic(tmpBuffer, (short) 1, imeiBuffer, (short) 0, (short) 15);
    +    }
    +
     }
    
    変更箇所では以下の機能を実装しています。
    • メニュー項目 Test を追加
    • 端末にコマンド PROVIDE LOCAL INFORMATION を送り、IMEIを取得
    • IMEIを文字列に変換する
    • スマートフォンのSIM Toolkitアプリ上でメニュー項目 Test をタップすると、IMEIをダイアログ表示する

    注釈

    PROVIDE LOCAL INFORMATION コマンドの詳細については、ソースコードコメントに記載した参照規格 (ETSI TS 102223など) を参照してください。

  2. 上記の変更を行ったら、アプレットがビルドできることを確認してください。 (View → Tool Windows → Antを開き、Verification ターゲットを実行する)

IMSIおよびICCIDの取得(SIMーアプレット間のインタフェース方法)

UICC API・USIM APIを利用して、SIM上の USIM Applicationが保持しているファイルシステムにアクセスすることができます。

以下では、APIの実装例として、IMSIおよびICCIDの取得機能をサンプルアプレットに追加します。

  1. 下記のように SampleApplet.java を変更します。

    src/internal/sim/sample_applet/SampleApplet.java (変更後のファイル)

    diff (src/internal/sim/sample_applet/SampleApplet.java)
    --- 3/SampleApplet.java	2023-06-29 14:22:30.908912600 +0900
    +++ 4/SampleApplet.java	2023-06-29 14:22:31.248869200 +0900
    @@ -2,9 +2,17 @@
     
     import javacard.framework.*;
     import javacardx.framework.math.BCDUtil;
    +import uicc.access.*;
     import uicc.toolkit.*;
    +import uicc.usim.access.USIMConstants;
     
     public class SampleApplet extends Applet implements ToolkitInterface, ToolkitConstants {
    +    //SIMカード上で稼働中のUSIM ApplicationのAID値
    +    private static final byte[] usimAID = {
    +        (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x87, (byte) 0x10, (byte) 0x02, (byte) 0xFF,
    +        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x89, (byte) 0x03, (byte) 0x02, (byte) 0x00, (byte) 0x00};
    +    private FileView uiccFileView;
    +    private FileView usimAppFileView;
     
         private ToolkitRegistry toolkitRegistry;
         private byte menuItem1;
    @@ -24,6 +32,8 @@
     
         private byte[] readBuffer;
         private byte[] imeiBuffer;
    +    private byte[] imsiBuffer;
    +    private byte[] iccidBuffer;
     
         public static void install(byte[] bArray, short bOffset, byte bLength) throws ISOException {
             SampleApplet sampleApplet = new SampleApplet();
    @@ -39,6 +49,9 @@
                 PRO_CMD_SELECT_ITEM, false, (byte) 0, (short) 0);
             menuItem2 = toolkitRegistry.initMenuEntry(menuItem2Text, (short) 0, (short) menuItem2Text.length,
                 PRO_CMD_SELECT_ITEM, false, (byte) 0, (short) 0);
    +
    +        uiccFileView = UICCSystem.getTheUICCView(JCSystem.NOT_A_TRANSIENT_OBJECT);
    +        usimAppFileView = UICCSystem.getTheFileView(usimAID, (short) 0, (byte) usimAID.length, JCSystem.NOT_A_TRANSIENT_OBJECT);
         }
     
         private SampleApplet() {
    @@ -50,6 +63,8 @@
     
             readBuffer = JCSystem.makeTransientByteArray((short) 256, JCSystem.CLEAR_ON_RESET);
             imeiBuffer = JCSystem.makeTransientByteArray((short) 15, JCSystem.CLEAR_ON_RESET);
    +        imsiBuffer = JCSystem.makeTransientByteArray((short) (1 + 15), JCSystem.CLEAR_ON_RESET);
    +        iccidBuffer = JCSystem.makeTransientByteArray((short) (1 + 20), JCSystem.CLEAR_ON_RESET);
         }
     
         public Shareable getShareableInterfaceObject(AID clientAID, byte parameter) {
    @@ -114,6 +129,10 @@
                 if (selectedItemId == menuItem2) {
                     loadIMEI();
                     DiagUtil.text(imeiBuffer);
    +                loadIMSI();
    +                DiagUtil.text(imsiBuffer, (short) 1, (short) imsiBuffer[0]);
    +                loadICCID(true);
    +                DiagUtil.text(iccidBuffer, (short) 1, (short) iccidBuffer[0]);
                 }
             }
     
    @@ -148,4 +167,52 @@
             Util.arrayCopyNonAtomic(tmpBuffer, (short) 1, imeiBuffer, (short) 0, (short) 15);
         }
     
    +    private void loadICCID(boolean removePadding) throws ToolkitException {
    +        //ICCIDをEF_ICCIDから読み出す
    +        // 参照規格: ETSI TS 102221 Clause 13.2
    +        short length = 10;
    +        short digits = 20;
    +        readBinaryFromEF(uiccFileView, UICCConstants.FID_EF_ICCID, readBuffer, (short) 0, length);
    +        //文字列に変換
    +        ByteUtil.nibbleSwap(readBuffer, (short) 0, length);
    +        ByteUtil.bytesToHex(readBuffer, (short) 0, length, iccidBuffer, (short) 1);
    +
    +        if (removePadding) {
    +            // 末尾が'F'の場合は桁数をつめる
    +            while (iccidBuffer[(short) (digits)] == (byte) 'F') {
    +                digits--;
    +            }
    +        }
    +        iccidBuffer[0] = (byte) digits;
    +    }
    +
    +    private void loadIMSI() throws ToolkitException {
    +        //IMSI (固定長9バイト)をUSIM ApplicationのEF_IMSIから読み出す
    +        short length = 9;
    +        readBinaryFromEF(usimAppFileView, USIMConstants.FID_EF_IMSI, readBuffer, (short) 0, length);
    +
    +        // IMSIのデータ形式を考慮して文字列に変換する
    +        length = readBuffer[0];
    +        if (length == 0 || length > 8) {
    +            imsiBuffer[0] = 0;
    +            return;
    +        }
    +        ByteUtil.nibbleSwap(readBuffer, (short) 1, length);
    +        ByteUtil.bytesToHex(readBuffer, (short) 1, length, tmpBuffer, (short) 0);
    +
    +        // 末尾が'F'の時は桁数をつめる
    +        // 参照規格: 3GPP TS 31.102 Clause 4.2.2 など
    +        short digits = (short) (length * 2 - 1);
    +        if (tmpBuffer[(short) (digits - 1)] == (byte) 'F') {
    +            digits--;
    +        }
    +        imsiBuffer[0] = (byte) digits;
    +        Util.arrayCopyNonAtomic(tmpBuffer, (short) 1, imsiBuffer, (short) 1, digits);
    +    }
    +
    +    private short readBinaryFromEF(FileView fileView, short FID, byte[] dstBuffer, short dstOffset, short readLength) {
    +        fileView.select(FID);
    +        return fileView.readBinary((short) 0, dstBuffer, (short) dstOffset, readLength);
    +    }
    +
     }
    

    変更箇所では以下の機能を実装しています。

    メソッド 機能
    readBinaryFromEF() UICC FileView APIを利用してUSIM Applicationに接続し、ファイル (EF, Elementary File )の内容を読み出す
    loadICCID(), loadIMSI() EFからICCID, IMSIを読み出し、それぞれのデータ形式を考慮して文字列に変換する
    processToolkit(), processToolkitEvent() スマートフォンのSIM Toolkitアプリ上でメニュー項目 Test をタップすると、ICCID, IMSIをダイアログに表示する

    注釈

    処理詳細については、ソースコードコメントに記載した参照規格 (ETSI TS 102221, 3GPP TS 31.102など) を参照してください。

  2. 上記の変更を行ったら、アプレットがビルドできることを確認してください。 (View → Tool Windows → Antを開き、Verification ターゲットを実行する)

HTTPクライアント(アプレットー外部サーバ間のインタフェース方法)

SIM自体には通信機能はありませんが、ETSI TS 102223 規格に規定される Bearer Independent Protocol 機能を利用すると、 端末 (スマートフォンや通信モジュール等)がSIMに代わってTCPまたはUDPで外部サーバへ接続して、SIMから外部サーバへの通信を中継させることができます。

以下では、APIの実装例として、HTTPでアプレットコンソールに接続してIMEI・IMSI・ICCIDの各値を送信する機能を追加します。

  1. ソースファイル SampleApplet.java を下記からダウンロードしたファイルで上書きします。

    src/internal/sim/sample_applet/SampleApplet.java (変更後のファイル)

    主要な変更内容は以下のとおりです。(詳細についてはソースコードを参照ください)

    メソッド 処理内容
    openChannel() OPEN CHANNEL コマンドを発行して、Bearer Independent Channel を開く (指定した外部IPアドレスへTCP接続するよう端末に指示する)
    closeChannel() Bearer Independent Channelを閉じ、端末側のリソースを開放する
    createHTTPHeader(), createJsonBody() HTTP HeaderおよびBody(JSON形式の文字列データ)をRAM上に生成する
    sendHTTPPost(), sendData() SEND DATAコマンドを発行して、Bearer Independent Channelを使用してTCPのペイロード(HTTP Request)を端末経由で宛先サーバへ送信する
    initUiccToolkit() アプレットインストール時に、イベントEVENT_EVENT_DOWNLOAD_DATA_AVAILABLE, EVENT_EVENT_DOWNLOAD_CHANNEL_STATUS をprocessToolkit() で受信するための設定を行う
    processToolkit(), processToolkitEvent(), processHTTPResponse()
    • イベントEVENT_EVENT_DOWNLOAD_DATA_AVAILABLE を受け取って、RECEIVE DATAコマンドを発行する。
    • RECEIVE DATAコマンドの応答から、TCPのペイロード (HTTP Response)を抽出し、先頭部分を ダイアログ表示する。
  2. 接続先IPアドレスは SampleApplet.java の変数 serverAddrにハードコードされていますので、適切な値に書き換えます。
    • 開通案内に記されているアプレットコンソールのホスト名 (xxxxx.sim-applet.com , xxxxxは契約毎に異なります) のIPアドレスを調べて書き換えてください。

    必要に応じて、 serverPort の値も変更してください。

  3. 上記の変更を行ったら、アプレットをビルドし、tools/install_applet.bat を実行してSIMにインストールします。

  4. アプレットコンソールにrootアカウントでログインし、アプレットをインストールしたSIMのICCIDを登録します。

    このとき、「Send Raw Data」を必ずチェックしてください。 AESKeyは適当な値を入力してください。

    register_iccid

    注釈

    既に同番でSIMが登録済の場合は、削除してから再登録してください。

    注釈

    本サンプルアプレットはHTTPペイロードを暗号化しないため、 Send Raw Data を必ずチェックする必要があります。

  5. アプレットをインストールしたSIMをスマートフォンに装着して起動し、SIM Toolkitアプリを起動します。

  6. メニュー項目 Test をタップします。 データの送信(HTTP POSTリクエスト)が成功すると、下図のようにHTTP Responseの先頭部分が表示されます。

    screenshot_http

  7. アプレットコンソール上では、下図のように IMSI, IMEI, Historyの各項目にデータが登録される様子を確認できます。

    screenshot_console