blob: 4482bf770a0957e72e82d118e3b3099351228dbe [file] [log] [blame]
/*
* 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.
*
* <p>Implementation based on GSMA TS.43-v5.0 2.6.1.
*
* @throws ServiceEntitlementException when getting an unexpected http response.
*/
@Nullable
public String queryEntitlementStatus(ImmutableList<String> 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.
*
* <p>The {@code response} should contain a EAP-AKA challenge from server, and the
* follow-up request could contain:
*
* <ul>
* <li>The EAP-AKA response message, and the follow-up response should contain the
* service entitlement configuration; or,
* <li>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).
* </ul>
*
* @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<String> 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}.
*
* <p>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<String> 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:
*
* <p>{@code 0<IMSI>@nai.epc.mnc<MNC>.mcc<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";
}
}