• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.telephony.uicc.euicc.apdu;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.os.Build;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.preference.PreferenceManager;
25 import android.telephony.IccOpenLogicalChannelResponse;
26 import android.util.Base64;
27 
28 import com.android.internal.annotations.GuardedBy;
29 import com.android.internal.telephony.CommandsInterface;
30 import com.android.internal.telephony.euicc.EuiccSession;
31 import com.android.internal.telephony.uicc.IccIoResult;
32 import com.android.internal.telephony.uicc.euicc.async.AsyncResultCallback;
33 import com.android.internal.telephony.uicc.euicc.async.AsyncResultHelper;
34 import com.android.telephony.Rlog;
35 
36 import java.io.ByteArrayOutputStream;
37 import java.io.IOException;
38 import java.util.List;
39 
40 /**
41  * This class sends a list of APDU commands to an AID on a UICC. A logical channel will be opened
42  * before sending and closed after all APDU commands are sent. The complete response of the last
43  * APDU command will be returned. If any APDU command returns an error status (other than
44  * {@link #STATUS_NO_ERROR}) or causing an exception, an {@link ApduException} will be returned
45  * immediately without sending the rest of commands.
46  *
47  * <p>If {@link EuiccSession} indicates ongoing session(s), the behavior changes: 1) before
48  * sending, check if a channel is opened already. If yes, reuse the channel and send APDU commands
49  * directly. If no, open a channel before sending. 2) The channel is closed when EuiccSession
50  * class ends all sessions, independent of APDU sending.
51  *
52  * <p>This class is thread-safe.
53  *
54  * @hide
55  */
56 public class ApduSender {
57     // Parameter and response used by the command to get extra responses of an APDU command.
58     private static final int INS_GET_MORE_RESPONSE = 0xC0;
59     private static final int SW1_MORE_RESPONSE = 0x61;
60 
61     // Status code of APDU response
62     private static final int STATUS_NO_ERROR = 0x9000;
63     private static final int SW1_NO_ERROR = 0x91;
64     private static final int STATUS_CHANNEL_CLOSED = 0x6881; // b/359336875
65 
66     private static final int WAIT_TIME_MS = 2000;
67     private static final String CHANNEL_ID_PRE = "esim-channel";
68     static final String ISD_R_AID = "A0000005591010FFFFFFFF8900000100";
69     private static final String CHANNEL_RESPONSE_ID_PRE = "esim-res-id";
70 
71     private final String mAid;
72     private final boolean mSupportExtendedApdu;
73     private final OpenLogicalChannelInvocation mOpenChannel;
74     private final CloseLogicalChannelInvocation mCloseChannel;
75     private final TransmitApduLogicalChannelInvocation mTransmitApdu;
76     private final Context mContext;
77     private final String mChannelKey;
78     private final String mChannelResponseKey;
79     // closeAnyOpenChannel() needs a handler for its async callbacks.
80     private final Handler mHandler;
81     private final String mLogTag;
82 
83     // Lock for accessing mChannelInUse. We only allow to open a single logical
84     // channel at any time for an AID and to invoke one command at any time.
85     // Only the thread (and its async callbacks) that sets mChannelInUse
86     // can open/close/send, and update mChannelOpened.
87     private final Object mChannelInUseLock = new Object();
88     @GuardedBy("mChannelInUseLock")
89     private boolean mChannelInUse;
90     private boolean mChannelOpened;
91 
92     /**
93      * @param aid The AID that will be used to open a logical channel to.
94      */
ApduSender(Context context, int phoneId, CommandsInterface ci, String aid, boolean supportExtendedApdu)95     public ApduSender(Context context, int phoneId, CommandsInterface ci, String aid,
96             boolean supportExtendedApdu) {
97         if (!aid.equals(ISD_R_AID) && !"user".equals(Build.TYPE)) {
98             throw new IllegalArgumentException("Only ISD-R AID is supported.");
99         }
100         mLogTag = "ApduSender-" + phoneId;
101         mAid = aid;
102         mContext = context;
103         mSupportExtendedApdu = supportExtendedApdu;
104         mOpenChannel = new OpenLogicalChannelInvocation(ci);
105         mCloseChannel = new CloseLogicalChannelInvocation(ci);
106         mTransmitApdu = new TransmitApduLogicalChannelInvocation(ci);
107         mChannelKey = CHANNEL_ID_PRE + "_" + phoneId;
108         mChannelResponseKey = CHANNEL_RESPONSE_ID_PRE + "_" + phoneId;
109         mHandler = new Handler();
110         mChannelInUse = false;
111     }
112 
113     /**
114      * Sends APDU commands.
115      *
116      * @param requestProvider Will be called after a logical channel is opened successfully. This is
117      *     in charge of building a request with all APDU commands to be sent. This won't be called
118      *     if any error happens when opening a logical channel.
119      * @param resultCallback Will be called after an error or the last APDU command has been
120      *     executed. The result will be the full response of the last APDU command. Error will be
121      *     returned as an {@link ApduException} exception.
122      * @param handler The handler that {@code requestProvider} and {@code resultCallback} will be
123      *     executed on.
124      */
send( RequestProvider requestProvider, ApduSenderResultCallback resultCallback, Handler handler)125     public void send(
126             RequestProvider requestProvider,
127             ApduSenderResultCallback resultCallback,
128             Handler handler) {
129         if (!acquireChannelLock()) {
130             AsyncResultHelper.throwException(
131                     new ApduException("The logical channel is still in use."),
132                     resultCallback,
133                     handler);
134             return;
135         }
136 
137         boolean euiccSession = EuiccSession.get(mContext).hasSession();
138         // Case 1, channel was already opened AND EuiccSession is ongoing.
139         // sendCommand directly. Do not immediately close channel after sendCommand.
140         // Case 2, channel was already opened AND EuiccSession is not ongoing. This means
141         // EuiccSession#endSession is already called but closeAnyOpenChannel() is not
142         // yet executed because of waiting to acquire lock hold by this thread.
143         // sendCommand directly. Close channel immediately anyways after sendCommand.
144         // Case 3, channel is not open AND EuiccSession is ongoing. Open channel
145         // before sendCommand. Do not immediately close channel after sendCommand.
146         // Case 4, channel is not open AND EuiccSession is not ongoing. Open channel
147         // before sendCommand. Close channel immediately after sendCommand.
148         if (mChannelOpened) {  // Case 1 or 2
149             if (euiccSession) {
150                 EuiccSession.get(mContext).noteChannelOpen(this);
151             }
152             RequestBuilder builder = getRequestBuilderWithOpenedChannel(requestProvider,
153                     !euiccSession /* closeChannelImmediately */, resultCallback, handler);
154             if (builder == null) {
155                 return;
156             }
157             sendCommand(builder.getCommands(), 0 /* index */,
158                     !euiccSession /* closeChannelImmediately */, resultCallback, handler);
159         } else {  // Case 3 or 4
160             if (euiccSession) {
161                 EuiccSession.get(mContext).noteChannelOpen(this);
162             }
163             openChannel(requestProvider,
164                     !euiccSession /* closeChannelImmediately */, resultCallback, handler);
165         }
166     }
167 
getRequestBuilderWithOpenedChannel( RequestProvider requestProvider, boolean closeChannelImmediately, ApduSenderResultCallback resultCallback, Handler handler)168     private RequestBuilder getRequestBuilderWithOpenedChannel(
169             RequestProvider requestProvider,
170             boolean closeChannelImmediately,
171             ApduSenderResultCallback resultCallback,
172             Handler handler) {
173         Throwable requestException = null;
174         int channel =
175                 PreferenceManager.getDefaultSharedPreferences(mContext)
176                         .getInt(mChannelKey, IccOpenLogicalChannelResponse.INVALID_CHANNEL);
177         String storedResponse =
178                 PreferenceManager.getDefaultSharedPreferences(mContext)
179                         .getString(mChannelResponseKey, "");
180         byte[] selectResponse = Base64.decode(storedResponse, Base64.DEFAULT);
181         RequestBuilder builder = new RequestBuilder(channel, mSupportExtendedApdu);
182         try {
183             requestProvider.buildRequest(selectResponse, builder);
184         } catch (Throwable e) {
185             requestException = e;
186         }
187         if (builder.getCommands().isEmpty() || requestException != null) {
188             logd("Release as commands are empty or exception occurred");
189             returnRespnseOrException(channel, closeChannelImmediately,
190                     null /* response */, requestException, resultCallback, handler);
191             return null;
192         }
193         return builder;
194     }
195 
openChannel( RequestProvider requestProvider, boolean closeChannelImmediately, ApduSenderResultCallback resultCallback, Handler handler)196     private void openChannel(
197             RequestProvider requestProvider,
198             boolean closeChannelImmediately,
199             ApduSenderResultCallback resultCallback,
200             Handler handler) {
201         mOpenChannel.invoke(mAid, new AsyncResultCallback<IccOpenLogicalChannelResponse>() {
202                     @Override
203                     public void onResult(IccOpenLogicalChannelResponse openChannelResponse) {
204                         int channel = openChannelResponse.getChannel();
205                         int status = openChannelResponse.getStatus();
206                         byte[] selectResponse = openChannelResponse.getSelectResponse();
207                         if (status == IccOpenLogicalChannelResponse.STATUS_NO_SUCH_ELEMENT) {
208                             channel = PreferenceManager.getDefaultSharedPreferences(mContext)
209                                             .getInt(mChannelKey,
210                                                     IccOpenLogicalChannelResponse.INVALID_CHANNEL);
211                             if (channel != IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
212                                 logv("Try to use already opened channel: " + channel);
213                                 status = IccOpenLogicalChannelResponse.STATUS_NO_ERROR;
214                                 String storedResponse = PreferenceManager
215                                         .getDefaultSharedPreferences(mContext)
216                                               .getString(mChannelResponseKey, "");
217                                 selectResponse = Base64.decode(storedResponse, Base64.DEFAULT);
218                             }
219                         }
220 
221                         if (channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL
222                                 || status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR) {
223                             mChannelOpened = false;
224                             returnRespnseOrException(
225                                     channel,
226                                     /* closeChannelImmediately= */ false,
227                                     /* response= */ null,
228                                     new ApduException(
229                                             "Failed to open logical channel for AID: "
230                                                     + mAid
231                                                     + ", with status: "
232                                                     + status),
233                                     resultCallback,
234                                     handler);
235                             return;
236                         }
237                         PreferenceManager.getDefaultSharedPreferences(mContext)
238                                 .edit()
239                                 .putInt(mChannelKey, channel)
240                                 .putString(mChannelResponseKey,
241                                     Base64.encodeToString(selectResponse, Base64.DEFAULT)).apply();
242                         mChannelOpened = true;
243 
244                         RequestBuilder builder =
245                                 getRequestBuilderWithOpenedChannel(requestProvider,
246                                         closeChannelImmediately, resultCallback, handler);
247                         if (builder == null) {
248                             return;
249                         }
250 
251                         sendCommand(builder.getCommands(), 0 /* index */,
252                                 closeChannelImmediately, resultCallback, handler);
253                     }
254                 },
255                 handler);
256     }
257 
258     /**
259      * Sends the current command and then continue to send the next one. If this is the last
260      * command or any error happens, {@code resultCallback} will be called.
261      *
262      * @param commands All commands to be sent.
263      * @param index The current command index.
264      */
sendCommand( List<ApduCommand> commands, int index, boolean closeChannelImmediately, ApduSenderResultCallback resultCallback, Handler handler)265     private void sendCommand(
266             List<ApduCommand> commands,
267             int index,
268             boolean closeChannelImmediately,
269             ApduSenderResultCallback resultCallback,
270             Handler handler) {
271         ApduCommand command = commands.get(index);
272         mTransmitApdu.invoke(command, new AsyncResultCallback<IccIoResult>() {
273             @Override
274             public void onResult(IccIoResult response) {
275                 // A long response may need to be fetched by multiple following-up APDU
276                 // commands. Makes sure that we get the complete response.
277                 getCompleteResponse(command.channel, response, null /* responseBuilder */,
278                         new AsyncResultCallback<IccIoResult>() {
279                             @Override
280                             public void onResult(IccIoResult fullResponse) {
281                                 logv("Full APDU response: " + fullResponse);
282                                 int status = (fullResponse.sw1 << 8) | fullResponse.sw2;
283                                 if (status != STATUS_NO_ERROR
284                                         && fullResponse.sw1 != SW1_NO_ERROR) {
285                                     if (status == STATUS_CHANNEL_CLOSED) {
286                                         // Channel is closed by EUICC e.g. REFRESH.
287                                         tearDownPreferences();
288                                         mChannelOpened = false;
289                                         // TODO: add retry
290                                     }
291                                     returnRespnseOrException(
292                                             command.channel,
293                                             closeChannelImmediately,
294                                             null /* response */,
295                                             new ApduException(status),
296                                             resultCallback,
297                                             handler);
298                                     return;
299                                 }
300 
301                                 boolean continueSendCommand = index < commands.size() - 1
302                                         // Checks intermediate APDU result except the last one
303                                         && resultCallback.shouldContinueOnIntermediateResult(
304                                                 fullResponse);
305                                 if (continueSendCommand) {
306                                     // Sends the next command
307                                     sendCommand(commands, index + 1,
308                                             closeChannelImmediately, resultCallback, handler);
309                                 } else {
310                                     // Returns the result of the last command
311                                     returnRespnseOrException(
312                                             command.channel,
313                                             closeChannelImmediately,
314                                             fullResponse.payload,
315                                             null /* exception */,
316                                             resultCallback,
317                                             handler);
318                                 }
319                             }
320                         }, handler);
321             }
322         }, handler);
323     }
324 
325     /**
326      * Gets the full response.
327      *
328      * @param lastResponse Will be checked to see if we need to fetch more.
329      * @param responseBuilder For continuously building the full response. It should not contain the
330      *     last response. If it's null, a new builder will be created.
331      * @param resultCallback Error will be included in the result and no exception will be returned.
332      */
getCompleteResponse( int channel, IccIoResult lastResponse, @Nullable ByteArrayOutputStream responseBuilder, AsyncResultCallback<IccIoResult> resultCallback, Handler handler)333     private void getCompleteResponse(
334             int channel,
335             IccIoResult lastResponse,
336             @Nullable ByteArrayOutputStream responseBuilder,
337             AsyncResultCallback<IccIoResult> resultCallback,
338             Handler handler) {
339         ByteArrayOutputStream resultBuilder =
340             responseBuilder == null ? new ByteArrayOutputStream() : responseBuilder;
341         if (lastResponse.payload != null) {
342             try {
343                 resultBuilder.write(lastResponse.payload);
344             } catch (IOException e) {
345                 // Should never reach here.
346             }
347         }
348         if (lastResponse.sw1 != SW1_MORE_RESPONSE) {
349             lastResponse.payload = resultBuilder.toByteArray();
350             resultCallback.onResult(lastResponse);
351             return;
352         }
353 
354         mTransmitApdu.invoke(
355                 new ApduCommand(channel, 0 /* cls  */, INS_GET_MORE_RESPONSE, 0 /* p1 */,
356                         0 /* p2 */, lastResponse.sw2, "" /* cmdHex */),
357                 new AsyncResultCallback<IccIoResult>() {
358                     @Override
359                     public void onResult(IccIoResult response) {
360                         getCompleteResponse(
361                                 channel, response, resultBuilder, resultCallback, handler);
362                     }
363                 }, handler);
364     }
365 
tearDownPreferences()366     private void tearDownPreferences() {
367         PreferenceManager.getDefaultSharedPreferences(mContext)
368                 .edit()
369                 .remove(mChannelKey)
370                 .remove(mChannelResponseKey)
371                 .apply();
372     }
373 
374     /**
375      * Fires the {@code resultCallback} to return a response or exception. Also
376      * closes the open logical channel if {@code closeChannelImmediately} is {@code true}.
377      */
returnRespnseOrException( int channel, boolean closeChannelImmediately, @Nullable byte[] response, @Nullable Throwable exception, ApduSenderResultCallback resultCallback, Handler handler)378     private void returnRespnseOrException(
379             int channel,
380             boolean closeChannelImmediately,
381             @Nullable byte[] response,
382             @Nullable Throwable exception,
383             ApduSenderResultCallback resultCallback,
384             Handler handler) {
385         if (closeChannelImmediately) {
386             closeAndReturn(
387                     channel,
388                     response,
389                     exception,
390                     resultCallback,
391                     handler);
392         } else {
393             releaseChannelLockAndReturn(
394                     response,
395                     exception,
396                     resultCallback,
397                     handler);
398         }
399     }
400 
401     /**
402      * Closes the opened logical channel.
403      *
404      * @param response If {@code exception} is null, this will be returned to {@code resultCallback}
405      *     after the channel has been closed.
406      * @param exception If not null, this will be returned to {@code resultCallback} after the
407      *     channel has been closed.
408      */
closeAndReturn( int channel, @Nullable byte[] response, @Nullable Throwable exception, ApduSenderResultCallback resultCallback, Handler handler)409     private void closeAndReturn(
410             int channel,
411             @Nullable byte[] response,
412             @Nullable Throwable exception,
413             ApduSenderResultCallback resultCallback,
414             Handler handler) {
415         mCloseChannel.invoke(channel, new AsyncResultCallback<Boolean>() {
416             @Override
417             public void onResult(Boolean aBoolean) {
418                 tearDownPreferences();
419                 mChannelOpened = false;
420                 releaseChannelLock();
421 
422                 if (exception == null) {
423                     resultCallback.onResult(response);
424                 } else {
425                     resultCallback.onException(exception);
426                 }
427             }
428         }, handler);
429     }
430 
431     /**
432      * Cleanup the existing opened channel which remained opened earlier due
433      * to:
434      *
435      * <p> 1) onging EuiccSession. This will be called by {@link EuiccSession#endSession()}
436      * from non-main-thread. Or,
437      *
438      * <p> 2) telephony crash. This will be called by constructor from main-thread.
439      */
closeAnyOpenChannel()440     public void closeAnyOpenChannel() {
441         if (!acquireChannelLock()) {
442             // This cannot happen for case 2) when called by constructor
443             loge("[closeAnyOpenChannel] failed to acquire channel lock");
444             return;
445         }
446         int channelId = PreferenceManager.getDefaultSharedPreferences(mContext)
447                 .getInt(mChannelKey, IccOpenLogicalChannelResponse.INVALID_CHANNEL);
448         if (channelId == IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
449             releaseChannelLock();
450             return;
451         }
452         logv("[closeAnyOpenChannel] closing the open channel : " +  channelId);
453         mCloseChannel.invoke(channelId, new AsyncResultCallback<Boolean>() {
454             @Override
455             public void onResult(Boolean isSuccess) {
456                 if (isSuccess) {
457                     logv("[closeAnyOpenChannel] Channel closed successfully: " + channelId);
458                     tearDownPreferences();
459                 }
460                 // Even if CloseChannel failed, pretend that the channel is closed.
461                 // So next send() will try open the channel again. If the channel is
462                 // indeed still open, we use the channelId saved in sharedPref.
463                 mChannelOpened = false;
464                 releaseChannelLock();
465             }
466         }, mHandler);
467     }
468 
469     // releases channel and callback
releaseChannelLockAndReturn( @ullable byte[] response, @Nullable Throwable exception, ApduSenderResultCallback resultCallback, Handler handler)470     private void releaseChannelLockAndReturn(
471             @Nullable byte[] response,
472             @Nullable Throwable exception,
473             ApduSenderResultCallback resultCallback,
474             Handler handler) {
475         handler.post(
476                 () -> {
477                     releaseChannelLock();
478                     if (exception == null) {
479                         resultCallback.onResult(response);
480                     } else {
481                         resultCallback.onException(exception);
482                     }
483                 });
484     }
485 
releaseChannelLock()486     private void releaseChannelLock() {
487         synchronized (mChannelInUseLock) {
488             logd("Channel lock released.");
489             mChannelInUse = false;
490             mChannelInUseLock.notify();
491         }
492     }
493 
494     /**
495      * Acquires channel lock and returns {@code true} if successful.
496      *
497      * <p>It fails and returns {@code false} when:
498      * <ul>
499      *   <li>Called from main thread, and mChannelInUse=true, fails immediately.
500      *   <li>Called from non main thread, and mChannelInUse=true after 2 seconds waiting, fails.
501      * </ul>
502      */
acquireChannelLock()503     private boolean acquireChannelLock() {
504         synchronized (mChannelInUseLock) {
505             if (mChannelInUse) {
506                 if (!Looper.getMainLooper().equals(Looper.myLooper())) {
507                     logd("Logical channel is in use. Wait.");
508                     try {
509                         mChannelInUseLock.wait(WAIT_TIME_MS);
510                     } catch (InterruptedException e) {
511                         // nothing to do
512                     }
513                     if (mChannelInUse) {
514                         return false;
515                     }
516                 } else {
517                     return false;
518                 }
519             }
520             mChannelInUse = true;
521             logd("Channel lock acquired.");
522             return true;
523         }
524     }
525 
logv(String msg)526     private void logv(String msg) {
527         Rlog.v(mLogTag, msg);
528     }
529 
logd(String msg)530     private void logd(String msg) {
531         Rlog.d(mLogTag, msg);
532     }
533 
loge(String msg)534     private void loge(String msg) {
535         Rlog.e(mLogTag, msg);
536     }
537 }
538