diff options
author | samalin <samalin@google.com> | 2020-12-26 01:08:36 +0800 |
---|---|---|
committer | Meng Wang <mewan@google.com> | 2021-01-15 17:32:59 -0800 |
commit | b7eabb05450b9e382c03cf3fee3a79ae9f90072b (patch) | |
tree | f837a3154781d2c467a1ca8568ae268ca8b156bd | |
parent | e2877642062a4ee7ef20219755b9fa3bae39eba7 (diff) | |
download | service_entitlement-b7eabb05450b9e382c03cf3fee3a79ae9f90072b.tar.gz |
Add implementations for service-entitlement lib
Implements the APIs for retrieving the VoWifi entitlement status through
EAP-AKA authenticate method which defines in GSMA TS.43 specification.
Bug: 173450048
Test: presubmit
Change-Id: I7549e2a0287c1294b71a83c8189109f708e5b31f
21 files changed, 2395 insertions, 688 deletions
@@ -20,9 +20,13 @@ java_library { "java/**/*.java", ], libs: [ + "androidx.annotation_annotation", "auto_value_annotations", ], + static_libs: [ + "guava", + ], plugins: ["auto_value_plugin"], sdk_version: "current", - min_sdk_version: "30", + min_sdk_version: "29", } diff --git a/java/com/android/libraries/entitlement/CarrierConfig.java b/java/com/android/libraries/entitlement/CarrierConfig.java new file mode 100644 index 0000000..8f040c1 --- /dev/null +++ b/java/com/android/libraries/entitlement/CarrierConfig.java @@ -0,0 +1,51 @@ +/* + * 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; + +import com.google.auto.value.AutoValue; + +/** + * Carrier specific customization to be used in the service entitlement queries and operations. + * + * @see #ServiceEntitlement + */ +@AutoValue +public abstract class CarrierConfig { + /** + * Returns the carrier's entitlement server URL. If not set, will use {@code + * https://aes.mnc<MNC>.mcc<MCC>.pub.3gppnetwork.org} as defined in GSMA spec TS.43 section 2.1. + */ + public abstract String serverUrl(); + + /** Returns a new {@link Builder} object. */ + public static Builder builder() { + return new AutoValue_CarrierConfig.Builder() + .setServerUrl(""); + } + + /** Builder. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract CarrierConfig build(); + + /** + * Set's the carrier's entitlement server URL. If not set, will use {@code + * https://aes.mnc<MNC>.mcc<MCC>.pub.3gppnetwork.org} as defined in GSMA spec TS.43 section 2.1. + */ + public abstract Builder setServerUrl(String url); + } +} diff --git a/java/com/android/libraries/entitlement/EsimOdsaOperation.java b/java/com/android/libraries/entitlement/EsimOdsaOperation.java new file mode 100644 index 0000000..d7d23ac --- /dev/null +++ b/java/com/android/libraries/entitlement/EsimOdsaOperation.java @@ -0,0 +1,292 @@ +/* + * 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; + +import com.google.auto.value.AutoValue; + +/** + * HTTP request parameters specific to on device service actiavation (ODSA). See GSMA spec TS.43 + * section 6.2. + */ +@AutoValue +public abstract class EsimOdsaOperation { + /** OSDA operation: CheckEligibility. */ + public static final String OPERATION_CHECK_ELIGIBILITY = "CheckEligibility"; + /** OSDA operation: ManageSubscription. */ + public static final String OPERATION_MANAGE_SUBSCRIPTION = "ManageSubscription"; + /** OSDA operation: ManageService. */ + public static final String OPERATION_MANAGE_SERVICE = "ManageService"; + /** OSDA operation: AcquireConfiguration. */ + public static final String OPERATION_ACQUIRE_CONFIGURATION = "AcquireConfiguration"; + + /** Indicates that operation_type is not set. */ + static final int OPERATION_TYPE_NOT_SET = -1; + /** To activate a subscription, used by {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ + public static final int OPERATION_TYPE_SUBSCRIBE = 0; + /** To cancel a subscription, used by {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ + public static final int OPERATION_TYPE_UNSUBSCRIBE = 1; + /** To manage an existing subscription, for {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ + public static final int OPERATION_TYPE_CHANGE_SUBSCRIPTION = 2; + /** + * To transfer a subscription from an existing device, used by {@link + * #OPERATION_MANAGE_SUBSCRIPTION}. + */ + public static final int OPERATION_TYPE_TRANSFER_SUBSCRIPTION = 3; + /** + * To inform the network of a subscription update, used by + * {@link #OPERATION_MANAGE_SUBSCRIPTION}. + */ + public static final int OPERATION_TYPE_UPDATE_SUBSCRIPTION = 4; + /** To activate a service, used by {@link #OPERATION_MANAGE_SERVICE}. */ + public static final int OPERATION_TYPE_ACTIVATE_SERVICE = 10; + /** To deactivate a service, used by {@link #OPERATION_MANAGE_SERVICE}. */ + public static final int OPERATION_TYPE_DEACTIVATE_SERVICE = 11; + + /** Indicates the companion device carries the same MSISDN as the primary device. */ + public static final String COMPANION_SERVICE_SHAERED_NUMBER = "SharedNumber"; + /** Indicates the companion device carries a different MSISDN as the primary device. */ + public static final String COMPANION_SERVICE_DIFFERENT_NUMBER = "DiffNumber"; + + /** Returns the eSIM ODSA operation. Used by HTTP parameter "operation". */ + public abstract String operation(); + + /** + * Returns the detiled type of the eSIM ODSA operation. Used by HTTP parameter "operation_type". + */ + public abstract int operationType(); + + /** + * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * "companion_terminal_id". + */ + public abstract String companionTerminalId(); + + /** + * Returns the OEM of the companion device. Used by HTTP parameter "companion_terminal_vendor". + */ + public abstract String companionTerminalVendor(); + + /** + * Returns the model of the companion device. Used by HTTP parameter "companion_terminal_model". + */ + public abstract String companionTerminalModel(); + + /** + * Returns the software version of the companion device. Used by HTTP parameter + * "companion_terminal_sw_version". + */ + public abstract String companionTerminalSoftwareVersion(); + + /** + * Returns the user-friendly version of the companion device. Used by HTTP parameter + * "companion_terminal_friendly_name". + */ + public abstract String companionTerminalFriendlyName(); + + /** + * Returns the service type of the companion device, e.g. if the MSISDN is same as the primary + * device. Used by HTTP parameter "companion_terminal_service". + */ + public abstract String companionTerminalService(); + + /** + * Returns the ICCID of the companion device. Used by HTTP parameter "companion_terminal_iccid". + */ + public abstract String companionTerminalIccid(); + + /** + * Returns the ICCID of the companion device. Used by HTTP parameter "companion_terminal_iccid". + */ + public abstract String companionTerminalEid(); + + /** Returns the ICCID of the primary device eSIM. Used by HTTP parameter "terminal_eid". */ + public abstract String terminalIccid(); + + /** + * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter + * "terminal_eid". + */ + public abstract String terminalEid(); + + /** + * Returns the unique identifier of the primary device eSIM, like the IMEI associated with the + * eSIM. Used by HTTP parameter "target_terminal_id". + */ + public abstract String targetTerminalId(); + + /** Returns the ICCID primary device eSIM. Used by HTTP parameter "target_terminal_iccid". */ + public abstract String targetTerminalIccid(); + + /** + * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter + * "target_terminal_eid". + */ + public abstract String targetTerminalEid(); + + /** Returns a new {@link Builder} object. */ + public static Builder builder() { + return new AutoValue_EsimOdsaOperation.Builder().setOperationType(OPERATION_TYPE_NOT_SET); + } + + /** + * Builder. + * + * <p>For ODSA, the rule of which parameters are required varies or each + * operation/opeation_type. + * The Javadoc below gives high-level description, but please refer to GMSA spec TS.43 section + * 6.2 + * for details. + */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Sets the eSIM ODSA operation. Used by HTTP parameter "operation". + * + * <p>Required. + * + * @see #OPERATION_CHECK_ELIGIBILITY + * @see #OPERATION_MANAGE_SUBSCRIPTION + * @see #OPERATION_MANAGE_SERVICE + * @see #OPERATION_ACQUIRE_CONFIGURATION + */ + public abstract Builder setOperation(String value); + + /** + * Sets the detiled type of the eSIM ODSA operation. Used by HTTP parameter "operation_type" + * if set. + * + * <p>Required by some operation. + * + * @see #OPERATION_TYPE_SUBSCRIBE + * @see #OPERATION_TYPE_UNSUBSCRIBE + * @see #OPERATION_TYPE_CHANGE_SUBSCRIPTION + * @see #OPERATION_TYPE_TRANSFER_SUBSCRIPTION + * @see #OPERATION_TYPE_UPDATE_SUBSCRIPTION + * @see #OPERATION_TYPE_ACTIVATE_SERVICE + * @see #OPERATION_TYPE_DEACTIVATE_SERVICE + */ + public abstract Builder setOperationType(int value); + + /** + * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * "companion_terminal_id" if set. + * + * <p>Used by companion device ODSA operation. + */ + public abstract Builder setCompanionTerminalId(String value); + + /** + * Sets the OEM of the companion device. Used by HTTP parameter "companion_terminal_vendor" + * if set. + * + * <p>Used by companion device ODSA operation. + */ + public abstract Builder setCompanionTerminalVendor(String value); + + /** + * Sets the model of the companion device. Used by HTTP parameter "companion_terminal_model" + * if set. + * + * <p>Used by companion device ODSA operation. + */ + public abstract Builder setCompanionTerminalModel(String value); + + /** + * Sets the software version of the companion device. Used by HTTP parameter + * "companion_terminal_sw_version" if set. + * + * <p>Used by companion device ODSA operation. + */ + public abstract Builder setCompanionTerminalSoftwareVersion(String value); + + /** + * Sets the user-friendly version of the companion device. Used by HTTP parameter + * "companion_terminal_friendly_name" if set. + * + * <p>Used by companion device ODSA operation. + */ + public abstract Builder setCompanionTerminalFriendlyName(String value); + + /** + * Sets the service type of the companion device, e.g. if the MSISDN is same as the primary + * device. Used by HTTP parameter "companion_terminal_service" if set. + * + * <p>Used by companion device ODSA operation. + * + * @see #COMPANION_SERVICE_SHAERED_NUMBER + * @see #COMPANION_SERVICE_DIFFERENT_NUMBER + */ + public abstract Builder setCompanionTerminalService(String value); + + /** + * Sets the ICCID of the companion device. Used by HTTP parameter "companion_terminal_iccid" + * if set. + * + * <p>Used by companion device ODSA operation. + */ + public abstract Builder setCompanionTerminalIccid(String value); + + /** + * Sets the eUICC identifier (EID) of the companion device. Used by HTTP parameter + * "companion_terminal_eid" if set. + * + * <p>Used by companion device ODSA operation. + */ + public abstract Builder setCompanionTerminalEid(String value); + + /** + * Sets the ICCID of the primary device eSIM. Used by HTTP parameter "terminal_eid" if set. + * + * <p>Used by primary device ODSA operation. + */ + public abstract Builder setTerminalIccid(String value); + + /** + * Sets the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter + * "terminal_eid" if set. + * + * <p>Used by primary device ODSA operation. + */ + public abstract Builder setTerminalEid(String value); + + /** + * Sets the unique identifier of the primary device eSIM, like the IMEI associated with the + * eSIM. Used by HTTP parameter "target_terminal_id" if set. + * + * <p>Used by primary device ODSA operation. + */ + public abstract Builder setTargetTerminalId(String value); + + /** + * Sets the ICCID primary device eSIM. Used by HTTP parameter "target_terminal_iccid" if + * set. + * + * <p>Used by primary device ODSA operation. + */ + public abstract Builder setTargetTerminalIccid(String value); + + /** + * Sets the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter + * "target_terminal_eid" if set. + * + * <p>Used by primary device ODSA operation. + */ + public abstract Builder setTargetTerminalEid(String value); + + public abstract EsimOdsaOperation build(); + } +} diff --git a/java/com/android/libraries/entitlement/ServiceEntitlement.java b/java/com/android/libraries/entitlement/ServiceEntitlement.java new file mode 100644 index 0000000..c196783 --- /dev/null +++ b/java/com/android/libraries/entitlement/ServiceEntitlement.java @@ -0,0 +1,144 @@ +/* + * 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; + +import android.content.Context; + +import com.android.libraries.entitlement.eapaka.EapAkaApi; + +import java.util.List; + +import androidx.annotation.Nullable; + +/** + * Implemnets protocol for carrier service entitlement configuration query and operation, based on + * GSMA TS.43 spec. + */ +public class ServiceEntitlement { + /** App ID for Voice-Over-LTE entitlement. */ + public static final String APP_VOLTE = "ap2003"; + /** App ID for Voice-Over-WiFi entitlement. */ + public static final String APP_VOWIFI = "ap2004"; + /** App ID for SMS-Over-IP entitlement. */ + public static final String APP_SMSOIP = "ap2005"; + /** App ID for on device service activation (OSDA) for companion device. */ + public static final String APP_ODSA_COMPANION = "ap2006"; + /** App ID for on device service activation (OSDA) for primary device. */ + public static final String APP_ODSA_PRIMARY = "ap2009"; + + private final Context context; + private final int simSubscriptionId; + private final CarrierConfig carrierConfig; + + /** + * Creates an instance for service entitlement configuration query and operation for the + * carrier. + * + * @param context context of application + * @param carrierConfig carrier specific configs used in the queries and operations. + * @param simSubscriptionId the subscroption ID of the carrier's SIM on device. This indicates + * which SIM to retrieve IMEI/IMSI from and perform EAP-AKA + * authentication with. See {@link + * android.telephony.SubscriptionManager} for how to get the + * subscroption ID. + */ + public ServiceEntitlement(Context context, CarrierConfig carrierConfig, int simSubscriptionId) { + this.context = context; + this.simSubscriptionId = simSubscriptionId; + this.carrierConfig = carrierConfig; + } + + /** + * Retrieves service entitlement configuration. For on device service activation (ODSA) of eSIM + * for companion/primary devices, use {@link #performEsimOdsa} instead. + * + * <p>Supported {@code appId}: {@link #APP_VOLTE}, {@link #APP_VOWIFI}, {@link #APP_SMSOIP}. + * + * <p>This method sends an HTTP GET request to entitlement server, responds to EAP-AKA + * challenge + * if needed, and returns the raw configuration doc as a string. The following parameters are + * set + * in the HTTP request: + * + * <ul> + * <li>"app": {@code appId} + * <li>"vers": 0, or {@code request.configurationVersion()} if it's not 0. + * <li>"entitlement_version": "2.0", or {@code request.entitlementVersion()} if it's not empty. + * <li>"token": not set, or {@code request.authenticationToken()} if it's not empty. + * <li>"IMSI": if "token" is set, set to {@link android.telephony.TelephonyManager#getImei}. + * <li>"EAP_ID": if "token" is not set, set this parameter to trigger embedded EAP-AKA + * authentication as decribed in TS.43 section 2.6.1. Its value is derived from IMSI as per + * GSMA spec RCC.14 section C.2. + * <li>"terminal_id": IMEI, or {@code request.terminalId()} if it's not empty. + * <li>"terminal_vendor": {@link android.os.Build#MANUFACTURER}, or {@code + * request.terminalVendor()} if it's not empty. + * <li>"terminal_model": {@link android.os.Build#MODEL}, or {@code request.terminalModel()} if + * it's not empty. + * <li>"terminal_sw_version": {@llink android.os.Build.VERSION#BASE_OS}, or {@code + * request.terminalSoftwareVersion()} if it's not empty. + * <li>"app_name": not set, or {@code request.appName()} if it's not empty. + * <li>"app_version": not set, or {@code request.appVersion()} if it's not empty. + * <li>"notif_token": not set, or {@code request.notificationToken()} if it's not empty. + * <li>"notif_action": {@code request.notificationAction()} if "notif_token" is set, otherwise + * not set. + * </ul> + * + * <p>Requires permission: READ_PRIVILEGED_PHONE_STATE, or carrier privilege. + * + * @param appId an app ID string defined in TS.43 section 2.2, e.g. {@link #APP_VOWIFI}. + * @param request contains parameters that can be used in the HTTP request. + */ + @Nullable + public String queryEntitlementStatus(String appId, ServiceEntitlementRequest request) + throws ServiceEntitlementException { + EapAkaApi eapAkaApi = new EapAkaApi(context, simSubscriptionId); + return eapAkaApi.queryEntitlementStatus(appId, carrierConfig.serverUrl(), request); + } + + /** + * Retrieves service entitlement configurations for multiple app IDs in one HTTP + * request/response. + * For on device service activation (ODSA) of eSIM for companion/primary devices, use {@link + * #performEsimOdsa} instead. + * + * <p>Same with {@link #queryEntitlementStatus(String, ServiceEntitlementRequest)} except that + * multiple "app" parameters will be set in the HTTP request, in the order as they appear in + * parameter {@code appIds}. + */ + public String queryEntitlementStatus(List<String> appIds, ServiceEntitlementRequest request) + throws ServiceEntitlementException { + // TODO(b/177544547): Add implementation + return null; + } + + /** + * Performs on device service activation (ODSA) of eSIM for companion/primary devices. + * + * <p>Supported {@code appId}: {@link #APP_ODSA_COMPANION}, {@link #APP_ODSA_PRIMARY}. + * + * <p>Similar to {@link #queryEntitlementStatus(String, ServiceEntitlementRequest)}, this method + * sends an HTTP GET request to entitlement server, responds to EAP-AKA challenge if needed, and + * returns the raw configuration doc as a string. Additional parameters from {@code operation} + * are set to the HTTP request. See {@link EsimOdsaOperation} for details. + */ + public String performEsimOdsa( + String appId, ServiceEntitlementRequest request, EsimOdsaOperation operation) + throws ServiceEntitlementException { + // TODO(b/177544547): Add implementation + return null; + } +} diff --git a/java/com/android/libraries/entitlement/ServiceEntitlementException.java b/java/com/android/libraries/entitlement/ServiceEntitlementException.java new file mode 100644 index 0000000..499a032 --- /dev/null +++ b/java/com/android/libraries/entitlement/ServiceEntitlementException.java @@ -0,0 +1,77 @@ +/* + * 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; + +/** Indicates errors happened in retrieving service entitlement configuration. */ +public class ServiceEntitlementException extends Exception { + /** Unknown error. */ + public static final int ERROR_UNKNOWN = 0; + /** Android telephony is unable to provide info like IMSI, e.g. when modem crashed. */ + public static final int ERROR_PHONE_NOT_AVAILABLE = 1; + /** + * SIM not returning a response to the EAP-AKA challenge, e.g. when the challenge is invalid. + * This + * can happen only when an embedded EAP-AKA challange is conducted, as per GMSA spec TS.43 + * section + * 2.6.1. + */ + public static final int ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE = 2; + /** + * Cannot connect to the entitlment server, e.g. due to weak mobile network and Wi-Fi + * connection. + */ + public static final int ERROR_SEVER_NOT_CONNECTABLE = 3; + /** + * HTTP response received with a status code indicating failure, e.g. 4xx and 5xx. Use {@link + * #getHttpStatus} to get the status code and {@link #getMessage} the error message in the + * response body. + */ + public static final int ERROR_HTTP_STATUS_NOT_SUCCESS = 4; + + public ServiceEntitlementException(String message) { + // TODO(b/177544547): add implementation + } + + public ServiceEntitlementException( + int error, int httpStatus, String retryAfter, String message, Throwable cause) { + // TODO(b/177544547): add implementation + } + + /** Returns the error code, see {@link #ERROR_*}. */ + public int getErrorCode() { + // TODO(b/177544547): add implementation + return ERROR_UNKNOWN; + } + + /** Returns the HTTP status code returned by entitlement server; 0 if unavailable. */ + public int getHttpStatus() { + // TODO(b/177544547): add implementation + return ERROR_SEVER_NOT_CONNECTABLE; + } + + /** + * Returns the "Retry-After" header in HTTP response, often set with HTTP status code 503; an + * empty string if unavailable. + * + * @return the HTTP-date or a number of seconds to delay, as defiend in RFC 7231: + * https://tools.ietf.org/html/rfc7231#section-7.1.3 + */ + public String getRetryAfter() { + // TODO(b/177544547): add implementation + return null; + } +} diff --git a/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java new file mode 100644 index 0000000..6bbfee6 --- /dev/null +++ b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java @@ -0,0 +1,199 @@ +/* + * 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; + +import android.os.Build; +import android.os.Build.VERSION; + +import com.google.auto.value.AutoValue; + +/** Service entitlement HTTP request parameters, as defiend in GSMA spec TS.43 section 2.2. */ +@AutoValue +public abstract class ServiceEntitlementRequest { + /** Disables notification token. */ + public static final int NOTICATION_ACTION_DISABLE = 0; + /** Enables FCM notification token. */ + public static final int NOTICATION_ACTION_ENABLE_FCM = 2; + + /** + * Returns the version of configuration currently stored on the client. Used by HTTP parameter + * "vers". + */ + public abstract int configurationVersion(); + + /** + * Returns the version of the entitlement specification. Used by HTTP parameter + * "entitlement_version". + */ + public abstract String entitlementVersion(); + + /** Returns the authentication token. Used by HTTP parameter "token". */ + public abstract String authenticationToken(); + + /** + * Returns the unique identifier of the device like IMEI. Used by HTTP parameter "terminal_id". + */ + public abstract String terminalId(); + + /** Returns the OEM of the device. Used by HTTP parameter "terminal_vendor". */ + public abstract String terminalVendor(); + + /** Returns the model of the device. Used by HTTP parameter "terminal_model". */ + public abstract String terminalModel(); + + /** Returns the software version of the device. Used by HTTP parameter "terminal_sw_version". */ + public abstract String terminalSoftwareVersion(); + + /** + * Returns the name of the device application making the request. Used by HTTP parameter + * "app_name". + */ + public abstract String appName(); + + /** + * Returns the version of the device application making the request. Used by HTTP parameter + * "app_version". + */ + public abstract String appVersion(); + + /** + * Returns the FCM registration token used to register for entitlement configuration request + * from + * network. Used by HTTP parameter "notif_token". + */ + public abstract String notificationToken(); + + /** + * Returns the action associated with the FCM registration token. Used by HTTP parameter + * "notif_action". + * + * @see #NOTICATION_ACTION_ENABLE_FCM + * @see #NOTICATION_ACTION_DISABLE + */ + public abstract int notificationAction(); + + /** Returns a new {@link Builder} object. */ + public static Builder builder() { + return new AutoValue_ServiceEntitlementRequest.Builder() + .setConfigurationVersion(0) + .setEntitlementVersion("2.0") + .setAuthenticationToken("") + .setTerminalId("") + .setTerminalVendor(Build.MANUFACTURER) + .setTerminalModel(Build.MODEL) + .setTerminalSoftwareVersion(VERSION.BASE_OS) + .setAppName("") + .setAppVersion("") + .setNotificationToken("") + .setNotificationAction(NOTICATION_ACTION_ENABLE_FCM); + } + + /** Builder. */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Sets the version of configuration currently stored on the client. Used by HTTP parameter + * "vers". + * + * <p>If not set, default to 0 indicating no existing configuration. + */ + public abstract Builder setConfigurationVersion(int value); + + /** + * Sets the current version of the entitlement specification. Used by HTTP parameter + * "entitlement_version". + * + * <p>If not set, default to "2.0" base on TS.43-v5.0. + */ + public abstract Builder setEntitlementVersion(String value); + + /** + * Sets the authentication token. Used by HTTP parameter "token". + * + * <p>If not set, will trigger embedded EAP-AKA authentication as decribed in TS.43 section + * 2.6.1. + */ + public abstract Builder setAuthenticationToken(String value); + + /** + * Sets the unique identifier of the device like IMEI. Used by HTTP parameter "terminal_id". + * + * <p>If not set, will use the device IMEI. + */ + public abstract Builder setTerminalId(String value); + + /** + * Sets the OEM of the device. Used by HTTP parameter "terminal_vendor". + * + * <p>If not set, will use {@link android.os.Build#MANUFACTURER}. + */ + public abstract Builder setTerminalVendor(String value); + + /** + * Sets the model of the device. Used by HTTP parameter "terminal_model". + * + * <p>If not set, will use {@link android.os.Build#MODEL}. + */ + public abstract Builder setTerminalModel(String value); + + /** + * Sets the software version of the device. Used by HTTP parameter "terminal_sw_version". + * + * <p>If not set, will use {@link android.os.Build.VERSION#BASE_OS}. + */ + public abstract Builder setTerminalSoftwareVersion(String value); + + /** + * Sets the name of the device application making the request. Used by HTTP parameter + * "app_name". + * + * <p>Optional. + */ + public abstract Builder setAppName(String value); + + /** + * Sets the version of the device application making the request. Used by HTTP parameter + * "app_version". + * + * <p>Optional. + */ + public abstract Builder setAppVersion(String value); + + /** + * Sets the FCM registration token used to register for entitlement configuration request + * from + * network. Used by HTTP parameter "notif_token". + * + * <p>Optional. + */ + public abstract Builder setNotificationToken(String value); + + /** + * Sets the action associated with the FCM registration token. Used by HTTP parameter + * "notif_action". + * + * <p>Required if a token is set with {@link #setNotificationToken}, and default to {@link + * #NOTICATION_ACTION_ENABLE_FCM}; otherwise ignored. + * + * @see #NOTICATION_ACTION_ENABLE_FCM + * @see #NOTICATION_ACTION_DISABLE + */ + public abstract Builder setNotificationAction(int value); + + public abstract ServiceEntitlementRequest build(); + } +} diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java new file mode 100644 index 0000000..898b782 --- /dev/null +++ b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java @@ -0,0 +1,236 @@ +/* + * 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 android.content.Context; +import android.net.Uri; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +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.ContentType; +import com.android.libraries.entitlement.http.HttpConstants.RequestMethod; +import com.android.libraries.entitlement.http.HttpRequest; +import com.android.libraries.entitlement.http.HttpResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +import androidx.annotation.Nullable; + +import com.google.common.net.HttpHeaders; + +import java.net.CookieHandler; +import java.net.CookieManager; + +public class EapAkaApi { + private static final String TAG = "ServiceEntitlement"; + + public static final String EAP_CHALLENGE_RESPONSE = "eap-relay-packet"; + + /** Current version of the entitlement configuration. */ + private static final String VERS = "vers"; + /** Version of the entitlement configuration. */ + private static final String ENTITLEMENT_VERSION = "entitlement_version"; + /** + * Unique identifier for the device. Refer to {@link + * android.telephony.TelephonyManager#getImei()}. + */ + private static final String TERMINAL_ID = "terminal_id"; + /** Device manufacturer. */ + private static final String TERMINAL_VENDOR = "terminal_vendor"; + /** Device model. */ + private static final String TERMINAL_MODEL = "terminal_model"; + /** Device software version. */ + private static final String TERMIAL_SW_VERSION = "terminal_sw_version"; + /** Identifier for the requested entitlement. */ + private static final String APP = "app"; + /** NAI needed for EAP-AKA authentication. */ + private static final String EAP_ID = "EAP_ID"; + + private static final String IMSI = "IMSI"; + private static final String TOKEN = "token"; + /** Action for the notification registration token. */ + private static final String NOTIF_ACTION = "notif_action"; + /** Attribute name of the notification registration token. */ + private static final String NOTIF_TOKEN = "notif_token"; + /** Attribute name of the app version. */ + private static final String APP_VERSION = "app_version"; + /** Attribute name of the app name. */ + private static final String APP_NAME = "app_name"; + + private final Context context; + private final int simSubscriptionId; + private final HttpClient httpClient; + + public EapAkaApi(Context context, int simSubscriptionId) { + this.context = context; + this.simSubscriptionId = simSubscriptionId; + this.httpClient = new 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( + String appId, String serverUrl, ServiceEntitlementRequest request) + throws ServiceEntitlementException { + // TODO(b/177562073): localize cookie management instead of VM global CookieHandler + CookieHandler.setDefault(new CookieManager()); + + HttpRequest httpRequest = + HttpRequest.builder() + .setUrl(entitlementStatusUrl(appId, serverUrl, request)) + .setRequestMethod(RequestMethod.GET) + .addRequestProperty( + HttpHeaders.ACCEPT, + "application/vnd.gsma.eap-relay.v1.0+json, text/vnd.wap" + + ".connectivity-xml") + .build(); + HttpResponse response = httpClient.request(httpRequest); + if (response == null) { + throw new ServiceEntitlementException("Null http response"); + } + if (response.contentType() == ContentType.JSON) { + try { + // EapAka token challenge for initial AuthN + Log.d(TAG, "initial AuthN"); + String akaChallengeResponse = + new EapAkaResponse( + new JSONObject(response.body()).getString(EAP_CHALLENGE_RESPONSE)) + .getEapAkaChallengeResponse(context, simSubscriptionId); + JSONObject postData = new JSONObject(); + postData.put(EAP_CHALLENGE_RESPONSE, akaChallengeResponse); + return challengeResponse(postData, serverUrl); + } catch (JSONException jsonException) { + Log.e(TAG, "queryEntitlementStatus failed. jsonException: " + jsonException); + return null; + } + } else if (response.contentType() == ContentType.XML) { + // Result of fast AuthN + Log.d(TAG, "fast AuthN"); + return response.body(); + } + throw new ServiceEntitlementException("Unexpected http ContentType"); + } + + private String challengeResponse(JSONObject postData, String serverUrl) + throws ServiceEntitlementException { + Log.d(TAG, "challengeResponse"); + HttpRequest request = + HttpRequest.builder() + .setUrl(serverUrl) + .setRequestMethod(RequestMethod.POST) + .setPostData(postData) + .addRequestProperty( + HttpHeaders.ACCEPT, + "application/vnd.gsma.eap-relay.v1.0+json, text/vnd.wap" + + ".connectivity-xml") + .addRequestProperty(HttpHeaders.CONTENT_TYPE, + "application/vnd.gsma.eap-relay.v1.0+json") + .build(); + + HttpResponse response = httpClient.request(request); + if (response == null || response.contentType() != ContentType.XML) { + throw new ServiceEntitlementException("Unexpected http response."); + } + + return response.body(); + } + + private String entitlementStatusUrl( + String appId, String serverUrl, ServiceEntitlementRequest request) { + TelephonyManager telephonyManager = + context.getSystemService(TelephonyManager.class).createForSubscriptionId( + simSubscriptionId); + Uri.Builder urlBuilder = Uri.parse(serverUrl).buildUpon(); + 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 + if (!TextUtils.isEmpty(request.appVersion())) { + urlBuilder.appendQueryParameter(APP_VERSION, request.appVersion()); + } + + if (!TextUtils.isEmpty(request.appName())) { + urlBuilder.appendQueryParameter(APP_NAME, request.appName()); + } + + return 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(APP, appId) + .appendQueryParameter(VERS, Integer.toString(request.configurationVersion())) + .appendQueryParameter(ENTITLEMENT_VERSION, request.entitlementVersion()) + .toString(); + } + + /** + * 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 + 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"; + } +} diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java b/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java new file mode 100644 index 0000000..5021317 --- /dev/null +++ b/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java @@ -0,0 +1,370 @@ +/* + * 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 android.content.Context; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import com.android.libraries.entitlement.ServiceEntitlementException; +import com.android.libraries.entitlement.eapaka.utils.BytesConverter; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import androidx.annotation.Nullable; + +/** Generate the response of EAP-AKA token challenge for initial AUTN. */ +class EapAkaResponse { + private static final String TAG = "ServiceEntitlement"; + + private static final int EAP_AKA_HEADER_LENGTH = 8; + private static final byte CODE_REQUEST = 0x01; + private static final byte CODE_RESPONSE = 0x02; + private static final byte TYPE_EAP_AKA = 0x17; + private static final byte SUBTYPE_AKA_CHALLENGE = 0x01; + private static final byte ATTRIBUTE_RAND = 0x01; + private static final byte ATTRIBUTE_AUTN = 0x02; + private static final byte ATTRIBUTE_RES = 0x03; + private static final byte ATTRIBUTE_MAC = 0x0B; + private static final String ALGORITHM_HMAC_SHA1 = "HmacSHA1"; + private static final int RAND_LENGTH = 20; + private static final int AUTN_LENGTH = 20; + private static final int SHA1_OUTPUT_LENGTH = 20; + + /** RAND length 16. */ + private static final byte RAND_LEN = 0x10; + /** AUTN length 16. */ + private static final byte AUTN_LEN = 0x10; + + /* 1 for Request, 2 for Response*/ + private byte code = -1; + /* The identifier of Response must same as Request */ + private byte identifier = -1; + /* The total length of full EAP-AKA message, include code, identifier, ... */ + private int length = -1; + /* In EAP-AKA, the Type field is set to 23 */ + private byte type = -1; + /* SubType for AKA-Challenge should be 1 */ + private byte subType = -1; + /* The value of AT_AUTN, network authentication token */ + private byte[] autn; + /* The value of AT_RAND, RAND random number*/ + private byte[] rand; + + private boolean valid; + + public EapAkaResponse(String eapAkaChallenge) { + try { + parseEapAkaChallengeRequest(eapAkaChallenge); + } catch (Exception e) { + Log.e(TAG, "parseEapAkaChallengeRequest Exception:", e); + valid = false; + } + } + + /** Refer to RFC 4187 Section 8.1 Message Format/RFC 3748 Session 4 EAP Packet Format. */ + private void parseEapAkaChallengeRequest(String request) { + if (TextUtils.isEmpty(request)) { + return; + } + + try { + byte[] data = Base64.decode(request, Base64.DEFAULT); + if (parseEapAkaHeader(data) && parseRandAndAutn(data)) { + valid = true; + } else { + Log.d(TAG, "Invalid data!"); + } + } catch (IllegalArgumentException illegalArgumentException) { + Log.e(TAG, "Invalid base-64 content"); + } + } + + /** + * Parse EAP-AKA header, 8 bytes include 2 reserved bytes. + * + * @param data raw bytes of request data. + * @return {@code true} if success to parse the header of request data. + */ + private boolean parseEapAkaHeader(byte[] data) { + if (data.length <= EAP_AKA_HEADER_LENGTH) { + return false; + } + code = data[0]; + identifier = data[1]; + length = ((data[2] & 0xff) << 8) | (data[3] & 0xff); + type = data[4]; + subType = data[5]; + + // valid header + if (code != CODE_REQUEST + || length != data.length + || type != TYPE_EAP_AKA + || subType != SUBTYPE_AKA_CHALLENGE) { + Log.d( + TAG, + "Invalid EAP-AKA Header, code=" + + code + + ", length=" + + length + + ", real length=" + + data.length + + ", type=" + + type + + ", subType=" + + subType); + return false; + } + + return true; + } + + /** + * Refer to RFC 4187 section 10.6 AT_RAND/RFC 4187 section 10.7 AT_AUTN. + * + * @param data raw bytes of request data. + * @return {@code true} if success to parse the RAND and AUTN data. + */ + private boolean parseRandAndAutn(byte[] data) { + int index = EAP_AKA_HEADER_LENGTH; + while (index < data.length) { + int remainsLength = data.length - index; + if (remainsLength <= 2) { + Log.d(TAG, "Error! remainsLength = " + remainsLength); + return false; + } + + byte attributeType = data[index]; + + // the length of this attribute in multiples of 4 bytes, include attribute type and + // length + int length = (data[index + 1] & 0xff) * 4; + if (length > remainsLength) { + Log.d(TAG, + "Length Error! length is " + length + " but only remains " + remainsLength); + return false; + } + + // see RFC 4187 section 11 for attribute type + if (attributeType == ATTRIBUTE_RAND) { + if (length != RAND_LENGTH) { + Log.d(TAG, "AT_RAND length is " + length); + return false; + } + rand = new byte[16]; + System.arraycopy(data, index + 4, rand, 0, 16); + } else if (attributeType == ATTRIBUTE_AUTN) { + if (length != AUTN_LENGTH) { + Log.d(TAG, "AT_AUTN length is " + length); + return false; + } + autn = new byte[16]; + System.arraycopy(data, index + 4, autn, 0, 16); + } + + index += length; + } // while + + // check has AT_RAND and AT_AUTH + if (rand == null || autn == null) { + Log.d(TAG, "Invalid Type Datas!"); + return false; + } + + return true; + } + + /** + * Returns EAP-AKA challenge response message which generated with SIM EAP-AKA authentication + * with + * network provided EAP-AKA challenge request message. + */ + public String getEapAkaChallengeResponse(Context context, int simSubscriptionId) + throws ServiceEntitlementException { + if (!valid) { + throw new ServiceEntitlementException("EAP-AKA Challenge message not valid!"); + } + + TelephonyManager telephonyManager = + context.getSystemService(TelephonyManager.class).createForSubscriptionId( + simSubscriptionId); + + // process EAP-AKA authentication with SIM + String response = + telephonyManager.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + getSimAuthChallengeData()); + + EapAkaSecurityContext securityContext = EapAkaSecurityContext.from(response); + // RFC 4187, section 7. Key Generation + // generate master key + MasterKey mk = + MasterKey.create( + EapAkaApi.getImsiEap(telephonyManager.getSubscriberId(), + telephonyManager.getSimOperator()), + securityContext.getIk(), + securityContext.getCk()); + // K_aut is the key used to calculate MAC + if (mk.getAut() == null) { + throw new ServiceEntitlementException("Can't generate K_Aut!"); + } + + // generate EAP-AKA Challenge Response message + byte[] challengeResponse = + generateEapAkaChallengeResponse(securityContext.getRes(), mk.getAut()); + if (challengeResponse == null) { + throw new ServiceEntitlementException( + "Failed to generate EAP-AKA Challenge Response data!"); + } + + return Base64.encodeToString(challengeResponse, Base64.NO_WRAP).trim(); + } + + /** Returns Base64 encoded GSM/3G security context for SIM Authentication request. */ + @Nullable + private String getSimAuthChallengeData() { + if (!valid) { + return null; + } + + byte[] challengeData = new byte[RAND_LEN + AUTN_LEN + 2]; + challengeData[0] = RAND_LEN; + System.arraycopy(rand, 0, challengeData, 1, RAND_LEN); + challengeData[RAND_LEN + 1] = AUTN_LEN; + System.arraycopy(autn, 0, challengeData, RAND_LEN + 2, AUTN_LEN); + + return Base64.encodeToString(challengeData, Base64.NO_WRAP).trim(); + } + + /** Returns EAP-AKA Challenge response message byte array data or null if failed to generate. */ + @Nullable + public byte[] generateEapAkaChallengeResponse(@Nullable byte[] res, byte[] aut) { + if (res == null || aut == null) { + return null; + } + + byte[] message = createEapAkaChallengeResponse(res); + + // use K_aut as key to calculate mac + byte[] mac = calculateMac(aut, message); + if (mac == null) { + return null; + } + + // fill MAC value to the message + // The value start index is 8 + AT_RES (4 + res.length) + header of AT_MAC (4) + int index = 8 + 4 + res.length + 4; + System.arraycopy(mac, 0, message, index, mac.length); + + return message; + } + + // AT_MAC/AT_RES are must included in response message + // + // Reference RFC 4187 Section 8.1 Message Format + // RFC 4187 Section 9.4 EAP-Response/AKA-Challenge + // RFC 3748, Section 4.1. Request and Response + private byte[] createEapAkaChallengeResponse(byte[] res) { + // size = 8 (header) + resHeader (4) + res.length + AT_MAC (20 bytes) + byte[] message = new byte[32 + res.length]; + + // set up header + message[0] = CODE_RESPONSE; + // Identifier need to same with request + message[1] = identifier; + // length include entire EAP-AKA message + byte[] lengthBytes = BytesConverter.convertIntegerTo4Bytes(message.length); + message[2] = lengthBytes[2]; + message[3] = lengthBytes[3]; + message[4] = TYPE_EAP_AKA; + message[5] = SUBTYPE_AKA_CHALLENGE; + // Reserved 2 bytes + message[6] = 0x00; + message[7] = 0x00; + + int index = 8; + + // set up AT_RES, RFC 4187, Section 10.8 AT_RES + message[index++] = ATTRIBUTE_RES; + // The length of the AT_RES attribute must be a multiple of 4 bytes which identifies the + // exact length of the RES in bits. To pad 4 onto the length to ensure the reserved buffer + // size large enough after convert to byte count. + int resLength = (res.length + 4) / 4; + message[index++] = (byte) (resLength & 0xff); + // The value field of this attribute begins with the 2-byte RES Length, which identifies + // the exact length of the RES in bits. + byte[] resBitLength = BytesConverter.convertIntegerTo4Bytes(res.length * 8); + message[index++] = resBitLength[2]; + message[index++] = resBitLength[3]; + System.arraycopy(res, 0, message, index, res.length); + index += res.length; + + // set up AT_MAC, RFC 4187, 10.15 AT_MAC + message[index++] = ATTRIBUTE_MAC; + // fixed length, 5*4 = 20 + message[index++] = 0x05; + // With two bytes reserved + message[index++] = 0x00; + message[index++] = 0x00; + + // The MAC is calculated over the whole EAP packet and concatenated with optional + // message-specific data, with the exception that the value field of the + // MAC attribute is set to zero when calculating the MAC. + for (int i = 0; i < 16; i++) { + message[index++] = 0x00; + } + + return message; + } + + // See RFC 4187, 10.15 AT_MAC, snippet as below, the key must be k_aut + // + // The MAC algorithm is HMAC-SHA1-128 [RFC2104] keyed hash value. (The + // HMAC-SHA1-128 value is obtained from the 20-byte HMAC-SHA1 value by + // truncating the output to 16 bytes. Hence, the length of the MAC is + // 16 bytes.) The derivation of the authentication key (K_aut) used in + // the calculation of the MAC is specified in Section 7. + @Nullable + private byte[] calculateMac(byte[] key, byte[] message) { + try { + Mac mac = Mac.getInstance(ALGORITHM_HMAC_SHA1); + SecretKeySpec secret = new SecretKeySpec(key, ALGORITHM_HMAC_SHA1); + mac.init(secret); + byte[] output = mac.doFinal(message); + + if (output == null || output.length != SHA1_OUTPUT_LENGTH) { + Log.e(TAG, "Invalid result! length should be 20, but " + output.length); + return null; + } + + byte[] macValue = new byte[16]; + System.arraycopy(output, 0, macValue, 0, 16); + return macValue; + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + Log.e(TAG, "calculateMac failed!", e); + } + + return null; + } +} diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaSecurityContext.java b/java/com/android/libraries/entitlement/eapaka/EapAkaSecurityContext.java new file mode 100644 index 0000000..bb451fd --- /dev/null +++ b/java/com/android/libraries/entitlement/eapaka/EapAkaSecurityContext.java @@ -0,0 +1,151 @@ +/* + * 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 android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import com.android.libraries.entitlement.ServiceEntitlementException; + +/** + * Provide format to handle request/response SIM Authentication with GSM/3G security context. + * + * <p>Reference ETSI TS 131 102, Section 7.1.2.1 GSM/3G security context. + */ +class EapAkaSecurityContext { + private static final String TAG = "ServiceEntitlement"; + + private static final byte RESPONSE_TAG_SUCCESS = (byte) 0xDB; + + private boolean valid; + + /* Authentication result from SIM */ + private byte[] res; + /* Cipher Key */ + private byte[] ck; + /* Integrity Key */ + private byte[] ik; + + private EapAkaSecurityContext() { + } + + /** Provide {@link EapAkaSecurityContext} from response data. */ + public static EapAkaSecurityContext from(String response) + throws ServiceEntitlementException { + EapAkaSecurityContext securityContext = new EapAkaSecurityContext(); + securityContext.parseResponseData(response); + if (!securityContext.isValid()) { + throw new ServiceEntitlementException("Invalid SIM EAP-AKA authentication response!"); + } + return securityContext; + } + + /** + * Parses SIM EAP-AKA Authentication responsed data and returns valid {@link + * EapAkaSecurityContext} + * for successful data; otherwise, returns invalid. + */ + void parseResponseData(String response) { + if (TextUtils.isEmpty(response)) { + Log.d(TAG, "parseResponseData but input empty data!"); + return; + } + + try { + byte[] data = Base64.decode(response, Base64.DEFAULT); + Log.d(TAG, "decoded data length=" + data.length); + + if (data.length <= 2) { + return; + } + + int index = 0; + + // check tag + if (data[index] != RESPONSE_TAG_SUCCESS) { + Log.d(TAG, "Not successful data, tag=" + data[index]); + return; + } + + // Parse RES + index++; // move to RES length byte + res = parseTag(index, data); + if (res == null) { + Log.d(TAG, "Invalid data! can't parse RES!"); + return; + } + // Parse CK + index += res.length + 1; // move to CK length byte + ck = parseTag(index, data); + if (ck == null) { + Log.d(TAG, "Invalid data! can't parse CK!"); + return; + } + // Parse IK + index += ck.length + 1; // move to IK length byte + ik = parseTag(index, data); + if (ik == null) { + Log.d(TAG, "Invalid data! can't parse IK!"); + return; + } + + valid = true; + } catch (IllegalArgumentException illegalArgumentException) { + Log.e(TAG, "Invalid base-64 content"); + } + } + + + private byte[] parseTag(int index, byte[] src) { + // index at the length byte + if (index >= src.length) { + Log.d(TAG, "No length byte!"); + return null; + } + int length = src[index] & 0xff; + if (index + length >= src.length) { + Log.d(TAG, "Invalid data length!"); + return null; + } + index++; // move to first byte of tag value + byte[] dest = new byte[length]; + System.arraycopy(src, index, dest, 0, length); + + return dest; + } + + /** Returns {@code valid}. */ + boolean isValid() { + return valid; + } + + /** Returns {@code res}. */ + public byte[] getRes() { + return res; + } + + /** Returns {@code ck}. */ + public byte[] getCk() { + return ck; + } + + /** Returns {@code ik}. */ + public byte[] getIk() { + return ik; + } +} diff --git a/java/com/android/libraries/entitlement/eapaka/MasterKey.java b/java/com/android/libraries/entitlement/eapaka/MasterKey.java new file mode 100644 index 0000000..652fa65 --- /dev/null +++ b/java/com/android/libraries/entitlement/eapaka/MasterKey.java @@ -0,0 +1,391 @@ +/* + * 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 java.nio.charset.StandardCharsets.UTF_8; + +import android.text.TextUtils; +import android.util.Log; + +import com.android.libraries.entitlement.ServiceEntitlementException; +import com.android.libraries.entitlement.eapaka.utils.BytesConverter; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import androidx.annotation.Nullable; + +/** + * The class for Master Key. + * + * <p>Reference : RFC 4187, Section 7. Key Generation MK = SHA1(Identity|IK|CK) + */ +class MasterKey { + private static final String TAG = "ServiceEntitlement"; + /* K_encr (128 bits) */ + private static final int LENGTH_K_ENCR = 16; + /* K_aut (128 bits) */ + private static final int LENGTH_K_AUT = 16; + /* Master Session Key (64 bytes) */ + private static final int LENGTH_MSK = 64; + /* Extended Master Session Key (64 bytes) */ + private static final int LENGTH_EMSK = 64; + /* Transient EAP Keys : K_enrc + K_aut + MSK + EMSK */ + private static final int LENGTH_TEKS = 160; + + /* Master Key */ + private byte[] masterKey; + + /* Transient EAP Keys */ + private byte[] encr; + private byte[] aut; + private byte[] msk; + private byte[] emsk; + + private MasterKey() { + } + + /** Create the {@code masterKey}. */ + public static MasterKey create(String identity, @Nullable byte[] ik, @Nullable byte[] ck) + throws ServiceEntitlementException { + if (TextUtils.isEmpty(identity) + || ik == null + || ik.length == 0 + || ck == null + || ck.length == 0) { + Log.d(TAG, "Can't create master key due to invalid input!"); + return null; + } + MasterKey mk = new MasterKey(); + mk.from(identity, ik, ck); + return mk; + } + + void from(String identity, byte[] ik, byte[] ck) { + // concatenate Identity/IK/CK + byte[] identityBytes = identity.getBytes(UTF_8); + byte[] data = new byte[identityBytes.length + ik.length + ck.length]; + int index = 0; + System.arraycopy(identityBytes, 0, data, index, identityBytes.length); + index += identityBytes.length; + System.arraycopy(ik, 0, data, index, ik.length); + index += ik.length; + System.arraycopy(ck, 0, data, index, ck.length); + + // process SHA1 + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); + messageDigest.update(data); + masterKey = messageDigest.digest(); + } catch (NoSuchAlgorithmException e) { + Log.d(TAG, "process sHA-1 failed", e); + } + + // Generate TEKs + generateTransientEapKeys(); + } + + /** + * Generates TEKs base on RFC 4187, Section 7. Key Generation, snippet as below + * + * <p>The Master Key is fed into a Pseudo-Random number Function (PRF), which generates + * separate + * Transient EAP Keys (TEKs) for protecting EAP-AKA packets, as well as a Master Session Key + * (MSK) + * for link layer security and an Extended Master Session Key (EMSK) for other purposes. + */ + void generateTransientEapKeys() { + byte[] teks = generatePsudoRandomNumber(); + + if (teks == null || teks.length != 160) { + Log.e(TAG, "Invalid TEKs data!"); + return; + } + + int index = 0; + encr = new byte[LENGTH_K_ENCR]; + System.arraycopy(teks, index, encr, 0, LENGTH_K_ENCR); + index += LENGTH_K_ENCR; + aut = new byte[LENGTH_K_AUT]; + System.arraycopy(teks, index, aut, 0, LENGTH_K_AUT); + index += LENGTH_K_AUT; + msk = new byte[LENGTH_MSK]; + System.arraycopy(teks, index, msk, 0, LENGTH_MSK); + index += LENGTH_MSK; + emsk = new byte[LENGTH_EMSK]; + System.arraycopy(teks, index, emsk, 0, LENGTH_EMSK); + } + + /** Returns {@code aut}. */ + public byte[] getAut() { + return aut; + } + + // RFC 4187 Appendix A. Pseudo-Random Number Generator + @Nullable + private byte[] generatePsudoRandomNumber() { + // Step 1: Choose a new, secret value for the seed-key, XKEY + byte[] key = masterKey; + + // 160-bit XKEY and XVAL values are used, so b = 160. On each full + // authentication, the Master Key is used as the initial secret seed-key + // XKEY. + if (key == null || key.length != 20) { + Log.e(TAG, "Not a valid XKey!length=" + (key == null ? "null" : key.length)); + return null; + } + + // Step 2: In hexadecimal notation let + // t = 67452301 EFCDAB89 98BADCFE 10325476 C3D2E1F0 + // This is the initial value for H0|H1|H2|H3|H4 + // in the FIPS SHS [SHA-1] + int[] t = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0}; + + // Step 3: For j = 0 to m - 1 do + // 3.1. XSEED_j = 0 /* no optional user input */ + // 3.2. For i = 0 to 1 do + // a. XVAL = (XKEY + XSEED_j) mod 2^b + // b. w_i = G(t, XVAL) + // c. XKEY = (1 + XKEY + w_i) mod 2^b + // 3.3. x_j = w_0|w_1 + // Step 3: For j = 0 to m - 1 do + // + // Base on below snippet from RFC 4187, b is 160, x_j is 40 bytes, w_i is 20 bytes, TEKs + // length is 160 bytes and m is 160/40=4 + // + // 160-bit XKEY and XVAL values are used, so b = 160. On each full + // authentication, the Master Key is used as the initial secret seed-key + // XKEY. The optional user input values (XSEED_j) in step 3.1 are set + // to zero. + // On full authentication, the resulting 320-bit random numbers x_0, + // x_1, ..., x_m-1 are concatenated and partitioned into suitable-sized + // chunks and used as keys in the following order: K_encr (128 bits), + // K_aut (128 bits), Master Session Key (64 bytes), Extended Master + // Session Key (64 bytes). + byte[] teks = new byte[LENGTH_TEKS]; + int index = 0; + for (int j = 0; j < 4; j++) { + // 3.1. XSEED_j = 0, do nothing + // 3.2. For i = 0 to 1 do + for (int i = 0; i < 2; i++) { + // a. XVAL = (XKEY + XSEED_j) mod 2^b + byte[] val = key; + + // b. w_i = G(t, XVAL) + byte[] w = doFunctionG(t, val); + if (w == null || w.length != 20) { + Log.e(TAG, "Get invalid w value from G function!"); + return null; + } + // fill w to teks + System.arraycopy(w, 0, teks, index, 20); + index += 20; + + // c. XKEY = (1 + XKEY + w_i) mod 2^b + // XKEY is 20 bytes, 160 bits, mod 2^160 is for make sure XKEY just 160 bits + int carry = 1; + for (int k = 19; k >= 0; k--) { + carry += (key[k] & 0xff) + (w[k] & 0xff); + key[k] = (byte) (carry & 0xff); + // shift one byte and keep carry for next byte calculate + carry >>= 8; + } + } + // 3.3. x_j = w_0|w_1, already copy w_0/w_1 to output + } + + return teks; + } + + // See FIPS 186-2 APPENDIX 3.3. CONSTRUCTING THE FUNCTION G FROM THE SHA-1, snippet as below + // + // G(t,c) may be constructed using steps (a) - (e) in section 7 of the Specifications for the + // Secure Hash Standard. Before executing these steps, {Hj} and M1 must be initialized as + // follows: + // + // i. Initialize the {Hj} by dividing the 160 bit value t into five 32-bit segments as follows: + // t = t0 || t1 || t2 || t3 || t4 + // Then Hj = tj for j = 0 through 4. + // + // ii. There will be only one message block, M1, which is initialized as follows: + // M1 = c || 0^(512-b) + // (The first b bits of M1 contain c, and the remaining (512-b) bits are set to zero). + // + // Then steps (a) through (e) of section 7 are executed, and G(t,c) is the 160 bit string + // represented by the five words: + // H0 || H1 || H2 || H3 || H4 + // at the end of step (e). + private byte[] doFunctionG(int[] t, byte[] c) { + // i. Initialize the {Hj} by dividing the 160 bit value t into five 32-bit segments + // 5 segments and every segments is 32 bits/4 bytes + byte[][] bytesH = new byte[5][4]; + for (int i = 0; i < 5; i++) { + System.arraycopy(BytesConverter.convertIntegerTo4Bytes(t[i]), 0, bytesH[i], 0, 4); + } + + // ii. init message block, M1 + // The first b bits of M1 contain c, and the remaining (512-b) bits are set to zero + byte[] bytesM1 = new byte[64]; + System.arraycopy(c, 0, bytesM1, 0, 20); + for (int i = 20; i < 64; i++) { + bytesM1[i] = 0x00; + } + + // See FIPS PUB 180-1, Secure Hash Standard + // Section 7. COMPUTING THE MESSAGE DIGEST which defined steps (a) - (e) + + // The words of the 80-word sequence are labeled W0, W1,..., W79. + byte[][] bytesW = new byte[80][4]; + + // a. Divide Mi into 16 words W0, W1, ... , W15, where W0 is the left-most word. + for (int i = 0; i < 16; i++) { + System.arraycopy(bytesM1, i * 4, bytesW[i], 0, 4); + } + + // b. For t = 16 to 79 let Wt = S^1(Wt-3 XOR Wt-8 XOR Wt-14 XOR Wt-16). + for (int i = 16; i < 80; i++) { + bytesW[i] = + doFunctionS(1, + doXor(bytesW[i - 3], bytesW[i - 8], bytesW[i - 14], bytesW[i - 16])); + } + + // c. Let A = H0, B = H1, C = H2, D = H3, E = H4. + byte[] bytesA = new byte[4]; + byte[] bytesB = new byte[4]; + byte[] bytesC = new byte[4]; + byte[] bytesD = new byte[4]; + byte[] bytesE = new byte[4]; + System.arraycopy(bytesH[0], 0, bytesA, 0, 4); + System.arraycopy(bytesH[1], 0, bytesB, 0, 4); + System.arraycopy(bytesH[2], 0, bytesC, 0, 4); + System.arraycopy(bytesH[3], 0, bytesD, 0, 4); + System.arraycopy(bytesH[4], 0, bytesE, 0, 4); + + // d. For t = 0 to 79 do + // TEMP = S^5(A) + ft(B,C,D) + E + Wt + Kt; + // E = D; D = C; C = S^30(B); B = A; A = TEMP; + for (int i = 0; i < 80; i++) { + int tmpA = new BigInteger(doFunctionS(5, bytesA)).intValue(); + int tmpF = doFunctionF(i, bytesB, bytesC, bytesD); + int tmpE = new BigInteger(bytesE).intValue(); + int tmpW = new BigInteger(bytesW[i]).intValue(); + int tmpK = doFunctionK(i); + int temp = tmpA + tmpF + tmpE + tmpW + tmpK; + bytesE = bytesD; + bytesD = bytesC; + bytesC = doFunctionS(30, bytesB); + bytesB = bytesA; + bytesA = BytesConverter.convertIntegerTo4Bytes(temp); + } + + // e. Let H0 = H0 + A, H1 = H1 + B, H2 = H2 + C, H3 = H3 + D, H4 = H4 + E. + bytesH[0] = addTwoBytes(bytesH[0], bytesA); + bytesH[1] = addTwoBytes(bytesH[1], bytesB); + bytesH[2] = addTwoBytes(bytesH[2], bytesC); + bytesH[3] = addTwoBytes(bytesH[3], bytesD); + bytesH[4] = addTwoBytes(bytesH[4], bytesE); + + // After processing Mn, the message digest is the 160-bit string represented by the 5 words + // H0 H1 H2 H3 H4. + byte[] output = new byte[20]; + System.arraycopy(bytesH[0], 0, output, 0, 4); + System.arraycopy(bytesH[1], 0, output, 4, 4); + System.arraycopy(bytesH[2], 0, output, 8, 4); + System.arraycopy(bytesH[3], 0, output, 12, 4); + System.arraycopy(bytesH[4], 0, output, 16, 4); + + return output; + } + + private static byte[] addTwoBytes(byte[] a, byte[] b) { + BigInteger iA = new BigInteger(a); + BigInteger iB = new BigInteger(b); + return BytesConverter.convertIntegerTo4Bytes(iA.add(iB).intValue()); + } + + // See FIPS PUB 180-1, Section 3. OPERATIONS ON WORDS + // Sn(X) = (X << n) OR (X >> 32-n). + private static byte[] doFunctionS(int n, byte[] dataX) { + BigInteger leftShiftValue = new BigInteger(dataX).shiftLeft(n); + + // BigInteger.shiftRight would fill 1 if the left-most bit is 1, so use '>>>' + int value = new BigInteger(dataX).intValue(); + value = value >>> (32 - n); // X should be 32 bits + BigInteger rightShiftValue = BigInteger.valueOf(value); + BigInteger result = leftShiftValue.or(rightShiftValue); + return BytesConverter.convertIntegerTo4Bytes(result.intValue()); + } + + private static byte[] doXor(byte[] a, byte[] b, byte[] c, byte[] d) { + BigInteger iA = new BigInteger(a); + BigInteger iB = new BigInteger(b); + BigInteger iC = new BigInteger(c); + BigInteger iD = new BigInteger(d); + BigInteger result = iA.xor(iB).xor(iC).xor(iD); + return BytesConverter.convertIntegerTo4Bytes(result.intValue()); + } + + // See FIPS PUB 180-1, Section 5. FUNCTIONS USED + // A sequence of logical functions f0, f1,..., f79 is used in the SHA-1. Each ft, 0 <= t <= 79, + // operates on three 32-bit words B, C, D and produces a 32-bit word as output. ft(B,C,D) is + // defined as follows: for words B, C, D, + // + // ft(B,C,D) = (B AND C) OR ((NOT B) AND D) (0 <= t <= 19) + // ft(B,C,D) = B XOR C XOR D (20 <= t <= 39) + // ft(B,C,D) = (B AND C) OR (B AND D) OR (C AND D) (40 <= t <= 59) + // ft(B,C,D) = B XOR C XOR D (60 <= t <= 79). + private static int doFunctionF(int t, byte[] b, byte[] c, byte[] d) { + BigInteger iB = new BigInteger(b); + BigInteger iC = new BigInteger(c); + BigInteger iD = new BigInteger(d); + BigInteger result = BigInteger.valueOf(-1); + if (0 <= t && t <= 19) { + result = iB.and(iC).or(iB.not().and(iD)); + } else if (20 <= t && t <= 39) { + result = iB.xor(iC).xor(iD); + } else if (40 <= t && t <= 59) { + result = iB.and(iC).or(iB.and(iD)).or(iC.and(iD)); + } else if (60 <= t && t <= 79) { + result = iB.xor(iC).xor(iD); + } + + return result.intValue(); + } + + // See FIPS PUB 180-1, Section 6. CONSTANTS USED + // + // A sequence of constant words K(0), K(1), ... , K(79) is used in the SHA-1. In hex these are + // given by + // K = 5A827999 ( 0 <= t <= 19) + // Kt = 6ED9EBA1 (20 <= t <= 39) + // Kt = 8F1BBCDC (40 <= t <= 59) + // Kt = CA62C1D6 (60 <= t <= 79). + private static int doFunctionK(int t) { + if (0 <= t && t <= 19) { + return 0x5A827999; + } else if (20 <= t && t <= 39) { + return 0x6ED9EBA1; + } else if (40 <= t && t <= 59) { + return 0x8F1BBCDC; + } else if (60 <= t && t <= 79) { + return 0xCA62C1D6; + } + + return -1; + } +} diff --git a/java/com/android/libraries/entitlement/http/HttpClient.java b/java/com/android/libraries/entitlement/http/HttpClient.java new file mode 100644 index 0000000..79f42ff --- /dev/null +++ b/java/com/android/libraries/entitlement/http/HttpClient.java @@ -0,0 +1,170 @@ +/* + * 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.http; + +import static com.android.libraries.entitlement.http.HttpConstants.RequestMethod.POST; + +import static com.google.common.base.Strings.nullToEmpty; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.text.TextUtils; +import android.util.Log; + +import com.android.libraries.entitlement.ServiceEntitlementException; +import com.android.libraries.entitlement.http.HttpConstants.ContentType; +import com.android.libraries.entitlement.utils.StreamUtils; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.Map; + +import androidx.annotation.WorkerThread; + +/** Implement the HTTP request method according to TS.43 specification. */ +public class HttpClient { + private static final String TAG = "ServiceEntitlement"; + private static final boolean DEBUG = false; // STOPSHIP if true + + private static final int SOCKET_TIMEOUT_VALUE = (int) SECONDS.toMillis(30); + private static final int CONNECT_TIMEOUT_VALUE = (int) SECONDS.toMillis(30); + + private HttpURLConnection connection; + + @WorkerThread + // TODO(b/177544547): Add debug messages + public HttpResponse request(HttpRequest request) throws ServiceEntitlementException { + try { + logd("HttpClient.request url: " + request.url()); + createConnection(request); + if (connection == null) { + logd("HttpClient.request connection is null"); + throw new ServiceEntitlementException("No connection"); + } + logd("HttpClient.request headers (partial): " + connection.getRequestProperties()); + if (POST.equals(request.requestMethod())) { + try (OutputStream out = new DataOutputStream(connection.getOutputStream())) { + out.write(request.postData().toString().getBytes(UTF_8)); + logd("HttpClient.request post data: " + request.postData()); + } + } + connection.connect(); // This is to trigger SocketTimeoutException early + HttpResponse response = getHttpResponse(connection); + Log.d(TAG, "HttpClient.response : " + response); + return response; + } catch (IOException e) { + InputStream errorStream = connection.getErrorStream(); + Log.e( + TAG, + "HttpClient.request() error: " + StreamUtils.inputStreamToStringSafe( + errorStream)); + throw new ServiceEntitlementException("request failed! exception: " + e.getMessage()); + } finally { + closeConnection(); + } + } + + private void createConnection(HttpRequest request) throws ServiceEntitlementException { + try { + URL url = new URL(request.url()); + connection = (HttpURLConnection) url.openConnection(); + + // add HTTP headers + for (Map.Entry<String, String> entry : request.requestProperties().entrySet()) { + connection.addRequestProperty(entry.getKey(), entry.getValue()); + } + + // set parameters + connection.setRequestMethod(request.requestMethod()); + connection.setConnectTimeout(CONNECT_TIMEOUT_VALUE); + connection.setReadTimeout(SOCKET_TIMEOUT_VALUE); + if (POST.equals(request.requestMethod())) { + connection.setDoOutput(true); + } + } catch (IOException e) { + Log.e(TAG, "IOException: " + e.getMessage()); + throw new ServiceEntitlementException("Configure connection failed!" + e.getMessage()); + } + } + + private void closeConnection() { + if (connection != null) { + connection.disconnect(); + connection = null; + } + } + + private static HttpResponse getHttpResponse(HttpURLConnection connection) + throws ServiceEntitlementException { + try { + int responseCode = connection.getResponseCode(); + logd("HttpClient.response headers: " + connection.getHeaderFields()); + if (responseCode != HttpURLConnection.HTTP_OK) { + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS, responseCode, null, + "Invalid connection response", null); + } + String responseBody = readResponse(connection); + logd("HttpClient.response body: " + responseBody); + return HttpResponse.builder() + .setContentType(getContentType(connection)) + .setBody(responseBody) + .setResponseCode(responseCode) + .setResponseMessage(nullToEmpty(connection.getResponseMessage())) + .build(); + } catch (IOException e) { + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS, 0, null, + "Read response failed!", e); + } + } + + private static String readResponse(URLConnection connection) throws IOException { + try (InputStream in = connection.getInputStream()) { + return StreamUtils.inputStreamToStringSafe(in); + } + } + + private static int getContentType(URLConnection connection) { + String contentType = connection.getHeaderField(ContentType.NAME); + if (TextUtils.isEmpty(contentType)) { + return ContentType.UNKNOWN; + } + + if (contentType.contains("xml")) { + return ContentType.XML; + } else if ("text/vnd.wap.connectivity".equals(contentType)) { + // Workaround that a server vendor uses this type for XML + return ContentType.XML; + } else if (contentType.contains("json")) { + return ContentType.JSON; + } + return ContentType.UNKNOWN; + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } +} diff --git a/java/com/android/libraries/entitlement/http/HttpConstants.java b/java/com/android/libraries/entitlement/http/HttpConstants.java new file mode 100644 index 0000000..c4ed5e2 --- /dev/null +++ b/java/com/android/libraries/entitlement/http/HttpConstants.java @@ -0,0 +1,41 @@ +/* + * 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.http; + +/** Http constants using for entitlement flow of TS.43. */ +public final class HttpConstants { + private HttpConstants() {} + + /** Possible request methods for Entitlement server response. */ + public static final class RequestMethod { + private RequestMethod() {} + + public static final String GET = "GET"; + public static final String POST = "POST"; + } + + /** Possible content type for Entitlement server response. */ + public static final class ContentType { + private ContentType() {} + + public static final int UNKNOWN = -1; + public static final int JSON = 0; + public static final int XML = 1; + + public static final String NAME = "Content-Type"; + } +} diff --git a/java/com/android/libraries/entitlement/http/HttpRequest.java b/java/com/android/libraries/entitlement/http/HttpRequest.java new file mode 100644 index 0000000..af81b10 --- /dev/null +++ b/java/com/android/libraries/entitlement/http/HttpRequest.java @@ -0,0 +1,80 @@ +/* + * 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.http; + +import android.util.ArrayMap; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +import org.json.JSONObject; + +/** The parameters of the http request. */ +@AutoValue +public abstract class HttpRequest { + + public abstract String url(); + + public abstract String requestMethod(); + + public abstract JSONObject postData(); + + public abstract ImmutableMap<String, String> requestValues(); + + public abstract ImmutableMap<String, String> requestProperties(); + + /** Builder of {@link HttpRequest}. */ + @AutoValue.Builder + public abstract static class Builder { + + private final Map<String, String> values = new ArrayMap<>(); + private final Map<String, String> properties = new ArrayMap<>(); + + public abstract HttpRequest build(); + + public abstract Builder setUrl(String url); + + public abstract Builder setRequestMethod(String requestMethod); + + public abstract Builder setPostData(JSONObject postData); + + abstract Builder setRequestValues(ImmutableMap<String, String> value); + + abstract Builder setRequestProperties(ImmutableMap<String, String> properties); + + public Builder addRequestValues(String key, String value) { + values.put(key, value); + return this.setRequestValues(ImmutableMap.copyOf(values)); + } + + public Builder addRequestProperty(String key, String value) { + properties.put(key, value); + return this.setRequestProperties(ImmutableMap.copyOf(properties)); + } + } + + public static Builder builder() { + return new AutoValue_HttpRequest.Builder() + .setUrl("") + .setRequestMethod("") + .setPostData(new JSONObject()) + .setRequestValues(ImmutableMap.of()) + .setRequestProperties(ImmutableMap.of()); + } +} diff --git a/java/com/android/libraries/entitlement/http/HttpResponse.java b/java/com/android/libraries/entitlement/http/HttpResponse.java new file mode 100644 index 0000000..1cb165e --- /dev/null +++ b/java/com/android/libraries/entitlement/http/HttpResponse.java @@ -0,0 +1,74 @@ +/* + * 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.http; + +import com.android.libraries.entitlement.http.HttpConstants.ContentType; + +import com.google.auto.value.AutoValue; + +/** The response of the http request. */ +@AutoValue +public abstract class HttpResponse { + + /** Content type of the response. */ + public abstract int contentType(); + + public abstract String body(); + + public abstract int responseCode(); + + public abstract String responseMessage(); + + /** Builder of {@link HttpResponse}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract HttpResponse build(); + + public abstract Builder setContentType(int contentType); + + public abstract Builder setBody(String body); + + public abstract Builder setResponseCode(int responseCode); + + public abstract Builder setResponseMessage(String responseMessage); + } + + public static Builder builder() { + return new AutoValue_HttpResponse.Builder() + .setContentType(ContentType.UNKNOWN) + .setBody("") + .setResponseCode(0) + .setResponseMessage(""); + } + + @Override + public final String toString() { + return new StringBuilder("HttpResponse{") + .append("contentType=") + .append(contentType()) + .append(" body=(") + .append(body().length()) + .append(" characters)") + .append(" responseCode=") + .append(responseCode()) + .append(" responseMessage=") + .append(responseMessage()) + .append("}") + .toString(); + } +} diff --git a/java/com/android/libraries/entitlement/utils/BytesConverter.java b/java/com/android/libraries/entitlement/utils/BytesConverter.java new file mode 100644 index 0000000..034ac9c --- /dev/null +++ b/java/com/android/libraries/entitlement/utils/BytesConverter.java @@ -0,0 +1,60 @@ +/* + * 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.utils; + +import java.nio.ByteBuffer; + +import androidx.annotation.Nullable; + +public class BytesConverter { + private static final int INTEGER_SIZE = 4; // 4 bytes + + // A table mapping from a number to a hex character for fast encoding hex strings. + private static final char[] HEX_CHARS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + /** + * Converts a byte array into a String of hexadecimal characters. + * + * @param bytes an array of bytes + * @return hex string representation of bytes array + */ + @Nullable + public static String convertBytesToHexString(byte[] bytes) { + if (bytes == null) { + return null; + } + + StringBuilder ret = new StringBuilder(2 * bytes.length); + + for (int i = 0; i < bytes.length; i++) { + int b; + b = 0x0f & (bytes[i] >> 4); + ret.append(HEX_CHARS[b]); + b = 0x0f & bytes[i]; + ret.append(HEX_CHARS[b]); + } + + return ret.toString(); + } + + /** Converts integer to 4 bytes. */ + public static byte[] convertIntegerTo4Bytes(int value) { + return ByteBuffer.allocate(INTEGER_SIZE).putInt(value).array(); + } +} diff --git a/java/com/android/libraries/entitlement/utils/StreamUtils.java b/java/com/android/libraries/entitlement/utils/StreamUtils.java new file mode 100644 index 0000000..1ff2ffc --- /dev/null +++ b/java/com/android/libraries/entitlement/utils/StreamUtils.java @@ -0,0 +1,54 @@ +/* + * 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.utils; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** Utility methods about InputStream. */ +public final class StreamUtils { + + private static final int BUFFER_SIZE = 1024; + + private StreamUtils() { + } + + /** Reads an {@link InputStream} into a string. */ + public static String inputStreamToString(InputStream inputStream) throws IOException { + try (BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); + ByteArrayOutputStream result = new ByteArrayOutputStream()) { + byte[] buffer = new byte[BUFFER_SIZE]; + int length = 0; + while ((length = inputStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toString(StandardCharsets.UTF_8.name()); + } + } + + /** Reads an {@link InputStream} into a string. Returns an empty string if any error. */ + public static String inputStreamToStringSafe(InputStream inputStream) { + try { + return inputStreamToString(inputStream); + } catch (IOException e) { + return ""; + } + } +} diff --git a/java/com/google/android/libraries/entitlement/CarrierData.java b/java/com/google/android/libraries/entitlement/CarrierData.java deleted file mode 100644 index 2153fda..0000000 --- a/java/com/google/android/libraries/entitlement/CarrierData.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2020 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.google.android.libraries.entitlement; - -import com.google.auto.value.AutoValue; - -/** - * Carrier specific customization to be used in the service entitlement queries and operations. - * - * @see #ServiceEntitlement - */ -@AutoValue -public abstract class CarrierData { - /** - * The carrier's entitlement server URL. If not set, will use {@code - * https://aes.mnc<MNC>.mcc<MCC>.pub.3gppnetwork.org} as defined in GSMA spec TS.43 section 2.1. - */ - public abstract String serverUrl(); - - // Builder... -} diff --git a/java/com/google/android/libraries/entitlement/EsimOdsaOperation.java b/java/com/google/android/libraries/entitlement/EsimOdsaOperation.java deleted file mode 100644 index 1247824..0000000 --- a/java/com/google/android/libraries/entitlement/EsimOdsaOperation.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright (C) 2020 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.google.android.libraries.entitlement; - -import com.google.auto.value.AutoValue; - -/** - * HTTP request parameters specific to on device service actiavation (ODSA). See GSMA spec TS.43 - * section 6.2. - */ -@AutoValue -public abstract class EsimOdsaOperation { - /** OSDA operation: CheckEligibility. */ - public static final String OPERATION_CHECK_ELIGIBILITY = "CheckEligibility"; - /** OSDA operation: ManageSubscription. */ - public static final String OPERATION_MANAGE_SUBSCRIPTION = "ManageSubscription"; - /** OSDA operation: ManageService. */ - public static final String OPERATION_MANAGE_SERVICE = "ManageService"; - /** OSDA operation: AcquireConfiguration. */ - public static final String OPERATION_ACQUIRE_CONFIGURATION = "AcquireConfiguration"; - - /** Indicates that operation_type is not set. */ - static final int OPERATION_TYPE_NOT_SET = -1; - /** To activate a subscription, used by {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ - public static final int OPERATION_TYPE_SUBSCRIBE = 0; - /** To cancel a subscription, used by {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ - public static final int OPERATION_TYPE_UNSUBSCRIBE = 1; - /** To manage an existing subscription, for {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ - public static final int OPERATION_TYPE_CHANGE_SUBSCRIPTION = 2; - /** - * To transfer a subscription from an existing device, used by {@link - * #OPERATION_MANAGE_SUBSCRIPTION}. - */ - public static final int OPERATION_TYPE_TRANSFER_SUBSCRIPTION = 3; - /** - * To inform the network of a subscription update, used by {@link #OPERATION_MANAGE_SUBSCRIPTION}. - */ - public static final int OPERATION_TYPE_UPDATE_SUBSCRIPTION = 4; - /** To activate a service, used by {@link #OPERATION_MANAGE_SERVICE}. */ - public static final int OPERATION_TYPE_ACTIVATE_SERVICE = 10; - /** To deactivate a service, used by {@link #OPERATION_MANAGE_SERVICE}. */ - public static final int OPERATION_TYPE_DEACTIVATE_SERVICE = 11; - - /** Indicates the companion device carries the same MSISDN as the primary device. */ - public static final String COMPANION_SERVICE_SHAERED_NUMBER = "SharedNumber"; - /** Indicates the companion device carries a different MSISDN as the primary device. */ - public static final String COMPANION_SERVICE_DIFFERENT_NUMBER = "DiffNumber"; - - /** Returns the eSIM ODSA operation. Used by HTTP parameter "operation". */ - public abstract String operation(); - - /** - * Returns the detiled type of the eSIM ODSA operation. Used by HTTP parameter "operation_type". - */ - public abstract int operationType(); - - /** - * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter - * "companion_terminal_id". - */ - public abstract String companionTerminalId(); - - /** - * Returns the OEM of the companion device. Used by HTTP parameter "companion_terminal_vendor". - */ - public abstract String companionTerminalVendor(); - - /** - * Returns the model of the companion device. Used by HTTP parameter "companion_terminal_model". - */ - public abstract String companionTerminalModel(); - - /** - * Returns the software version of the companion device. Used by HTTP parameter - * "companion_terminal_sw_version". - */ - public abstract String companionTerminalSoftwareVersion(); - - /** - * Returns the user-friendly version of the companion device. Used by HTTP parameter - * "companion_terminal_friendly_name". - */ - public abstract String companionTerminalFriendlyName(); - - /** - * Returns the service type of the companion device, e.g. if the MSISDN is same as the primary - * device. Used by HTTP parameter "companion_terminal_service". - */ - public abstract String companionTerminalService(); - - /** - * Returns the ICCID of the companion device. Used by HTTP parameter "companion_terminal_iccid". - */ - public abstract String companionTerminalIccid(); - - /** - * Returns the ICCID of the companion device. Used by HTTP parameter "companion_terminal_iccid". - */ - public abstract String companionTerminalEid(); - - /** Returns the ICCID of the primary device eSIM. Used by HTTP parameter "terminal_eid". */ - public abstract String terminalIccid(); - - /** - * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter - * "terminal_eid". - */ - public abstract String terminalEid(); - - /** - * Returns the unique identifier of the primary device eSIM, like the IMEI associated with the - * eSIM. Used by HTTP parameter "target_terminal_id". - */ - public abstract String targetTerminalId(); - - /** Returns the ICCID primary device eSIM. Used by HTTP parameter "target_terminal_iccid". */ - public abstract String targetTerminalIccid(); - - /** - * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter - * "target_terminal_eid". - */ - public abstract String targetTerminalEid(); - - /** Returns a new {@link Builder} object. */ - public static Builder builder() { - return new AutoValue_EsimOdsaOperation.Builder().setOperationType(OPERATION_TYPE_NOT_SET); - } - - /** - * Builder. - * - * <p>For ODSA, the rule of which parameters are required varies or each operation/opeation_type. - * The Javadoc below gives high-level description, but please refer to GMSA spec TS.43 section 6.2 - * for details. - */ - @AutoValue.Builder - public abstract static class Builder { - /** - * Sets the eSIM ODSA operation. Used by HTTP parameter "operation". - * - * <p>Required. - * - * @see #OPERATION_CHECK_ELIGIBILITY - * @see #OPERATION_MANAGE_SUBSCRIPTION - * @see #OPERATION_MANAGE_SERVICE - * @see #OPERATION_ACQUIRE_CONFIGURATION - */ - public abstract Builder setOperation(String value); - - /** - * Sets the detiled type of the eSIM ODSA operation. Used by HTTP parameter "operation_type" if - * set. - * - * <p>Required by some operation. - * - * @see #OPERATION_TYPE_SUBSCRIBE - * @see #OPERATION_TYPE_UNSUBSCRIBE - * @see #OPERATION_TYPE_CHANGE_SUBSCRIPTION - * @see #OPERATION_TYPE_TRANSFER_SUBSCRIPTION - * @see #OPERATION_TYPE_UPDATE_SUBSCRIPTION - * @see #OPERATION_TYPE_ACTIVATE_SERVICE - * @see #OPERATION_TYPE_DEACTIVATE_SERVICE - */ - public abstract Builder setOperationType(int value); - - /** - * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter - * "companion_terminal_id" if set. - * - * <p>Used by companion device ODSA operation. - */ - public abstract Builder setCompanionTerminalId(String value); - - /** - * Sets the OEM of the companion device. Used by HTTP parameter "companion_terminal_vendor" if - * set. - * - * <p>Used by companion device ODSA operation. - */ - public abstract Builder setCompanionTerminalVendor(String value); - - /** - * Sets the model of the companion device. Used by HTTP parameter "companion_terminal_model" if - * set. - * - * <p>Used by companion device ODSA operation. - */ - public abstract Builder setCompanionTerminalModel(String value); - - /** - * Sets the software version of the companion device. Used by HTTP parameter - * "companion_terminal_sw_version" if set. - * - * <p>Used by companion device ODSA operation. - */ - public abstract Builder setCompanionTerminalSoftwareVersion(String value); - - /** - * Sets the user-friendly version of the companion device. Used by HTTP parameter - * "companion_terminal_friendly_name" if set. - * - * <p>Used by companion device ODSA operation. - */ - public abstract Builder setCompanionTerminalFriendlyName(String value); - - /** - * Sets the service type of the companion device, e.g. if the MSISDN is same as the primary - * device. Used by HTTP parameter "companion_terminal_service" if set. - * - * <p>Used by companion device ODSA operation. - * - * @see #COMPANION_SERVICE_SHAERED_NUMBER - * @see #COMPANION_SERVICE_DIFFERENT_NUMBER - */ - public abstract Builder setCompanionTerminalService(String value); - - /** - * Sets the ICCID of the companion device. Used by HTTP parameter "companion_terminal_iccid" if - * set. - * - * <p>Used by companion device ODSA operation. - */ - public abstract Builder setCompanionTerminalIccid(String value); - - /** - * Sets the eUICC identifier (EID) of the companion device. Used by HTTP parameter - * "companion_terminal_eid" if set. - * - * <p>Used by companion device ODSA operation. - */ - public abstract Builder setCompanionTerminalEid(String value); - - /** - * Sets the ICCID of the primary device eSIM. Used by HTTP parameter "terminal_eid" if set. - * - * <p>Used by primary device ODSA operation. - */ - public abstract Builder setTerminalIccid(String value); - - /** - * Sets the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter - * "terminal_eid" if set. - * - * <p>Used by primary device ODSA operation. - */ - public abstract Builder setTerminalEid(String value); - - /** - * Sets the unique identifier of the primary device eSIM, like the IMEI associated with the - * eSIM. Used by HTTP parameter "target_terminal_id" if set. - * - * <p>Used by primary device ODSA operation. - */ - public abstract Builder setTargetTerminalId(String value); - - /** - * Sets the ICCID primary device eSIM. Used by HTTP parameter "target_terminal_iccid" if set. - * - * <p>Used by primary device ODSA operation. - */ - public abstract Builder setTargetTerminalIccid(String value); - - /** - * Sets the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter - * "target_terminal_eid" if set. - * - * <p>Used by primary device ODSA operation. - */ - public abstract Builder setTargetTerminalEid(String value); - - public abstract EsimOdsaOperation build(); - } -} diff --git a/java/com/google/android/libraries/entitlement/ServiceEntitlement.java b/java/com/google/android/libraries/entitlement/ServiceEntitlement.java deleted file mode 100644 index ea4eec1..0000000 --- a/java/com/google/android/libraries/entitlement/ServiceEntitlement.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2020 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.google.android.libraries.entitlement; - -import java.util.List; - -/** - * Implemnets protocol for carrier service entitlement configuration query and operation, based on - * GSMA TS.43 spec. - */ -public class ServiceEntitlement { - /** App ID for Voice-Over-LTE entitlement. */ - public static final String APP_VOLTE = "ap2003"; - /** App ID for Voice-Over-WiFi entitlement. */ - public static final String APP_VOWIFI = "ap2004"; - /** App ID for SMS-Over-IP entitlement. */ - public static final String APP_SMSOIP = "ap2005"; - /** App ID for on device service activation (OSDA) for companion device. */ - public static final String APP_ODSA_COMPANION = "ap2006"; - /** App ID for on device service activation (OSDA) for primary device. */ - public static final String APP_ODSA_PRIMARY = "ap2009"; - - /** - * Creates an instance for service entitlement configuration query and operation for the carrier. - * - * @param carrierData carrier specific data used in the queries and operations. - * @param simSubscriptionId the subscroption ID of the carrier's SIM on device. This indicates - * which SIM to retrieve IMEI/IMSI from and perform EAP-AKA authentication with. See {@link - * android.telephony.SubscriptionManager} for how to get the subscroption ID. - */ - public ServiceEntitlement(CarrierData carrierData, int simSubscriptionId) {} - - /** - * Retrieves service entitlement configuration. For on device service activation (ODSA) of eSIM - * for companion/primary devices, use {@link #performEsimOdsa} instead. - * - * <p>Supported {@code appId}: {@link #APP_VOLTE}, {@link #APP_VOWIFI}, {@link #APP_SMSOIP}. - * - * <p>This method sends an HTTP GET request to entitlement server, responds to EAP-AKA challenge - * if needed, and returns the raw configuration doc as a string. The following parameters are set - * in the HTTP request: - * - * <ul> - * <li>"app": {@code appId} - * <li>"vers": 0, or {@code request.configurationVersion()} if it's not 0. - * <li>"entitlement_version": "2.0", or {@code request.entitlementVersion()} if it's not empty. - * <li>"token": not set, or {@code request.authenticationToken()} if it's not empty. - * <li>"IMSI": if "token" is set, set to {@link android.telephony.TelephonyManager#getImei}. - * <li>"EAP_ID": if "token" is not set, set this parameter to trigger embedded EAP-AKA - * authentication as decribed in TS.43 section 2.6.1. Its value is derived from IMSI as per - * GSMA spec RCC.14 section C.2. - * <li>"terminal_id": IMEI, or {@code request.terminalId()} if it's not empty. - * <li>"terminal_vendor": {@link android.os.Build#MANUFACTURER}, or {@code - * request.terminalVendor()} if it's not empty. - * <li>"terminal_model": {@link android.os.Build#MODEL}, or {@code request.terminalModel()} if - * it's not empty. - * <li>"terminal_sw_version": {@llink android.os.Build.VERSION#BASE_OS}, or {@code - * request.terminalSoftwareVersion()} if it's not empty. - * <li>"app_name": not set, or {@code request.appName()} if it's not empty. - * <li>"app_version": not set, or {@code request.appVersion()} if it's not empty. - * <li>"notif_token": not set, or {@code request.notificationToken()} if it's not empty. - * <li>"notif_action": {@code request.notificationAction()} if "notif_token" is set, otherwise - * not set. - * </ul> - * - * <p>Requires permission: READ_PRIVILEGED_PHONE_STATE, or carrier privilege. - * - * @param appId an app ID string defined in TS.43 section 2.2, e.g. {@link #APP_VOWIFI}. - * @param request contains parameters that can be used in the HTTP request. - */ - public String queryEntitlementStatus(String appId, ServiceEntitlementRequest request) - throws ServiceEntitlementException { - // TODO(samalin): Add implementation - return null; - } - - /** - * Retrieves service entitlement configurations for multiple app IDs in one HTTP request/response. - * For on device service activation (ODSA) of eSIM for companion/primary devices, use {@link - * #performEsimOdsa} instead. - * - * <p>Same with {@link #queryEntitlementStatus(String, ServiceEntitlementRequest)} except that - * multiple "app" parameters will be set in the HTTP request, in the order as they appear in - * parameter {@code appIds}. - */ - public String queryEntitlementStatus(List<String> appIds, ServiceEntitlementRequest request) - throws ServiceEntitlementException { - // TODO(samalin): Add implementation - return null; - } - - /** - * Performs on device service activation (ODSA) of eSIM for companion/primary devices. - * - * <p>Supported {@code appId}: {@link #APP_ODSA_COMPANION}, {@link #APP_ODSA_PRIMARY}. - * - * <p>Similar to {@link #queryEntitlementStatus(String, ServiceEntitlementRequest)}, this method - * sends an HTTP GET request to entitlement server, responds to EAP-AKA challenge if needed, and - * returns the raw configuration doc as a string. Additional parameters from {@code operation} - * are set to the HTTP request. See {@link EsimOdsaOperation} for details. - */ - public String performEsimOdsa( - String appId, ServiceEntitlementRequest request, EsimOdsaOperation operation) - throws ServiceEntitlementException { - // TODO(samalin): Add implementation - return null; - } -} diff --git a/java/com/google/android/libraries/entitlement/ServiceEntitlementException.java b/java/com/google/android/libraries/entitlement/ServiceEntitlementException.java deleted file mode 100644 index b632e2f..0000000 --- a/java/com/google/android/libraries/entitlement/ServiceEntitlementException.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2020 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.google.android.libraries.entitlement; - -/** Indicates errors happened in retrieving service entitlement configuration. */ -public class ServiceEntitlementException extends Exception { - /** Unknown error. */ - public static final int ERROR_UNKNOWN = 0; - /** Android telephony is unable to provide info like IMSI, e.g. when modem crashed. */ - public static final int ERROR_PHONE_NOT_AVAILABLE = 1; - /** - * SIM not returning a response to the EAP-AKA challenge, e.g. when the challenge is invalid. This - * can happen only when an embedded EAP-AKA challange is conducted, as per GMSA spec TS.43 section - * 2.6.1. - */ - public static final int ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE = 2; - /** - * Cannot connect to the entitlment server, e.g. due to weak mobile network and Wi-Fi connection. - */ - public static final int ERROR_SEVER_NOT_CONNECTABLE = 3; - /** - * HTTP response received with a status code indicating failure, e.g. 4xx and 5xx. Use {@link - * #getHttpStatus} to get the status code and {@link #getMessage} the error message in the - * response body. - */ - public static final int ERROR_HTTP_STATUS_NOT_SUCCESS = 4; - - public ServiceEntitlementException( - int error, int httpStatus, String retryAfter, String message, Throwable cause) {} - - /** Returns the error code, see {@link #ERROR_*}. */ - public int getErrorCode() { - // TODO(samalin): add implementation - return ERROR_UNKNOWN; - } - /** Returns the HTTP status code returned by entitlement server; 0 if unavailable. */ - public int getHttpStatus() { - // TODO(samalin): add implementation - return ERROR_SEVER_NOT_CONNECTABLE; - } - /** - * Returns the "Retry-After" header in HTTP response, often set with HTTP status code 503; an - * empty string if unavailable. - * - * @return the HTTP-date or a number of seconds to delay, as defiend in RFC 7231: - * https://tools.ietf.org/html/rfc7231#section-7.1.3 - */ - public String getRetryAfter() { - // TODO(samalin): add implementation - return null; - } -} diff --git a/java/com/google/android/libraries/entitlement/ServiceEntitlementRequest.java b/java/com/google/android/libraries/entitlement/ServiceEntitlementRequest.java deleted file mode 100644 index 032579c..0000000 --- a/java/com/google/android/libraries/entitlement/ServiceEntitlementRequest.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2020 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.google.android.libraries.entitlement; - -import com.google.auto.value.AutoValue; - -/** Service entitlement HTTP request parameters, as defiend in GSMA spec TS.43 section 2.2. */ -@AutoValue -public abstract class ServiceEntitlementRequest { - /** Disables notification token. */ - public static final int NOTICATION_ACTION_DISABLE = 0; - /** Enables FCM notification token. */ - public static final int NOTICATION_ACTION_ENABLE_FCM = 2; - - /** - * Returns the version of configuration currently stored on the client. Used by HTTP parameter - * "vers". - */ - public abstract int configurationVersion(); - - /** - * Returns the version of the entitlement specification. Used by HTTP parameter - * "entitlement_version". - */ - public abstract String entitlementVersion(); - - /** Returns the authentication token. Used by HTTP parameter "token". */ - public abstract String authenticationToken(); - - /** - * Returns the unique identifier of the device like IMEI. Used by HTTP parameter "terminal_id". - */ - public abstract String terminalId(); - - /** Returns the OEM of the device. Used by HTTP parameter "terminal_vendor". */ - public abstract String terminalVendor(); - - /** Returns the model of the device. Used by HTTP parameter "terminal_model". */ - public abstract String terminalModel(); - - /** Returns the software version of the device. Used by HTTP parameter "terminal_sw_version". */ - public abstract String terminalSoftwareVersion(); - - /** - * Returns the name of the device application making the request. Used by HTTP parameter - * "app_name". - */ - public abstract String appName(); - - /** - * Returns the version of the device application making the request. Used by HTTP parameter - * "app_version". - */ - public abstract String appVersion(); - - /** - * Returns the FCM registration token used to register for entitlement configuration request from - * network. Used by HTTP parameter "notif_token". - */ - public abstract String notificationToken(); - - /** - * Returns the action associated with the FCM registration token. Used by HTTP parameter - * "notif_action". - * - * @see #NOTICATION_ACTION_ENABLE_FCM - * @see #NOTICATION_ACTION_DISABLE - */ - public abstract int notificationAction(); - - /** Returns a new {@link Builder} object. */ - public static Builder builder() { - return new AutoValue_ServiceEntitlementRequest.Builder() - .setConfigurationVersion(0) - .setEntitlementVersion("2.0") - .setNotificationAction(NOTICATION_ACTION_ENABLE_FCM); - } - - /** Builder. */ - @AutoValue.Builder - public abstract static class Builder { - /** - * Sets the version of configuration currently stored on the client. Used by HTTP parameter - * "vers". - * - * <p>If not set, default to 0 indicating no existing configuration. - */ - public abstract Builder setConfigurationVersion(int value); - /** - * Sets the version of configuration currently stored on the client. Used by HTTP parameter - * "vers". - * - * <p>If not set, default to "2.0". - */ - public abstract Builder setEntitlementVersion(String value); - /** - * Sets the authentication token. Used by HTTP parameter "token". - * - * <p>If not set, will trigger embedded EAP-AKA authentication as decribed in TS.43 section - * 2.6.1. - */ - public abstract Builder setAuthenticationToken(String value); - /** - * Sets the unique identifier of the device like IMEI. Used by HTTP parameter "terminal_id". - * - * <p>If not set, will use the device IMEI. - */ - public abstract Builder setTerminalId(String value); - /** - * Sets the OEM of the device. Used by HTTP parameter "terminal_vendor". - * - * <p>If not set, will use {@link android.os.Build#MANUFACTURER}. - */ - public abstract Builder setTerminalVendor(String value); - /** - * Sets the model of the device. Used by HTTP parameter "terminal_model". - * - * <p>If not set, will use {@link android.os.Build#MODEL}. - */ - public abstract Builder setTerminalModel(String value); - /** - * Sets the software version of the device. Used by HTTP parameter "terminal_sw_version". - * - * <p>If not set, will use {@link android.os.Build.VERSION#BASE_OS}. - */ - public abstract Builder setTerminalSoftwareVersion(String value); - /** - * Sets the name of the device application making the request. Used by HTTP parameter - * "app_name". - * - * <p>Optional. - */ - public abstract Builder setAppName(String value); - /** - * Sets the version of the device application making the request. Used by HTTP parameter - * "app_version". - * - * <p>Optional. - */ - public abstract Builder setAppVersion(String value); - /** - * Sets the FCM registration token used to register for entitlement configuration request from - * network. Used by HTTP parameter "notif_token". - * - * <p>Optional. - */ - public abstract Builder setNotificationToken(String value); - /** - * Sets the action associated with the FCM registration token. Used by HTTP parameter - * "notif_action". - * - * <p>Required if a token is set with {@link #setNotificationToken}, and default to {@link - * #NOTICATION_ACTION_ENABLE_FCM}; otherwise ignored. - * - * @see #NOTICATION_ACTION_ENABLE_FCM - * @see #NOTICATION_ACTION_DISABLE - */ - public abstract Builder setNotificationAction(int value); - - public abstract ServiceEntitlementRequest build(); - } -} |