Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 388fa404 authored by Jun Yin's avatar Jun Yin
Browse files

ApduSender

ApduSender sends a list of APDU commands to an AID on a UICC. A
logical channel will be will be opened before sending and closed
after all APDU commands are sent.

Bug: 38206971
Test: unit test
Change-Id: I59796b82cf3f1aed1132c5f0ad83cd6fa5f3f006
parent 6d5e9e59
Loading
Loading
Loading
Loading
+63 −0
Original line number Diff line number Diff line
/*
 * 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.telephony.IccOpenLogicalChannelResponse;

/**
 * The exception of failing to execute an APDU command. It can be caused by an error happening on
 * opening the basic or logical channel, or the response of the APDU command is not success
 * ({@link ApduSender#STATUS_NO_ERROR}).
 *
 * @hide
 */
public class ApduException extends Exception {
    private final int mApduStatus;

    /** Creates an exception with the apduStatus code of the response of an APDU command. */
    public ApduException(int apduStatus) {
        super();
        mApduStatus = apduStatus;
    }

    public ApduException(String message) {
        super(message);
        mApduStatus = 0;
    }

    /**
     * @return The error status of the response of an APDU command. An error status can be any
     *         positive 16-bit integer (i.e., SW1 & SW2) other than
     *         {@link ApduSender#STATUS_NO_ERROR} which means no error. For an error encountered
     *         when opening a logical channel before the APDU command gets sent, this is not the
     *         status defined in {@link IccOpenLogicalChannelResponse}. In this caes, 0 will be
     *         returned and the message of this exception will have the detailed error information.
     */
    public int getApduStatus() {
        return mApduStatus;
    }

    /** @return The hex string of the error status. */
    public String getStatusHex() {
        return Integer.toHexString(mApduStatus);
    }

    @Override
    public String getMessage() {
        return super.getMessage() + " (apduStatus=" + getStatusHex() + ")";
    }
}
+253 −0
Original line number Diff line number Diff line
/*
 * 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<byte[]> 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<IccOpenLogicalChannelResponse>() {
            @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<ApduCommand> commands,
            int index,
            AsyncResultCallback<byte[]> resultCallback,
            Handler handler) {
        ApduCommand command = commands.get(index);
        mTransmitApdu.invoke(command, new AsyncResultCallback<IccIoResult>() {
            @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<IccIoResult>() {
                            @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<IccIoResult> 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<IccIoResult>() {
                    @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<byte[]> resultCallback,
            Handler handler) {
        mCloseChannel.invoke(channel, new AsyncResultCallback<Boolean>() {
            @Override
            public void onResult(Boolean aBoolean) {
                synchronized (mChannelLock) {
                    mChannelOpened = false;
                }

                if (exception == null) {
                    resultCallback.onResult(response);
                } else {
                    resultCallback.onException(exception);
                }
            }
        }, handler);
    }
}
+300 −0

File added.

Preview size limit exceeded, changes collapsed.

+117 −0
Original line number Diff line number Diff line
/*
 * 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 static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;

import android.annotation.Nullable;
import android.os.AsyncResult;
import android.os.Message;

import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.uicc.IccIoResult;
import com.android.internal.telephony.uicc.IccUtils;

import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

/** Utility to set up mocks for communication with UICC through logical channel. */
public final class LogicalChannelMocker {
    private static final int LOGICAL_CHANNEL = 1;

    /**
     * @param responseObject Can be either a string or an exception.
     * @return The mock channel number.
     */
    public static int mockOpenLogicalChannelResponse(CommandsInterface mockCi,
            @Nullable Object responseObject) {
        boolean isException = responseObject instanceof Throwable;
        int[] responseInts = isException ? null : getSelectResponse(responseObject.toString());
        Throwable exception = isException ? (Throwable) responseObject : null;

        ArgumentCaptor<Message> response = ArgumentCaptor.forClass(Message.class);
        doAnswer((Answer<Void>) invocation -> {
            Message msg = response.getValue();
            AsyncResult.forMessage(msg, responseInts, exception);
            msg.sendToTarget();
            return null;
        }).when(mockCi).iccOpenLogicalChannel(anyString(), anyInt(), response.capture());
        return LOGICAL_CHANNEL;
    }

    /**
     * @param responseObjects Can be either a string or an exception. For string, the last 4
     *         digits are the status (sw1, sw1).
     */
    public static void mockSendToLogicalChannel(CommandsInterface mockCi, int channel,
            Object... responseObjects) {
        ArgumentCaptor<Message> response = ArgumentCaptor.forClass(Message.class);
        doAnswer(new Answer() {
            private int mIndex = 0;

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Object responseObject = responseObjects[mIndex++];
                boolean isException = responseObject instanceof Throwable;
                int sw1 = 0;
                int sw2 = 0;
                String hex = responseObject.toString();
                if (!isException) {
                    int l = hex.length();
                    sw1 = Integer.parseInt(hex.substring(l - 4, l - 2), 16);
                    sw2 = Integer.parseInt(hex.substring(l - 2), 16);
                    hex = hex.substring(0, l - 4);
                }
                IccIoResult result = isException ? null : new IccIoResult(sw1, sw2, hex);
                Throwable exception = isException ? (Throwable) responseObject : null;

                Message msg = response.getValue();
                AsyncResult.forMessage(msg, result, exception);
                msg.sendToTarget();
                return null;
            }
        }).when(mockCi).iccTransmitApduLogicalChannel(eq(channel), anyInt(), anyInt(), anyInt(),
                anyInt(), anyInt(), anyString(), response.capture());
    }

    public static void mockCloseLogicalChannel(CommandsInterface mockCi, int channel) {
        ArgumentCaptor<Message> response = ArgumentCaptor.forClass(Message.class);
        doAnswer((Answer<Void>) invocation -> {
            Message msg = response.getValue();
            AsyncResult.forMessage(msg);
            msg.sendToTarget();
            return null;
        }).when(mockCi).iccCloseLogicalChannel(eq(channel), response.capture());
    }

    private static int[] getSelectResponse(String responseHex) {
        byte[] responseBytes = IccUtils.hexStringToBytes("00" + responseHex);
        int[] responseInts = new int[responseBytes.length];
        responseInts[0] = LOGICAL_CHANNEL;
        for (int i = 1; i < responseInts.length; ++i) {
            responseInts[i] = responseBytes[i];
        }
        return responseInts;
    }

    private LogicalChannelMocker() {
    }
}