diff options
author | TreeHugger Robot <treehugger-gerrit@google.com> | 2021-03-04 05:11:34 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2021-03-04 05:11:34 +0000 |
commit | 7683e077e4d612988bff5c928455cbe2289ca190 (patch) | |
tree | 08e179b6912aebb95c8f882eb93a23709c25e162 | |
parent | b73e510b0f8aa35b1c3ae34ea5e3acf480896c65 (diff) | |
parent | 1d74d2d9c825b08dd4e5666f443fc6feba75ca77 (diff) | |
download | service_entitlement-7683e077e4d612988bff5c928455cbe2289ca190.tar.gz |
Merge "Handle EAP synchronization failure" into sc-dev
11 files changed, 894 insertions, 568 deletions
diff --git a/java/com/android/libraries/entitlement/EapAkaHelper.java b/java/com/android/libraries/entitlement/EapAkaHelper.java index 86e3149..05c3964 100644 --- a/java/com/android/libraries/entitlement/EapAkaHelper.java +++ b/java/com/android/libraries/entitlement/EapAkaHelper.java @@ -22,6 +22,7 @@ import android.telephony.TelephonyManager; import androidx.annotation.Nullable; import com.android.libraries.entitlement.eapaka.EapAkaApi; +import com.android.libraries.entitlement.eapaka.EapAkaChallenge; import com.android.libraries.entitlement.eapaka.EapAkaResponse; /** @@ -76,8 +77,11 @@ public class EapAkaHelper { @Nullable public String getEapAkaChallengeResponse(String challenge) { try { - return new EapAkaResponse(challenge) - .getEapAkaChallengeResponse(mContext, mSimSubscriptionId); + EapAkaChallenge eapAkaChallenge = EapAkaChallenge.parseEapAkaChallenge(challenge); + EapAkaResponse response = + EapAkaResponse.respondToEapAkaChallenge( + mContext, mSimSubscriptionId, eapAkaChallenge); + return response.response(); // Would be null on synchrinization failure } catch (ServiceEntitlementException e) { return null; } diff --git a/java/com/android/libraries/entitlement/ServiceEntitlementException.java b/java/com/android/libraries/entitlement/ServiceEntitlementException.java index d650726..45b1b9b 100644 --- a/java/com/android/libraries/entitlement/ServiceEntitlementException.java +++ b/java/com/android/libraries/entitlement/ServiceEntitlementException.java @@ -24,33 +24,43 @@ public class ServiceEntitlementException extends Exception { * Unknown error. */ public static final int ERROR_UNKNOWN = 0; + + // Android telephony related failures /** * Android telephony is unable to provide info like IMSI, e.g. when modem crashed. */ - public static final int ERROR_PHONE_NOT_AVAILABLE = 1; + public static final int ERROR_PHONE_NOT_AVAILABLE = 10; + + // EAP-AKA authentication related falures /** * 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; + public static final int ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE = 20; + /** + * EAP-AKA synchronization failure that cannot be recoverd even after the "Sequence number + * synchronization" procedure as defined in RFC 4187. + */ + public static final int ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE = 21; + + // HTTP related failures /** * 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; + public static final int ERROR_SERVER_NOT_CONNECTABLE = 30; /** * 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 static final int ERROR_HTTP_STATUS_NOT_SUCCESS = 31; /** * HTTP response received with a malformed format. e.g. the response with content-type JSON but * failing JSON parser. */ - public static final int MALFORMED_HTTP_RESPONSE = 5; + public static final int ERROR_MALFORMED_HTTP_RESPONSE = 32; /** * Default HTTP status if not been specified. diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java index 065b556..dd15426 100644 --- a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java +++ b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java @@ -16,7 +16,8 @@ package com.android.libraries.entitlement.eapaka; -import static com.android.libraries.entitlement.ServiceEntitlementException.MALFORMED_HTTP_RESPONSE; +import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE; +import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE; import android.content.Context; import android.net.Uri; @@ -110,6 +111,9 @@ public class EapAkaApi { private static final String REQUEST_CONTENT_TYPE_JSON = "application/vnd.gsma.eap-relay.v1.0+json"; + // In case of EAP-AKA synchronization failure, we try to recover for at most two times. + private static final int FOLLOW_SYNC_FAILURE_MAX_COUNT = 2; + private final Context mContext; private final int mSimSubscriptionId; private final HttpClient mHttpClient; @@ -146,43 +150,84 @@ public class EapAkaApi { .setNetwork(carrierConfig.network()) .build(); HttpResponse response = mHttpClient.request(httpRequest); - if (request.authenticationToken().isEmpty()) { - // EapAka token challenge for initial AuthN - if (response.contentType() != ContentType.JSON) { - throw new ServiceEntitlementException( - MALFORMED_HTTP_RESPONSE, "Unexpected http ContentType"); - } + if (!request.authenticationToken().isEmpty()) { + // Fast Re-Authentication flow with pre-existing auth token + Log.d(TAG, "Fast Re-Authentication"); + return response.body(); + } + // Full Authentication flow + Log.d(TAG, "Full Authentication"); + return respondToEapAkaChallenge(carrierConfig, response, FOLLOW_SYNC_FAILURE_MAX_COUNT) + .body(); + } - Log.d(TAG, "initial AuthN"); - String responseData = ""; - try { - responseData = new JSONObject(response.body()).getString( - EAP_CHALLENGE_RESPONSE); - } catch (JSONException jsonException) { + /** + * Sends a follow-up HTTP request to the HTTP {@code response} using the same cookie, and + * returns the follow-up HTTP response. + * + * <p>The {@code response} should contain a EAP-AKA challenge from server, and the + * follow-up request could contain: + * + * <ul> + * <li>The EAP-AKA response message, and the follow-up response should contain the + * service entitlement configuration; or, + * <li>The EAP-AKA synchronization failure message, and the follow-up response should + * contain the new EAP-AKA challenge. Then this method calls itself to follow-up + * the new challenge and return a new response, if {@code followSyncFailureCount} + * is greater than zero. When this method call itself {@code followSyncFailureCount} is + * reduced by one to prevent infinite loop (unlikely in practice, but just in case). + * </ul> + */ + private HttpResponse respondToEapAkaChallenge( + CarrierConfig carrierConfig, HttpResponse response, int followSyncFailureCount) + throws ServiceEntitlementException { + if (response.contentType() != ContentType.JSON) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, "Unexpected http ContentType"); + } + String eapAkaChallenge; + try { + eapAkaChallenge = new JSONObject(response.body()).getString(EAP_CHALLENGE_RESPONSE); + } catch (JSONException jsonException) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, "Failed to parse json object", jsonException); + } + EapAkaChallenge challenge = EapAkaChallenge.parseEapAkaChallenge(eapAkaChallenge); + EapAkaResponse eapAkaResponse = + EapAkaResponse.respondToEapAkaChallenge(mContext, mSimSubscriptionId, challenge); + // This could be a successful authentication, or synchronization failure. + if (eapAkaResponse.response() != null) { // successful authentication + return challengeResponse(eapAkaResponse.response(), carrierConfig, response.cookie()); + } else if (eapAkaResponse.synchronizationFailureResponse() != null) { + Log.d(TAG, "synchronization failure"); + HttpResponse newChallenge = + challengeResponse( + eapAkaResponse.synchronizationFailureResponse(), + carrierConfig, + response.cookie()); + if (followSyncFailureCount > 0) { + return respondToEapAkaChallenge( + carrierConfig, newChallenge, followSyncFailureCount - 1); + } else { throw new ServiceEntitlementException( - MALFORMED_HTTP_RESPONSE, "Failed to parse json object", jsonException); + ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE, + "Unable to recover from EAP-AKA synchroinization failure"); } - return challengeResponse( - new EapAkaResponse(responseData).getEapAkaChallengeResponse(mContext, - mSimSubscriptionId), carrierConfig, response.cookie()); - } else { - // Result of fast AuthN - Log.d(TAG, "fast AuthN"); - return response.body(); + } else { // not possible + throw new AssertionError("EapAkaResponse invalid."); } } - private String challengeResponse(String akaChallengeResponse, - CarrierConfig carrierConfig, - String cookie) + private HttpResponse challengeResponse( + String eapAkaChallengeResponse, CarrierConfig carrierConfig, String cookie) throws ServiceEntitlementException { Log.d(TAG, "challengeResponse"); JSONObject postData = new JSONObject(); try { - postData.put(EAP_CHALLENGE_RESPONSE, akaChallengeResponse); + postData.put(EAP_CHALLENGE_RESPONSE, eapAkaChallengeResponse); } catch (JSONException jsonException) { throw new ServiceEntitlementException( - MALFORMED_HTTP_RESPONSE, "Failed to put post data", jsonException); + ERROR_MALFORMED_HTTP_RESPONSE, "Failed to put post data", jsonException); } HttpRequest request = HttpRequest.builder() @@ -195,7 +240,7 @@ public class EapAkaApi { .setTimeoutInSec(carrierConfig.timeoutInSec()) .setNetwork(carrierConfig.network()) .build(); - return mHttpClient.request(request).body(); + return mHttpClient.request(request); } String entitlementStatusUrl( diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaChallenge.java b/java/com/android/libraries/entitlement/eapaka/EapAkaChallenge.java new file mode 100644 index 0000000..6563ede --- /dev/null +++ b/java/com/android/libraries/entitlement/eapaka/EapAkaChallenge.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.libraries.entitlement.eapaka; + +import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.util.Base64; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.libraries.entitlement.ServiceEntitlementException; + +/** + * Parses EAP-AKA challenge from server. Refer to RFC 4187 Section 8.1 Message + * Format/RFC 3748 Session 4 EAP Packet Format. + */ +public class EapAkaChallenge { + private static final String TAG = "ServiceEntitlement"; + + private static final int EAP_AKA_HEADER_LENGTH = 8; + private static final byte CODE_REQUEST = 0x01; + static final byte TYPE_EAP_AKA = 0x17; + static final byte SUBTYPE_AKA_CHALLENGE = 0x01; + private static final byte ATTRIBUTE_RAND = 0x01; + private static final byte ATTRIBUTE_AUTN = 0x02; + private static final int ATTRIBUTE_LENGTH = 20; + private static final int RAND_LENGTH = 16; + private static final int AUTN_LENGTH = 16; + + // The identifier of Response must same as Request + private byte mIdentifier = -1; + // The value of AT_AUTN, network authentication token + private byte[] mAutn; + // The value of AT_RAND, random challenge + private byte[] mRand; + + // Base64 encoded 3G security context for SIM Authentication request + private String mSimAuthenticationRequest; + + private EapAkaChallenge() {} + + /** Parses a EAP-AKA challenge request message encoded in base64. */ + public static EapAkaChallenge parseEapAkaChallenge(String challenge) + throws ServiceEntitlementException { + byte[] data; + try { + data = Base64.decode(challenge.getBytes(UTF_8), Base64.DEFAULT); + } catch (IllegalArgumentException illegalArgumentException) { + throw new ServiceEntitlementException( + ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, + "EAP-AKA challenge is not a valid base64!"); + } + EapAkaChallenge result = new EapAkaChallenge(); + if (result.parseEapAkaHeader(data) && result.parseRandAndAutn(data)) { + result.mSimAuthenticationRequest = result.getSimAuthChallengeData(); + return result; + } else { + throw new ServiceEntitlementException( + ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, + "EAP-AKA challenge message is not valid"); + } + } + + /** + * Returns the base64 encoded 3G security context for SIM Authentication request, + * or {@code null} if the EAP-AKA challenge is not valid. + */ + @Nullable + public String getSimAuthenticationRequest() { + return mSimAuthenticationRequest; + } + + /** Returns the EAP package identifier in the EAP-AKA challenge. */ + public byte getIdentifier() { + return mIdentifier; + } + + /** + * Parses EAP-AKA header, 8 bytes including 2 reserved bytes. + * + * @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 for EAP-request should be CODE_REQUEST + byte code = data[0]; + // EAP package identifier + mIdentifier = data[1]; + // Total length of full EAP-AKA message, include code, identifier, ... + int length = ((data[2] & 0xff) << 8) | (data[3] & 0xff); + // Type for EAP-AKA should be TYPE_EAP_AKA + byte type = data[4]; + // SubType for AKA-Challenge should be SUBTYPE_AKA_CHALLENGE + byte subType = data[5]; + + // Validate 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; + } + + /** + * Parses AT_RAND and AT_AUTN. Refer to RFC 4187 section 10.6 AT_RAND/section 10.7 AT_AUTN. + * + * @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 != ATTRIBUTE_LENGTH) { + Log.d(TAG, "AT_RAND length is " + length); + return false; + } + mRand = new byte[RAND_LENGTH]; + System.arraycopy(data, index + 4, mRand, 0, RAND_LENGTH); + } else if (attributeType == ATTRIBUTE_AUTN) { + if (length != ATTRIBUTE_LENGTH) { + Log.d(TAG, "AT_AUTN length is " + length); + return false; + } + mAutn = new byte[AUTN_LENGTH]; + System.arraycopy(data, index + 4, mAutn, 0, AUTN_LENGTH); + } + + index += length; + } + + // check has AT_RAND and AT_AUTH + if (mRand == null || mAutn == null) { + Log.d(TAG, "Invalid Type Datas!"); + return false; + } + + return true; + } + + /** + * Returns Base64 encoded 3G security context for SIM Authentication request. + */ + @Nullable + private String getSimAuthChallengeData() { + byte[] challengeData = new byte[RAND_LENGTH + AUTN_LENGTH + 2]; + challengeData[0] = (byte) RAND_LENGTH; + System.arraycopy(mRand, 0, challengeData, 1, RAND_LENGTH); + challengeData[RAND_LENGTH + 1] = (byte) AUTN_LENGTH; + System.arraycopy(mAutn, 0, challengeData, RAND_LENGTH + 2, AUTN_LENGTH); + + return Base64.encodeToString(challengeData, Base64.NO_WRAP).trim(); + } +} diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java b/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java index 81b8046..12716f5 100644 --- a/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java +++ b/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java @@ -17,10 +17,11 @@ package com.android.libraries.entitlement.eapaka; import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE; +import static com.android.libraries.entitlement.eapaka.EapAkaChallenge.SUBTYPE_AKA_CHALLENGE; +import static com.android.libraries.entitlement.eapaka.EapAkaChallenge.TYPE_EAP_AKA; import android.content.Context; import android.telephony.TelephonyManager; -import android.text.TextUtils; import android.util.Base64; import android.util.Log; @@ -32,262 +33,133 @@ import com.android.libraries.entitlement.utils.BytesConverter; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; /** - * Generate the response of EAP-AKA token challenge. Refer to RFC 4187 Section 8.1 Message + * Generates the response of EAP-AKA challenge. Refer to RFC 4187 Section 8.1 Message * Format/RFC 3748 Session 4 EAP Packet Format. */ public 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 SUBTYPE_SYNC_FAILURE = 0x04; private static final byte ATTRIBUTE_RES = 0x03; + private static final byte ATTRIBUTE_AUTS = 0x04; private static final byte ATTRIBUTE_MAC = 0x0B; private static final String ALGORITHM_HMAC_SHA1 = "HmacSHA1"; - private static final int ATTRIBUTE_LENGTH = 20; private static final int SHA1_OUTPUT_LENGTH = 20; + private static final int MAC_LENGTH = 16; - private static final byte RAND_LEN = 0x10; - private static final byte AUTN_LEN = 0x10; + // RFC 4187 Section 9.4 EAP-Response/AKA-Challenge + private String mResponse; + // RFC 4187 Section 9.6 EAP-Response/AKA-Synchronization-Failure + private String mSynchronizationFailureResponse; - /** - * {@link #CODE_REQUEST} for Request, {@link #CODE_RESPONSE} for Response - */ - private byte mCode = -1; - /** - * The identifier of Response must same as Request - */ - private byte mIdentifier = -1; - /** - * The total length of full EAP-AKA message, include code, identifier, ... - */ - private int mLength = -1; - /** - * In EAP-AKA, the Type field is set to {@link #TYPE_EAP_AKA} - */ - private byte mType = -1; - /** - * SubType for AKA-Challenge should be {@link #SUBTYPE_AKA_CHALLENGE} - */ - private byte mSubType = -1; - /** - * The value of AT_AUTN, network authentication token - */ - private byte[] mAutn; - /** - * The value of AT_RAND, RAND random number - */ - private byte[] mRand; - - private boolean mValid; - - public EapAkaResponse(String eapAkaChallenge) { - try { - parseEapAkaChallengeRequest(eapAkaChallenge); - } catch (Exception e) { - Log.e(TAG, "parseEapAkaChallengeRequest Exception:", e); - mValid = false; - } - } - - private void parseEapAkaChallengeRequest(String request) { - if (TextUtils.isEmpty(request)) { - return; - } + private EapAkaResponse() {} - try { - byte[] data = Base64.decode(request, Base64.DEFAULT); - if (parseEapAkaHeader(data) && parseRandAndAutn(data)) { - mValid = 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; - } - mCode = data[0]; - mIdentifier = data[1]; - mLength = ((data[2] & 0xff) << 8) | (data[3] & 0xff); - mType = data[4]; - mSubType = data[5]; - - // valid header - if (mCode != CODE_REQUEST - || mLength != data.length - || mType != TYPE_EAP_AKA - || mSubType != SUBTYPE_AKA_CHALLENGE) { - Log.d( - TAG, - "Invalid EAP-AKA Header, code=" - + mCode - + ", length=" - + mLength - + ", real length=" - + data.length - + ", type=" - + mType - + ", subType=" - + mSubType); - return false; - } - - return true; + /** Returns EAP-Response/AKA-Challenge, if authentication success. Otherwise {@code null}. */ + @Nullable + public String response() { + return mResponse; } /** - * 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. + * Returns EAP-Response/AKA-Synchronization-Failure, if synchronization failure detected. + * Otherwise {@code null}. */ - 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 != ATTRIBUTE_LENGTH) { - Log.d(TAG, "AT_RAND length is " + length); - return false; - } - mRand = new byte[16]; - System.arraycopy(data, index + 4, mRand, 0, 16); - } else if (attributeType == ATTRIBUTE_AUTN) { - if (length != ATTRIBUTE_LENGTH) { - Log.d(TAG, "AT_AUTN length is " + length); - return false; - } - mAutn = new byte[16]; - System.arraycopy(data, index + 4, mAutn, 0, 16); - } - - index += length; - } - - // check has AT_RAND and AT_AUTH - if (mRand == null || mAutn == null) { - Log.d(TAG, "Invalid Type Datas!"); - return false; - } - - return true; + @Nullable + public String synchronizationFailureResponse() { + return mSynchronizationFailureResponse; } /** * 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) + public static EapAkaResponse respondToEapAkaChallenge( + Context context, int simSubscriptionId, EapAkaChallenge eapAkaChallenge) throws ServiceEntitlementException { - if (!mValid) { - throw new ServiceEntitlementException( - ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, "EAP-AKA Challenge message not valid!"); - } - TelephonyManager telephonyManager = - context.getSystemService(TelephonyManager.class).createForSubscriptionId( - simSubscriptionId); + 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.getSimOperator(), - telephonyManager.getSubscriberId()), - securityContext.getIk(), - securityContext.getCk()); - // K_aut is the key used to calculate MAC - if (mk.getAut() == null) { + eapAkaChallenge.getSimAuthenticationRequest()); + if (response == null) { throw new ServiceEntitlementException( - ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, "Can't generate K_Aut!"); + ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, "EAP-AKA response is null!"); } - // generate EAP-AKA Challenge Response message - byte[] challengeResponse = - generateEapAkaChallengeResponse(securityContext.getRes(), mk.getAut()); - if (challengeResponse == null) { - throw new ServiceEntitlementException( - ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, - "Failed to generate EAP-AKA Challenge Response data!"); - } + EapAkaSecurityContext securityContext = EapAkaSecurityContext.from(response); + EapAkaResponse result = new EapAkaResponse(); + + if (securityContext.getRes() != null + && securityContext.getIk() != null + && securityContext.getCk() != null) { // Success authentication + + // generate master key - refer to RFC 4187, section 7. Key Generation + MasterKey mk = + MasterKey.create( + EapAkaApi.getImsiEap(telephonyManager.getSimOperator(), + telephonyManager.getSubscriberId()), + securityContext.getIk(), + securityContext.getCk()); + // K_aut is the key used to calculate MAC + if (mk.getAut() == null) { + throw new ServiceEntitlementException( + ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, "Can't generate K_Aut!"); + } - return Base64.encodeToString(challengeResponse, Base64.NO_WRAP).trim(); - } + // generate EAP-AKA challenge response message + byte[] challengeResponse = + generateEapAkaChallengeResponse( + securityContext.getRes(), eapAkaChallenge.getIdentifier(), mk.getAut()); + if (challengeResponse == null) { + throw new ServiceEntitlementException( + ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, + "Failed to generate EAP-AKA Challenge Response data!"); + } + // base64 encoding + result.mResponse = Base64.encodeToString(challengeResponse, Base64.NO_WRAP).trim(); - /** - * Returns Base64 encoded GSM/3G security context for SIM Authentication request. - */ - @Nullable - private String getSimAuthChallengeData() { - if (!mValid) { - return null; - } + } else if (securityContext.getAuts() != null) { + + byte[] syncFailure = + generateEapAkaSynchronizationFailureResponse( + securityContext.getAuts(), eapAkaChallenge.getIdentifier()); + result.mSynchronizationFailureResponse = + Base64.encodeToString(syncFailure, Base64.NO_WRAP).trim(); - byte[] challengeData = new byte[RAND_LEN + AUTN_LEN + 2]; - challengeData[0] = RAND_LEN; - System.arraycopy(mRand, 0, challengeData, 1, RAND_LEN); - challengeData[RAND_LEN + 1] = AUTN_LEN; - System.arraycopy(mAutn, 0, challengeData, RAND_LEN + 2, AUTN_LEN); + } else { + throw new ServiceEntitlementException( + ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, + "Invalid SIM EAP-AKA authentication response!"); + } - return Base64.encodeToString(challengeData, Base64.NO_WRAP).trim(); + return result; } /** - * Returns EAP-AKA Challenge response message or {@code null} if failed to generate. + * Returns EAP-Response/AKA-Challenge message, or {@code null} if failed to generate. + * Refer to RFC 4187 section 9.4 EAP-Response/AKA-Challenge. */ @VisibleForTesting @Nullable - byte[] generateEapAkaChallengeResponse(@Nullable byte[] res, @Nullable byte[] aut) { + static byte[] generateEapAkaChallengeResponse( + @Nullable byte[] res, byte identifier, @Nullable byte[] aut) { if (res == null || aut == null) { return null; } - byte[] message = createEapAkaChallengeResponse(res); + byte[] message = createEapAkaChallengeResponse(res, identifier); // use K_aut as key to calculate mac byte[] mac = calculateMac(aut, message); @@ -303,26 +175,60 @@ public class EapAkaResponse { return message; } + /** + * Returns EAP-Response/AKA-Synchronization-Failure, or {@code null} if failed to generate. + * Refer to RFC 4187 section 9.6 EAP-Response/AKA-Synchronization-Failure. + */ + @VisibleForTesting + @Nullable + static byte[] generateEapAkaSynchronizationFailureResponse( + @Nullable byte[] auts, byte identifier) { + // size = 8 (header) + 2 (attribute & length) + AUTS + byte[] message = new byte[10 + auts.length]; + + // set up header + message[0] = CODE_RESPONSE; + // identifier: same as 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_SYNC_FAILURE; + // reserved: 2 bytes + message[6] = 0x00; + message[7] = 0x00; + + // set up AT_AUTS. RFC 4187, Section 10.9 AT_AUTS + message[8] = ATTRIBUTE_AUTS; + // length (in 4-bytes): 4, because AUTS is 14 bytes, plus the attribute (1 byte) and + // the length (1 byte). + message[9] = 0x04; + System.arraycopy(auts, 0, message, 10, auts.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) { + // RFC 3748 Section 4.1 Request and Response + private static byte[] createEapAkaChallengeResponse(byte[] res, byte identifier) { // 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] = mIdentifier; - // length include entire EAP-AKA message + // identifier: same as 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 + // reserved: 2 bytes message[6] = 0x00; message[7] = 0x00; @@ -330,9 +236,10 @@ public class EapAkaResponse { // 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. + // Length (in 4-bytes): + // The length of the RES should already be a multiple of 4 bytes. + // Add 4 to the attribute length to account for the attribute (1 byte), the length (1 byte), + // and the length of the RES in bits (2 bytes). 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 @@ -343,9 +250,10 @@ public class EapAkaResponse { System.arraycopy(res, 0, message, index, res.length); index += res.length; - // set up AT_MAC, RFC 4187, 10.15 AT_MAC + // set up AT_MAC, RFC 4187, Section 10.15 AT_MAC message[index++] = ATTRIBUTE_MAC; - // fixed length, 5*4 = 20 + // length (in 4-bytes): 5, because MAC is 16 bytes, plus the attribute (1 byte), + // the length (1 byte), and reserved bytes (2 bytes). message[index++] = 0x05; // With two bytes reserved message[index++] = 0x00; @@ -354,9 +262,7 @@ public class EapAkaResponse { // 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; - } + Arrays.fill(message, index, index + 16, (byte) 0x00); return message; } @@ -369,7 +275,7 @@ public class EapAkaResponse { // 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) { + private static byte[] calculateMac(byte[] key, byte[] message) { try { Mac mac = Mac.getInstance(ALGORITHM_HMAC_SHA1); SecretKeySpec secret = new SecretKeySpec(key, ALGORITHM_HMAC_SHA1); @@ -381,8 +287,8 @@ public class EapAkaResponse { return null; } - byte[] macValue = new byte[16]; - System.arraycopy(output, 0, macValue, 0, 16); + byte[] macValue = new byte[MAC_LENGTH]; + System.arraycopy(output, 0, macValue, 0, MAC_LENGTH); return macValue; } catch (NoSuchAlgorithmException | InvalidKeyException e) { Log.e(TAG, "calculateMac failed!", e); diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaSecurityContext.java b/java/com/android/libraries/entitlement/eapaka/EapAkaSecurityContext.java index cdb9427..145773d 100644 --- a/java/com/android/libraries/entitlement/eapaka/EapAkaSecurityContext.java +++ b/java/com/android/libraries/entitlement/eapaka/EapAkaSecurityContext.java @@ -18,10 +18,13 @@ package com.android.libraries.entitlement.eapaka; import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE; -import android.text.TextUtils; +import static java.nio.charset.StandardCharsets.UTF_8; + import android.util.Base64; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.libraries.entitlement.ServiceEntitlementException; /** @@ -33,18 +36,20 @@ class EapAkaSecurityContext { private static final String TAG = "ServiceEntitlement"; private static final byte RESPONSE_TAG_SUCCESS = (byte) 0xDB; + private static final byte RESPONSE_TAG_SYNC_FAILURE = (byte) 0xDC; private boolean mValid; - /* Authentication result from SIM */ + // User response, populated on successful authentication private byte[] mRes; - /* Cipher Key */ + // Cipher Key, populated on successful authentication private byte[] mCk; - /* Integrity Key */ + // Integrity Key, populated on successful authentication private byte[] mIk; + // AUTS, populated on synchronization failure + private byte[] mAuts; - private EapAkaSecurityContext() { - } + private EapAkaSecurityContext() {} /** * Provide {@link EapAkaSecurityContext} from response data. @@ -65,56 +70,59 @@ class EapAkaSecurityContext { * Parses SIM EAP-AKA Authentication responded data. */ private void parseResponseData(String response) { - if (TextUtils.isEmpty(response)) { - Log.d(TAG, "parseResponseData but input empty data!"); - return; - } + byte[] data = null; 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; - } + data = Base64.decode(response.getBytes(UTF_8), Base64.DEFAULT); + Log.d(TAG, "Decoded response data length = " + data.length + " bytes"); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Response is not a valid base-64 content"); + return; + } + if (data.length == 0) { + return; + } + // Check tag, the initial byte + int index = 0; + if (data[index] == RESPONSE_TAG_SUCCESS) { // Parse RES index++; // move to RES length byte mRes = parseTag(index, data); if (mRes == null) { - Log.d(TAG, "Invalid data! can't parse RES!"); + Log.d(TAG, "Invalid data: can't parse RES!"); return; } // Parse CK index += mRes.length + 1; // move to CK length byte mCk = parseTag(index, data); if (mCk == null) { - Log.d(TAG, "Invalid data! can't parse CK!"); + Log.d(TAG, "Invalid data: can't parse CK!"); return; } // Parse IK index += mCk.length + 1; // move to IK length byte mIk = parseTag(index, data); if (mIk == null) { - Log.d(TAG, "Invalid data! can't parse IK!"); + Log.d(TAG, "Invalid data: can't parse IK!"); return; } - mValid = true; - } catch (IllegalArgumentException illegalArgumentException) { - Log.e(TAG, "Invalid base-64 content"); + } else if (data[index] == RESPONSE_TAG_SYNC_FAILURE) { + // Parse AUTS + index++; // move to AUTS length byte + mAuts = parseTag(index, data); + if (mAuts == null) { + Log.d(TAG, "Invalid data: can't parse AUTS!"); + return; + } + mValid = true; + } else { + Log.d(TAG, "Not a valid tag, tag=" + data[index]); + return; } } - private byte[] parseTag(int index, byte[] src) { // index at the length byte if (index >= src.length) { @@ -133,31 +141,39 @@ class EapAkaSecurityContext { return dest; } - /** - * Returns {@code valid}. - */ - boolean isValid() { + private boolean isValid() { return mValid; } /** - * Returns {@code res}. + * Returns RES, or {@code null} for a synchronization failure. */ + @Nullable public byte[] getRes() { return mRes; } /** - * Returns {@code ck}. + * Returns CK, or {@code null} for a synchronization failure. */ + @Nullable public byte[] getCk() { return mCk; } /** - * Returns {@code ik}. + * Returns IK, or {@code null} for a synchronization failure. */ + @Nullable public byte[] getIk() { return mIk; } + + /** + * Returns AUTS, or {@code null} for a successful authentication. + */ + @Nullable + public byte[] getAuts() { + return mAuts; + } } diff --git a/java/com/android/libraries/entitlement/http/HttpClient.java b/java/com/android/libraries/entitlement/http/HttpClient.java index 8476164..76eeaec 100644 --- a/java/com/android/libraries/entitlement/http/HttpClient.java +++ b/java/com/android/libraries/entitlement/http/HttpClient.java @@ -17,8 +17,8 @@ package com.android.libraries.entitlement.http; import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS; -import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_SEVER_NOT_CONNECTABLE; -import static com.android.libraries.entitlement.ServiceEntitlementException.MALFORMED_HTTP_RESPONSE; +import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE; +import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_SERVER_NOT_CONNECTABLE; import static com.android.libraries.entitlement.http.HttpConstants.RequestMethod.POST; import static com.android.libraries.entitlement.utils.DebugUtils.logPii; @@ -107,7 +107,7 @@ public class HttpClient { } } catch (IOException ioe) { throw new ServiceEntitlementException( - ERROR_SEVER_NOT_CONNECTABLE, "Configure connection failed!", ioe); + ERROR_SERVER_NOT_CONNECTABLE, "Configure connection failed!", ioe); } } @@ -143,7 +143,7 @@ public class HttpClient { responseBuilder.setBody(responseBody); } catch (IOException e) { throw new ServiceEntitlementException( - MALFORMED_HTTP_RESPONSE, "Read response body/message failed!", e); + ERROR_MALFORMED_HTTP_RESPONSE, "Read response body/message failed!", e); } return responseBuilder.build(); } diff --git a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java index 62297a7..cc80ea0 100644 --- a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java +++ b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java @@ -16,6 +16,11 @@ package com.android.libraries.entitlement.eapaka; +import static com.android.libraries.entitlement.eapaka.EapAkaChallengeTest.EAP_AKA_CHALLENGE_REQUEST; +import static com.android.libraries.entitlement.eapaka.EapAkaChallengeTest.EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED; +import static com.android.libraries.entitlement.eapaka.EapAkaResponseTest.EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS; +import static com.android.libraries.entitlement.eapaka.EapAkaResponseTest.EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; @@ -58,17 +63,10 @@ import org.mockito.junit.MockitoRule; public class EapAkaApiTest { private static final String TEST_URL = "https://test.url/test-path"; private static final String EAP_AKA_CHALLENGE = - "{\"eap-relay-packet\":\"" - + "AQIAfBcBAAABBQAAXOZSkCjxysgE4" - + "3GWqHJvgQIFAABrikWGrekAALNU4TxmCDPoCwUAAJT0nqXeAYlqzT0UGXINENWBBQAA7z3fhImk" - + "q+vcCKWIZBdvuYIJAAAPRUFp7KWFo+Thr78Qj9hEkB2zA0i6KakODsufBC+BJQ==\"}"; + "{\"eap-relay-packet\":\"" + EAP_AKA_CHALLENGE_REQUEST + "\"}"; // com.google.common.net.HttpHeaders.COOKIE private static final String HTTP_HEADER_COOKIE = "Cookie"; private static final String COOKIE_VALUE = "COOKIE=abcdefg"; - private static final String GSM_SECURITY_CONTEXT_REQUEST = - "EFzmUpAo8crIBONxlqhyb4EQa4pFhq3pAACzVOE8Zggz6A=="; - private static final String GSM_SECURITY_CONTEXT_RESPONSE = - "2wjHnwKln8mjjxDzMKJvLBzMHtm0X9SNBsUWEAbEiAdD7xeqqZ7nsXzukRkIhd6SDZ4bj7s="; private static final String RESPONSE_XML = "<wap-provisioningdoc version=\"1.1\">\n" + " <characteristic type=\"TOKEN\">\n" @@ -115,11 +113,6 @@ public class EapAkaApiTest { .thenReturn(mMockTelephonyManagerForSubId); when(mMockTelephonyManagerForSubId.getSubscriberId()).thenReturn(IMSI); when(mMockTelephonyManagerForSubId.getSimOperator()).thenReturn(MCCMNC); - when(mMockTelephonyManagerForSubId.getIccAuthentication( - TelephonyManager.APPTYPE_USIM, - TelephonyManager.AUTHTYPE_EAP_AKA, - GSM_SECURITY_CONTEXT_REQUEST)) - .thenReturn(GSM_SECURITY_CONTEXT_RESPONSE); } @Test @@ -146,6 +139,11 @@ public class EapAkaApiTest { @Test public void queryEntitlementStatus_noAuthenticationToken() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); HttpResponse eapChallengeResponse = HttpResponse .builder().setContentType(ContentType.JSON).setBody(EAP_AKA_CHALLENGE) @@ -216,7 +214,7 @@ public class EapAkaApiTest { request)); assertThat(exception.getErrorCode()).isEqualTo( - ServiceEntitlementException.MALFORMED_HTTP_RESPONSE); + ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); assertThat(exception.getMessage()).isEqualTo("Unexpected http ContentType"); } @@ -238,8 +236,44 @@ public class EapAkaApiTest { request)); assertThat(exception.getErrorCode()).isEqualTo( - ServiceEntitlementException.MALFORMED_HTTP_RESPONSE); + ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); assertThat(exception.getMessage()).isEqualTo("Failed to parse json object"); assertThat(exception.getCause()).isInstanceOf(JSONException.class); } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_handleEapAkaSyncFailure() + throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse + .builder().setContentType(ContentType.JSON).setBody(EAP_AKA_CHALLENGE) + .setCookie(COOKIE_VALUE).build(); + HttpResponse xmlResponse = + HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + String response = + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + assertThat(response).isEqualTo(RESPONSE_XML); + // Verify that the 2nd/3rd request has cookie set by the 1st/2nd response + verify(mMockHttpClient, times(3)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) + .containsEntry(HTTP_HEADER_COOKIE, COOKIE_VALUE); + assertThat(mHttpRequestCaptor.getAllValues().get(2).requestProperties()) + .containsEntry(HTTP_HEADER_COOKIE, COOKIE_VALUE); + } } diff --git a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaChallengeTest.java b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaChallengeTest.java new file mode 100644 index 0000000..318c350 --- /dev/null +++ b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaChallengeTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.libraries.entitlement.eapaka; + +import static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.assertThrows; + +import android.util.Base64; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.libraries.entitlement.ServiceEntitlementException; + +import com.google.common.io.BaseEncoding; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class EapAkaChallengeTest { + // EAP-AKA challenge request : AQIAfBcBAAABBQAAXOZSkCjxysgE43GWqHJvgQIFAABrikWGrekAALNU4TxmCDPo + // CwUAAJT0nqXeAYlqzT0UGXINENWBBQAA7z3fhImkq+vcCKWIZBdvuYIJAAAPRUFp + // 7KWFo+Thr78Qj9hEkB2zA0i6KakODsufBC+BJQ== + // Base64 decoded : 0102007C17010000010500005CE6529028F1CAC804E37196A8726F81020500006B8A4586ADE + // 90000B354E13C660833E80B05000094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF + // 8489A4ABEBDC08A58864176FB9820900000F454169ECA585A3E4E1AFBF108FD844901DB3034 + // 8BA29A90E0ECB9F042F8125 + // HEADER + // Code : 0x01 (Request) + // Identifier : 0x02 + // Length : 0x007C = 124 + // Type : 0x17=23 (EAP-AKA) + // SubType : 0x01 = 1 (AKA Challenge) + // Reserved : 0x0000 + // + // Type-DATA + // + // Attribute Type : 0x01 (AT_RAND) + // Length : 0x05 = 5's 4 bytes = 20 bytes + // Reserved : 0x0000 + // RAND value : 0x5CE6529028F1CAC804E37196A8726F81 + // + // Attribute Type : 0x02 (AT_AUTN) + // Length : 0x05 = 5's 4 bytes = 20 bytes + // Reserved : 0x0000 + // AUTN value : 0x6B8A4586ADE90000B354E13C660833E8 + // + // Attribute Type : 0x0B (AT_MAC) + // Length : 0x05 = 20 bytes + // Reserved : 0x0000 + // MAC value : 0x94F49EA5DE01896ACD3D1419720D10D5 + // + // Attribute Type : 0x81 (AT_IV) + // Length : 0x05 + // Reserved : 0x0000 + // IV value : 0xEF3DDF8489A4ABEBDC08A58864176FB9 + // + // Attribute Type : 0x82 (AT_ENCR_DATA) + // Length : 0x09 = 36 bytes + // Reserved : 0x0000 + // ENCR value : 0x0F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125 + // + // Derived 3G security context for SIM Authentication request + // Hex : 105CE6529028F1CAC804E37196A8726F81106B8A4586ADE90000B354E13C660833E8 + // Base64 encoded : EFzmUpAo8crIBONxlqhyb4EQa4pFhq3pAACzVOE8Zggz6A== + public static final String EAP_AKA_CHALLENGE_REQUEST = + "AQIAfBcBAAABBQAAXOZSkCjxysgE4" + + "3GWqHJvgQIFAABrikWGrekAALNU4TxmCDPoCwUAAJT0nqXeAYlqzT0UGXINENWBBQAA7z3fhImk" + + "g+vcCKWIZBdvuYIJAAAPRUFp7KWFo+Thr78Qj9hEkB2zA0i6KakODsufBC+BJQ=="; + public static final String EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED = + "EFzmUpAo8crIBONxlqhyb4EQa4pFhq3pAACzVOE8Zggz6A=="; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITHOUT_RAND = + "0102006817010000020500006" + + "B8A4586ADE90000B354E13C660833E80B05000094F49EA5DE01896ACD3D1419720D10D58105" + + "0000EF3DDF8489A4ABEBDC08A58864176FB9820900000F454169ECA585A3E4E1AFBF108FD84" + + "4901DB30348BA29A90E0ECB9F042F8125"; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITHOUT_AUTN = + "0102006817010000010500005" + + "CE6529028F1CAC804E37196A8726F810B05000094F49EA5DE01896ACD3D1419720D10D5810" + + "50000EF3DDF8489A4ABEBDC08A58864176FB9820900000F454169ECA585A3E4E1AFBF108FD" + + "844901DB30348BA29A90E0ECB9F042F8125"; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITHOUT_RAND_AUTN = + "01020054170100000B05" + + "000094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176F" + + "B9820900000F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125"; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_CODE_RESPONSE = + "02020054170100000B0" + + "5000094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176F" + + "B9820900000F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125"; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_WRONG_LENGTH = + "01020099170100000B05" + + "000094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176FB" + + "9820900000F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125"; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_EAP_TYPE_99 = + "01020054990100000B050" + + "00094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176FB9" + + "F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125820900000"; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_SUBTYPE_99 = + "01020054179900000B0500" + + "0094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176FB98" + + "454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F812520900000F"; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_LENGTH_OVER_DATA = + "0102001C17010000010600005CE6529028F1CAC804E37196A8726F81"; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_RAND_LENGTH_16 = + "0102001817010000010400005CE6529028F1CAC804E37196"; + private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_AUTN_LENGTH_16 = + "0102001817010000020400006B8A4586ADE90000B354E13C"; + + @Test + public void parseEapAkaChallengeRequest() throws Exception { + EapAkaChallenge message = + EapAkaChallenge.parseEapAkaChallenge(EAP_AKA_CHALLENGE_REQUEST); + + assertThat(message.getIdentifier()).isEqualTo((byte) 0x02); + assertThat(message.getSimAuthenticationRequest()) + .isEqualTo(EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED); + } + + @Test + public void parseEapAkaChallengeRequest_withoutRand() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITHOUT_RAND); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_withoutAutn() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITHOUT_AUTN); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_withoutRandAndAutn() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITHOUT_RAND_AUTN); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_withCodeResponse() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_CODE_RESPONSE); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_withWrongLength() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_WRONG_LENGTH); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_withEapType99() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_EAP_TYPE_99); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_withSubType99() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_SUBTYPE_99); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_withLengthOverData() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_LENGTH_OVER_DATA); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_withRandLength16() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_RAND_LENGTH_16); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_withAutnLength16() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_AUTN_LENGTH_16); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + @Test + public void parseEapAkaChallengeRequest_notValid_throwException() { + byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_WRONG_LENGTH); + String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + + assertThrows( + ServiceEntitlementException.class, + () -> EapAkaChallenge.parseEapAkaChallenge(encodedData)); + } + + private static byte[] convertHexStringToBytes(String input) { + return BaseEncoding.base16().decode(input); + } +} diff --git a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaResponseTest.java b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaResponseTest.java index 068346a..073a973 100644 --- a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaResponseTest.java +++ b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaResponseTest.java @@ -16,20 +16,24 @@ package com.android.libraries.entitlement.eapaka; +import static com.android.libraries.entitlement.eapaka.EapAkaChallengeTest.EAP_AKA_CHALLENGE_REQUEST; +import static com.android.libraries.entitlement.eapaka.EapAkaChallengeTest.EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED; + import static com.google.common.truth.Truth.assertThat; -import static org.testng.Assert.assertThrows; -import static org.testng.Assert.fail; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import android.content.Context; +import android.telephony.TelephonyManager; import android.util.Base64; +import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; -import com.android.libraries.entitlement.ServiceEntitlementException; - import com.google.common.io.BaseEncoding; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,250 +43,93 @@ import org.mockito.junit.MockitoRule; @RunWith(AndroidJUnit4.class) public class EapAkaResponseTest { - // EAP-AKA Challenge Request : AQIAfBcBAAABBQAAXOZSkCjxysgE43GWqHJvgQIFAABrikWGrekAALNU4TxmCDPo - // CwUAAJT0nqXeAYlqzT0UGXINENWBBQAA7z3fhImkq+vcCKWIZBdvuYIJAAAPRUFp - // 7KWFo+Thr78Qj9hEkB2zA0i6KakODsufBC+BJQ== - // Base64 decoded : 0102007C17010000010500005CE6529028F1CAC804E37196A8726F81020500006B8A4586ADE - // 90000B354E13C660833E80B05000094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF - // 8489A4ABEBDC08A58864176FB9820900000F454169ECA585A3E4E1AFBF108FD844901DB3034 - // 8BA29A90E0ECB9F042F8125 - // HEADER - // Code : 0x01 (Request) - // Identifier : 0x02 - // Length : 0x007C = 124 - // Type : 0x17=23 (EAP-AKA) - // SubType : 0x01 = 1 (AKA Challenge) - // Reserver : 0x0000 - // - // Type-DATA - // - // Attribute Type : 0x01 (AT_RAND) - // Length : 0x05 = 5's 4 bytes = 20 bytes - // Reserved : 0x0000 - // RAND value : 0x5CE6529028F1CAC804E37196A8726F81 - // - // Attribute Type : 0x02 (AT_AUTN) - // Length : 0x05 = 5's 4 bytes = 20 bytes - // Reserved : 0x0000 - // AUTN value : 0x6B8A4586ADE90000B354E13C660833E8 - // - // Attribute Type : 0x0B (AT_MAC) - // Length : 0x05 = 20 bytes - // Reserved : 0x0000 - // MAC value : 0x94F49EA5DE01896ACD3D1419720D10D5 + // EAP_AKA_CHALLENGE_REQUEST: + // Identifier : + // 0x02 + // Derived 3G security context for SIM authentication request : + // EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED + + // 3G security context for SIM authentication response // - // Attribute Type : 0x81 (AT_IV) - // Length : 0x05 - // Reserved : 0x0000 - // IV value : 0xEF3DDF8489A4ABEBDC08A58864176FB9 + // Success: + // Hex : DB08C79F02A59FC9A38F10F330A26F2C1CCC1ED9B45FD48D06C5161006C4880743EF17AAA99EE7B17 + // CEE91190885DE920D9E1B8FBB + // Base64 encoded : 2wjHnwKln8mjjxDzMKJvLBzMHtm0X9SNBsUWEAbEiAdD7xeqqZ7nsXzukRkIhd6SDZ4bj7s= // - // Attribute Type : 0x82 (AT_ENCR_DATA) - // Length : 0x09 = 36 bytes - // Reserved : 0x0000 - // ENCR value : 0x0F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125 - private static final String EAP_AKA_CHALLENGE_REQUEST_EXPECTED = - "AQIAfBcBAAABBQAAXOZSkCjxysgE4" - + "3GWqHJvgQIFAABrikWGrekAALNU4TxmCDPoCwUAAJT0nqXeAYlqzT0UGXINENWBBQAA7z3fhImk" - + "g+vcCKWIZBdvuYIJAAAPRUFp7KWFo+Thr78Qj9hEkB2zA0i6KakODsufBC+BJQ=="; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITHOUT_RAND = - "0102006817010000020500006" - + "B8A4586ADE90000B354E13C660833E80B05000094F49EA5DE01896ACD3D1419720D10D58105" - + "0000EF3DDF8489A4ABEBDC08A58864176FB9820900000F454169ECA585A3E4E1AFBF108FD84" - + "4901DB30348BA29A90E0ECB9F042F8125"; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITHOUT_AUTN = - "0102006817010000010500005" - + "CE6529028F1CAC804E37196A8726F810B05000094F49EA5DE01896ACD3D1419720D10D5810" - + "50000EF3DDF8489A4ABEBDC08A58864176FB9820900000F454169ECA585A3E4E1AFBF108FD" - + "844901DB30348BA29A90E0ECB9F042F8125"; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITHOUT_RAND_AUTN = - "01020054170100000B05" - + "000094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176F" - + "B9820900000F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125"; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_CODE_RESPONSE = - "02020054170100000B0" - + "5000094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176F" - + "B9820900000F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125"; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_WRONG_LENGTH = - "01020099170100000B05" - + "000094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176FB" - + "9820900000F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125"; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_EAP_TYPE_99 = - "01020054990100000B050" - + "00094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176FB9" - + "F454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F8125820900000"; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_SUBTYPE_99 = - "01020054179900000B0500" - + "0094F49EA5DE01896ACD3D1419720D10D581050000EF3DDF8489A4ABEBDC08A58864176FB98" - + "454169ECA585A3E4E1AFBF108FD844901DB30348BA29A90E0ECB9F042F812520900000F"; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_LENGTH_OVER_DATA = - "0102001C17010000010600005CE6529028F1CAC804E37196A8726F81"; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_RAND_LENGTH_16 = - "0102001817010000010400005CE6529028F1CAC804E37196"; - private static final String EAP_AKA_CHALLENGE_REQUEST_WITH_AUTN_LENGTH_16 = - "0102001817010000020400006B8A4586ADE90000B354E13C"; - - // For calculate AKA challenge response - private static final String RES = "C79F02A59FC9A38F"; - private static final String AUT = "4A2137E6E292679DD4C3FD8AB67F13DA"; - private static final String EXPECTED_AKA_CHALLENGE_RESPONSE = + // Synchronization failure: + // Hex : DC0E33D58D39619C213367E376115315 + // Base64 encoded : 3A4z1Y05YZwhM2fjdhFTFQ== + public static final String EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS = + "2wjHnwKln8mjjxDzMKJvLBzMHtm0X9SNBsUWEAbEiAdD7xeqqZ7nsXzukRkIhd6SDZ4bj7s="; + public static final String EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE = + "3A4z1Y05YZwhM2fjdhFTFQ=="; + + // EAP-AKA challenge response + // Identifier : 0x02, same as in EAP_AKA_CHALLENGE_REQUEST + // Success + private static final String EAP_AKA_CHALLENGE_RESPONSE = "020200281701000003030040C79F02A59FC9A38F0B0500001C141BDDDE8BEF2E502FB6793808DE7C"; - private static final int SUB_ID = 0; - - @Rule - public final MockitoRule rule = MockitoJUnit.rule(); - @Mock - Context mMockContext; - - @Test - public void parseEapAkaChallengeRequest_withoutRand() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITHOUT_RAND); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); - } - - @Test - public void parseEapAkaChallengeRequest_withoutAutn() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITHOUT_AUTN); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); - } - - @Test - public void parseEapAkaChallengeRequest_withoutRandAndAutn() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITHOUT_RAND_AUTN); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); - } - - @Test - public void parseEapAkaChallengeRequest_withCodeResponse() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_CODE_RESPONSE); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); - } - - @Test - public void parseEapAkaChallengeRequest_withWrongLength() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_WRONG_LENGTH); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); + // Synchronization failure + private static final String EAP_AKA_CHALLENGE_SYNC_FAILURE = + "0202001817040000040433D58D39619C213367E376115315"; - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); - } - - @Test - public void parseEapAkaChallengeRequest_withEapType99() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_EAP_TYPE_99); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); - } - - @Test - public void parseEapAkaChallengeRequest_withSubType99() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_SUBTYPE_99); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); - } - - @Test - public void parseEapAkaChallengeRequest_withLengthOverData() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_LENGTH_OVER_DATA); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); - } - - @Test - public void parseEapAkaChallengeRequest_withRandLength16() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_RAND_LENGTH_16); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); - } - - @Test - public void parseEapAkaChallengeRequest_withAutnLength16() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_AUTN_LENGTH_16); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - assertThrows( - ServiceEntitlementException.class, - () -> message.getEapAkaChallengeResponse(mMockContext, SUB_ID)); + private static final int SUB_ID = 0; + private static final String IMSI = "234107813240779"; + private static final String MCCMNC = "23410"; + + @Rule public final MockitoRule rule = MockitoJUnit.rule(); + @Mock private TelephonyManager mMockTelephonyManager; + @Mock private TelephonyManager mMockTelephonyManagerForSubId; + private Context mContext; + + @Before + public void setUp() { + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getSystemService(TelephonyManager.class)) + .thenReturn(mMockTelephonyManager); + when(mMockTelephonyManager.createForSubscriptionId(SUB_ID)) + .thenReturn(mMockTelephonyManagerForSubId); + when(mMockTelephonyManagerForSubId.getSubscriberId()).thenReturn(IMSI); + when(mMockTelephonyManagerForSubId.getSimOperator()).thenReturn(MCCMNC); } @Test - public void parseEapAkaChallengeRequest_notValid_throwException() { - byte[] data = convertHexStringToBytes(EAP_AKA_CHALLENGE_REQUEST_WITH_WRONG_LENGTH); - String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - - EapAkaResponse message = new EapAkaResponse(encodedData); - - try { - message.getEapAkaChallengeResponse(mMockContext, SUB_ID); - fail(); - } catch (ServiceEntitlementException exception) { - assertThat(exception.getErrorCode()).isEqualTo( - ServiceEntitlementException.ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE); - assertThat(exception.getMessage()).isEqualTo("EAP-AKA Challenge message not valid!"); - } + public void generateEapAkaChallengeResponse_authSuccess() throws Exception { + EapAkaChallenge challenge = EapAkaChallenge.parseEapAkaChallenge(EAP_AKA_CHALLENGE_REQUEST); + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + String expectedResponse = + Base64.encodeToString( + convertHexStringToBytes(EAP_AKA_CHALLENGE_RESPONSE), Base64.NO_WRAP); + + EapAkaResponse challengeResponse = + EapAkaResponse.respondToEapAkaChallenge(mContext, SUB_ID, challenge); + + assertThat(challengeResponse.response()).isEqualTo(expectedResponse); } @Test - public void generateEapAkaChallengeResponse_pass() { - EapAkaResponse message = new EapAkaResponse(EAP_AKA_CHALLENGE_REQUEST_EXPECTED); - - byte[] challengeResponse = - message.generateEapAkaChallengeResponse( - convertHexStringToBytes(RES), convertHexStringToBytes(AUT)); - - assertThat(challengeResponse) - .isEqualTo(convertHexStringToBytes(EXPECTED_AKA_CHALLENGE_RESPONSE)); + public void generateEapAkaChallengeResponse_syncFailure() throws Exception { + EapAkaChallenge challenge = EapAkaChallenge.parseEapAkaChallenge(EAP_AKA_CHALLENGE_REQUEST); + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE); + String expectedResponse = + Base64.encodeToString( + convertHexStringToBytes(EAP_AKA_CHALLENGE_SYNC_FAILURE), Base64.NO_WRAP); + + EapAkaResponse challengeResponse = + EapAkaResponse.respondToEapAkaChallenge(mContext, SUB_ID, challenge); + + assertThat(challengeResponse.synchronizationFailureResponse()).isEqualTo(expectedResponse); } - private byte[] convertHexStringToBytes(String input) { + private static byte[] convertHexStringToBytes(String input) { return BaseEncoding.base16().decode(input); } } diff --git a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaSecurityContextTest.java b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaSecurityContextTest.java index 15c4c77..219737d 100644 --- a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaSecurityContextTest.java +++ b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaSecurityContextTest.java @@ -19,7 +19,7 @@ package com.android.libraries.entitlement.eapaka; import static com.google.common.truth.Truth.assertThat; import static org.testng.Assert.assertThrows; -import static org.testng.Assert.fail; +import static org.testng.Assert.expectThrows; import android.util.Base64; @@ -51,10 +51,8 @@ public class EapAkaSecurityContextTest { private static final String EXPECTED_IK = "06C4880743EF17AAA99EE7B17CEE9119"; private static final String EXPECTED_CK = "F330A26F2C1CCC1ED9B45FD48D06C516"; private static final String EXPECTED_RES = "C79F02A59FC9A38F"; - private static final String GSM_SECURITY_CONTEXT_RESPONSE_TAG_DC = - "DC08C79F02A59FC9A38F10F330A" - + "26F2C1CCC1ED9B45FD48D06C5161006C4880743EF17AAA99EE7B17CEE91190885DE920D9E1B" - + "8FBB"; + // Invalid data + private static final String GSM_SECURITY_CONTEXT_RESPONSE_TAG_DE = "DE08C79F02A59FC9A38F"; private static final String GSM_SECURITY_CONTEXT_RESPONSE_INVALID_RES_LENGTH = "DC40C79F02A59FC" + "9A38F10F330A26F2C1CCC1ED9B45FD48D06C5161006C4880743EF17AAA99EE7B17CEE911908" @@ -74,13 +72,19 @@ public class EapAkaSecurityContextTest { private static final String GSM_SECURITY_CONTEXT_RESPONSE_NO_KC_KEY = "DB08C79F02A59FC9A38F10F3" + "30A26F2C1CCC1ED9B45FD48D06C5161006C4880743EF17AAA99EE7B17CEE9119"; + // Base64 data : 3A4z1Y05YZwhM2fjdhFTFQ== + // RAW DATA : DC0E33D58D39619C213367E376115315 + // TAG : DC + // AUTS length : 0E + // AUTS : 33D58D39619C213367E376115315 + private static final String RESPONSE_SYNC_FAILURE = "3A4z1Y05YZwhM2fjdhFTFQ=="; + private static final String EXPECTED_AUTS = "33D58D39619C213367E376115315"; @Test public void parseResponseData_validResponse_pass() throws Exception { EapAkaSecurityContext securityContext = EapAkaSecurityContext.from(GSM_SECURITY_CONTEXT_RESPONSE); - assertThat(securityContext.isValid()).isTrue(); assertThat(securityContext.getIk()).isEqualTo(convertHexStringToBytes(EXPECTED_IK)); assertThat(securityContext.getCk()).isEqualTo(convertHexStringToBytes(EXPECTED_CK)); assertThat(securityContext.getRes()).isEqualTo(convertHexStringToBytes(EXPECTED_RES)); @@ -88,18 +92,18 @@ public class EapAkaSecurityContextTest { @Test public void parseResponseData_invalidWithWrongTag_throwsException() { - byte[] data = convertHexStringToBytes(GSM_SECURITY_CONTEXT_RESPONSE_TAG_DC); + byte[] data = convertHexStringToBytes(GSM_SECURITY_CONTEXT_RESPONSE_TAG_DE); String encodedData = Base64.encodeToString(data, Base64.NO_WRAP).trim(); - try { - EapAkaSecurityContext.from(encodedData); - fail(); - } catch (ServiceEntitlementException exception) { - assertThat(exception.getErrorCode()).isEqualTo( - ServiceEntitlementException.ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE); - assertThat(exception.getMessage()).isEqualTo( - "Invalid SIM EAP-AKA authentication response!"); - } + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> EapAkaSecurityContext.from(encodedData)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE); + assertThat(exception.getMessage()) + .isEqualTo("Invalid SIM EAP-AKA authentication response!"); } @Test @@ -137,7 +141,6 @@ public class EapAkaSecurityContextTest { EapAkaSecurityContext securityContext = EapAkaSecurityContext.from(encodedData); - assertThat(securityContext.isValid()).isTrue(); assertThat(securityContext.getIk()).isEqualTo(convertHexStringToBytes(EXPECTED_IK)); assertThat(securityContext.getCk()).isEqualTo(convertHexStringToBytes(EXPECTED_CK)); assertThat(securityContext.getRes()).isEqualTo(convertHexStringToBytes(EXPECTED_RES)); @@ -150,13 +153,22 @@ public class EapAkaSecurityContextTest { EapAkaSecurityContext securityContext = EapAkaSecurityContext.from(encodedData); - assertThat(securityContext.isValid()).isTrue(); assertThat(securityContext.getIk()).isEqualTo(convertHexStringToBytes(EXPECTED_IK)); assertThat(securityContext.getCk()).isEqualTo(convertHexStringToBytes(EXPECTED_CK)); assertThat(securityContext.getRes()).isEqualTo(convertHexStringToBytes(EXPECTED_RES)); } + @Test + public void parseResponseData_syncFailure() throws Exception { + EapAkaSecurityContext securityContext = EapAkaSecurityContext.from(RESPONSE_SYNC_FAILURE); + + assertThat(securityContext.getAuts()).isEqualTo(convertHexStringToBytes(EXPECTED_AUTS)); + assertThat(securityContext.getRes()).isNull(); + assertThat(securityContext.getCk()).isNull(); + assertThat(securityContext.getIk()).isNull(); + } + private byte[] convertHexStringToBytes(String input) { return BaseEncoding.base16().decode(input); } -}
\ No newline at end of file +} |