diff options
Diffstat (limited to 'src/java/com/android/internal/net/eap/statemachine/EapAkaMethodStateMachine.java')
-rw-r--r-- | src/java/com/android/internal/net/eap/statemachine/EapAkaMethodStateMachine.java | 612 |
1 files changed, 612 insertions, 0 deletions
diff --git a/src/java/com/android/internal/net/eap/statemachine/EapAkaMethodStateMachine.java b/src/java/com/android/internal/net/eap/statemachine/EapAkaMethodStateMachine.java new file mode 100644 index 00000000..b26c8e95 --- /dev/null +++ b/src/java/com/android/internal/net/eap/statemachine/EapAkaMethodStateMachine.java @@ -0,0 +1,612 @@ +/* + * Copyright (C) 2019 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.internal.net.eap.statemachine; + +import static com.android.internal.net.eap.EapAuthenticator.LOG; +import static com.android.internal.net.eap.message.EapData.EAP_NOTIFICATION; +import static com.android.internal.net.eap.message.EapData.EAP_TYPE_AKA; +import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_FAILURE; +import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_SUCCESS; +import static com.android.internal.net.eap.message.simaka.EapAkaTypeData.EAP_AKA_AUTHENTICATION_REJECT; +import static com.android.internal.net.eap.message.simaka.EapAkaTypeData.EAP_AKA_CHALLENGE; +import static com.android.internal.net.eap.message.simaka.EapAkaTypeData.EAP_AKA_CLIENT_ERROR; +import static com.android.internal.net.eap.message.simaka.EapAkaTypeData.EAP_AKA_IDENTITY; +import static com.android.internal.net.eap.message.simaka.EapAkaTypeData.EAP_AKA_NOTIFICATION; +import static com.android.internal.net.eap.message.simaka.EapAkaTypeData.EAP_AKA_SYNCHRONIZATION_FAILURE; +import static com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.EAP_AT_ANY_ID_REQ; +import static com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.EAP_AT_AUTN; +import static com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.EAP_AT_BIDDING; +import static com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.EAP_AT_ENCR_DATA; +import static com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.EAP_AT_FULLAUTH_ID_REQ; +import static com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.EAP_AT_IV; +import static com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.EAP_AT_MAC; +import static com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.EAP_AT_PERMANENT_ID_REQ; +import static com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.EAP_AT_RAND; + +import android.annotation.Nullable; +import android.content.Context; +import android.net.eap.EapSessionConfig.EapAkaConfig; +import android.telephony.TelephonyManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.eap.EapResult; +import com.android.internal.net.eap.EapResult.EapError; +import com.android.internal.net.eap.EapResult.EapFailure; +import com.android.internal.net.eap.EapResult.EapSuccess; +import com.android.internal.net.eap.crypto.Fips186_2Prf; +import com.android.internal.net.eap.exceptions.EapInvalidRequestException; +import com.android.internal.net.eap.exceptions.EapSilentException; +import com.android.internal.net.eap.exceptions.simaka.EapAkaInvalidAuthenticationResponse; +import com.android.internal.net.eap.exceptions.simaka.EapSimAkaAuthenticationFailureException; +import com.android.internal.net.eap.exceptions.simaka.EapSimAkaIdentityUnavailableException; +import com.android.internal.net.eap.exceptions.simaka.EapSimAkaInvalidAttributeException; +import com.android.internal.net.eap.exceptions.simaka.EapSimAkaInvalidLengthException; +import com.android.internal.net.eap.message.EapData.EapMethod; +import com.android.internal.net.eap.message.EapMessage; +import com.android.internal.net.eap.message.simaka.EapAkaTypeData; +import com.android.internal.net.eap.message.simaka.EapAkaTypeData.EapAkaTypeDataDecoder; +import com.android.internal.net.eap.message.simaka.EapSimAkaAttribute; +import com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.AtAutn; +import com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.AtAuts; +import com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.AtBidding; +import com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.AtClientErrorCode; +import com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.AtIdentity; +import com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.AtRandAka; +import com.android.internal.net.eap.message.simaka.EapSimAkaAttribute.AtRes; +import com.android.internal.net.eap.message.simaka.EapSimAkaTypeData.DecodeResult; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * EapAkaMethodStateMachine represents the valid paths possible for the EAP-AKA protocol. + * + * <p>EAP-AKA sessions will always follow the path: + * + * Created --+--> Identity --+--> Challenge --> Final + * | | + * +---------------+ + * + * Note: If the EAP-Request/AKA-Challenge message contains an AUTN with an invalid sequence number, + * the peer will indicate a synchronization failure to the server and a new challenge will be + * attempted. + * + * Note: EAP-Request/Notification messages can be received at any point in the above state machine + * At most one EAP-AKA/Notification message is allowed per EAP-AKA session. + * + * @see <a href="https://tools.ietf.org/html/rfc4187">RFC 4187, Extensible Authentication + * Protocol for Authentication and Key Agreement (EAP-AKA)</a> + */ +class EapAkaMethodStateMachine extends EapSimAkaMethodStateMachine { + private static final String TAG = EapAkaMethodStateMachine.class.getSimpleName(); + + // EAP-AKA identity prefix (RFC 4187#4.1.1.6) + private static final String AKA_IDENTITY_PREFIX = "0"; + + private final EapAkaTypeDataDecoder mEapAkaTypeDataDecoder; + private final boolean mSupportsEapAkaPrime; + + protected EapAkaMethodStateMachine( + Context context, byte[] eapIdentity, EapAkaConfig eapAkaConfig) { + this(context, eapIdentity, eapAkaConfig, false); + } + + EapAkaMethodStateMachine( + Context context, + byte[] eapIdentity, + EapAkaConfig eapAkaConfig, + boolean supportsEapAkaPrime) { + this( + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE), + eapIdentity, + eapAkaConfig, + EapAkaTypeData.getEapAkaTypeDataDecoder(), + supportsEapAkaPrime); + } + + @VisibleForTesting + protected EapAkaMethodStateMachine( + TelephonyManager telephonyManager, + byte[] eapIdentity, + EapAkaConfig eapAkaConfig, + EapAkaTypeDataDecoder eapAkaTypeDataDecoder, + boolean supportsEapAkaPrime) { + super( + telephonyManager.createForSubscriptionId(eapAkaConfig.subId), + eapIdentity, + eapAkaConfig); + mEapAkaTypeDataDecoder = eapAkaTypeDataDecoder; + mSupportsEapAkaPrime = supportsEapAkaPrime; + + transitionTo(new CreatedState()); + } + + @Override + @EapMethod + int getEapMethod() { + return EAP_TYPE_AKA; + } + + protected DecodeResult<EapAkaTypeData> decode(byte[] typeData) { + return mEapAkaTypeDataDecoder.decode(typeData); + } + + /** + * This exists so we can override the identity prefix in the EapAkaPrimeMethodStateMachine. + * + * @return the Identity prefix for this EAP method + */ + protected String getIdentityPrefix() { + return AKA_IDENTITY_PREFIX; + } + + protected ChallengeState buildChallengeState() { + return new ChallengeState(); + } + + protected ChallengeState buildChallengeState(byte[] identity) { + return new ChallengeState(identity); + } + + protected class CreatedState extends EapMethodState { + private final String mTAG = CreatedState.class.getSimpleName(); + + public EapResult process(EapMessage message) { + EapResult result = handleEapSuccessFailureNotification(mTAG, message); + if (result != null) { + return result; + } + + DecodeResult<? extends EapAkaTypeData> decodeResult = + decode(message.eapData.eapTypeData); + if (!decodeResult.isSuccessfulDecode()) { + return buildClientErrorResponse( + message.eapIdentifier, getEapMethod(), decodeResult.atClientErrorCode); + } + + EapAkaTypeData eapAkaTypeData = decodeResult.eapTypeData; + switch (eapAkaTypeData.eapSubtype) { + case EAP_AKA_IDENTITY: + return transitionAndProcess(new IdentityState(), message); + case EAP_AKA_CHALLENGE: + return transitionAndProcess(buildChallengeState(), message); + case EAP_AKA_NOTIFICATION: + return handleEapSimAkaNotification( + mTAG, + true, // isPreChallengeState + message.eapIdentifier, + eapAkaTypeData); + default: + return buildClientErrorResponse( + message.eapIdentifier, + getEapMethod(), + AtClientErrorCode.UNABLE_TO_PROCESS); + } + } + } + + protected class IdentityState extends EapMethodState { + private final String mTAG = IdentityState.class.getSimpleName(); + + private byte[] mIdentity; + + public EapResult process(EapMessage message) { + EapResult result = handleEapSuccessFailureNotification(mTAG, message); + if (result != null) { + return result; + } + + DecodeResult<? extends EapAkaTypeData> decodeResult = + decode(message.eapData.eapTypeData); + if (!decodeResult.isSuccessfulDecode()) { + return buildClientErrorResponse( + message.eapIdentifier, getEapMethod(), decodeResult.atClientErrorCode); + } + + EapAkaTypeData eapAkaTypeData = decodeResult.eapTypeData; + switch (eapAkaTypeData.eapSubtype) { + case EAP_AKA_IDENTITY: + break; + case EAP_AKA_CHALLENGE: + return transitionAndProcess(buildChallengeState(mIdentity), message); + case EAP_AKA_NOTIFICATION: + return handleEapSimAkaNotification( + mTAG, + true, // isPreChallengeState + message.eapIdentifier, + eapAkaTypeData); + default: + return buildClientErrorResponse( + message.eapIdentifier, + getEapMethod(), + AtClientErrorCode.UNABLE_TO_PROCESS); + } + + if (!isValidIdentityAttributes(eapAkaTypeData)) { + LOG.e(mTAG, "Invalid attributes: " + eapAkaTypeData.attributeMap.keySet()); + return buildClientErrorResponse( + message.eapIdentifier, + EAP_TYPE_AKA, + AtClientErrorCode.UNABLE_TO_PROCESS); + } + + String imsi = mTelephonyManager.getSubscriberId(); + if (imsi == null) { + LOG.e(mTAG, "Unable to get IMSI for subId=" + mEapUiccConfig.subId); + return new EapError( + new EapSimAkaIdentityUnavailableException( + "IMSI for subId (" + mEapUiccConfig.subId + ") not available")); + } + String identityString = getIdentityPrefix() + imsi; + mIdentity = identityString.getBytes(StandardCharsets.US_ASCII); + LOG.d(mTAG, "EAP-AKA/Identity=" + LOG.pii(identityString)); + + AtIdentity atIdentity; + try { + atIdentity = AtIdentity.getAtIdentity(mIdentity); + } catch (EapSimAkaInvalidAttributeException ex) { + LOG.wtf(mTAG, "Exception thrown while making AtIdentity attribute", ex); + return new EapError(ex); + } + + return buildResponseMessage( + getEapMethod(), + EAP_AKA_IDENTITY, + message.eapIdentifier, + Arrays.asList(atIdentity)); + } + + private boolean isValidIdentityAttributes(EapAkaTypeData eapAkaTypeData) { + Set<Integer> attrs = eapAkaTypeData.attributeMap.keySet(); + + // exactly one ID request type required + int idRequests = 0; + idRequests += attrs.contains(EAP_AT_PERMANENT_ID_REQ) ? 1 : 0; + idRequests += attrs.contains(EAP_AT_ANY_ID_REQ) ? 1 : 0; + idRequests += attrs.contains(EAP_AT_FULLAUTH_ID_REQ) ? 1 : 0; + + if (idRequests != 1) { + return false; + } + + // can't contain mac, iv, encr data + if (attrs.contains(EAP_AT_MAC) + || attrs.contains(EAP_AT_IV) + || attrs.contains(EAP_AT_ENCR_DATA)) { + return false; + } + return true; + } + } + + protected class ChallengeState extends EapMethodState { + private final String mTAG = ChallengeState.class.getSimpleName(); + + @VisibleForTesting boolean mHadSuccessfulChallenge = false; + @VisibleForTesting protected final byte[] mIdentity; + + // IK and CK lengths defined as 16B (RFC 4187#1) + private final int mIkLenBytes = 16; + private final int mCkLenBytes = 16; + + // Tags for Successful and Synchronization responses + private final byte mSuccess = (byte) 0xDB; + private final byte mSynchronization = (byte) 0xDC; + + ChallengeState() { + // use the EAP-Identity for the default value (RFC 4187#7) + this(mEapIdentity); + } + + ChallengeState(byte[] identity) { + this.mIdentity = identity; + } + + public EapResult process(EapMessage message) { + if (message.eapCode == EAP_CODE_SUCCESS) { + if (!mHadSuccessfulChallenge) { + LOG.e(mTAG, "Received unexpected EAP-Success"); + return new EapError( + new EapInvalidRequestException( + "Received an EAP-Success in the ChallengeState")); + } + transitionTo(new FinalState()); + return new EapSuccess(mMsk, mEmsk); + } else if (message.eapCode == EAP_CODE_FAILURE) { + transitionTo(new FinalState()); + return new EapFailure(); + } else if (message.eapData.eapType == EAP_NOTIFICATION) { + return handleEapNotification(mTAG, message); + } + + if (message.eapData.eapType != getEapMethod()) { + return new EapError(new EapInvalidRequestException( + "Expected EAP Type " + getEapMethod() + + ", received " + message.eapData.eapType)); + } + + DecodeResult<? extends EapAkaTypeData> decodeResult = + decode(message.eapData.eapTypeData); + if (!decodeResult.isSuccessfulDecode()) { + return buildClientErrorResponse( + message.eapIdentifier, getEapMethod(), decodeResult.atClientErrorCode); + } + + EapAkaTypeData eapAkaTypeData = decodeResult.eapTypeData; + switch (eapAkaTypeData.eapSubtype) { + case EAP_AKA_CHALLENGE: + break; + case EAP_AKA_NOTIFICATION: + return handleEapSimAkaNotification( + mTAG, + false, // isPreChallengeState + message.eapIdentifier, + eapAkaTypeData); + default: + return buildClientErrorResponse( + message.eapIdentifier, + getEapMethod(), + AtClientErrorCode.UNABLE_TO_PROCESS); + } + + if (!isValidChallengeAttributes(eapAkaTypeData)) { + LOG.e(mTAG, "Invalid attributes: " + eapAkaTypeData.attributeMap.keySet()); + return buildClientErrorResponse( + message.eapIdentifier, getEapMethod(), AtClientErrorCode.UNABLE_TO_PROCESS); + } + + return handleChallengeAuthentication(message, eapAkaTypeData); + } + + protected EapResult handleChallengeAuthentication( + EapMessage message, EapAkaTypeData eapAkaTypeData) { + RandChallengeResult result; + try { + result = getRandChallengeResult(eapAkaTypeData); + } catch (EapAkaInvalidAuthenticationResponse ex) { + return new EapError(ex); + } catch (EapSimAkaInvalidLengthException | BufferUnderflowException ex) { + LOG.e(mTAG, "Invalid response returned from SIM", ex); + return buildClientErrorResponse( + message.eapIdentifier, getEapMethod(), AtClientErrorCode.UNABLE_TO_PROCESS); + } catch (EapSimAkaAuthenticationFailureException ex) { + // Return EAP-Response/AKA-Authentication-Reject when the AUTN is rejected + // (RFC 4187#6.3.1) + return buildAuthenticationRejectMessage(message.eapIdentifier); + } + + if (!result.isSuccessfulResult()) { + try { + return buildResponseMessage( + getEapMethod(), + EAP_AKA_SYNCHRONIZATION_FAILURE, + message.eapIdentifier, + Arrays.asList(new AtAuts(result.auts))); + } catch (EapSimAkaInvalidAttributeException ex) { + LOG.wtf(mTAG, "Error creating an AtAuts attr", ex); + return new EapError(ex); + } + } + + EapResult eapResult = + generateAndPersistEapAkaKeys(result, message.eapIdentifier, eapAkaTypeData); + if (eapResult != null) { + return eapResult; + } + + try { + if (!isValidMac(mTAG, message, eapAkaTypeData, new byte[0])) { + return buildClientErrorResponse( + message.eapIdentifier, + getEapMethod(), + AtClientErrorCode.UNABLE_TO_PROCESS); + } + } catch (GeneralSecurityException + | EapSilentException + | EapSimAkaInvalidAttributeException ex) { + // if the MAC can't be generated, we can't continue + LOG.e(mTAG, "Error computing MAC for EapMessage", ex); + return new EapError(ex); + } + + // before sending a response, check for bidding-down attacks (RFC 5448#4) + if (mSupportsEapAkaPrime) { + AtBidding atBidding = (AtBidding) eapAkaTypeData.attributeMap.get(EAP_AT_BIDDING); + if (atBidding != null && atBidding.doesServerSupportEapAkaPrime) { + LOG.w( + mTAG, + "Potential bidding down attack. AT_BIDDING attr included and EAP-AKA'" + + " is supported"); + return buildAuthenticationRejectMessage(message.eapIdentifier); + } + } + + // server has been authenticated, so we can send a response + try { + mHadSuccessfulChallenge = true; + return buildResponseMessageWithMac( + message.eapIdentifier, + EAP_AKA_CHALLENGE, + new byte[0], + Arrays.asList(AtRes.getAtRes(result.res))); + } catch (EapSimAkaInvalidAttributeException ex) { + LOG.wtf(mTAG, "Error creating AtRes value", ex); + return new EapError(ex); + } + } + + @VisibleForTesting + class RandChallengeResult { + public final byte[] res; + public final byte[] ik; + public final byte[] ck; + public final byte[] auts; + + RandChallengeResult(byte[] res, byte[] ik, byte[] ck) + throws EapSimAkaInvalidLengthException { + if (!AtRes.isValidResLen(res.length)) { + throw new EapSimAkaInvalidLengthException("Invalid RES length"); + } else if (ik.length != mIkLenBytes) { + throw new EapSimAkaInvalidLengthException("Invalid IK length"); + } else if (ck.length != mCkLenBytes) { + throw new EapSimAkaInvalidLengthException("Invalid CK length"); + } + + this.res = res; + this.ik = ik; + this.ck = ck; + this.auts = null; + } + + RandChallengeResult(byte[] auts) throws EapSimAkaInvalidLengthException { + if (auts.length != AtAuts.AUTS_LENGTH) { + throw new EapSimAkaInvalidLengthException("Invalid AUTS length"); + } + + this.res = null; + this.ik = null; + this.ck = null; + this.auts = auts; + } + + private boolean isSuccessfulResult() { + return res != null && ik != null && ck != null; + } + } + + private boolean isValidChallengeAttributes(EapAkaTypeData eapAkaTypeData) { + Set<Integer> attrs = eapAkaTypeData.attributeMap.keySet(); + + // must contain: AT_RAND, AT_AUTN, AT_MAC + return attrs.contains(EAP_AT_RAND) + && attrs.contains(EAP_AT_AUTN) + && attrs.contains(EAP_AT_MAC); + } + + private RandChallengeResult getRandChallengeResult(EapAkaTypeData eapAkaTypeData) + throws EapSimAkaAuthenticationFailureException, EapSimAkaInvalidLengthException { + AtRandAka atRandAka = (AtRandAka) eapAkaTypeData.attributeMap.get(EAP_AT_RAND); + AtAutn atAutn = (AtAutn) eapAkaTypeData.attributeMap.get(EAP_AT_AUTN); + + // pre-Base64 formatting needs to be: [Length][RAND][Length][AUTN] + int randLen = atRandAka.rand.length; + int autnLen = atAutn.autn.length; + ByteBuffer formattedChallenge = ByteBuffer.allocate(1 + randLen + 1 + autnLen); + formattedChallenge.put((byte) randLen); + formattedChallenge.put(atRandAka.rand); + formattedChallenge.put((byte) autnLen); + formattedChallenge.put(atAutn.autn); + + byte[] challengeResponse = + processUiccAuthentication( + mTAG, + TelephonyManager.AUTHTYPE_EAP_AKA, + formattedChallenge.array()); + ByteBuffer buffer = ByteBuffer.wrap(challengeResponse); + byte tag = buffer.get(); + + switch (tag) { + case mSuccess: + // response format: [tag][RES length][RES][IK length][IK][CK length][CK] + break; + case mSynchronization: + // response format: [tag][AUTS length][AUTS] + byte[] auts = new byte[Byte.toUnsignedInt(buffer.get())]; + buffer.get(auts); + + LOG.i(mTAG, "Synchronization Failure"); + LOG.d( + mTAG, + "RAND=" + LOG.pii(atRandAka.rand) + + " AUTN=" + LOG.pii(atAutn.autn) + + " AUTS=" + LOG.pii(auts)); + + return new RandChallengeResult(auts); + default: + throw new EapAkaInvalidAuthenticationResponse( + "Invalid tag for UICC response: " + String.format("%02X", tag)); + } + + byte[] res = new byte[Byte.toUnsignedInt(buffer.get())]; + buffer.get(res); + + byte[] ik = new byte[Byte.toUnsignedInt(buffer.get())]; + buffer.get(ik); + + byte[] ck = new byte[Byte.toUnsignedInt(buffer.get())]; + buffer.get(ck); + + LOG.d( + mTAG, + "RAND=" + LOG.pii(atRandAka.rand) + + " AUTN=" + LOG.pii(atAutn.autn) + + " RES=" + LOG.pii(res) + + " IK=" + LOG.pii(ik) + + " CK=" + LOG.pii(ck)); + + return new RandChallengeResult(res, ik, ck); + } + + protected EapResult buildAuthenticationRejectMessage(int eapIdentifier) { + return buildResponseMessage( + getEapMethod(), + EAP_AKA_AUTHENTICATION_REJECT, + eapIdentifier, + new ArrayList<>()); + } + + @Nullable + protected EapResult generateAndPersistEapAkaKeys( + RandChallengeResult result, int eapIdentifier, EapAkaTypeData eapAkaTypeData) { + try { + MessageDigest sha1 = MessageDigest.getInstance(MASTER_KEY_GENERATION_ALG); + byte[] mkInputData = getMkInputData(result); + generateAndPersistKeys(mTAG, sha1, new Fips186_2Prf(), mkInputData); + return null; + } catch (NoSuchAlgorithmException | BufferUnderflowException ex) { + LOG.e(mTAG, "Error while creating keys", ex); + return buildClientErrorResponse( + eapIdentifier, EAP_TYPE_AKA, AtClientErrorCode.UNABLE_TO_PROCESS); + } + } + + private byte[] getMkInputData(RandChallengeResult result) { + int numInputBytes = mIdentity.length + result.ik.length + result.ck.length; + ByteBuffer buffer = ByteBuffer.allocate(numInputBytes); + buffer.put(mIdentity); + buffer.put(result.ik); + buffer.put(result.ck); + return buffer.array(); + } + } + + EapAkaTypeData getEapSimAkaTypeData(AtClientErrorCode clientErrorCode) { + return new EapAkaTypeData(EAP_AKA_CLIENT_ERROR, Arrays.asList(clientErrorCode)); + } + + EapAkaTypeData getEapSimAkaTypeData(int eapSubtype, List<EapSimAkaAttribute> attributes) { + return new EapAkaTypeData(eapSubtype, attributes); + } +} |