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