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