• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.libraries.entitlement.eapaka;
18 
19 import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE;
20 import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE;
21 
22 import android.content.Context;
23 import android.net.Uri;
24 import android.telephony.TelephonyManager;
25 import android.text.TextUtils;
26 import android.util.Log;
27 
28 import androidx.annotation.Nullable;
29 import androidx.annotation.VisibleForTesting;
30 
31 import com.android.libraries.entitlement.CarrierConfig;
32 import com.android.libraries.entitlement.EsimOdsaOperation;
33 import com.android.libraries.entitlement.ServiceEntitlementException;
34 import com.android.libraries.entitlement.ServiceEntitlementRequest;
35 import com.android.libraries.entitlement.http.HttpClient;
36 import com.android.libraries.entitlement.http.HttpConstants.RequestMethod;
37 import com.android.libraries.entitlement.http.HttpRequest;
38 import com.android.libraries.entitlement.http.HttpResponse;
39 
40 import com.google.common.collect.ImmutableList;
41 import com.google.common.net.HttpHeaders;
42 
43 import org.json.JSONException;
44 import org.json.JSONObject;
45 
46 public class EapAkaApi {
47     private static final String TAG = "ServiceEntitlement";
48 
49     public static final String EAP_CHALLENGE_RESPONSE = "eap-relay-packet";
50 
51     private static final String VERS = "vers";
52     private static final String ENTITLEMENT_VERSION = "entitlement_version";
53     private static final String TERMINAL_ID = "terminal_id";
54     private static final String TERMINAL_VENDOR = "terminal_vendor";
55     private static final String TERMINAL_MODEL = "terminal_model";
56     private static final String TERMIAL_SW_VERSION = "terminal_sw_version";
57     private static final String APP = "app";
58     private static final String EAP_ID = "EAP_ID";
59     private static final String IMSI = "IMSI";
60     private static final String TOKEN = "token";
61     private static final String NOTIF_ACTION = "notif_action";
62     private static final String NOTIF_TOKEN = "notif_token";
63     private static final String APP_VERSION = "app_version";
64     private static final String APP_NAME = "app_name";
65 
66     private static final String OPERATION = "operation";
67     private static final String OPERATION_TYPE = "operation_type";
68     private static final String COMPANION_TERMINAL_ID = "companion_terminal_id";
69     private static final String COMPANION_TERMINAL_VENDOR = "companion_terminal_vendor";
70     private static final String COMPANION_TERMINAL_MODEL = "companion_terminal_model";
71     private static final String COMPANION_TERMINAL_SW_VERSION = "companion_terminal_sw_version";
72     private static final String COMPANION_TERMINAL_FRIENDLY_NAME =
73             "companion_terminal_friendly_name";
74     private static final String COMPANION_TERMINAL_SERVICE = "companion_terminal_service";
75     private static final String COMPANION_TERMINAL_ICCID = "companion_terminal_iccid";
76     private static final String COMPANION_TERMINAL_EID = "companion_terminal_eid";
77 
78     private static final String TERMINAL_ICCID = "terminal_iccid";
79     private static final String TERMINAL_EID = "terminal_eid";
80 
81     private static final String TARGET_TERMINAL_ID = "target_terminal_id";
82     private static final String TARGET_TERMINAL_ICCID = "target_terminal_iccid";
83     private static final String TARGET_TERMINAL_EID = "target_terminal_eid";
84 
85     // In case of EAP-AKA synchronization failure, we try to recover for at most two times.
86     private static final int FOLLOW_SYNC_FAILURE_MAX_COUNT = 2;
87 
88     private final Context mContext;
89     private final int mSimSubscriptionId;
90     private final HttpClient mHttpClient;
91 
EapAkaApi(Context context, int simSubscriptionId)92     public EapAkaApi(Context context, int simSubscriptionId) {
93         this(context, simSubscriptionId, new HttpClient());
94     }
95 
96     @VisibleForTesting
EapAkaApi(Context context, int simSubscriptionId, HttpClient httpClient)97     EapAkaApi(Context context, int simSubscriptionId, HttpClient httpClient) {
98         this.mContext = context;
99         this.mSimSubscriptionId = simSubscriptionId;
100         this.mHttpClient = httpClient;
101     }
102 
103     /**
104      * Retrieves raw entitlement configuration doc though EAP-AKA authentication.
105      *
106      * <p>Implementation based on GSMA TS.43-v5.0 2.6.1.
107      *
108      * @throws ServiceEntitlementException when getting an unexpected http response.
109      */
110     @Nullable
queryEntitlementStatus(ImmutableList<String> appIds, CarrierConfig carrierConfig, ServiceEntitlementRequest request)111     public String queryEntitlementStatus(ImmutableList<String> appIds,
112             CarrierConfig carrierConfig, ServiceEntitlementRequest request)
113             throws ServiceEntitlementException {
114         Uri.Builder urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon();
115         appendParametersForServiceEntitlementRequest(urlBuilder, appIds, request);
116         if (!TextUtils.isEmpty(request.authenticationToken())) {
117             // Fast Re-Authentication flow with pre-existing auth token
118             Log.d(TAG, "Fast Re-Authentication");
119             return httpGet(
120                     urlBuilder.toString(), carrierConfig, request.acceptContentType()).body();
121         } else {
122             // Full Authentication flow
123             Log.d(TAG, "Full Authentication");
124             HttpResponse challengeResponse =
125                     httpGet(
126                         urlBuilder.toString(),
127                         carrierConfig,
128                         ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON);
129             return respondToEapAkaChallenge(
130                     carrierConfig,
131                     challengeResponse,
132                     FOLLOW_SYNC_FAILURE_MAX_COUNT,
133                     request.acceptContentType())
134                     .body();
135         }
136     }
137 
138     /**
139      * Sends a follow-up HTTP request to the HTTP {@code response} using the same cookie, and
140      * returns the follow-up HTTP response.
141      *
142      * <p>The {@code response} should contain a EAP-AKA challenge from server, and the
143      * follow-up request could contain:
144      *
145      * <ul>
146      *   <li>The EAP-AKA response message, and the follow-up response should contain the
147      *       service entitlement configuration; or,
148      *   <li>The EAP-AKA synchronization failure message, and the follow-up response should
149      *       contain the new EAP-AKA challenge. Then this method calls itself to follow-up
150      *       the new challenge and return a new response, if {@code followSyncFailureCount}
151      *       is greater than zero. When this method call itself {@code followSyncFailureCount} is
152      *       reduced by one to prevent infinite loop (unlikely in practice, but just in case).
153      * </ul>
154      *
155      * @param response Challenge response from server which its content type is JSON
156      */
respondToEapAkaChallenge( CarrierConfig carrierConfig, HttpResponse response, int followSyncFailureCount, String contentType)157     private HttpResponse respondToEapAkaChallenge(
158             CarrierConfig carrierConfig,
159             HttpResponse response,
160             int followSyncFailureCount,
161             String contentType)
162             throws ServiceEntitlementException {
163         String eapAkaChallenge;
164         try {
165             eapAkaChallenge = new JSONObject(response.body()).getString(EAP_CHALLENGE_RESPONSE);
166         } catch (JSONException jsonException) {
167             throw new ServiceEntitlementException(
168                     ERROR_MALFORMED_HTTP_RESPONSE, "Failed to parse json object", jsonException);
169         }
170         EapAkaChallenge challenge = EapAkaChallenge.parseEapAkaChallenge(eapAkaChallenge);
171         EapAkaResponse eapAkaResponse =
172                 EapAkaResponse.respondToEapAkaChallenge(mContext, mSimSubscriptionId, challenge);
173         // This could be a successful authentication, or synchronization failure.
174         if (eapAkaResponse.response() != null) { // successful authentication
175             return challengeResponse(
176                             eapAkaResponse.response(),
177                             carrierConfig,
178                             response.cookies(),
179                             contentType);
180         } else if (eapAkaResponse.synchronizationFailureResponse() != null) {
181             Log.d(TAG, "synchronization failure");
182             HttpResponse newChallenge =
183                     challengeResponse(
184                             eapAkaResponse.synchronizationFailureResponse(),
185                             carrierConfig,
186                             response.cookies(),
187                             ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON);
188             if (followSyncFailureCount > 0) {
189                 return respondToEapAkaChallenge(
190                         carrierConfig, newChallenge, followSyncFailureCount - 1, contentType);
191             } else {
192                 throw new ServiceEntitlementException(
193                         ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE,
194                         "Unable to recover from EAP-AKA synchroinization failure");
195             }
196         } else { // not possible
197             throw new AssertionError("EapAkaResponse invalid.");
198         }
199     }
200 
challengeResponse( String eapAkaChallengeResponse, CarrierConfig carrierConfig, ImmutableList<String> cookies, String contentType)201     private HttpResponse challengeResponse(
202             String eapAkaChallengeResponse,
203             CarrierConfig carrierConfig,
204             ImmutableList<String> cookies,
205             String contentType)
206             throws ServiceEntitlementException {
207         Log.d(TAG, "challengeResponse");
208         JSONObject postData = new JSONObject();
209         try {
210             postData.put(EAP_CHALLENGE_RESPONSE, eapAkaChallengeResponse);
211         } catch (JSONException jsonException) {
212             throw new ServiceEntitlementException(
213                     ERROR_MALFORMED_HTTP_RESPONSE, "Failed to put post data", jsonException);
214         }
215         HttpRequest request =
216                 HttpRequest.builder()
217                         .setUrl(carrierConfig.serverUrl())
218                         .setRequestMethod(RequestMethod.POST)
219                         .setPostData(postData)
220                         .addRequestProperty(HttpHeaders.ACCEPT, contentType)
221                         .addRequestProperty(
222                                 HttpHeaders.CONTENT_TYPE,
223                                 ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON)
224                         .addRequestProperty(HttpHeaders.COOKIE, cookies)
225                         .setTimeoutInSec(carrierConfig.timeoutInSec())
226                         .setNetwork(carrierConfig.network())
227                         .build();
228         return mHttpClient.request(request);
229     }
230 
231     /**
232      * Retrieves raw doc of performing ODSA operations. For operation type, see {@link
233      * EsimOdsaOperation}.
234      *
235      * <p>Implementation based on GSMA TS.43-v5.0 6.1.
236      */
performEsimOdsaOperation(String appId, CarrierConfig carrierConfig, ServiceEntitlementRequest request, EsimOdsaOperation odsaOperation)237     public String performEsimOdsaOperation(String appId, CarrierConfig carrierConfig,
238             ServiceEntitlementRequest request, EsimOdsaOperation odsaOperation)
239             throws ServiceEntitlementException {
240         Uri.Builder urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon();
241         appendParametersForServiceEntitlementRequest(urlBuilder, ImmutableList.of(appId), request);
242         appendParametersForEsimOdsaOperation(urlBuilder, odsaOperation);
243 
244         if (!TextUtils.isEmpty(request.authenticationToken())) {
245             // Fast Re-Authentication flow with pre-existing auth token
246             Log.d(TAG, "Fast Re-Authentication");
247             return httpGet(
248                     urlBuilder.toString(), carrierConfig, request.acceptContentType()).body();
249         } else {
250             // Full Authentication flow
251             Log.d(TAG, "Full Authentication");
252             HttpResponse challengeResponse =
253                     httpGet(
254                             urlBuilder.toString(),
255                             carrierConfig,
256                             ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON);
257             return respondToEapAkaChallenge(
258                     carrierConfig,
259                     challengeResponse,
260                     FOLLOW_SYNC_FAILURE_MAX_COUNT,
261                     request.acceptContentType())
262                     .body();
263         }
264     }
265 
appendParametersForServiceEntitlementRequest( Uri.Builder urlBuilder, ImmutableList<String> appIds, ServiceEntitlementRequest request)266     private void appendParametersForServiceEntitlementRequest(
267             Uri.Builder urlBuilder, ImmutableList<String> appIds,
268             ServiceEntitlementRequest request) {
269         TelephonyManager telephonyManager = mContext.getSystemService(
270                 TelephonyManager.class).createForSubscriptionId(mSimSubscriptionId);
271         if (TextUtils.isEmpty(request.authenticationToken())) {
272             // EAP_ID required for initial AuthN
273             urlBuilder.appendQueryParameter(
274                     EAP_ID,
275                     getImsiEap(telephonyManager.getSimOperator(),
276                             telephonyManager.getSubscriberId()));
277         } else {
278             // IMSI and token required for fast AuthN.
279             urlBuilder
280                     .appendQueryParameter(IMSI, telephonyManager.getSubscriberId())
281                     .appendQueryParameter(TOKEN, request.authenticationToken());
282         }
283 
284         if (!TextUtils.isEmpty(request.notificationToken())) {
285             urlBuilder
286                     .appendQueryParameter(NOTIF_ACTION,
287                             Integer.toString(request.notificationAction()))
288                     .appendQueryParameter(NOTIF_TOKEN, request.notificationToken());
289         }
290 
291         // Assign terminal ID with device IMEI if not set.
292         if (TextUtils.isEmpty(request.terminalId())) {
293             urlBuilder.appendQueryParameter(TERMINAL_ID, telephonyManager.getImei());
294         } else {
295             urlBuilder.appendQueryParameter(TERMINAL_ID, request.terminalId());
296         }
297 
298         // Optional query parameters, append them if not empty
299         appendOptionalQueryParameter(urlBuilder, APP_VERSION, request.appVersion());
300         appendOptionalQueryParameter(urlBuilder, APP_NAME, request.appName());
301 
302         for (String appId : appIds) {
303             urlBuilder.appendQueryParameter(APP, appId);
304         }
305 
306         urlBuilder
307                 // Identity and Authentication parameters
308                 .appendQueryParameter(TERMINAL_VENDOR, request.terminalVendor())
309                 .appendQueryParameter(TERMINAL_MODEL, request.terminalModel())
310                 .appendQueryParameter(TERMIAL_SW_VERSION, request.terminalSoftwareVersion())
311                 // General Service parameters
312                 .appendQueryParameter(VERS, Integer.toString(request.configurationVersion()))
313                 .appendQueryParameter(ENTITLEMENT_VERSION, request.entitlementVersion());
314     }
315 
appendParametersForEsimOdsaOperation( Uri.Builder urlBuilder, EsimOdsaOperation odsaOperation)316     private void appendParametersForEsimOdsaOperation(
317             Uri.Builder urlBuilder, EsimOdsaOperation odsaOperation) {
318         urlBuilder.appendQueryParameter(OPERATION, odsaOperation.operation());
319         if (odsaOperation.operationType() != EsimOdsaOperation.OPERATION_TYPE_NOT_SET) {
320             urlBuilder.appendQueryParameter(OPERATION_TYPE,
321                     Integer.toString(odsaOperation.operationType()));
322         }
323         appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_ID,
324                 odsaOperation.companionTerminalId());
325         appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_VENDOR,
326                 odsaOperation.companionTerminalVendor());
327         appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_MODEL,
328                 odsaOperation.companionTerminalModel());
329         appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_SW_VERSION,
330                 odsaOperation.companionTerminalSoftwareVersion());
331         appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_FRIENDLY_NAME,
332                 odsaOperation.companionTerminalFriendlyName());
333         appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_SERVICE,
334                 odsaOperation.companionTerminalService());
335         appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_ICCID,
336                 odsaOperation.companionTerminalIccid());
337         appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_EID,
338                 odsaOperation.companionTerminalEid());
339         appendOptionalQueryParameter(urlBuilder, TERMINAL_ICCID,
340                 odsaOperation.terminalIccid());
341         appendOptionalQueryParameter(urlBuilder, TERMINAL_EID, odsaOperation.terminalEid());
342         appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_ID,
343                 odsaOperation.targetTerminalId());
344         appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_ICCID,
345                 odsaOperation.targetTerminalIccid());
346         appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_EID,
347                 odsaOperation.targetTerminalEid());
348     }
349 
httpGet(String url, CarrierConfig carrierConfig, String contentType)350     private HttpResponse httpGet(String url, CarrierConfig carrierConfig, String contentType)
351             throws ServiceEntitlementException {
352         HttpRequest httpRequest =
353                 HttpRequest.builder()
354                         .setUrl(url)
355                         .setRequestMethod(RequestMethod.GET)
356                         .addRequestProperty(HttpHeaders.ACCEPT, contentType)
357                         .setTimeoutInSec(carrierConfig.timeoutInSec())
358                         .setNetwork(carrierConfig.network())
359                         .build();
360         return mHttpClient.request(httpRequest);
361     }
362 
appendOptionalQueryParameter(Uri.Builder urlBuilder, String key, String value)363     private void appendOptionalQueryParameter(Uri.Builder urlBuilder, String key, String value) {
364         if (!TextUtils.isEmpty(value)) {
365             urlBuilder.appendQueryParameter(key, value);
366         }
367     }
368 
369     /**
370      * Returns the IMSI EAP value. The resulting realm part of the Root NAI in 3GPP TS 23.003 clause
371      * 19.3.2 will be in the form:
372      *
373      * <p>{@code 0<IMSI>@nai.epc.mnc<MNC>.mcc<MCC>.3gppnetwork.org}
374      */
375     @Nullable
getImsiEap(@ullable String mccmnc, @Nullable String imsi)376     public static String getImsiEap(@Nullable String mccmnc, @Nullable String imsi) {
377         if (mccmnc == null || mccmnc.length() < 5 || imsi == null) {
378             return null;
379         }
380 
381         String mcc = mccmnc.substring(0, 3);
382         String mnc = mccmnc.substring(3);
383         if (mnc.length() == 2) {
384             mnc = "0" + mnc;
385         }
386         return "0" + imsi + "@nai.epc.mnc" + mnc + ".mcc" + mcc + ".3gppnetwork.org";
387     }
388 }
389