All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.libraries.entitlement.eapaka.EapAkaApi Maven / Gradle / Ivy

/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.libraries.entitlement.eapaka;

import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE;
import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE;

import android.content.Context;
import android.net.Uri;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.libraries.entitlement.CarrierConfig;
import com.android.libraries.entitlement.EsimOdsaOperation;
import com.android.libraries.entitlement.ServiceEntitlementException;
import com.android.libraries.entitlement.ServiceEntitlementRequest;
import com.android.libraries.entitlement.http.HttpClient;
import com.android.libraries.entitlement.http.HttpConstants.RequestMethod;
import com.android.libraries.entitlement.http.HttpRequest;
import com.android.libraries.entitlement.http.HttpResponse;

import com.google.common.collect.ImmutableList;
import com.google.common.net.HttpHeaders;

import org.json.JSONException;
import org.json.JSONObject;

public class EapAkaApi {
    private static final String TAG = "ServiceEntitlement";

    public static final String EAP_CHALLENGE_RESPONSE = "eap-relay-packet";

    private static final String VERS = "vers";
    private static final String ENTITLEMENT_VERSION = "entitlement_version";
    private static final String TERMINAL_ID = "terminal_id";
    private static final String TERMINAL_VENDOR = "terminal_vendor";
    private static final String TERMINAL_MODEL = "terminal_model";
    private static final String TERMIAL_SW_VERSION = "terminal_sw_version";
    private static final String APP = "app";
    private static final String EAP_ID = "EAP_ID";
    private static final String IMSI = "IMSI";
    private static final String TOKEN = "token";
    private static final String NOTIF_ACTION = "notif_action";
    private static final String NOTIF_TOKEN = "notif_token";
    private static final String APP_VERSION = "app_version";
    private static final String APP_NAME = "app_name";

    private static final String OPERATION = "operation";
    private static final String OPERATION_TYPE = "operation_type";
    private static final String COMPANION_TERMINAL_ID = "companion_terminal_id";
    private static final String COMPANION_TERMINAL_VENDOR = "companion_terminal_vendor";
    private static final String COMPANION_TERMINAL_MODEL = "companion_terminal_model";
    private static final String COMPANION_TERMINAL_SW_VERSION = "companion_terminal_sw_version";
    private static final String COMPANION_TERMINAL_FRIENDLY_NAME =
            "companion_terminal_friendly_name";
    private static final String COMPANION_TERMINAL_SERVICE = "companion_terminal_service";
    private static final String COMPANION_TERMINAL_ICCID = "companion_terminal_iccid";
    private static final String COMPANION_TERMINAL_EID = "companion_terminal_eid";

    private static final String TERMINAL_ICCID = "terminal_iccid";
    private static final String TERMINAL_EID = "terminal_eid";

    private static final String TARGET_TERMINAL_ID = "target_terminal_id";
    private static final String TARGET_TERMINAL_ICCID = "target_terminal_iccid";
    private static final String TARGET_TERMINAL_EID = "target_terminal_eid";

    // In case of EAP-AKA synchronization failure, we try to recover for at most two times.
    private static final int FOLLOW_SYNC_FAILURE_MAX_COUNT = 2;

    private final Context mContext;
    private final int mSimSubscriptionId;
    private final HttpClient mHttpClient;

    public EapAkaApi(Context context, int simSubscriptionId) {
        this(context, simSubscriptionId, new HttpClient());
    }

    @VisibleForTesting
    EapAkaApi(Context context, int simSubscriptionId, HttpClient httpClient) {
        this.mContext = context;
        this.mSimSubscriptionId = simSubscriptionId;
        this.mHttpClient = httpClient;
    }

    /**
     * Retrieves raw entitlement configuration doc though EAP-AKA authentication.
     *
     * 

Implementation based on GSMA TS.43-v5.0 2.6.1. * * @throws ServiceEntitlementException when getting an unexpected http response. */ @Nullable public String queryEntitlementStatus(ImmutableList appIds, CarrierConfig carrierConfig, ServiceEntitlementRequest request) throws ServiceEntitlementException { Uri.Builder urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon(); appendParametersForServiceEntitlementRequest(urlBuilder, appIds, request); if (!TextUtils.isEmpty(request.authenticationToken())) { // Fast Re-Authentication flow with pre-existing auth token Log.d(TAG, "Fast Re-Authentication"); return httpGet( urlBuilder.toString(), carrierConfig, request.acceptContentType()).body(); } else { // Full Authentication flow Log.d(TAG, "Full Authentication"); HttpResponse challengeResponse = httpGet( urlBuilder.toString(), carrierConfig, ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON); return respondToEapAkaChallenge( carrierConfig, challengeResponse, FOLLOW_SYNC_FAILURE_MAX_COUNT, request.acceptContentType()) .body(); } } /** * Sends a follow-up HTTP request to the HTTP {@code response} using the same cookie, and * returns the follow-up HTTP response. * *

The {@code response} should contain a EAP-AKA challenge from server, and the * follow-up request could contain: * *

    *
  • The EAP-AKA response message, and the follow-up response should contain the * service entitlement configuration; or, *
  • The EAP-AKA synchronization failure message, and the follow-up response should * contain the new EAP-AKA challenge. Then this method calls itself to follow-up * the new challenge and return a new response, if {@code followSyncFailureCount} * is greater than zero. When this method call itself {@code followSyncFailureCount} is * reduced by one to prevent infinite loop (unlikely in practice, but just in case). *
* * @param response Challenge response from server which its content type is JSON */ private HttpResponse respondToEapAkaChallenge( CarrierConfig carrierConfig, HttpResponse response, int followSyncFailureCount, String contentType) throws ServiceEntitlementException { String eapAkaChallenge; try { eapAkaChallenge = new JSONObject(response.body()).getString(EAP_CHALLENGE_RESPONSE); } catch (JSONException jsonException) { throw new ServiceEntitlementException( ERROR_MALFORMED_HTTP_RESPONSE, "Failed to parse json object", jsonException); } EapAkaChallenge challenge = EapAkaChallenge.parseEapAkaChallenge(eapAkaChallenge); EapAkaResponse eapAkaResponse = EapAkaResponse.respondToEapAkaChallenge(mContext, mSimSubscriptionId, challenge); // This could be a successful authentication, or synchronization failure. if (eapAkaResponse.response() != null) { // successful authentication return challengeResponse( eapAkaResponse.response(), carrierConfig, response.cookies(), contentType); } else if (eapAkaResponse.synchronizationFailureResponse() != null) { Log.d(TAG, "synchronization failure"); HttpResponse newChallenge = challengeResponse( eapAkaResponse.synchronizationFailureResponse(), carrierConfig, response.cookies(), ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON); if (followSyncFailureCount > 0) { return respondToEapAkaChallenge( carrierConfig, newChallenge, followSyncFailureCount - 1, contentType); } else { throw new ServiceEntitlementException( ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE, "Unable to recover from EAP-AKA synchroinization failure"); } } else { // not possible throw new AssertionError("EapAkaResponse invalid."); } } private HttpResponse challengeResponse( String eapAkaChallengeResponse, CarrierConfig carrierConfig, ImmutableList cookies, String contentType) throws ServiceEntitlementException { Log.d(TAG, "challengeResponse"); JSONObject postData = new JSONObject(); try { postData.put(EAP_CHALLENGE_RESPONSE, eapAkaChallengeResponse); } catch (JSONException jsonException) { throw new ServiceEntitlementException( ERROR_MALFORMED_HTTP_RESPONSE, "Failed to put post data", jsonException); } HttpRequest request = HttpRequest.builder() .setUrl(carrierConfig.serverUrl()) .setRequestMethod(RequestMethod.POST) .setPostData(postData) .addRequestProperty(HttpHeaders.ACCEPT, contentType) .addRequestProperty( HttpHeaders.CONTENT_TYPE, ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON) .addRequestProperty(HttpHeaders.COOKIE, cookies) .setTimeoutInSec(carrierConfig.timeoutInSec()) .setNetwork(carrierConfig.network()) .build(); return mHttpClient.request(request); } /** * Retrieves raw doc of performing ODSA operations. For operation type, see {@link * EsimOdsaOperation}. * *

Implementation based on GSMA TS.43-v5.0 6.1. */ public String performEsimOdsaOperation(String appId, CarrierConfig carrierConfig, ServiceEntitlementRequest request, EsimOdsaOperation odsaOperation) throws ServiceEntitlementException { Uri.Builder urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon(); appendParametersForServiceEntitlementRequest(urlBuilder, ImmutableList.of(appId), request); appendParametersForEsimOdsaOperation(urlBuilder, odsaOperation); if (!TextUtils.isEmpty(request.authenticationToken())) { // Fast Re-Authentication flow with pre-existing auth token Log.d(TAG, "Fast Re-Authentication"); return httpGet( urlBuilder.toString(), carrierConfig, request.acceptContentType()).body(); } else { // Full Authentication flow Log.d(TAG, "Full Authentication"); HttpResponse challengeResponse = httpGet( urlBuilder.toString(), carrierConfig, ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON); return respondToEapAkaChallenge( carrierConfig, challengeResponse, FOLLOW_SYNC_FAILURE_MAX_COUNT, request.acceptContentType()) .body(); } } private void appendParametersForServiceEntitlementRequest( Uri.Builder urlBuilder, ImmutableList appIds, ServiceEntitlementRequest request) { TelephonyManager telephonyManager = mContext.getSystemService( TelephonyManager.class).createForSubscriptionId(mSimSubscriptionId); if (TextUtils.isEmpty(request.authenticationToken())) { // EAP_ID required for initial AuthN urlBuilder.appendQueryParameter( EAP_ID, getImsiEap(telephonyManager.getSimOperator(), telephonyManager.getSubscriberId())); } else { // IMSI and token required for fast AuthN. urlBuilder .appendQueryParameter(IMSI, telephonyManager.getSubscriberId()) .appendQueryParameter(TOKEN, request.authenticationToken()); } if (!TextUtils.isEmpty(request.notificationToken())) { urlBuilder .appendQueryParameter(NOTIF_ACTION, Integer.toString(request.notificationAction())) .appendQueryParameter(NOTIF_TOKEN, request.notificationToken()); } // Assign terminal ID with device IMEI if not set. if (TextUtils.isEmpty(request.terminalId())) { urlBuilder.appendQueryParameter(TERMINAL_ID, telephonyManager.getImei()); } else { urlBuilder.appendQueryParameter(TERMINAL_ID, request.terminalId()); } // Optional query parameters, append them if not empty appendOptionalQueryParameter(urlBuilder, APP_VERSION, request.appVersion()); appendOptionalQueryParameter(urlBuilder, APP_NAME, request.appName()); for (String appId : appIds) { urlBuilder.appendQueryParameter(APP, appId); } urlBuilder // Identity and Authentication parameters .appendQueryParameter(TERMINAL_VENDOR, request.terminalVendor()) .appendQueryParameter(TERMINAL_MODEL, request.terminalModel()) .appendQueryParameter(TERMIAL_SW_VERSION, request.terminalSoftwareVersion()) // General Service parameters .appendQueryParameter(VERS, Integer.toString(request.configurationVersion())) .appendQueryParameter(ENTITLEMENT_VERSION, request.entitlementVersion()); } private void appendParametersForEsimOdsaOperation( Uri.Builder urlBuilder, EsimOdsaOperation odsaOperation) { urlBuilder.appendQueryParameter(OPERATION, odsaOperation.operation()); if (odsaOperation.operationType() != EsimOdsaOperation.OPERATION_TYPE_NOT_SET) { urlBuilder.appendQueryParameter(OPERATION_TYPE, Integer.toString(odsaOperation.operationType())); } appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_ID, odsaOperation.companionTerminalId()); appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_VENDOR, odsaOperation.companionTerminalVendor()); appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_MODEL, odsaOperation.companionTerminalModel()); appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_SW_VERSION, odsaOperation.companionTerminalSoftwareVersion()); appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_FRIENDLY_NAME, odsaOperation.companionTerminalFriendlyName()); appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_SERVICE, odsaOperation.companionTerminalService()); appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_ICCID, odsaOperation.companionTerminalIccid()); appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_EID, odsaOperation.companionTerminalEid()); appendOptionalQueryParameter(urlBuilder, TERMINAL_ICCID, odsaOperation.terminalIccid()); appendOptionalQueryParameter(urlBuilder, TERMINAL_EID, odsaOperation.terminalEid()); appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_ID, odsaOperation.targetTerminalId()); appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_ICCID, odsaOperation.targetTerminalIccid()); appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_EID, odsaOperation.targetTerminalEid()); } private HttpResponse httpGet(String url, CarrierConfig carrierConfig, String contentType) throws ServiceEntitlementException { HttpRequest httpRequest = HttpRequest.builder() .setUrl(url) .setRequestMethod(RequestMethod.GET) .addRequestProperty(HttpHeaders.ACCEPT, contentType) .setTimeoutInSec(carrierConfig.timeoutInSec()) .setNetwork(carrierConfig.network()) .build(); return mHttpClient.request(httpRequest); } private void appendOptionalQueryParameter(Uri.Builder urlBuilder, String key, String value) { if (!TextUtils.isEmpty(value)) { urlBuilder.appendQueryParameter(key, value); } } /** * Returns the IMSI EAP value. The resulting realm part of the Root NAI in 3GPP TS 23.003 clause * 19.3.2 will be in the form: * *

{@code 0@nai.epc.mnc.mcc.3gppnetwork.org} */ @Nullable public static String getImsiEap(@Nullable String mccmnc, @Nullable String imsi) { if (mccmnc == null || mccmnc.length() < 5 || imsi == null) { return null; } String mcc = mccmnc.substring(0, 3); String mnc = mccmnc.substring(3); if (mnc.length() == 2) { mnc = "0" + mnc; } return "0" + imsi + "@nai.epc.mnc" + mnc + ".mcc" + mcc + ".3gppnetwork.org"; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy