1 /*
2  * Copyright (C) 2021 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.server.uwb.secure;
18 
19 import static com.android.server.uwb.secure.iso7816.StatusWord.SW_NO_ERROR;
20 import static com.android.server.uwb.secure.iso7816.StatusWord.SW_NO_SPECIFIC_DIAGNOSTIC;
21 
22 import android.util.Log;
23 
24 import androidx.annotation.NonNull;
25 import androidx.annotation.VisibleForTesting;
26 import androidx.annotation.WorkerThread;
27 
28 import com.android.server.uwb.secure.csml.FiRaCommand;
29 import com.android.server.uwb.secure.iso7816.CommandApdu;
30 import com.android.server.uwb.secure.iso7816.ResponseApdu;
31 import com.android.server.uwb.secure.iso7816.StatusWord;
32 import com.android.server.uwb.secure.omapi.OmapiConnection;
33 import com.android.server.uwb.secure.omapi.OmapiConnection.InitCompletionCallback;
34 
35 import java.io.IOException;
36 import java.util.Arrays;
37 
38 /** Manages the Secure Element and allows communications with the FiRa applet. */
39 @WorkerThread
40 public class SecureElementChannel {
41     private static final String LOG_TAG = "SecureElementChannel";
42     private static final int MAX_SE_OPERATION_RETRIES = 3;
43     private static final int DELAY_BETWEEN_SE_RETRY_ATTEMPTS_MILLIS = 10;
44 
45     private static final StatusWord SW_TEMPORARILY_UNAVAILABLE =
46             StatusWord.SW_CONDITIONS_NOT_SATISFIED;
47 
48     private final OmapiConnection mOmapiConnection;
49     private final boolean mRemoveDelayBetweenRetriesForTest;
50 
51     private boolean mIsOpened = false;
52 
53     /**
54      * The constructor of the SecureElementChannel.
55      */
SecureElementChannel(@onNull OmapiConnection omapiConnection)56     public SecureElementChannel(@NonNull OmapiConnection omapiConnection) {
57         this(omapiConnection, /* removeDelayBetweenRetries= */ false);
58     }
59 
60     // This constructor is made visible because we need to remove the delay between SE operations
61     // during tests. Calling Thread.sleep in tests actually causes the thread running the test to
62     // sleep and leads to the test timing out.
63     @VisibleForTesting
SecureElementChannel( @onNull OmapiConnection omapiConnection, boolean removeDelayBetweenRetriesForTest)64     SecureElementChannel(
65             @NonNull OmapiConnection omapiConnection, boolean removeDelayBetweenRetriesForTest) {
66         this.mOmapiConnection = omapiConnection;
67         this.mRemoveDelayBetweenRetriesForTest = removeDelayBetweenRetriesForTest;
68     }
69 
70     /**
71      * Initializes the SecureElementChannel.
72      */
init(@onNull InitCompletionCallback callback)73     public void init(@NonNull InitCompletionCallback callback) {
74         mOmapiConnection.init(callback::onInitCompletion);
75     }
76 
77     /**
78      * Opens the channel to the FiRa applet, true if success.
79      */
openChannel()80     public boolean openChannel() {
81         try {
82             ResponseApdu responseApdu = openChannelWithResponse();
83             if (responseApdu.getStatusWord() != SW_NO_ERROR.toInt()) {
84                 logw("Received error [" + responseApdu + "] while opening channel");
85                 return false;
86             }
87         } catch (IOException e) {
88             loge("Encountered exception while opening channel" + e);
89             return false;
90         }
91         return true;
92     }
93 
94     /**
95      * Opens the channel to the FiRa applet, returns the Response APDU.
96      */
97     @NonNull
openChannelWithResponse()98     public ResponseApdu openChannelWithResponse() throws IOException {
99         ResponseApdu responseApdu = ResponseApdu.fromStatusWord(SW_TEMPORARILY_UNAVAILABLE);
100         for (int i = 0; i < MAX_SE_OPERATION_RETRIES; i++) {
101             responseApdu = mOmapiConnection.openChannel();
102 
103             if (!shouldRetryOpenChannel(responseApdu)) {
104                 break;
105             }
106 
107             logw(
108                     "Open channel failed because SE is temporarily unavailable. "
109                             + "Total attempts so far: "
110                             + (i + 1));
111 
112             threadSleep(DELAY_BETWEEN_SE_RETRY_ATTEMPTS_MILLIS);
113         }
114 
115         if (responseApdu.getStatusWord() == StatusWord.SW_NO_ERROR.toInt()) {
116             mIsOpened = true;
117         } else {
118             logw("All open channel attempts failed!");
119         }
120         return responseApdu;
121     }
122 
123     /**
124      * Checks if current channel is opened or not.
125      */
isOpened()126     public boolean isOpened() {
127         return mIsOpened;
128     }
129 
shouldRetryOpenChannel(ResponseApdu responseApdu)130     private boolean shouldRetryOpenChannel(ResponseApdu responseApdu) {
131         return Arrays.asList(SW_TEMPORARILY_UNAVAILABLE, SW_NO_SPECIFIC_DIAGNOSTIC)
132                 .contains(StatusWord.fromInt(responseApdu.getStatusWord()));
133     }
134 
135     /**
136      * Closes the channel to the FiRa applet.
137      * @return
138      */
closeChannel()139     public boolean closeChannel() {
140         try {
141             mOmapiConnection.closeChannel();
142         } catch (IOException e) {
143             logw("Encountered exception while closing channel" + e);
144             return false;
145         }
146         mIsOpened = false;
147         return true;
148     }
149 
150     /**
151      * Transmits a Command APDU defined by the FiRa to the FiRa applet.
152      */
153     @NonNull
transmit(@onNull FiRaCommand fiRaCommand)154     public ResponseApdu transmit(@NonNull FiRaCommand fiRaCommand) throws IOException {
155         return transmit(fiRaCommand.getCommandApdu());
156     }
157 
158     /**
159      * Transmits a Command APDU to FiRa applet.
160      */
161     @NonNull
transmit(@onNull CommandApdu command)162     public ResponseApdu transmit(@NonNull CommandApdu command) throws IOException {
163         ResponseApdu responseApdu = ResponseApdu.fromStatusWord(SW_TEMPORARILY_UNAVAILABLE);
164 
165         if (!mIsOpened) {
166             return responseApdu;
167         }
168         for (int i = 0; i < MAX_SE_OPERATION_RETRIES; i++) {
169             responseApdu = mOmapiConnection.transmit(command);
170             if (responseApdu.getStatusWord() != SW_TEMPORARILY_UNAVAILABLE.toInt()) {
171                 return responseApdu;
172             }
173             logw(
174                     "Transmit failed because SE is temporarily unavailable. "
175                             + "Total attempts so far: "
176                             + (i + 1));
177             threadSleep(DELAY_BETWEEN_SE_RETRY_ATTEMPTS_MILLIS);
178         }
179         logw("All transmit attempts for SE failed!");
180         return responseApdu;
181     }
182 
threadSleep(long millis)183     private void threadSleep(long millis) {
184         if (!mRemoveDelayBetweenRetriesForTest) {
185             try {
186                 Thread.sleep(millis);
187             } catch (InterruptedException e) {
188                 logw("Thread sleep interrupted.");
189             }
190         }
191     }
192 
logw(String dbgMsg)193     private void logw(String dbgMsg) {
194         Log.w(LOG_TAG, dbgMsg);
195     }
196 
loge(String dbgMsg)197     private void loge(String dbgMsg) {
198         Log.e(LOG_TAG, dbgMsg);
199     }
200 }
201