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