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