| /* |
| * 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.ike.eap.statemachine; |
| |
| import static com.android.ike.eap.EapAuthenticator.LOG; |
| import static com.android.ike.eap.message.EapMessage.EAP_CODE_REQUEST; |
| import static com.android.ike.eap.message.EapMessage.EAP_CODE_RESPONSE; |
| import static com.android.ike.eap.message.simaka.EapSimAkaAttribute.EAP_AT_MAC; |
| import static com.android.ike.eap.message.simaka.EapSimAkaAttribute.EAP_AT_NOTIFICATION; |
| |
| import android.telephony.TelephonyManager; |
| import android.util.Base64; |
| |
| import com.android.ike.eap.EapResult; |
| import com.android.ike.eap.EapResult.EapError; |
| import com.android.ike.eap.EapResult.EapResponse; |
| import com.android.ike.eap.EapSessionConfig.EapUiccConfig; |
| import com.android.ike.eap.crypto.Fips186_2Prf; |
| import com.android.ike.eap.exceptions.EapInvalidRequestException; |
| import com.android.ike.eap.exceptions.EapSilentException; |
| import com.android.ike.eap.exceptions.simaka.EapSimAkaAuthenticationFailureException; |
| import com.android.ike.eap.exceptions.simaka.EapSimAkaInvalidAttributeException; |
| import com.android.ike.eap.message.EapData; |
| import com.android.ike.eap.message.EapMessage; |
| import com.android.ike.eap.message.simaka.EapSimAkaAttribute; |
| import com.android.ike.eap.message.simaka.EapSimAkaAttribute.AtClientErrorCode; |
| import com.android.ike.eap.message.simaka.EapSimAkaAttribute.AtMac; |
| import com.android.ike.eap.message.simaka.EapSimAkaAttribute.AtNotification; |
| import com.android.ike.eap.message.simaka.EapSimAkaTypeData; |
| import com.android.ike.utils.Log; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.nio.ByteBuffer; |
| import java.security.GeneralSecurityException; |
| import java.security.MessageDigest; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| import javax.crypto.Mac; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| /** |
| * EapSimAkaMethodStateMachine represents an abstract state machine for managing EAP-SIM and EAP-AKA |
| * sessions. |
| * |
| * @see <a href="https://tools.ietf.org/html/rfc4186">RFC 4186, Extensible Authentication |
| * Protocol for Subscriber Identity Modules (EAP-SIM)</a> |
| * @see <a href="https://tools.ietf.org/html/rfc4187">RFC 4187, Extensible Authentication |
| * Protocol for Authentication and Key Agreement (EAP-AKA)</a> |
| */ |
| public abstract class EapSimAkaMethodStateMachine extends EapMethodStateMachine { |
| public static final String MASTER_KEY_GENERATION_ALG = "SHA-1"; |
| public static final String MAC_ALGORITHM_STRING = "HmacSHA1"; |
| |
| // K_encr and K_aut lengths are 16 bytes (RFC 4186#7, RFC 4187#7) |
| public static final int KEY_LEN = 16; |
| |
| // Session Key lengths are 64 bytes (RFC 4186#7, RFC 4187#7) |
| public static final int SESSION_KEY_LENGTH = 64; |
| |
| public final byte[] mKEncr = new byte[KEY_LEN]; |
| public final byte[] mKAut = new byte[KEY_LEN]; |
| public final byte[] mMsk = new byte[SESSION_KEY_LENGTH]; |
| public final byte[] mEmsk = new byte[SESSION_KEY_LENGTH]; |
| |
| @VisibleForTesting boolean mHasReceivedSimAkaNotification = false; |
| |
| final TelephonyManager mTelephonyManager; |
| final byte[] mEapIdentity; |
| final EapUiccConfig mEapUiccConfig; |
| |
| @VisibleForTesting Mac mMacAlgorithm; |
| |
| EapSimAkaMethodStateMachine( |
| TelephonyManager telephonyManager, byte[] eapIdentity, EapUiccConfig eapUiccConfig) { |
| if (telephonyManager == null) { |
| throw new IllegalArgumentException("TelephonyManager must be non-null"); |
| } else if (eapIdentity == null) { |
| throw new IllegalArgumentException("EapIdentity must be non-null"); |
| } else if (eapUiccConfig == null) { |
| throw new IllegalArgumentException("EapUiccConfig must be non-null"); |
| } |
| this.mTelephonyManager = telephonyManager; |
| this.mEapIdentity = eapIdentity; |
| this.mEapUiccConfig = eapUiccConfig; |
| |
| LOG.d( |
| this.getClass().getSimpleName(), |
| mEapUiccConfig.getClass().getSimpleName() + ":" |
| + " subId=" + mEapUiccConfig.subId |
| + " apptype=" + mEapUiccConfig.apptype); |
| } |
| |
| @Override |
| EapResult handleEapNotification(String tag, EapMessage message) { |
| return EapStateMachine.handleNotification(tag, message); |
| } |
| |
| @VisibleForTesting |
| EapResult buildClientErrorResponse( |
| int eapIdentifier, |
| int eapMethodType, |
| AtClientErrorCode clientErrorCode) { |
| EapSimAkaTypeData eapSimAkaTypeData = getEapSimAkaTypeData(clientErrorCode); |
| byte[] encodedTypeData = eapSimAkaTypeData.encode(); |
| |
| EapData eapData = new EapData(eapMethodType, encodedTypeData); |
| try { |
| EapMessage response = new EapMessage(EAP_CODE_RESPONSE, eapIdentifier, eapData); |
| return EapResult.EapResponse.getEapResponse(response); |
| } catch (EapSilentException ex) { |
| return new EapResult.EapError(ex); |
| } |
| } |
| |
| @VisibleForTesting |
| EapResult buildResponseMessage( |
| int eapType, |
| int eapSubtype, |
| int identifier, |
| List<EapSimAkaAttribute> attributes) { |
| EapSimAkaTypeData eapSimTypeData = getEapSimAkaTypeData(eapSubtype, attributes); |
| EapData eapData = new EapData(eapType, eapSimTypeData.encode()); |
| |
| try { |
| EapMessage eapMessage = new EapMessage(EAP_CODE_RESPONSE, identifier, eapData); |
| return EapResult.EapResponse.getEapResponse(eapMessage); |
| } catch (EapSilentException ex) { |
| return new EapResult.EapError(ex); |
| } |
| } |
| |
| @VisibleForTesting |
| void generateAndPersistKeys( |
| String tag, |
| MessageDigest sha1, |
| Fips186_2Prf prf, |
| byte[] mkInput) { |
| byte[] mk = sha1.digest(mkInput); |
| |
| // run mk through FIPS 186-2 |
| int outputBytes = mKEncr.length + mKAut.length + mMsk.length + mEmsk.length; |
| byte[] prfResult = prf.getRandom(mk, outputBytes); |
| |
| ByteBuffer prfResultBuffer = ByteBuffer.wrap(prfResult); |
| prfResultBuffer.get(mKEncr); |
| prfResultBuffer.get(mKAut); |
| prfResultBuffer.get(mMsk); |
| prfResultBuffer.get(mEmsk); |
| |
| // Log as hash unless PII debug mode enabled |
| LOG.d(tag, "K_encr=" + LOG.pii(mKEncr)); |
| LOG.d(tag, "K_aut=" + LOG.pii(mKAut)); |
| LOG.d(tag, "MSK=" + LOG.pii(mMsk)); |
| LOG.d(tag, "EMSK=" + LOG.pii(mEmsk)); |
| } |
| |
| @VisibleForTesting |
| byte[] processUiccAuthentication(String tag, int authType, byte[] formattedChallenge) throws |
| EapSimAkaAuthenticationFailureException { |
| String base64Challenge = Base64.encodeToString(formattedChallenge, Base64.NO_WRAP); |
| String base64Response = |
| mTelephonyManager.getIccAuthentication( |
| mEapUiccConfig.apptype, |
| authType, |
| base64Challenge); |
| |
| if (base64Response == null) { |
| String msg = "UICC authentication failed. Input: " + LOG.pii(formattedChallenge); |
| LOG.e(tag, msg); |
| throw new EapSimAkaAuthenticationFailureException(msg); |
| } |
| |
| return Base64.decode(base64Response, Base64.DEFAULT); |
| } |
| |
| @VisibleForTesting |
| boolean isValidMac(String tag, EapMessage message, EapSimAkaTypeData typeData, byte[] extraData) |
| throws GeneralSecurityException, EapSimAkaInvalidAttributeException, |
| EapSilentException { |
| mMacAlgorithm = Mac.getInstance(MAC_ALGORITHM_STRING); |
| mMacAlgorithm.init(new SecretKeySpec(mKAut, MAC_ALGORITHM_STRING)); |
| |
| byte[] mac = getMac(message.eapCode, message.eapIdentifier, typeData, extraData); |
| // attributes are 'valid', so must have AtMac |
| AtMac atMac = (AtMac) typeData.attributeMap.get(EAP_AT_MAC); |
| |
| boolean isValidMac = Arrays.equals(mac, atMac.mac); |
| if (!isValidMac) { |
| // MAC in message != calculated mac |
| LOG.e( |
| tag, |
| "Received message with invalid Mac." |
| + " expected=" + Log.byteArrayToHexString(mac) |
| + ", actual=" + Log.byteArrayToHexString(atMac.mac)); |
| } |
| |
| return isValidMac; |
| } |
| |
| @VisibleForTesting |
| byte[] getMac(int eapCode, int eapIdentifier, EapSimAkaTypeData typeData, byte[] extraData) |
| throws EapSimAkaInvalidAttributeException, EapSilentException { |
| if (mMacAlgorithm == null) { |
| throw new IllegalStateException( |
| "Can't calculate MAC before mMacAlgorithm is set in ChallengeState"); |
| } |
| |
| // cache original Mac so it can be restored after calculating the Mac |
| AtMac originalMac = (AtMac) typeData.attributeMap.get(EAP_AT_MAC); |
| typeData.attributeMap.put(EAP_AT_MAC, new AtMac()); |
| |
| byte[] typeDataWithEmptyMac = typeData.encode(); |
| EapData eapData = new EapData(getEapMethod(), typeDataWithEmptyMac); |
| EapMessage messageForMac = new EapMessage(eapCode, eapIdentifier, eapData); |
| |
| ByteBuffer buffer = ByteBuffer.allocate(messageForMac.eapLength + extraData.length); |
| buffer.put(messageForMac.encode()); |
| buffer.put(extraData); |
| byte[] mac = mMacAlgorithm.doFinal(buffer.array()); |
| |
| typeData.attributeMap.put(EAP_AT_MAC, originalMac); |
| |
| // need HMAC-SHA1-128 - first 16 bytes of SHA1 (RFC 4186#10.14, RFC 4187#10.15) |
| return Arrays.copyOfRange(mac, 0, AtMac.MAC_LENGTH); |
| } |
| |
| @VisibleForTesting |
| EapResult buildResponseMessageWithMac(int identifier, int eapSubtype, byte[] extraData) { |
| // capacity of 1 for AtMac to be added |
| return buildResponseMessageWithMac(identifier, eapSubtype, extraData, new ArrayList<>(1)); |
| } |
| |
| @VisibleForTesting |
| EapResult buildResponseMessageWithMac( |
| int identifier, int eapSubtype, byte[] extraData, List<EapSimAkaAttribute> attributes) { |
| try { |
| attributes = new ArrayList<>(attributes); |
| attributes.add(new AtMac()); |
| EapSimAkaTypeData eapSimAkaTypeData = getEapSimAkaTypeData(eapSubtype, attributes); |
| |
| byte[] mac = getMac(EAP_CODE_RESPONSE, identifier, eapSimAkaTypeData, extraData); |
| |
| eapSimAkaTypeData.attributeMap.put(EAP_AT_MAC, new AtMac(mac)); |
| EapData eapData = new EapData(getEapMethod(), eapSimAkaTypeData.encode()); |
| EapMessage eapMessage = new EapMessage(EAP_CODE_RESPONSE, identifier, eapData); |
| return EapResponse.getEapResponse(eapMessage); |
| } catch (EapSimAkaInvalidAttributeException | EapSilentException ex) { |
| // this should never happen |
| return new EapError(ex); |
| } |
| } |
| |
| @VisibleForTesting |
| EapResult handleEapSimAkaNotification( |
| String tag, |
| boolean isPreChallengeState, |
| int identifier, |
| EapSimAkaTypeData eapSimAkaTypeData) { |
| // EAP-SIM exchanges must not include more than one EAP-SIM notification round |
| // (RFC 4186#6.1, RFC 4187#6.1) |
| if (mHasReceivedSimAkaNotification) { |
| return new EapError( |
| new EapInvalidRequestException("Received multiple EAP-SIM notifications")); |
| } |
| |
| mHasReceivedSimAkaNotification = true; |
| AtNotification atNotification = |
| (AtNotification) eapSimAkaTypeData.attributeMap.get(EAP_AT_NOTIFICATION); |
| |
| LOG.d( |
| tag, |
| "Received AtNotification:" |
| + " S=" + (atNotification.isSuccessCode ? "1" : "0") |
| + " P=" + (atNotification.isPreSuccessfulChallenge ? "1" : "0") |
| + " Code=" + atNotification.notificationCode); |
| |
| // P bit of notification code is only allowed after a successful challenge round. This is |
| // only possible in the ChallengeState (RFC 4186#6.1, RFC 4187#6.1) |
| if (isPreChallengeState && !atNotification.isPreSuccessfulChallenge) { |
| return buildClientErrorResponse( |
| identifier, getEapMethod(), AtClientErrorCode.UNABLE_TO_PROCESS); |
| } |
| |
| if (atNotification.isPreSuccessfulChallenge) { |
| // AT_MAC attribute must not be included when the P bit is set (RFC 4186#9.8, |
| // RFC 4187#9.10) |
| if (eapSimAkaTypeData.attributeMap.containsKey(EAP_AT_MAC)) { |
| return buildClientErrorResponse( |
| identifier, getEapMethod(), AtClientErrorCode.UNABLE_TO_PROCESS); |
| } |
| |
| return buildResponseMessage( |
| getEapMethod(), eapSimAkaTypeData.eapSubtype, identifier, Arrays.asList()); |
| } else if (!eapSimAkaTypeData.attributeMap.containsKey(EAP_AT_MAC)) { |
| // MAC must be included for messages with their P bit not set (RFC 4186#9.8, |
| // RFC 4187#9.10) |
| return buildClientErrorResponse( |
| identifier, getEapMethod(), AtClientErrorCode.UNABLE_TO_PROCESS); |
| } |
| |
| try { |
| byte[] mac = getMac(EAP_CODE_REQUEST, identifier, eapSimAkaTypeData, new byte[0]); |
| |
| AtMac atMac = (AtMac) eapSimAkaTypeData.attributeMap.get(EAP_AT_MAC); |
| if (!Arrays.equals(mac, atMac.mac)) { |
| // MAC in message != calculated mac |
| return buildClientErrorResponse( |
| identifier, getEapMethod(), AtClientErrorCode.UNABLE_TO_PROCESS); |
| } |
| } catch (EapSilentException | EapSimAkaInvalidAttributeException ex) { |
| // We can't continue if the MAC can't be generated |
| return new EapError(ex); |
| } |
| |
| // server has been authenticated, so we can send a response |
| return buildResponseMessageWithMac(identifier, eapSimAkaTypeData.eapSubtype, new byte[0]); |
| } |
| |
| abstract EapSimAkaTypeData getEapSimAkaTypeData(AtClientErrorCode clientErrorCode); |
| abstract EapSimAkaTypeData getEapSimAkaTypeData( |
| int eapSubtype, List<EapSimAkaAttribute> attributes); |
| } |