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_FAILURE;
20 import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE;
21 import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_JSON_COMPOSE_FAILURE;
22 import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE;
23 
24 import android.content.Context;
25 import android.content.pm.PackageInfo;
26 import android.net.Uri;
27 import android.telephony.TelephonyManager;
28 import android.text.TextUtils;
29 import android.util.Log;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.VisibleForTesting;
34 
35 import com.android.libraries.entitlement.CarrierConfig;
36 import com.android.libraries.entitlement.EsimOdsaOperation;
37 import com.android.libraries.entitlement.ServiceEntitlementException;
38 import com.android.libraries.entitlement.ServiceEntitlementRequest;
39 import com.android.libraries.entitlement.http.HttpClient;
40 import com.android.libraries.entitlement.http.HttpConstants.ContentType;
41 import com.android.libraries.entitlement.http.HttpConstants.RequestMethod;
42 import com.android.libraries.entitlement.http.HttpRequest;
43 import com.android.libraries.entitlement.http.HttpResponse;
44 
45 import com.google.common.collect.ImmutableList;
46 import com.google.common.net.HttpHeaders;
47 
48 import org.json.JSONException;
49 import org.json.JSONObject;
50 
51 import java.util.List;
52 
53 public class EapAkaApi {
54     private static final String TAG = "ServiceEntitlement";
55 
56     public static final String EAP_CHALLENGE_RESPONSE = "eap-relay-packet";
57     private static final String CONTENT_TYPE_EAP_RELAY_JSON =
58             "application/vnd.gsma.eap-relay.v1.0+json";
59 
60     private static final String VERS = "vers";
61     private static final String ENTITLEMENT_VERSION = "entitlement_version";
62     private static final String TERMINAL_ID = "terminal_id";
63     private static final String TERMINAL_VENDOR = "terminal_vendor";
64     private static final String TERMINAL_MODEL = "terminal_model";
65     private static final String TERMIAL_SW_VERSION = "terminal_sw_version";
66     private static final String APP = "app";
67     private static final String EAP_ID = "EAP_ID";
68     private static final String IMSI = "IMSI";
69     private static final String TOKEN = "token";
70     private static final String TEMPORARY_TOKEN = "temporary_token";
71     private static final String NOTIF_ACTION = "notif_action";
72     private static final String NOTIF_TOKEN = "notif_token";
73     private static final String APP_VERSION = "app_version";
74     private static final String APP_NAME = "app_name";
75 
76     private static final String OPERATION = "operation";
77     private static final String OPERATION_TYPE = "operation_type";
78     private static final String OPERATION_TARGETS = "operation_targets";
79     private static final String COMPANION_TERMINAL_ID = "companion_terminal_id";
80     private static final String COMPANION_TERMINAL_VENDOR = "companion_terminal_vendor";
81     private static final String COMPANION_TERMINAL_MODEL = "companion_terminal_model";
82     private static final String COMPANION_TERMINAL_SW_VERSION = "companion_terminal_sw_version";
83     private static final String COMPANION_TERMINAL_FRIENDLY_NAME =
84             "companion_terminal_friendly_name";
85     private static final String COMPANION_TERMINAL_SERVICE = "companion_terminal_service";
86     private static final String COMPANION_TERMINAL_ICCID = "companion_terminal_iccid";
87     private static final String COMPANION_TERMINAL_EID = "companion_terminal_eid";
88 
89     private static final String TERMINAL_ICCID = "terminal_iccid";
90     private static final String TERMINAL_EID = "terminal_eid";
91 
92     private static final String TARGET_TERMINAL_ID = "target_terminal_id";
93     // Non-standard params for Korean carriers
94     private static final String TARGET_TERMINAL_IDS = "target_terminal_imeis";
95     private static final String TARGET_TERMINAL_ICCID = "target_terminal_iccid";
96     private static final String TARGET_TERMINAL_EID = "target_terminal_eid";
97     // Non-standard params for Korean carriers
98     private static final String TARGET_TERMINAL_SERIAL_NUMBER = "target_terminal_sn";
99     // Non-standard params for Korean carriers
100     private static final String TARGET_TERMINAL_MODEL = "target_terminal_model";
101 
102     private static final String OLD_TERMINAL_ID = "old_terminal_id";
103     private static final String OLD_TERMINAL_ICCID = "old_terminal_iccid";
104 
105     private static final String BOOST_TYPE = "boost_type";
106 
107     private static final String MESSAGE_RESPONSE = "MSG_response";
108     private static final String MESSAGE_BUTTON = "MSG_btn";
109 
110     // In case of EAP-AKA synchronization failure or another challenge, we try to authenticate for
111     // at most three times.
112     private static final int MAX_EAP_AKA_ATTEMPTS = 3;
113 
114     // Max TERMINAL_* string length according to GSMA RCC.14 section 2.4
115     private static final int MAX_TERMINAL_VENDOR_LENGTH = 4;
116     private static final int MAX_TERMINAL_MODEL_LENGTH = 10;
117     private static final int MAX_TERMINAL_SOFTWARE_VERSION_LENGTH = 20;
118 
119     private final Context mContext;
120     private final int mSimSubscriptionId;
121     private final HttpClient mHttpClient;
122     private final String mBypassEapAkaResponse;
123     private final String mAppVersion;
124     private final TelephonyManager mTelephonyManager;
125 
EapAkaApi( Context context, int simSubscriptionId, boolean saveHistory, String bypassEapAkaResponse)126     public EapAkaApi(
127             Context context,
128             int simSubscriptionId,
129             boolean saveHistory,
130             String bypassEapAkaResponse) {
131         this(context, simSubscriptionId, new HttpClient(saveHistory), bypassEapAkaResponse);
132     }
133 
134     @VisibleForTesting
EapAkaApi( Context context, int simSubscriptionId, HttpClient httpClient, String bypassEapAkaResponse)135     EapAkaApi(
136             Context context,
137             int simSubscriptionId,
138             HttpClient httpClient,
139             String bypassEapAkaResponse) {
140         this.mContext = context;
141         this.mSimSubscriptionId = simSubscriptionId;
142         this.mHttpClient = httpClient;
143         this.mBypassEapAkaResponse = bypassEapAkaResponse;
144         this.mAppVersion = getAppVersion(context);
145         this.mTelephonyManager =
146                 mContext.getSystemService(TelephonyManager.class)
147                         .createForSubscriptionId(mSimSubscriptionId);
148     }
149 
150     /**
151      * Retrieves HTTP response with the entitlement configuration doc though EAP-AKA authentication.
152      *
153      * <p>Implementation based on GSMA TS.43-v5.0 2.6.1.
154      *
155      * @throws ServiceEntitlementException when getting an unexpected http response.
156      */
157     @NonNull
queryEntitlementStatus( ImmutableList<String> appIds, CarrierConfig carrierConfig, ServiceEntitlementRequest request)158     public HttpResponse queryEntitlementStatus(
159             ImmutableList<String> appIds,
160             CarrierConfig carrierConfig,
161             ServiceEntitlementRequest request)
162             throws ServiceEntitlementException {
163         Uri.Builder urlBuilder = null;
164         JSONObject postData = null;
165         if (carrierConfig.useHttpPost()) {
166             postData = new JSONObject();
167             appendParametersForAuthentication(postData, request);
168             appendParametersForServiceEntitlementRequest(postData, appIds, request);
169         } else {
170             urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon();
171             appendParametersForAuthentication(urlBuilder, request);
172             appendParametersForServiceEntitlementRequest(urlBuilder, appIds, request);
173         }
174 
175         if (!TextUtils.isEmpty(request.authenticationToken())) {
176             // Fast Re-Authentication flow with pre-existing auth token
177             Log.d(TAG, "Fast Re-Authentication");
178             return carrierConfig.useHttpPost()
179                     ? httpPost(
180                             postData,
181                             carrierConfig,
182                             request.acceptContentType(),
183                             request.terminalVendor(),
184                             request.terminalModel(),
185                             request.terminalSoftwareVersion())
186                     : httpGet(
187                             urlBuilder.toString(),
188                             carrierConfig,
189                             request.acceptContentType(),
190                             request.terminalVendor(),
191                             request.terminalModel(),
192                             request.terminalSoftwareVersion());
193         } else {
194             // Full Authentication flow
195             Log.d(TAG, "Full Authentication");
196             HttpResponse challengeResponse =
197                     carrierConfig.useHttpPost()
198                             ? httpPost(
199                                     postData,
200                                     carrierConfig,
201                                     CONTENT_TYPE_EAP_RELAY_JSON,
202                                     request.terminalVendor(),
203                                     request.terminalModel(),
204                                     request.terminalSoftwareVersion())
205                             : httpGet(
206                                     urlBuilder.toString(),
207                                     carrierConfig,
208                                     CONTENT_TYPE_EAP_RELAY_JSON,
209                                     request.terminalVendor(),
210                                     request.terminalModel(),
211                                     request.terminalSoftwareVersion());
212             String eapAkaChallenge = getEapAkaChallenge(challengeResponse);
213             if (eapAkaChallenge == null) {
214                 throw new ServiceEntitlementException(
215                         ERROR_MALFORMED_HTTP_RESPONSE,
216                         "Failed to parse EAP-AKA challenge: " + challengeResponse.body());
217             }
218             return respondToEapAkaChallenge(
219                     carrierConfig,
220                     eapAkaChallenge,
221                     challengeResponse.cookies(),
222                     MAX_EAP_AKA_ATTEMPTS,
223                     request.acceptContentType(),
224                     request.terminalVendor(),
225                     request.terminalModel(),
226                     request.terminalSoftwareVersion());
227         }
228     }
229 
230     /**
231      * Sends a follow-up HTTP request to the HTTP {@code response} using the same cookie, and
232      * returns the follow-up HTTP response.
233      *
234      * <p>The {@code eapAkaChallenge} should be the EAP-AKA challenge from server, and the follow-up
235      * request could contain:
236      *
237      * <ul>
238      *   <li>The EAP-AKA response message, and the follow-up response should contain the service
239      *       entitlement configuration, or another EAP-AKA challenge in which case the method calls
240      *       if {@code remainingAttempts} is greater than zero (If {@code remainingAttempts} reaches
241      *       0, the method will throw ServiceEntitlementException) ; or
242      *   <li>The EAP-AKA synchronization failure message, and the follow-up response should contain
243      *       the new EAP-AKA challenge. Then this method calls itself to follow-up the new challenge
244      *       and return a new response, as long as {@code remainingAttempts} is greater than zero.
245      * </ul>
246      *
247      * @return Challenge response from server whose content type is JSON
248      */
249     @NonNull
respondToEapAkaChallenge( CarrierConfig carrierConfig, String eapAkaChallenge, ImmutableList<String> cookies, int remainingAttempts, String acceptContentType, String terminalVendor, String terminalModel, String terminalSoftwareVersion)250     private HttpResponse respondToEapAkaChallenge(
251             CarrierConfig carrierConfig,
252             String eapAkaChallenge,
253             ImmutableList<String> cookies,
254             int remainingAttempts,
255             String acceptContentType,
256             String terminalVendor,
257             String terminalModel,
258             String terminalSoftwareVersion)
259             throws ServiceEntitlementException {
260         if (!mBypassEapAkaResponse.isEmpty()) {
261             return challengeResponse(
262                     mBypassEapAkaResponse,
263                     carrierConfig,
264                     cookies,
265                     CONTENT_TYPE_EAP_RELAY_JSON + ", " + acceptContentType,
266                     terminalVendor,
267                     terminalModel,
268                     terminalSoftwareVersion);
269         }
270 
271         EapAkaChallenge challenge = EapAkaChallenge.parseEapAkaChallenge(eapAkaChallenge);
272         EapAkaResponse eapAkaResponse =
273                 EapAkaResponse.respondToEapAkaChallenge(mContext, mSimSubscriptionId, challenge);
274         // This could be a successful authentication, another challenge, or synchronization failure.
275         if (eapAkaResponse.response() != null) {
276             HttpResponse response =
277                     challengeResponse(
278                             eapAkaResponse.response(),
279                             carrierConfig,
280                             cookies,
281                             CONTENT_TYPE_EAP_RELAY_JSON + ", " + acceptContentType,
282                             terminalVendor,
283                             terminalModel,
284                             terminalSoftwareVersion);
285             String nextEapAkaChallenge = getEapAkaChallenge(response);
286             // successful authentication
287             if (nextEapAkaChallenge == null) {
288                 return response;
289             }
290             // another challenge
291             Log.d(TAG, "Received another challenge");
292             if (remainingAttempts > 0) {
293                 return respondToEapAkaChallenge(
294                         carrierConfig,
295                         nextEapAkaChallenge,
296                         cookies,
297                         remainingAttempts - 1,
298                         acceptContentType,
299                         terminalVendor,
300                         terminalModel,
301                         terminalSoftwareVersion);
302             } else {
303                 throw new ServiceEntitlementException(
304                         ERROR_EAP_AKA_FAILURE, "Unable to EAP-AKA authenticate");
305             }
306         } else if (eapAkaResponse.synchronizationFailureResponse() != null) {
307             Log.d(TAG, "synchronization failure");
308             HttpResponse newChallenge =
309                     challengeResponse(
310                             eapAkaResponse.synchronizationFailureResponse(),
311                             carrierConfig,
312                             cookies,
313                             CONTENT_TYPE_EAP_RELAY_JSON,
314                             terminalVendor,
315                             terminalModel,
316                             terminalSoftwareVersion);
317             String nextEapAkaChallenge = getEapAkaChallenge(newChallenge);
318             if (nextEapAkaChallenge == null) {
319                 throw new ServiceEntitlementException(
320                         ERROR_MALFORMED_HTTP_RESPONSE,
321                         "Failed to parse EAP-AKA challenge: " + newChallenge.body());
322             }
323             if (remainingAttempts > 0) {
324                 return respondToEapAkaChallenge(
325                         carrierConfig,
326                         nextEapAkaChallenge,
327                         cookies,
328                         remainingAttempts - 1,
329                         acceptContentType,
330                         terminalVendor,
331                         terminalModel,
332                         terminalSoftwareVersion);
333             } else {
334                 throw new ServiceEntitlementException(
335                         ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE,
336                         "Unable to recover from EAP-AKA synchroinization failure");
337             }
338         } else { // not possible
339             throw new AssertionError("EapAkaResponse invalid.");
340         }
341     }
342 
343     @NonNull
challengeResponse( String eapAkaChallengeResponse, CarrierConfig carrierConfig, ImmutableList<String> cookies, String acceptContentType, String terminalVendor, String terminalModel, String terminalSoftwareVersion)344     private HttpResponse challengeResponse(
345             String eapAkaChallengeResponse,
346             CarrierConfig carrierConfig,
347             ImmutableList<String> cookies,
348             String acceptContentType,
349             String terminalVendor,
350             String terminalModel,
351             String terminalSoftwareVersion)
352             throws ServiceEntitlementException {
353         JSONObject postData = new JSONObject();
354         try {
355             postData.put(EAP_CHALLENGE_RESPONSE, eapAkaChallengeResponse);
356         } catch (JSONException jsonException) {
357             throw new ServiceEntitlementException(
358                     ERROR_MALFORMED_HTTP_RESPONSE, "Failed to put post data", jsonException);
359         }
360         return httpPost(
361                 postData,
362                 carrierConfig,
363                 acceptContentType,
364                 terminalVendor,
365                 terminalModel,
366                 terminalSoftwareVersion,
367                 CONTENT_TYPE_EAP_RELAY_JSON,
368                 cookies);
369     }
370 
371     /**
372      * Retrieves HTTP response from performing ODSA operations. For operation type, see {@link
373      * EsimOdsaOperation}.
374      *
375      * <p>Implementation based on GSMA TS.43-v5.0 6.1.
376      */
377     @NonNull
performEsimOdsaOperation( String appId, CarrierConfig carrierConfig, ServiceEntitlementRequest request, EsimOdsaOperation odsaOperation)378     public HttpResponse performEsimOdsaOperation(
379             String appId,
380             CarrierConfig carrierConfig,
381             ServiceEntitlementRequest request,
382             EsimOdsaOperation odsaOperation)
383             throws ServiceEntitlementException {
384         Uri.Builder urlBuilder = null;
385         JSONObject postData = null;
386         if (carrierConfig.useHttpPost()) {
387             postData = new JSONObject();
388             appendParametersForAuthentication(postData, request);
389             appendParametersForServiceEntitlementRequest(
390                     postData, ImmutableList.of(appId), request);
391             appendParametersForEsimOdsaOperation(postData, odsaOperation);
392         } else {
393             urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon();
394             appendParametersForAuthentication(urlBuilder, request);
395             appendParametersForServiceEntitlementRequest(
396                     urlBuilder, ImmutableList.of(appId), request);
397             appendParametersForEsimOdsaOperation(urlBuilder, odsaOperation);
398         }
399 
400         if (!TextUtils.isEmpty(request.authenticationToken())
401                 || !TextUtils.isEmpty(request.temporaryToken())) {
402             // Fast Re-Authentication flow with pre-existing auth token
403             Log.d(TAG, "Fast Re-Authentication");
404             return carrierConfig.useHttpPost()
405                     ? httpPost(
406                             postData,
407                             carrierConfig,
408                             request.acceptContentType(),
409                             request.terminalVendor(),
410                             request.terminalModel(),
411                             request.terminalSoftwareVersion())
412                     : httpGet(
413                             urlBuilder.toString(),
414                             carrierConfig,
415                             request.acceptContentType(),
416                             request.terminalVendor(),
417                             request.terminalModel(),
418                             request.terminalSoftwareVersion());
419         } else {
420             // Full Authentication flow
421             Log.d(TAG, "Full Authentication");
422             HttpResponse challengeResponse =
423                     carrierConfig.useHttpPost()
424                             ? httpPost(
425                                     postData,
426                                     carrierConfig,
427                                     CONTENT_TYPE_EAP_RELAY_JSON,
428                                     request.terminalVendor(),
429                                     request.terminalModel(),
430                                     request.terminalSoftwareVersion())
431                             : httpGet(
432                                     urlBuilder.toString(),
433                                     carrierConfig,
434                                     CONTENT_TYPE_EAP_RELAY_JSON,
435                                     request.terminalVendor(),
436                                     request.terminalModel(),
437                                     request.terminalSoftwareVersion());
438             String eapAkaChallenge = getEapAkaChallenge(challengeResponse);
439             if (eapAkaChallenge == null) {
440                 throw new ServiceEntitlementException(
441                         ERROR_MALFORMED_HTTP_RESPONSE,
442                         "Failed to parse EAP-AKA challenge: " + challengeResponse.body());
443             }
444             return respondToEapAkaChallenge(
445                     carrierConfig,
446                     eapAkaChallenge,
447                     challengeResponse.cookies(),
448                     MAX_EAP_AKA_ATTEMPTS,
449                     request.acceptContentType(),
450                     request.terminalVendor(),
451                     request.terminalModel(),
452                     request.terminalSoftwareVersion());
453         }
454     }
455 
456     /**
457      * Retrieves the endpoint for OpenID Connect(OIDC) authentication.
458      *
459      * <p>Implementation based on section 2.8.2 of TS.43
460      *
461      * <p>The user should call {@link #queryEntitlementStatusFromOidc(String, CarrierConfig,
462      * String)} with the authentication result to retrieve the service entitlement configuration.
463      */
464     @NonNull
acquireOidcAuthenticationEndpoint( String appId, CarrierConfig carrierConfig, ServiceEntitlementRequest request)465     public String acquireOidcAuthenticationEndpoint(
466             String appId, CarrierConfig carrierConfig, ServiceEntitlementRequest request)
467             throws ServiceEntitlementException {
468         Uri.Builder urlBuilder = null;
469         JSONObject postData = null;
470         if (carrierConfig.useHttpPost()) {
471             postData = new JSONObject();
472             appendParametersForServiceEntitlementRequest(
473                     postData, ImmutableList.of(appId), request);
474         } else {
475             urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon();
476             appendParametersForServiceEntitlementRequest(
477                     urlBuilder, ImmutableList.of(appId), request);
478         }
479 
480         HttpResponse response =
481                 carrierConfig.useHttpPost()
482                         ? httpPost(
483                                 postData,
484                                 carrierConfig,
485                                 request.acceptContentType(),
486                                 request.terminalVendor(),
487                                 request.terminalModel(),
488                                 request.terminalSoftwareVersion())
489                         : httpGet(
490                                 urlBuilder.toString(),
491                                 carrierConfig,
492                                 request.acceptContentType(),
493                                 request.terminalVendor(),
494                                 request.terminalModel(),
495                                 request.terminalSoftwareVersion());
496         return response.location();
497     }
498 
499     /**
500      * Retrieves the HTTP response with the service entitlement configuration from OIDC
501      * authentication result.
502      *
503      * <p>Implementation based on section 2.8.2 of TS.43.
504      *
505      * <p>{@link #acquireOidcAuthenticationEndpoint} must be called before calling this method.
506      */
507     @NonNull
queryEntitlementStatusFromOidc( String url, CarrierConfig carrierConfig, ServiceEntitlementRequest request)508     public HttpResponse queryEntitlementStatusFromOidc(
509             String url, CarrierConfig carrierConfig, ServiceEntitlementRequest request)
510             throws ServiceEntitlementException {
511         Uri.Builder urlBuilder = Uri.parse(url).buildUpon();
512         return httpGet(
513                 urlBuilder.toString(),
514                 carrierConfig,
515                 request.acceptContentType(),
516                 request.terminalVendor(),
517                 request.terminalModel(),
518                 request.terminalSoftwareVersion());
519     }
520 
521     @SuppressWarnings("HardwareIds")
appendParametersForAuthentication( Uri.Builder urlBuilder, ServiceEntitlementRequest request)522     private void appendParametersForAuthentication(
523             Uri.Builder urlBuilder, ServiceEntitlementRequest request) {
524         if (!TextUtils.isEmpty(request.authenticationToken())) {
525             // IMSI and token required for fast AuthN.
526             urlBuilder
527                     .appendQueryParameter(IMSI, mTelephonyManager.getSubscriberId())
528                     .appendQueryParameter(TOKEN, request.authenticationToken());
529         } else if (!TextUtils.isEmpty(request.temporaryToken())) {
530             // temporary_token required for fast AuthN.
531             urlBuilder.appendQueryParameter(TEMPORARY_TOKEN, request.temporaryToken());
532         } else {
533             // EAP_ID required for initial AuthN
534             urlBuilder.appendQueryParameter(
535                     EAP_ID,
536                     getImsiEap(
537                             mTelephonyManager.getSimOperator(),
538                             mTelephonyManager.getSubscriberId()));
539         }
540     }
541 
542     @SuppressWarnings("HardwareIds")
appendParametersForAuthentication( JSONObject postData, ServiceEntitlementRequest request)543     private void appendParametersForAuthentication(
544             JSONObject postData, ServiceEntitlementRequest request)
545             throws ServiceEntitlementException {
546         try {
547             if (!TextUtils.isEmpty(request.authenticationToken())) {
548                 // IMSI and token required for fast AuthN.
549                 postData.put(IMSI, mTelephonyManager.getSubscriberId());
550                 postData.put(TOKEN, request.authenticationToken());
551             } else if (!TextUtils.isEmpty(request.temporaryToken())) {
552                 // temporary_token required for fast AuthN.
553                 postData.put(TEMPORARY_TOKEN, request.temporaryToken());
554             } else {
555                 // EAP_ID required for initial AuthN
556                 postData.put(
557                         EAP_ID,
558                         getImsiEap(
559                                 mTelephonyManager.getSimOperator(),
560                                 mTelephonyManager.getSubscriberId()));
561             }
562         } catch (JSONException jsonException) {
563             // Should never happen
564             throw new ServiceEntitlementException(
565                     ERROR_JSON_COMPOSE_FAILURE, "Failed to compose JSON", jsonException);
566         }
567     }
568 
appendParametersForServiceEntitlementRequest( Uri.Builder urlBuilder, ImmutableList<String> appIds, ServiceEntitlementRequest request)569     private void appendParametersForServiceEntitlementRequest(
570             Uri.Builder urlBuilder,
571             ImmutableList<String> appIds,
572             ServiceEntitlementRequest request) {
573         if (!TextUtils.isEmpty(request.notificationToken())) {
574             urlBuilder
575                     .appendQueryParameter(
576                             NOTIF_ACTION, Integer.toString(request.notificationAction()))
577                     .appendQueryParameter(NOTIF_TOKEN, request.notificationToken());
578         }
579 
580         // Assign terminal ID with device IMEI if not set.
581         if (TextUtils.isEmpty(request.terminalId())) {
582             urlBuilder.appendQueryParameter(TERMINAL_ID, mTelephonyManager.getImei());
583         } else {
584             urlBuilder.appendQueryParameter(TERMINAL_ID, request.terminalId());
585         }
586 
587         // Optional query parameters, append them if not empty
588         appendOptionalQueryParameter(urlBuilder, APP_VERSION, request.appVersion());
589         appendOptionalQueryParameter(urlBuilder, APP_NAME, request.appName());
590         appendOptionalQueryParameter(urlBuilder, BOOST_TYPE, request.boostType());
591 
592         for (String appId : appIds) {
593             urlBuilder.appendQueryParameter(APP, appId);
594         }
595 
596         urlBuilder
597                 // Identity and Authentication parameters
598                 .appendQueryParameter(
599                         TERMINAL_VENDOR,
600                         trimString(request.terminalVendor(), MAX_TERMINAL_VENDOR_LENGTH))
601                 .appendQueryParameter(
602                         TERMINAL_MODEL,
603                         trimString(request.terminalModel(), MAX_TERMINAL_MODEL_LENGTH))
604                 .appendQueryParameter(
605                         TERMIAL_SW_VERSION,
606                         trimString(
607                                 request.terminalSoftwareVersion(),
608                                 MAX_TERMINAL_SOFTWARE_VERSION_LENGTH))
609                 // General Service parameters
610                 .appendQueryParameter(VERS, Integer.toString(request.configurationVersion()))
611                 .appendQueryParameter(ENTITLEMENT_VERSION, request.entitlementVersion());
612     }
613 
appendParametersForServiceEntitlementRequest( JSONObject postData, ImmutableList<String> appIds, ServiceEntitlementRequest request)614     private void appendParametersForServiceEntitlementRequest(
615             JSONObject postData, ImmutableList<String> appIds, ServiceEntitlementRequest request)
616             throws ServiceEntitlementException {
617         try {
618             if (!TextUtils.isEmpty(request.notificationToken())) {
619                 postData.put(NOTIF_ACTION, Integer.toString(request.notificationAction()));
620                 postData.put(NOTIF_TOKEN, request.notificationToken());
621             }
622 
623             // Assign terminal ID with device IMEI if not set.
624             if (TextUtils.isEmpty(request.terminalId())) {
625                 postData.put(TERMINAL_ID, mTelephonyManager.getImei());
626             } else {
627                 postData.put(TERMINAL_ID, request.terminalId());
628             }
629 
630             // Optional query parameters, append them if not empty
631             appendOptionalQueryParameter(postData, APP_VERSION, request.appVersion());
632             appendOptionalQueryParameter(postData, APP_NAME, request.appName());
633             appendOptionalQueryParameter(postData, BOOST_TYPE, request.boostType());
634 
635             if (appIds.size() == 1) {
636                 appendOptionalQueryParameter(postData, APP, appIds.get(0));
637             } else {
638                 appendOptionalQueryParameter(
639                         postData, APP, "[" + TextUtils.join(",", appIds) + "]");
640             }
641 
642             appendOptionalQueryParameter(
643                     postData,
644                     TERMINAL_VENDOR,
645                     trimString(request.terminalVendor(), MAX_TERMINAL_VENDOR_LENGTH));
646             appendOptionalQueryParameter(
647                     postData,
648                     TERMINAL_MODEL,
649                     trimString(request.terminalModel(), MAX_TERMINAL_MODEL_LENGTH));
650             appendOptionalQueryParameter(
651                     postData,
652                     TERMIAL_SW_VERSION,
653                     trimString(
654                             request.terminalSoftwareVersion(),
655                             MAX_TERMINAL_SOFTWARE_VERSION_LENGTH));
656             appendOptionalQueryParameter(
657                     postData, VERS, Integer.toString(request.configurationVersion()));
658             appendOptionalQueryParameter(
659                     postData, ENTITLEMENT_VERSION, request.entitlementVersion());
660         } catch (JSONException jsonException) {
661             // Should never happen
662             throw new ServiceEntitlementException(
663                     ERROR_JSON_COMPOSE_FAILURE, "Failed to compose JSON", jsonException);
664         }
665     }
666 
appendParametersForEsimOdsaOperation( Uri.Builder urlBuilder, EsimOdsaOperation odsaOperation)667     private void appendParametersForEsimOdsaOperation(
668             Uri.Builder urlBuilder, EsimOdsaOperation odsaOperation) {
669         urlBuilder.appendQueryParameter(OPERATION, odsaOperation.operation());
670         if (odsaOperation.operationType() != EsimOdsaOperation.OPERATION_TYPE_NOT_SET) {
671             urlBuilder.appendQueryParameter(
672                     OPERATION_TYPE, Integer.toString(odsaOperation.operationType()));
673         }
674         appendOptionalQueryParameter(
675                 urlBuilder,
676                 OPERATION_TARGETS,
677                 TextUtils.join(",", odsaOperation.operationTargets()));
678         appendOptionalQueryParameter(
679                 urlBuilder, COMPANION_TERMINAL_ID, odsaOperation.companionTerminalId());
680         appendOptionalQueryParameter(
681                 urlBuilder, COMPANION_TERMINAL_VENDOR, odsaOperation.companionTerminalVendor());
682         appendOptionalQueryParameter(
683                 urlBuilder, COMPANION_TERMINAL_MODEL, odsaOperation.companionTerminalModel());
684         appendOptionalQueryParameter(
685                 urlBuilder,
686                 COMPANION_TERMINAL_SW_VERSION,
687                 odsaOperation.companionTerminalSoftwareVersion());
688         appendOptionalQueryParameter(
689                 urlBuilder,
690                 COMPANION_TERMINAL_FRIENDLY_NAME,
691                 odsaOperation.companionTerminalFriendlyName());
692         appendOptionalQueryParameter(
693                 urlBuilder, COMPANION_TERMINAL_SERVICE, odsaOperation.companionTerminalService());
694         appendOptionalQueryParameter(
695                 urlBuilder, COMPANION_TERMINAL_ICCID, odsaOperation.companionTerminalIccid());
696         appendOptionalQueryParameter(
697                 urlBuilder, COMPANION_TERMINAL_EID, odsaOperation.companionTerminalEid());
698         appendOptionalQueryParameter(urlBuilder, TERMINAL_ICCID, odsaOperation.terminalIccid());
699         appendOptionalQueryParameter(urlBuilder, TERMINAL_EID, odsaOperation.terminalEid());
700         appendOptionalQueryParameter(
701                 urlBuilder, TARGET_TERMINAL_ID, odsaOperation.targetTerminalId());
702         appendOptionalQueryParameter(
703                 urlBuilder, TARGET_TERMINAL_IDS, odsaOperation.targetTerminalIds());
704         appendOptionalQueryParameter(
705                 urlBuilder, TARGET_TERMINAL_ICCID, odsaOperation.targetTerminalIccid());
706         appendOptionalQueryParameter(
707                 urlBuilder, TARGET_TERMINAL_EID, odsaOperation.targetTerminalEid());
708         appendOptionalQueryParameter(
709                 urlBuilder,
710                 TARGET_TERMINAL_SERIAL_NUMBER,
711                 odsaOperation.targetTerminalSerialNumber());
712         appendOptionalQueryParameter(
713                 urlBuilder, TARGET_TERMINAL_MODEL, odsaOperation.targetTerminalModel());
714         appendOptionalQueryParameter(
715                 urlBuilder, OLD_TERMINAL_ICCID, odsaOperation.oldTerminalIccid());
716         appendOptionalQueryParameter(urlBuilder, OLD_TERMINAL_ID, odsaOperation.oldTerminalId());
717         appendOptionalQueryParameter(urlBuilder, MESSAGE_RESPONSE, odsaOperation.messageResponse());
718         appendOptionalQueryParameter(urlBuilder, MESSAGE_BUTTON, odsaOperation.messageButton());
719     }
720 
appendParametersForEsimOdsaOperation( JSONObject postData, EsimOdsaOperation odsaOperation)721     private void appendParametersForEsimOdsaOperation(
722             JSONObject postData, EsimOdsaOperation odsaOperation)
723             throws ServiceEntitlementException {
724         try {
725             postData.put(OPERATION, odsaOperation.operation());
726             if (odsaOperation.operationType() != EsimOdsaOperation.OPERATION_TYPE_NOT_SET) {
727                 postData.put(OPERATION_TYPE, Integer.toString(odsaOperation.operationType()));
728             }
729             appendOptionalQueryParameter(
730                     postData,
731                     OPERATION_TARGETS,
732                     TextUtils.join(",", odsaOperation.operationTargets()));
733             appendOptionalQueryParameter(
734                     postData, COMPANION_TERMINAL_ID, odsaOperation.companionTerminalId());
735             appendOptionalQueryParameter(
736                     postData, COMPANION_TERMINAL_VENDOR, odsaOperation.companionTerminalVendor());
737             appendOptionalQueryParameter(
738                     postData, COMPANION_TERMINAL_MODEL, odsaOperation.companionTerminalModel());
739             appendOptionalQueryParameter(
740                     postData,
741                     COMPANION_TERMINAL_SW_VERSION,
742                     odsaOperation.companionTerminalSoftwareVersion());
743             appendOptionalQueryParameter(
744                     postData,
745                     COMPANION_TERMINAL_FRIENDLY_NAME,
746                     odsaOperation.companionTerminalFriendlyName());
747             appendOptionalQueryParameter(
748                     postData, COMPANION_TERMINAL_SERVICE, odsaOperation.companionTerminalService());
749             appendOptionalQueryParameter(
750                     postData, COMPANION_TERMINAL_ICCID, odsaOperation.companionTerminalIccid());
751             appendOptionalQueryParameter(
752                     postData, COMPANION_TERMINAL_EID, odsaOperation.companionTerminalEid());
753             appendOptionalQueryParameter(postData, TERMINAL_ICCID, odsaOperation.terminalIccid());
754             appendOptionalQueryParameter(postData, TERMINAL_EID, odsaOperation.terminalEid());
755             appendOptionalQueryParameter(
756                     postData, TARGET_TERMINAL_ID, odsaOperation.targetTerminalId());
757             appendOptionalQueryParameter(
758                     postData, TARGET_TERMINAL_IDS, odsaOperation.targetTerminalIds());
759             appendOptionalQueryParameter(
760                     postData, TARGET_TERMINAL_ICCID, odsaOperation.targetTerminalIccid());
761             appendOptionalQueryParameter(
762                     postData, TARGET_TERMINAL_EID, odsaOperation.targetTerminalEid());
763             appendOptionalQueryParameter(
764                     postData,
765                     TARGET_TERMINAL_SERIAL_NUMBER,
766                     odsaOperation.targetTerminalSerialNumber());
767             appendOptionalQueryParameter(
768                     postData, TARGET_TERMINAL_MODEL, odsaOperation.targetTerminalModel());
769             appendOptionalQueryParameter(
770                     postData, OLD_TERMINAL_ICCID, odsaOperation.oldTerminalIccid());
771             appendOptionalQueryParameter(postData, OLD_TERMINAL_ID, odsaOperation.oldTerminalId());
772             appendOptionalQueryParameter(
773                     postData, MESSAGE_RESPONSE, odsaOperation.messageResponse());
774             appendOptionalQueryParameter(postData, MESSAGE_BUTTON, odsaOperation.messageButton());
775         } catch (JSONException jsonException) {
776             // Should never happen
777             throw new ServiceEntitlementException(
778                     ERROR_JSON_COMPOSE_FAILURE, "Failed to compose JSON", jsonException);
779         }
780     }
781 
appendOptionalQueryParameter(Uri.Builder urlBuilder, String key, String value)782     private void appendOptionalQueryParameter(Uri.Builder urlBuilder, String key, String value) {
783         if (!TextUtils.isEmpty(value)) {
784             urlBuilder.appendQueryParameter(key, value);
785         }
786     }
787 
appendOptionalQueryParameter(JSONObject postData, String key, String value)788     private void appendOptionalQueryParameter(JSONObject postData, String key, String value)
789             throws JSONException {
790         if (!TextUtils.isEmpty(value)) {
791             postData.put(key, value);
792         }
793     }
794 
appendOptionalQueryParameter( Uri.Builder urlBuilder, String key, ImmutableList<String> values)795     private void appendOptionalQueryParameter(
796             Uri.Builder urlBuilder, String key, ImmutableList<String> values) {
797         for (String value : values) {
798             if (!TextUtils.isEmpty(value)) {
799                 urlBuilder.appendQueryParameter(key, value);
800             }
801         }
802     }
803 
appendOptionalQueryParameter( JSONObject postData, String key, ImmutableList<String> values)804     private void appendOptionalQueryParameter(
805             JSONObject postData, String key, ImmutableList<String> values) throws JSONException {
806         for (String value : values) {
807             if (!TextUtils.isEmpty(value)) {
808                 postData.put(key, value);
809             }
810         }
811     }
812 
813     @NonNull
httpGet( String url, CarrierConfig carrierConfig, String acceptContentType, String terminalVendor, String terminalModel, String terminalSoftwareVersion)814     private HttpResponse httpGet(
815             String url,
816             CarrierConfig carrierConfig,
817             String acceptContentType,
818             String terminalVendor,
819             String terminalModel,
820             String terminalSoftwareVersion)
821             throws ServiceEntitlementException {
822         HttpRequest.Builder builder =
823                 HttpRequest.builder()
824                         .setUrl(url)
825                         .setRequestMethod(RequestMethod.GET)
826                         .addRequestProperty(HttpHeaders.ACCEPT, acceptContentType)
827                         .setTimeoutInSec(carrierConfig.timeoutInSec())
828                         .setNetwork(carrierConfig.network());
829         String userAgent =
830                 getUserAgent(
831                         carrierConfig.clientTs43(),
832                         terminalVendor,
833                         terminalModel,
834                         terminalSoftwareVersion);
835         if (!TextUtils.isEmpty(userAgent)) {
836             builder.addRequestProperty(HttpHeaders.USER_AGENT, userAgent);
837         }
838         return mHttpClient.request(builder.build());
839     }
840 
841     @NonNull
httpPost( JSONObject postData, CarrierConfig carrierConfig, String acceptContentType, String terminalVendor, String terminalModel, String terminalSoftwareVersion)842     private HttpResponse httpPost(
843             JSONObject postData,
844             CarrierConfig carrierConfig,
845             String acceptContentType,
846             String terminalVendor,
847             String terminalModel,
848             String terminalSoftwareVersion)
849             throws ServiceEntitlementException {
850         return httpPost(
851                 postData,
852                 carrierConfig,
853                 acceptContentType,
854                 terminalVendor,
855                 terminalModel,
856                 terminalSoftwareVersion,
857                 ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON,
858                 ImmutableList.of());
859     }
860 
861     @NonNull
httpPost( JSONObject postData, CarrierConfig carrierConfig, String acceptContentType, String terminalVendor, String terminalModel, String terminalSoftwareVersion, String contentType, ImmutableList<String> cookies)862     private HttpResponse httpPost(
863             JSONObject postData,
864             CarrierConfig carrierConfig,
865             String acceptContentType,
866             String terminalVendor,
867             String terminalModel,
868             String terminalSoftwareVersion,
869             String contentType,
870             ImmutableList<String> cookies)
871             throws ServiceEntitlementException {
872         HttpRequest.Builder builder =
873                 HttpRequest.builder()
874                         .setUrl(carrierConfig.serverUrl())
875                         .setRequestMethod(RequestMethod.POST)
876                         .setPostData(postData)
877                         .addRequestProperty(HttpHeaders.ACCEPT, acceptContentType)
878                         .addRequestProperty(HttpHeaders.CONTENT_TYPE, contentType)
879                         .addRequestProperty(HttpHeaders.COOKIE, cookies)
880                         .setTimeoutInSec(carrierConfig.timeoutInSec())
881                         .setNetwork(carrierConfig.network());
882         String userAgent =
883                 getUserAgent(
884                         carrierConfig.clientTs43(),
885                         terminalVendor,
886                         terminalModel,
887                         terminalSoftwareVersion);
888         if (!TextUtils.isEmpty(userAgent)) {
889             builder.addRequestProperty(HttpHeaders.USER_AGENT, userAgent);
890         }
891         return mHttpClient.request(builder.build());
892     }
893 
894     @Nullable
getEapAkaChallenge(HttpResponse response)895     private String getEapAkaChallenge(HttpResponse response) throws ServiceEntitlementException {
896         String eapAkaChallenge = null;
897         String responseBody = response.body();
898         if (response.contentType() == ContentType.JSON) {
899             try {
900                 eapAkaChallenge =
901                         new JSONObject(responseBody).optString(EAP_CHALLENGE_RESPONSE, null);
902             } catch (JSONException jsonException) {
903                 throw new ServiceEntitlementException(
904                         ERROR_MALFORMED_HTTP_RESPONSE,
905                         "Failed to parse json object",
906                         jsonException);
907             }
908         } else if (response.contentType() == ContentType.XML) {
909             // EAP-AKA challenge is always in JSON format.
910             return null;
911         } else {
912             throw new ServiceEntitlementException(
913                     ERROR_MALFORMED_HTTP_RESPONSE, "Unknown HTTP content type");
914         }
915         return eapAkaChallenge;
916     }
917 
getAppVersion(Context context)918     private String getAppVersion(Context context) {
919         try {
920             PackageInfo packageInfo =
921                     context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
922             return packageInfo.versionName;
923         } catch (Exception e) {
924             // should be impossible
925         }
926         return "";
927     }
928 
getUserAgent( String clientTs43, String terminalVendor, String terminalModel, String terminalSoftwareVersion)929     private String getUserAgent(
930             String clientTs43,
931             String terminalVendor,
932             String terminalModel,
933             String terminalSoftwareVersion) {
934         if (!TextUtils.isEmpty(clientTs43)
935                 && !TextUtils.isEmpty(terminalVendor)
936                 && !TextUtils.isEmpty(terminalModel)
937                 && !TextUtils.isEmpty(terminalSoftwareVersion)) {
938             return String.format(
939                     "PRD-TS43 term-%s/%s %s/%s OS-Android/%s",
940                     trimString(terminalVendor, MAX_TERMINAL_VENDOR_LENGTH),
941                     trimString(terminalModel, MAX_TERMINAL_MODEL_LENGTH),
942                     clientTs43,
943                     mAppVersion,
944                     trimString(terminalSoftwareVersion, MAX_TERMINAL_SOFTWARE_VERSION_LENGTH));
945         }
946         return "";
947     }
948 
trimString(String s, int maxLength)949     private String trimString(String s, int maxLength) {
950         return s.substring(0, Math.min(s.length(), maxLength));
951     }
952 
953     /**
954      * Returns the IMSI EAP value. The resulting realm part of the Root NAI in 3GPP TS 23.003 clause
955      * 19.3.2 will be in the form:
956      *
957      * <p>{@code 0<IMSI>@nai.epc.mnc<MNC>.mcc<MCC>.3gppnetwork.org}
958      */
959     @Nullable
getImsiEap(@ullable String mccmnc, @Nullable String imsi)960     public static String getImsiEap(@Nullable String mccmnc, @Nullable String imsi) {
961         if (mccmnc == null || mccmnc.length() < 5 || imsi == null) {
962             return null;
963         }
964 
965         String mcc = mccmnc.substring(0, 3);
966         String mnc = mccmnc.substring(3);
967         if (mnc.length() == 2) {
968             mnc = "0" + mnc;
969         }
970         return "0" + imsi + "@nai.epc.mnc" + mnc + ".mcc" + mcc + ".3gppnetwork.org";
971     }
972 
973     /** Retrieves the history of past HTTP request and responses. */
974     @NonNull
getHistory()975     public List<String> getHistory() {
976         return mHttpClient.getHistory();
977     }
978 
979     /** Clears the history of past HTTP request and responses. */
clearHistory()980     public void clearHistory() {
981         mHttpClient.clearHistory();
982     }
983 }
984