/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.telephony.uicc.euicc.apdu; import android.annotation.Nullable; import android.os.Handler; import android.telephony.IccOpenLogicalChannelResponse; import android.telephony.Rlog; import com.android.internal.telephony.CommandsInterface; import com.android.internal.telephony.uicc.IccIoResult; import com.android.internal.telephony.uicc.euicc.async.AsyncResultCallback; import com.android.internal.telephony.uicc.euicc.async.AsyncResultHelper; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; /** * This class sends a list of APDU commands to an AID on a UICC. A logical channel will be opened * before sending and closed after all APDU commands are sent. The complete response of the last * APDU command will be returned. If any APDU command returns an error status (other than * {@link #STATUS_NO_ERROR}) or causing an exception, an {@link ApduException} will be returned * immediately without sending the rest of commands. This class is thread-safe. * * @hide */ public class ApduSender { private static final String LOG_TAG = "ApduSender"; // Parameter and response used by the command to get extra responses of an APDU command. private static final int INS_GET_MORE_RESPONSE = 0xC0; private static final int SW1_MORE_RESPONSE = 0x61; // Status code of APDU response private static final int STATUS_NO_ERROR = 0x9000; private static void logv(String msg) { Rlog.v(LOG_TAG, msg); } private final String mAid; private final boolean mSupportExtendedApdu; private final OpenLogicalChannelInvocation mOpenChannel; private final CloseLogicalChannelInvocation mCloseChannel; private final TransmitApduLogicalChannelInvocation mTransmitApdu; // Lock for accessing mChannelOpened. We only allow to open a single logical channel at any // time for an AID. private final Object mChannelLock = new Object(); private boolean mChannelOpened; /** * @param aid The AID that will be used to open a logical channel to. */ public ApduSender(CommandsInterface ci, String aid, boolean supportExtendedApdu) { mAid = aid; mSupportExtendedApdu = supportExtendedApdu; mOpenChannel = new OpenLogicalChannelInvocation(ci); mCloseChannel = new CloseLogicalChannelInvocation(ci); mTransmitApdu = new TransmitApduLogicalChannelInvocation(ci); } /** * Sends APDU commands. * * @param requestProvider Will be called after a logical channel is opened successfully. This is * in charge of building a request with all APDU commands to be sent. This won't be called * if any error happens when opening a logical channel. * @param resultCallback Will be called after an error or the last APDU command has been * executed. The result will be the full response of the last APDU command. Error will be * returned as an {@link ApduException} exception. * @param handler The handler that {@code requestProvider} and {@code resultCallback} will be * executed on. */ public void send( RequestProvider requestProvider, AsyncResultCallback resultCallback, Handler handler) { synchronized (mChannelLock) { if (mChannelOpened) { AsyncResultHelper.throwException( new ApduException("Logical channel has already been opened."), resultCallback, handler); return; } mChannelOpened = true; } mOpenChannel.invoke(mAid, new AsyncResultCallback() { @Override public void onResult(IccOpenLogicalChannelResponse openChannelResponse) { int channel = openChannelResponse.getChannel(); int status = openChannelResponse.getStatus(); if (channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL || status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR) { synchronized (mChannelLock) { mChannelOpened = false; } resultCallback.onException( new ApduException("Failed to open logical channel opened for AID: " + mAid + ", with status: " + status)); return; } RequestBuilder builder = new RequestBuilder(channel, mSupportExtendedApdu); Throwable requestException = null; try { requestProvider.buildRequest(openChannelResponse.getSelectResponse(), builder); } catch (Throwable e) { requestException = e; } if (builder.getCommands().isEmpty() || requestException != null) { // Just close the channel if we don't have commands to send or an error // was encountered. closeAndReturn(channel, null /* response */, requestException, resultCallback, handler); return; } sendCommand(builder.getCommands(), 0 /* index */, resultCallback, handler); } }, handler); } /** * Sends the current command and then continue to send the next one. If this is the last * command or any error happens, {@code resultCallback} will be called. * * @param commands All commands to be sent. * @param index The current command index. */ private void sendCommand( List commands, int index, AsyncResultCallback resultCallback, Handler handler) { ApduCommand command = commands.get(index); mTransmitApdu.invoke(command, new AsyncResultCallback() { @Override public void onResult(IccIoResult response) { // A long response may need to be fetched by multiple following-up APDU // commands. Makes sure that we get the complete response. getCompleteResponse(command.channel, response, null /* responseBuilder */, new AsyncResultCallback() { @Override public void onResult(IccIoResult fullResponse) { logv("Full APDU response: " + fullResponse); int status = (fullResponse.sw1 << 8) | fullResponse.sw2; if (status != STATUS_NO_ERROR) { closeAndReturn(command.channel, null /* response */, new ApduException(status), resultCallback, handler); return; } // Last command if (index == commands.size() - 1) { closeAndReturn(command.channel, fullResponse.payload, null /* exception */, resultCallback, handler); return; } // Sends the next command sendCommand(commands, index + 1, resultCallback, handler); } }, handler); } }, handler); } /** * Gets the full response. * * @param lastResponse Will be checked to see if we need to fetch more. * @param responseBuilder For continuously building the full response. It should not contain the * last response. If it's null, a new builder will be created. * @param resultCallback Error will be included in the result and no exception will be returned. */ private void getCompleteResponse( int channel, IccIoResult lastResponse, @Nullable ByteArrayOutputStream responseBuilder, AsyncResultCallback resultCallback, Handler handler) { ByteArrayOutputStream resultBuilder = responseBuilder == null ? new ByteArrayOutputStream() : responseBuilder; try { resultBuilder.write(lastResponse.payload); } catch (IOException e) { // Should never reach here. } if (lastResponse.sw1 != SW1_MORE_RESPONSE) { lastResponse.payload = resultBuilder.toByteArray(); resultCallback.onResult(lastResponse); return; } mTransmitApdu.invoke( new ApduCommand(channel, 0 /* cls */, INS_GET_MORE_RESPONSE, 0 /* p1 */, 0 /* p2 */, lastResponse.sw2, "" /* cmdHex */), new AsyncResultCallback() { @Override public void onResult(IccIoResult response) { getCompleteResponse( channel, response, resultBuilder, resultCallback, handler); } }, handler); } /** * Closes the opened logical channel. * * @param response If {@code exception} is null, this will be returned to {@code resultCallback} * after the channel has been closed. * @param exception If not null, this will be returned to {@code resultCallback} after the * channel has been closed. */ private void closeAndReturn( int channel, @Nullable byte[] response, @Nullable Throwable exception, AsyncResultCallback resultCallback, Handler handler) { mCloseChannel.invoke(channel, new AsyncResultCallback() { @Override public void onResult(Boolean aBoolean) { synchronized (mChannelLock) { mChannelOpened = false; } if (exception == null) { resultCallback.onResult(response); } else { resultCallback.onException(exception); } } }, handler); } }