UICC access with SEEK in CyanogenMod on Galaxy S2/S3

In our last blog post SEEK adaptations in RIL for Galaxy S3, we analyzed the RIL commands for APDU-based access to the UICC on Samsung's Galaxy S3 and provided some first details on how we implemented this functionality in our SuperNexus-based SuperSmile Android ROM. Now we want to go a step further and provide step-by-step instructions on how to integrate this functionality into CyanogenMod for Samsung's Galaxy S2 and Galaxy S3 (and possibly other devices based on Exynos 4).

First of all, you need to grab the CyanogenMod source:

$ mkdir cm-10.1
$ cd cm-10.1
$ repo init -u git://github.com/CyanogenMod/android.git -b cm-10.1

Note: We also provide a patch for CM 10.2, so you may use -b cm-10.2 as well.

$ repo sync
$ cd vendor/cm
$ ./get-prebuilts
$ cd ../..
$ source build/envsetup.sh
$ breakfast i9300

Note: Instead of (or in addition to) i9300 (Galaxy S3) you can also get the device specific code for Galaxy S2 (i9100) using breakfast i9100.

When you got your working copy of the CyanogenMod repository ready, you can apply SEEK's SmartCard API patches (download them here, you find them in smartcard-api-3_0_0.tgz):

$ curl -O https://seek-for-android.googlecode.com/files/smartcard-api-3_0_0.tgz
$ tar zxf smartcard-api-3_0_0.tgz
$ patch -p1 < smartcard-api-3_0_0/smartcard-api.patch
$ patch -p1 < smartcard-api-3_0_0/uicc.patch
$ patch -p1 < smartcard-api-3_0_0/emulator.patch

Now we have the CyanogenMod source tree with SEEK. While SEEK already provides patches for APDU-based access to the UICC through the RIL (uicc.patch and emulator.patch), these patches only work for the Android emulator and not for real devices. Therefore, we need to modify the RIL interface to permit APDU exchange with the UICC on real devices. Both, the Galaxy S2 and the Galaxy S3 are based on the Exynos 4 SoC platform. Their RIL interface is encapsulated in SamsungExynos4RIL.java (in frameworks/opt/telephony/src/java/com/android/internal/telephony/). So all we need to do is implement the Exynos 4-specific versions of the commands for UICC access in SamsungExynos4RIL.java:

  • iccOpenChannel(...) for opening a communication channel to an applet on the UICC,
  • iccExchangeAPDU(...) for sending APDU commands to the UICC, and
  • iccCloseChannel(...) for closing a previously opened communication channel.

First, we define some constants to tag the responses to the above commands so we can later detect them in the response message handler:

    private static final int RIL_OEM_HOOK_RAW_EVENT_EXCHANGE_APDU_DONE = 1;
    private static final int RIL_OEM_HOOK_RAW_EVENT_OPEN_CHANNEL_DONE  = 2;
    private static final int RIL_OEM_HOOK_RAW_EVENT_CLOSE_CHANNEL_DONE = 3;

Then, we add the methods to invoke the three commands (see our previous blog post for details on the implementation of these commands, the command message formats and the response message formats):

    @Override
    public void iccOpenChannel(String AID, Message result) {
        if (AID == null) {
            AID = "";
        }
        
        byte[] aidBytes = new byte[AID.length() / 2];
        for (int i = 0; i < aidBytes.length; ++i) {
            aidBytes[i] = (byte) Integer.parseInt(AID.substring(2 * i, 2 + 2 * i), 16);
        }
        
        ByteArrayOutputStream commandByteStream = new ByteArrayOutputStream();
        DataOutputStream commandOutputStream = new DataOutputStream(commandByteStream);
        try {
            commandOutputStream.writeByte(21);
            commandOutputStream.writeByte(9);
            commandOutputStream.writeShort(4 + aidBytes.length);
            commandOutputStream.write(aidBytes);
        } catch (IOException ex) {
            Log.e(LOG_TAG, "CMD_OPEN_CHANNEL: open failed", ex);
        }
        
        if (RILJ_LOGD) riljLog("sendRawRequest/iccOpenChannel: AID=" + AID);
        
        // inform response handler that this is an open channel request
        result.arg1 = RIL_OEM_HOOK_RAW_EVENT_OPEN_CHANNEL_DONE;
        
        invokeOemRilRequestRaw(commandByteStream.toByteArray(), result);
    }
    @Override
    public void iccExchangeAPDU(int cla, int command, int channel,
                                int p1, int p2, int p3, String data,
                                Message result) {
        ByteArrayOutputStream commandByteStream = new ByteArrayOutputStream();
        DataOutputStream commandOutputStream = new DataOutputStream(commandByteStream);
        try {
            commandOutputStream.writeByte(21);
            
            int commandLength = 12;
            if (p3 == -1) {
                commandOutputStream.writeByte(12);
            } else {
                commandOutputStream.writeByte(11);
                ++commandLength;
            }
            
            byte[] dataBytes = null;
            if (data != null) {
                dataBytes = new byte[data.length() / 2];
                commandLength += dataBytes.length;
                for (int i = 0; i < dataBytes.length; ++i) {
                    dataBytes[i] = (byte) Integer.parseInt(data.substring(2 * i, 2 + 2 * i), 16);
                }
            }
            
            commandOutputStream.writeShort(commandLength);
            commandOutputStream.writeByte(cla);
            commandOutputStream.writeByte(command);
            commandOutputStream.writeByte(p1);
            commandOutputStream.writeByte(p2);
            if (p3 != -1) {
                commandOutputStream.writeByte(p3);
            }
            commandOutputStream.writeInt(channel);
            if (dataBytes != null) {
                commandOutputStream.write(dataBytes);
            }
        } catch (IOException ex) {
            Log.e(LOG_TAG, "CMD_ECHANGE_APDU: send failed", ex);
        }
        
        if (RILJ_LOGD) riljLog("sendRawRequest/iccExchangeAPDU: " +
                               "channel=0x" + Integer.toHexString(channel) +
                               ", cla=0x" + Integer.toHexString(cla) +
                               ", command=0x" + Integer.toHexString(command) +
                               ", p1=0x" + Integer.toHexString(p1) +
                               ", p2=0x" + Integer.toHexString(p2) +
                               ", p3=0x" + Integer.toHexString(p3) +
                               ", data=" + data);

        // inform response handler that this is an exchange APDU request
        result.arg1 = RIL_OEM_HOOK_RAW_EVENT_EXCHANGE_APDU_DONE;
        
        invokeOemRilRequestRaw(commandByteStream.toByteArray(), result);
    }
    @Override
    public void iccCloseChannel(int channel, Message result) {
        ByteArrayOutputStream commandByteStream = new ByteArrayOutputStream();
        DataOutputStream commandOutputStream = new DataOutputStream(commandByteStream);
        try {
            commandOutputStream.writeByte(21);
            commandOutputStream.writeByte(10);
            if (channel != 0) {
                commandOutputStream.writeShort(8);
                commandOutputStream.writeInt(channel);
            } else {
                commandOutputStream.writeShort(4);
            }
        } catch (IOException ex) {
            Log.e(LOG_TAG, "CMD_CLOSE_CHANNEL: close failed", ex);
        }
        
        if (RILJ_LOGD) riljLog("sendRawRequest/iccCloseChannel: channel=0x" + Integer.toHexString(channel));
        
        //inform response handler that this is a close channel request
        result.arg1 = RIL_OEM_HOOK_RAW_EVENT_CLOSE_CHANNEL_DONE;
        
        invokeOemRilRequestRaw(commandByteStream.toByteArray(), result);
    }

Now we can send smartcard commands to the UICC. Next, we need to detect and decode the responses to the above commands. All three commands are wrapped inside an OEM_HOOK_RAW RIL request. We, therefore, need to intercept the response to this request and decode the responses to our new commands into the format expected by SEEK.

First, we want to intercept the response to OEM_HOOK_RAW. We, therefore, change the default handler for RIL_REQUEST_OEM_HOOK_RAW from responseRaw(...) to our customized handler responseRawCheckRequest(...):

    @Override
    protected void processSolicited (Parcel p) {
        [...]
        if (error == 0 || p.dataAvail() > 0) {
            // either command succeeds or command fails but with data payload
            try {switch (rr.mRequest) {
            [...]
            case RIL_REQUEST_RESET_RADIO: ret =  responseVoid(p); break;
            case RIL_REQUEST_OEM_HOOK_RAW: ret =  responseRawCheckRequest(p, rr.mResult); break;
            case RIL_REQUEST_OEM_HOOK_STRINGS: ret =  responseStrings(p); break;
            [...]

Within our new method responseRawCheckRequest(...), we need to check if the corresponding OEM_HOOK_RAW request was tagged with one of our RIL_OEM_HOOK_RAW_EVENT_* constants. If we find a tag, we need to decode the response into the format expected by SEEK, otherwise we pass the response to the original responseRaw(...) method:

    protected Object responseRawCheckRequest(Parcel p, Message mResult) {
        if (mResult == null || p == null) return responseRaw(p);
        
        Object ret = null;
        byte[] responseByteArray;
        int channel = 0;

        switch(mResult.arg1) {

For the iccExchangeAPDU(...) command, the response APDU (byte array) needs to be converted into an IccIoResult object (by splitting the R-APDU into the data field and the two bytes of the status word):

            case RIL_OEM_HOOK_RAW_EVENT_EXCHANGE_APDU_DONE:
                if (RILJ_LOGD) riljLog("iccExchangeAPDU done");
                
                responseByteArray = p.createByteArray();
                if (responseByteArray != null) {
                    if (RILJ_LOGD) riljLog("iccExchangeAPDU: received response byte array: " + IccUtils.bytesToHexString(responseByteArray));
                    if (responseByteArray.length >= 2) {
                        ret = new IccIoResult(
                            responseByteArray[responseByteArray.length - 2] & 0x0ff,
                            responseByteArray[responseByteArray.length - 1] & 0x0ff,
                            Arrays.copyOf(responseByteArray, responseByteArray.length - 2));
                    }
                }
                break;

For the iccOpenChannel(...) command, the response contains the assigned channel ID and the R-APDU that was received in response to the applet selection command. SEEK currently only processes the channel ID and does not expect a select response. Therefore, we extract the channel number and drop the select response. Note, however, that the SEEK SmartCard API contains a method to get the select response. Therefore, we should forward this select response to SEEK in a future implementation. For now the getSelectResponse() method will return null even if a response had been received.

            case RIL_OEM_HOOK_RAW_EVENT_OPEN_CHANNEL_DONE:
                if (RILJ_LOGD) riljLog("iccOpenChannel done");
                
                responseByteArray = p.createByteArray();
                if (responseByteArray != null) {
                    if (RILJ_LOGD) riljLog("iccOpenChannel: received response byte array: " + IccUtils.bytesToHexString(responseByteArray));
                    if (responseByteArray.length > 0) {
                        int channelLength = responseByteArray[0] & 0x0ff;
                        if (responseByteArray.length > channelLength) {
                            for (int i = channelLength; i > 0; --i) {
                                channel <<= 8;
                                channel |= responseByteArray[i] & 0x0ff;
                            }
                        }
                    }
                }
                ret = new int[]{ channel };
                break;

For the iccCloseChannel(...) command, SEEK does not process the response, so we simply forward any received response byte arrays:

            case RIL_OEM_HOOK_RAW_EVENT_CLOSE_CHANNEL_DONE:
                if (RILJ_LOGD) riljLog("iccCloseChannel done");
                
                responseByteArray = p.createByteArray();
                if (responseByteArray != null) {
                    if (RILJ_LOGD) riljLog("iccCloseChannel: received response byte array: " + IccUtils.bytesToHexString(responseByteArray));
                }
                
                ret = responseByteArray;
                break;

If this response to OEM_HOOK_RAW is not the result of any of our three new commands, we pass the response to the original responseRaw(...) method:

            default:
                ret = responseRaw(p);
                break;
        }
        
        return ret;
    }

Finally, we have to account for differently numbered error codes between the SEEK implementation and the actual behavior of Samsung's Exynos 4 RIL. We define the error code constants for Exynos 4:

    private static final int RIL_OEM_ERROR_MISSING_RESOURCE  = 29;
    private static final int RIL_OEM_ERROR_NO_SUCH_ELEMENT   = 30;
    private static final int RIL_OEM_ERROR_INVALID_PARAMETER = 27;

Then, we check any received error codes for these values and translate them to the values expected by SEEK:

    @Override
    protected void processSolicited (Parcel p) {
        [...]
        if (error != 0) {
            switch (rr.mRequest) {
                [...]
            }

            switch (error) {
                case RIL_OEM_ERROR_MISSING_RESOURCE:  error = MISSING_RESOURCE;  break;
                case RIL_OEM_ERROR_NO_SUCH_ELEMENT:   error = NO_SUCH_ELEMENT;   break;
                case RIL_OEM_ERROR_INVALID_PARAMETER: error = INVALID_PARAMETER; break;
                default: break;
            }

            rr.onError(error, ret);
            rr.release();
            return;
        }

Now you can compile your CyanogenMod ROM with APDU-based UICC access through SEEK (comparable to the stock ROM on Galaxy S3 and some versions of the Galaxy S2) for your Galaxy S2 or your Galaxy S3.

You can grab the patches here:

Thanks to Nikolay for debugging, testing with his S2 and the lively discussion.