blob: 03c419dbf6e72e33c3e7d0abd7f74b792c2074d2 [file] [log] [blame]
/*
* 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.ipsec.ike;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_CHILD_SA_NOT_FOUND;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_SYNTAX;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_NO_ADDITIONAL_SAS;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ErrorType;
import static com.android.internal.net.ipsec.ike.message.IkeHeader.EXCHANGE_TYPE_INFORMATIONAL;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.DECODE_STATUS_OK;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.DECODE_STATUS_PARTIAL;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.DECODE_STATUS_PROTECTED_ERROR;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_REKEY_SA;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_CP;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_DELETE;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_NOTIFY;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_VENDOR;
import android.annotation.IntDef;
import android.content.Context;
import android.net.IpSecManager;
import android.net.IpSecManager.ResourceUnavailableException;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.net.ipsec.ike.ChildSessionCallback;
import android.net.ipsec.ike.ChildSessionOptions;
import android.net.ipsec.ike.IkeSaProposal;
import android.net.ipsec.ike.IkeSessionCallback;
import android.net.ipsec.ike.IkeSessionOptions;
import android.net.ipsec.ike.IkeSessionOptions.IkeAuthConfig;
import android.net.ipsec.ike.IkeSessionOptions.IkeAuthDigitalSignRemoteConfig;
import android.net.ipsec.ike.IkeSessionOptions.IkeAuthPskConfig;
import android.net.ipsec.ike.exceptions.IkeException;
import android.net.ipsec.ike.exceptions.IkeInternalException;
import android.net.ipsec.ike.exceptions.IkeProtocolException;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.LongSparseArray;
import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.eap.EapAuthenticator;
import com.android.internal.net.eap.IEapCallback;
import com.android.internal.net.ipsec.ike.ChildSessionStateMachine.CreateChildSaHelper;
import com.android.internal.net.ipsec.ike.IkeLocalRequestScheduler.ChildLocalRequest;
import com.android.internal.net.ipsec.ike.IkeLocalRequestScheduler.LocalRequest;
import com.android.internal.net.ipsec.ike.SaRecord.IkeSaRecord;
import com.android.internal.net.ipsec.ike.crypto.IkeCipher;
import com.android.internal.net.ipsec.ike.crypto.IkeMacIntegrity;
import com.android.internal.net.ipsec.ike.crypto.IkeMacPrf;
import com.android.internal.net.ipsec.ike.exceptions.AuthenticationFailedException;
import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException;
import com.android.internal.net.ipsec.ike.exceptions.NoValidProposalChosenException;
import com.android.internal.net.ipsec.ike.message.IkeAuthDigitalSignPayload;
import com.android.internal.net.ipsec.ike.message.IkeAuthPayload;
import com.android.internal.net.ipsec.ike.message.IkeAuthPskPayload;
import com.android.internal.net.ipsec.ike.message.IkeCertPayload;
import com.android.internal.net.ipsec.ike.message.IkeCertX509CertPayload;
import com.android.internal.net.ipsec.ike.message.IkeDeletePayload;
import com.android.internal.net.ipsec.ike.message.IkeEapPayload;
import com.android.internal.net.ipsec.ike.message.IkeHeader;
import com.android.internal.net.ipsec.ike.message.IkeHeader.ExchangeType;
import com.android.internal.net.ipsec.ike.message.IkeIdPayload;
import com.android.internal.net.ipsec.ike.message.IkeInformationalPayload;
import com.android.internal.net.ipsec.ike.message.IkeKePayload;
import com.android.internal.net.ipsec.ike.message.IkeMessage;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResult;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResultError;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResultOk;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResultPartial;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResultProtectedError;
import com.android.internal.net.ipsec.ike.message.IkeNoncePayload;
import com.android.internal.net.ipsec.ike.message.IkeNotifyPayload;
import com.android.internal.net.ipsec.ike.message.IkePayload;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.DhGroupTransform;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.IkeProposal;
import com.android.internal.net.ipsec.ike.message.IkeTsPayload;
import com.android.internal.net.ipsec.ike.utils.Retransmitter;
import com.android.internal.util.State;
import dalvik.system.CloseGuard;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.security.GeneralSecurityException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
* IkeSessionStateMachine tracks states and manages exchanges of this IKE session.
*
* <p>IkeSessionStateMachine has two types of states. One type are states where there is no ongoing
* procedure affecting IKE session (non-procedure state), including Initial, Idle and Receiving. All
* other states are "procedure" states which are named as follows:
*
* <pre>
* State Name = [Procedure Type] + [Exchange Initiator] + [Exchange Type].
* - An IKE procedure consists of one or two IKE exchanges:
* Procedure Type = {CreateIke | DeleteIke | Info | RekeyIke | SimulRekeyIke}.
* - Exchange Initiator indicates whether local or remote peer is the exchange initiator:
* Exchange Initiator = {Local | Remote}
* - Exchange type defines the function of this exchange. To make it more descriptive, we separate
* Delete Exchange from generic Informational Exchange:
* Exchange Type = {IkeInit | IkeAuth | Create | Delete | Info}
* </pre>
*/
public class IkeSessionStateMachine extends AbstractSessionStateMachine {
private static final String TAG = "IkeSessionStateMachine";
// TODO: b/140579254 Allow users to configure fragment size.
// Default fragment size in bytes.
@VisibleForTesting static final int DEFAULT_FRAGMENT_SIZE = 1280;
// TODO: Add SA_HARD_LIFETIME_MS
// Time after which IKE SA needs to be rekeyed
@VisibleForTesting static final long SA_SOFT_LIFETIME_MS = TimeUnit.HOURS.toMillis(3L);
// Default delay time for retrying a request
@VisibleForTesting static final long RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(15L);
// Close IKE Session when all responses during this time were TEMPORARY_FAILURE(s). This
// indicates that something has gone wrong, and we are out of sync.
@VisibleForTesting
static final long TEMP_FAILURE_RETRY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(5L);
// TODO: Allow users to configure IKE lifetime
// Package private IKE exchange subtypes describe the specific function of a IKE
// request/response exchange. It helps IkeSessionStateMachine to do message validation according
// to the subtype specific rules.
@Retention(RetentionPolicy.SOURCE)
@IntDef({
IKE_EXCHANGE_SUBTYPE_INVALID,
IKE_EXCHANGE_SUBTYPE_IKE_INIT,
IKE_EXCHANGE_SUBTYPE_IKE_AUTH,
IKE_EXCHANGE_SUBTYPE_DELETE_IKE,
IKE_EXCHANGE_SUBTYPE_DELETE_CHILD,
IKE_EXCHANGE_SUBTYPE_REKEY_IKE,
IKE_EXCHANGE_SUBTYPE_REKEY_CHILD,
IKE_EXCHANGE_SUBTYPE_GENERIC_INFO
})
@interface IkeExchangeSubType {}
static final int IKE_EXCHANGE_SUBTYPE_INVALID = 0;
static final int IKE_EXCHANGE_SUBTYPE_IKE_INIT = 1;
static final int IKE_EXCHANGE_SUBTYPE_IKE_AUTH = 2;
static final int IKE_EXCHANGE_SUBTYPE_CREATE_CHILD = 3;
static final int IKE_EXCHANGE_SUBTYPE_DELETE_IKE = 4;
static final int IKE_EXCHANGE_SUBTYPE_DELETE_CHILD = 5;
static final int IKE_EXCHANGE_SUBTYPE_REKEY_IKE = 6;
static final int IKE_EXCHANGE_SUBTYPE_REKEY_CHILD = 7;
static final int IKE_EXCHANGE_SUBTYPE_GENERIC_INFO = 8;
private static final SparseArray<String> EXCHANGE_SUBTYPE_TO_STRING;
static {
EXCHANGE_SUBTYPE_TO_STRING = new SparseArray<>();
EXCHANGE_SUBTYPE_TO_STRING.put(IKE_EXCHANGE_SUBTYPE_INVALID, "Invalid");
EXCHANGE_SUBTYPE_TO_STRING.put(IKE_EXCHANGE_SUBTYPE_IKE_INIT, "IKE INIT");
EXCHANGE_SUBTYPE_TO_STRING.put(IKE_EXCHANGE_SUBTYPE_IKE_AUTH, "IKE AUTH");
EXCHANGE_SUBTYPE_TO_STRING.put(IKE_EXCHANGE_SUBTYPE_CREATE_CHILD, "Create Child");
EXCHANGE_SUBTYPE_TO_STRING.put(IKE_EXCHANGE_SUBTYPE_DELETE_IKE, "Delete IKE");
EXCHANGE_SUBTYPE_TO_STRING.put(IKE_EXCHANGE_SUBTYPE_DELETE_CHILD, "Delete Child");
EXCHANGE_SUBTYPE_TO_STRING.put(IKE_EXCHANGE_SUBTYPE_REKEY_IKE, "Rekey IKE");
EXCHANGE_SUBTYPE_TO_STRING.put(IKE_EXCHANGE_SUBTYPE_REKEY_CHILD, "Rekey Child");
EXCHANGE_SUBTYPE_TO_STRING.put(IKE_EXCHANGE_SUBTYPE_GENERIC_INFO, "Generic Info");
}
/** Package private signals accessible for testing code. */
private static final int CMD_GENERAL_BASE = CMD_PRIVATE_BASE;
/** Receive encoded IKE packet on IkeSessionStateMachine. */
static final int CMD_RECEIVE_IKE_PACKET = CMD_GENERAL_BASE + 1;
/** Receive encoded IKE packet with unrecognized IKE SPI on IkeSessionStateMachine. */
static final int CMD_RECEIVE_PACKET_INVALID_IKE_SPI = CMD_GENERAL_BASE + 2;
/** Receive an remote request for a Child procedure. */
static final int CMD_RECEIVE_REQUEST_FOR_CHILD = CMD_GENERAL_BASE + 3;
/** Receive payloads from Child Session for building an outbound IKE message. */
static final int CMD_OUTBOUND_CHILD_PAYLOADS_READY = CMD_GENERAL_BASE + 4;
/** A Child Session has finished its procedure. */
static final int CMD_CHILD_PROCEDURE_FINISHED = CMD_GENERAL_BASE + 5;
/** Send request/response payloads to ChildSessionStateMachine for further processing. */
static final int CMD_HANDLE_FIRST_CHILD_NEGOTIATION = CMD_GENERAL_BASE + 6;
/** Receive a local request to execute from the scheduler */
static final int CMD_EXECUTE_LOCAL_REQ = CMD_GENERAL_BASE + 7;
/** Trigger a retransmission. */
public static final int CMD_RETRANSMIT = CMD_GENERAL_BASE + 8;
/** Send EAP request payloads to EapAuthenticator for further processing. */
static final int CMD_EAP_START_EAP_AUTH = CMD_GENERAL_BASE + 9;
/** Send the outbound IKE-wrapped EAP-Response message. */
static final int CMD_EAP_OUTBOUND_MSG_READY = CMD_GENERAL_BASE + 10;
/** Proxy to IkeSessionStateMachine handler to notify of errors */
static final int CMD_EAP_ERRORED = CMD_GENERAL_BASE + 11;
/** Proxy to IkeSessionStateMachine handler to notify of failures */
static final int CMD_EAP_FAILED = CMD_GENERAL_BASE + 12;
/** Proxy to IkeSessionStateMachine handler to notify of success, to continue to post-auth */
static final int CMD_EAP_FINISH_EAP_AUTH = CMD_GENERAL_BASE + 14;
/** Force state machine to a target state for testing purposes. */
static final int CMD_FORCE_TRANSITION = CMD_GENERAL_BASE + 99;
static final int CMD_IKE_LOCAL_REQUEST_BASE = CMD_GENERAL_BASE + CMD_CATEGORY_SIZE;
static final int CMD_LOCAL_REQUEST_CREATE_IKE = CMD_IKE_LOCAL_REQUEST_BASE + 1;
static final int CMD_LOCAL_REQUEST_DELETE_IKE = CMD_IKE_LOCAL_REQUEST_BASE + 2;
static final int CMD_LOCAL_REQUEST_REKEY_IKE = CMD_IKE_LOCAL_REQUEST_BASE + 3;
static final int CMD_LOCAL_REQUEST_INFO = CMD_IKE_LOCAL_REQUEST_BASE + 4;
private static final SparseArray<String> CMD_TO_STR;
static {
CMD_TO_STR = new SparseArray<>();
CMD_TO_STR.put(CMD_RECEIVE_IKE_PACKET, "Rcv packet");
CMD_TO_STR.put(CMD_RECEIVE_PACKET_INVALID_IKE_SPI, "Rcv invalid IKE SPI");
CMD_TO_STR.put(CMD_RECEIVE_REQUEST_FOR_CHILD, "Rcv Child request");
CMD_TO_STR.put(CMD_OUTBOUND_CHILD_PAYLOADS_READY, "Out child payloads ready");
CMD_TO_STR.put(CMD_CHILD_PROCEDURE_FINISHED, "Child procedure finished");
CMD_TO_STR.put(CMD_HANDLE_FIRST_CHILD_NEGOTIATION, "Negotiate first Child");
CMD_TO_STR.put(CMD_EXECUTE_LOCAL_REQ, "Execute local request");
CMD_TO_STR.put(CMD_RETRANSMIT, "Retransmit");
CMD_TO_STR.put(CMD_EAP_START_EAP_AUTH, "Start EAP");
CMD_TO_STR.put(CMD_EAP_OUTBOUND_MSG_READY, "EAP outbound msg ready");
CMD_TO_STR.put(CMD_EAP_ERRORED, "EAP errored");
CMD_TO_STR.put(CMD_EAP_FAILED, "EAP failed");
CMD_TO_STR.put(CMD_EAP_FINISH_EAP_AUTH, "Finish EAP");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_CREATE_IKE, "Create IKE");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_DELETE_IKE, "Delete IKE");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_REKEY_IKE, "Rekey IKE");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_INFO, "Info");
}
private final IkeSessionOptions mIkeSessionOptions;
/** Map that stores all IkeSaRecords, keyed by locally generated IKE SPI. */
private final LongSparseArray<IkeSaRecord> mLocalSpiToIkeSaRecordMap;
/**
* Map that stores all ChildSessionStateMachines, keyed by remotely generated Child SPI for
* sending IPsec packet. Different SPIs may point to the same ChildSessionStateMachine if this
* Child Session is doing Rekey.
*/
private final SparseArray<ChildSessionStateMachine> mRemoteSpiToChildSessionMap;
private final Context mContext;
private final IpSecManager mIpSecManager;
private final IkeLocalRequestScheduler mScheduler;
private final Executor mUserCbExecutor;
private final IkeSessionCallback mIkeSessionCallback;
private final IkeEapAuthenticatorFactory mEapAuthenticatorFactory;
private final TempFailureHandler mTempFailHandler;
@VisibleForTesting
@GuardedBy("mChildCbToSessions")
final HashMap<ChildSessionCallback, ChildSessionStateMachine> mChildCbToSessions =
new HashMap<>();
/**
* Package private socket that sends and receives encoded IKE message. Initialized in Initial
* State.
*/
@VisibleForTesting IkeSocket mIkeSocket;
/** Local address assigned on device. Initialized in Initial State. */
@VisibleForTesting InetAddress mLocalAddress;
/** Remote address configured by users. Initialized in Initial State. */
@VisibleForTesting InetAddress mRemoteAddress;
/** Local port assigned on device. Initialized in Initial State. */
@VisibleForTesting int mLocalPort;
/** Indicates if local node is behind a NAT. */
@VisibleForTesting boolean mIsLocalBehindNat;
/** Indicates if remote node is behind a NAT. */
@VisibleForTesting boolean mIsRemoteBehindNat;
/** Indicates if both sides support fragmentation. Set in IKE INIT */
@VisibleForTesting boolean mSupportFragment;
/** Package private IkeSaProposal that represents the negotiated IKE SA proposal. */
@VisibleForTesting IkeSaProposal mSaProposal;
@VisibleForTesting IkeCipher mIkeCipher;
@VisibleForTesting IkeMacIntegrity mIkeIntegrity;
@VisibleForTesting IkeMacPrf mIkePrf;
// FIXME: b/131265898 Pass these parameters from CreateIkeLocalIkeInit to CreateIkeLocalIkeAuth
// as entry data when Android StateMachine can support that.
@VisibleForTesting byte[] mIkeInitRequestBytes;
@VisibleForTesting byte[] mIkeInitResponseBytes;
@VisibleForTesting IkeNoncePayload mIkeInitNoncePayload;
@VisibleForTesting IkeNoncePayload mIkeRespNoncePayload;
// FIXME: b/131265898 Pass these parameters from CreateIkeLocalIkeAuth through to
// CreateIkeLocalIkeAuthPostEap as entry data when Android StateMachine can support that.
@VisibleForTesting IkeIdPayload mInitIdPayload;
@VisibleForTesting IkeIdPayload mRespIdPayload;
@VisibleForTesting List<IkePayload> mFirstChildReqList;
// FIXME: b/131265898 Move into CreateIkeLocalIkeAuth, and pass through to
// CreateIkeLocalIkeAuthPostEap once passing entry data is supported
private ChildSessionOptions mFirstChildSessionOptions;
private ChildSessionCallback mFirstChildCallbacks;
/** Package */
@VisibleForTesting IkeSaRecord mCurrentIkeSaRecord;
/** Package */
@VisibleForTesting IkeSaRecord mLocalInitNewIkeSaRecord;
/** Package */
@VisibleForTesting IkeSaRecord mRemoteInitNewIkeSaRecord;
/** Package */
@VisibleForTesting IkeSaRecord mIkeSaRecordSurviving;
/** Package */
@VisibleForTesting IkeSaRecord mIkeSaRecordAwaitingLocalDel;
/** Package */
@VisibleForTesting IkeSaRecord mIkeSaRecordAwaitingRemoteDel;
// States
@VisibleForTesting final State mInitial = new Initial();
@VisibleForTesting final State mIdle = new Idle();
@VisibleForTesting final State mChildProcedureOngoing = new ChildProcedureOngoing();
@VisibleForTesting final State mReceiving = new Receiving();
@VisibleForTesting final State mCreateIkeLocalIkeInit = new CreateIkeLocalIkeInit();
@VisibleForTesting final State mCreateIkeLocalIkeAuth = new CreateIkeLocalIkeAuth();
@VisibleForTesting final State mCreateIkeLocalIkeAuthInEap = new CreateIkeLocalIkeAuthInEap();
@VisibleForTesting
final State mCreateIkeLocalIkeAuthPostEap = new CreateIkeLocalIkeAuthPostEap();
@VisibleForTesting final State mRekeyIkeLocalCreate = new RekeyIkeLocalCreate();
@VisibleForTesting final State mSimulRekeyIkeLocalCreate = new SimulRekeyIkeLocalCreate();
@VisibleForTesting
final State mSimulRekeyIkeLocalDeleteRemoteDelete = new SimulRekeyIkeLocalDeleteRemoteDelete();
@VisibleForTesting final State mSimulRekeyIkeLocalDelete = new SimulRekeyIkeLocalDelete();
@VisibleForTesting final State mSimulRekeyIkeRemoteDelete = new SimulRekeyIkeRemoteDelete();
@VisibleForTesting final State mRekeyIkeLocalDelete = new RekeyIkeLocalDelete();
@VisibleForTesting final State mRekeyIkeRemoteDelete = new RekeyIkeRemoteDelete();
@VisibleForTesting final State mDeleteIkeLocalDelete = new DeleteIkeLocalDelete();
// TODO: Add InfoLocal.
/** Constructor for testing. */
@VisibleForTesting
public IkeSessionStateMachine(
Looper looper,
Context context,
IpSecManager ipSecManager,
IkeSessionOptions ikeOptions,
ChildSessionOptions firstChildOptions,
Executor userCbExecutor,
IkeSessionCallback ikeSessionCallback,
ChildSessionCallback firstChildSessionCallback,
IkeEapAuthenticatorFactory eapAuthenticatorFactory) {
super(TAG, looper);
mIkeSessionOptions = ikeOptions;
mEapAuthenticatorFactory = eapAuthenticatorFactory;
mTempFailHandler = new TempFailureHandler(looper);
// There are at most three IkeSaRecords co-existing during simultaneous rekeying.
mLocalSpiToIkeSaRecordMap = new LongSparseArray<>(3);
mRemoteSpiToChildSessionMap = new SparseArray<>();
mContext = context;
mIpSecManager = ipSecManager;
mUserCbExecutor = userCbExecutor;
mIkeSessionCallback = ikeSessionCallback;
mFirstChildSessionOptions = firstChildOptions;
mFirstChildCallbacks = firstChildSessionCallback;
registerChildSessionCallback(firstChildOptions, firstChildSessionCallback, true);
addState(mInitial);
addState(mCreateIkeLocalIkeInit);
addState(mCreateIkeLocalIkeAuth);
addState(mCreateIkeLocalIkeAuthInEap);
addState(mCreateIkeLocalIkeAuthPostEap);
addState(mIdle);
addState(mChildProcedureOngoing);
addState(mReceiving);
addState(mRekeyIkeLocalCreate);
addState(mSimulRekeyIkeLocalCreate, mRekeyIkeLocalCreate);
addState(mSimulRekeyIkeLocalDeleteRemoteDelete);
addState(mSimulRekeyIkeLocalDelete, mSimulRekeyIkeLocalDeleteRemoteDelete);
addState(mSimulRekeyIkeRemoteDelete, mSimulRekeyIkeLocalDeleteRemoteDelete);
addState(mRekeyIkeLocalDelete);
addState(mRekeyIkeRemoteDelete);
addState(mDeleteIkeLocalDelete);
setInitialState(mInitial);
mScheduler =
new IkeLocalRequestScheduler(
localReq -> {
sendMessageAtFrontOfQueue(CMD_EXECUTE_LOCAL_REQ, localReq);
});
start();
}
/** Construct an instance of IkeSessionStateMachine. */
public IkeSessionStateMachine(
Looper looper,
Context context,
IpSecManager ipSecManager,
IkeSessionOptions ikeOptions,
ChildSessionOptions firstChildOptions,
Executor userCbExecutor,
IkeSessionCallback ikeSessionCallback,
ChildSessionCallback firstChildSessionCallback) {
this(
looper,
context,
ipSecManager,
ikeOptions,
firstChildOptions,
userCbExecutor,
ikeSessionCallback,
firstChildSessionCallback,
new IkeEapAuthenticatorFactory());
}
private boolean hasChildSessionCallback(ChildSessionCallback callback) {
synchronized (mChildCbToSessions) {
return mChildCbToSessions.containsKey(callback);
}
}
/**
* Synchronously builds and registers a child session.
*
* <p>Setup of the child state machines MUST be done in two stages to ensure that if an external
* caller calls openChildSession and then calls closeChildSession before the state machine has
* gotten a chance to negotiate the sessions, a valid callback mapping exists (and does not
* throw an exception that the callback was not found).
*
* <p>In the edge case where a child creation fails, and deletes itself, all pending requests
* will no longer find the session in the map. Assume it has errored/failed, and skip/ignore.
* This is safe, as closeChildSession() (previously) validated that the callback was registered.
*/
@VisibleForTesting
void registerChildSessionCallback(
ChildSessionOptions childOptions,
ChildSessionCallback callbacks,
boolean isFirstChild) {
synchronized (mChildCbToSessions) {
if (!isFirstChild && getCurrentState() == null) {
throw new IllegalStateException(
"Request rejected because IKE Session is being closed. ");
}
mChildCbToSessions.put(
callbacks,
ChildSessionStateMachineFactory.makeChildSessionStateMachine(
getHandler().getLooper(),
mContext,
childOptions,
mUserCbExecutor,
callbacks,
new ChildSessionSmCallback()));
}
}
/** Initiates IKE setup procedure. */
public void openSession() {
sendMessage(CMD_LOCAL_REQUEST_CREATE_IKE, new LocalRequest(CMD_LOCAL_REQUEST_CREATE_IKE));
}
/** Schedules a Create Child procedure. */
public void openChildSession(
ChildSessionOptions childSessionOptions, ChildSessionCallback childSessionCallback) {
if (childSessionCallback == null) {
throw new IllegalArgumentException("Child Session Callback must be provided");
}
if (hasChildSessionCallback(childSessionCallback)) {
throw new IllegalArgumentException("Child Session Callback handle already registered");
}
registerChildSessionCallback(
childSessionOptions, childSessionCallback, false /*isFirstChild*/);
sendMessage(
CMD_LOCAL_REQUEST_CREATE_CHILD,
new ChildLocalRequest(
CMD_LOCAL_REQUEST_CREATE_CHILD, childSessionCallback, childSessionOptions));
}
/** Schedules a Delete Child procedure. */
public void closeChildSession(ChildSessionCallback childSessionCallback) {
if (childSessionCallback == null) {
throw new IllegalArgumentException("Child Session Callback must be provided");
}
if (!hasChildSessionCallback(childSessionCallback)) {
throw new IllegalArgumentException("Child Session Callback handle not registered");
}
sendMessage(
CMD_LOCAL_REQUEST_DELETE_CHILD,
new ChildLocalRequest(CMD_LOCAL_REQUEST_DELETE_CHILD, childSessionCallback, null));
}
/** Initiates Delete IKE procedure. */
public void closeSession() {
sendMessage(CMD_LOCAL_REQUEST_DELETE_IKE, new LocalRequest(CMD_LOCAL_REQUEST_DELETE_IKE));
}
/** Forcibly close IKE Session. */
public void killSession() {
// TODO: b/142977160 Support closing IKE Sesison immediately.
}
private void scheduleRekeySession(LocalRequest rekeyRequest) {
// TODO: Make rekey timeout fuzzy
sendMessageDelayed(CMD_LOCAL_REQUEST_REKEY_IKE, rekeyRequest, SA_SOFT_LIFETIME_MS);
}
private void scheduleRetry(LocalRequest localRequest) {
sendMessageDelayed(localRequest.procedureType, localRequest, RETRY_INTERVAL_MS);
}
// TODO: Support initiating Delete IKE exchange when IKE SA expires
// TODO: Add interfaces to initiate IKE exchanges.
/**
* This class is for handling temporary failure.
*
* <p>Receiving a TEMPORARY_FAILURE is caused by a temporary condition. IKE Session should be
* closed if it continues to receive this error after several minutes.
*/
@VisibleForTesting
class TempFailureHandler extends Handler {
private static final int TEMP_FAILURE_RETRY_TIMEOUT = 1;
private boolean mTempFailureReceived = false;
TempFailureHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (msg.what == TEMP_FAILURE_RETRY_TIMEOUT) {
IOException error =
new IOException(
"Kept receiving TEMPORARY_FAILURE error. State information is out"
+ " of sync.");
mUserCbExecutor.execute(
() -> {
mIkeSessionCallback.onClosedExceptionally(
new IkeInternalException(error));
});
loge("Fatal error", error);
closeAllSaRecords(false /*expectSaClosed*/);
quitNow();
} else {
logWtf("Unknown message.what: " + msg.what);
}
}
/** Schedule retry when a request got rejected by TEMPORARY_FAILURE. */
public void handleTempFailure(LocalRequest localRequest) {
logd(
"TempFailureHandler: Receive TEMPORARY FAILURE. Reschedule request: "
+ localRequest.procedureType);
// TODO: Support customized delay time when this is a rekey request and SA is going to
// expire soon.
scheduleRetry(localRequest);
if (!mTempFailureReceived) {
sendEmptyMessageDelayed(TEMP_FAILURE_RETRY_TIMEOUT, TEMP_FAILURE_RETRY_TIMEOUT_MS);
mTempFailureReceived = true;
}
}
/** Stop tracking temporary condition when request was not rejected by TEMPORARY_FAILURE. */
public void reset() {
logd("TempFailureHandler: Reset Temporary failure retry timeout");
removeMessages(TEMP_FAILURE_RETRY_TIMEOUT);
mTempFailureReceived = false;
}
}
/**
* This class represents a reserved IKE SPI.
*
* <p>This class is created to avoid assigning same SPI to the same address.
*
* <p>Objects of this type are used to track reserved IKE SPI to avoid SPI collision. They can
* be obtained by calling {@link #allocateSecurityParameterIndex()} and must be released by
* calling {@link #close()} when they are no longer needed.
*
* <p>This class follows the pattern of {@link IpSecManager.SecurityParameterIndex}.
*
* <p>TODO: Move this class to a central place, like IkeManager.
*/
public static final class IkeSecurityParameterIndex implements AutoCloseable {
// Remember assigned IKE SPIs to avoid SPI collision.
private static final Set<Pair<InetAddress, Long>> sAssignedIkeSpis = new HashSet<>();
private static final int MAX_ASSIGN_IKE_SPI_ATTEMPTS = 100;
private static final SecureRandom IKE_SPI_RANDOM = new SecureRandom();
private final InetAddress mSourceAddress;
private final long mSpi;
private final CloseGuard mCloseGuard = CloseGuard.get();
private IkeSecurityParameterIndex(InetAddress sourceAddress, long spi) {
mSourceAddress = sourceAddress;
mSpi = spi;
mCloseGuard.open("close");
}
/**
* Get a new IKE SPI and maintain the reservation.
*
* @return an instance of IkeSecurityParameterIndex.
*/
public static IkeSecurityParameterIndex allocateSecurityParameterIndex(
InetAddress sourceAddress) throws IOException {
// TODO: Create specific Exception for SPI assigning error.
for (int i = 0; i < MAX_ASSIGN_IKE_SPI_ATTEMPTS; i++) {
long spi = IKE_SPI_RANDOM.nextLong();
// Zero value can only be used in the IKE responder SPI field of an IKE INIT
// request.
if (spi != 0L
&& sAssignedIkeSpis.add(new Pair<InetAddress, Long>(sourceAddress, spi))) {
return new IkeSecurityParameterIndex(sourceAddress, spi);
}
}
throw new IOException("Failed to generate IKE SPI.");
}
/**
* Get a new IKE SPI and maintain the reservation.
*
* @return an instance of IkeSecurityParameterIndex.
*/
public static IkeSecurityParameterIndex allocateSecurityParameterIndex(
InetAddress sourceAddress, long requestedSpi) throws IOException {
if (sAssignedIkeSpis.add(new Pair<InetAddress, Long>(sourceAddress, requestedSpi))) {
return new IkeSecurityParameterIndex(sourceAddress, requestedSpi);
}
throw new IOException(
"Failed to generate IKE SPI for "
+ requestedSpi
+ " with source address "
+ sourceAddress.getHostAddress());
}
/**
* Get the underlying SPI held by this object.
*
* @return the underlying IKE SPI.
*/
public long getSpi() {
return mSpi;
}
/** Release an SPI that was previously reserved. */
@Override
public void close() {
sAssignedIkeSpis.remove(new Pair<InetAddress, Long>(mSourceAddress, mSpi));
mCloseGuard.close();
}
/** Check that the IkeSecurityParameterIndex was closed properly. */
@Override
protected void finalize() throws Throwable {
if (mCloseGuard != null) {
mCloseGuard.warnIfOpen();
}
close();
}
}
// TODO: Add methods for building and validating general Informational packet.
@VisibleForTesting
void addIkeSaRecord(IkeSaRecord record) {
mLocalSpiToIkeSaRecordMap.put(record.getLocalSpi(), record);
// In IKE_INIT exchange, local SPI was registered with this IkeSessionStateMachine before
// IkeSaRecord is created. Calling this method at the end of exchange will double-register
// the SPI but it is safe because the key and value are not changed.
mIkeSocket.registerIke(record.getLocalSpi(), this);
scheduleRekeySession(record.getFutureRekeyEvent());
}
@VisibleForTesting
void removeIkeSaRecord(IkeSaRecord record) {
mIkeSocket.unregisterIke(record.getLocalSpi());
mLocalSpiToIkeSaRecordMap.remove(record.getLocalSpi());
}
/**
* Receive IKE packet from remote server.
*
* <p>This method is called synchronously from IkeSocket. It proxies the synchronous call as an
* asynchronous job to the IkeSessionStateMachine handler.
*
* @param ikeHeader the decoded IKE header.
* @param ikePacketBytes the byte array of the entire received IKE packet.
*/
public void receiveIkePacket(IkeHeader ikeHeader, byte[] ikePacketBytes) {
sendMessage(CMD_RECEIVE_IKE_PACKET, new ReceivedIkePacket(ikeHeader, ikePacketBytes));
}
/**
* ReceivedIkePacket is a package private data container consists of decoded IkeHeader and
* encoded IKE packet in a byte array.
*/
static class ReceivedIkePacket {
/** Decoded IKE header */
public final IkeHeader ikeHeader;
/** Entire encoded IKE message including IKE header */
public final byte[] ikePacketBytes;
ReceivedIkePacket(IkeHeader ikeHeader, byte[] ikePacketBytes) {
this.ikeHeader = ikeHeader;
this.ikePacketBytes = ikePacketBytes;
}
}
/** Class to group parameters for negotiating the first Child SA. */
private static class FirstChildNegotiationData {
public final ChildSessionOptions childSessionOptions;
public final ChildSessionCallback childSessionCallback;
public final List<IkePayload> reqPayloads;
public final List<IkePayload> respPayloads;
FirstChildNegotiationData(
ChildSessionOptions childSessionOptions,
ChildSessionCallback childSessionCallback,
List<IkePayload> reqPayloads,
List<IkePayload> respPayloads) {
this.childSessionOptions = childSessionOptions;
this.childSessionCallback = childSessionCallback;
this.reqPayloads = reqPayloads;
this.respPayloads = respPayloads;
}
}
/** Class to group parameters for building an outbound message for ChildSessions. */
private static class ChildOutboundData {
@ExchangeType public final int exchangeType;
public final boolean isResp;
public final List<IkePayload> payloadList;
public final ChildSessionStateMachine childSession;
ChildOutboundData(
@ExchangeType int exchangeType,
boolean isResp,
List<IkePayload> payloadList,
ChildSessionStateMachine childSession) {
this.exchangeType = exchangeType;
this.isResp = isResp;
this.payloadList = payloadList;
this.childSession = childSession;
}
}
/** Callback for ChildSessionStateMachine to notify IkeSessionStateMachine. */
@VisibleForTesting
class ChildSessionSmCallback implements ChildSessionStateMachine.IChildSessionSmCallback {
@Override
public void onChildSaCreated(int remoteSpi, ChildSessionStateMachine childSession) {
mRemoteSpiToChildSessionMap.put(remoteSpi, childSession);
}
@Override
public void onChildSaDeleted(int remoteSpi) {
mRemoteSpiToChildSessionMap.remove(remoteSpi);
}
@Override
public void scheduleLocalRequest(ChildLocalRequest futureRequest, long delayedTime) {
sendMessageDelayed(futureRequest.procedureType, futureRequest, delayedTime);
}
@Override
public void scheduleRetryLocalRequest(ChildLocalRequest childRequest) {
scheduleRetry(childRequest);
}
@Override
public void onOutboundPayloadsReady(
@ExchangeType int exchangeType,
boolean isResp,
List<IkePayload> payloadList,
ChildSessionStateMachine childSession) {
sendMessage(
CMD_OUTBOUND_CHILD_PAYLOADS_READY,
new ChildOutboundData(exchangeType, isResp, payloadList, childSession));
}
@Override
public void onProcedureFinished(ChildSessionStateMachine childSession) {
if (getHandler() == null) {
// If the state machine has quit (because IKE Session is being closed), do not send
// any message.
return;
}
sendMessage(CMD_CHILD_PROCEDURE_FINISHED, childSession);
}
@Override
public void onChildSessionClosed(ChildSessionCallback userCallbacks) {
synchronized (mChildCbToSessions) {
mChildCbToSessions.remove(userCallbacks);
}
}
@Override
public void onFatalIkeSessionError(boolean needsNotifyRemote) {
// TODO: If needsNotifyRemote is true, send a Delete IKE request and then kill the IKE
// Session. Otherwise, directly kill the IKE Session.
}
}
/** Top level state for handling uncaught exceptions for all subclasses. */
abstract class ExceptionHandler extends ExceptionHandlerBase {
@Override
protected void cleanUpAndQuit(RuntimeException e) {
// Clean up all SaRecords.
closeAllSaRecords(false /*expectSaClosed*/);
mUserCbExecutor.execute(
() -> {
mIkeSessionCallback.onClosedExceptionally(new IkeInternalException(e));
});
logWtf("Unexpected exception in " + getCurrentState().getName(), e);
quitNow();
}
@Override
protected String getCmdString(int cmd) {
return CMD_TO_STR.get(cmd);
}
}
/** Called when this StateMachine quits. */
@Override
protected void onQuitting() {
// Clean up all SaRecords.
closeAllSaRecords(true /*expectSaClosed*/);
synchronized (mChildCbToSessions) {
for (ChildSessionStateMachine child : mChildCbToSessions.values()) {
// Fire asynchronous call for Child Sessions to do cleanup and remove itself
// from the map.
child.killSession();
}
}
if (mIkeSocket == null) return;
mIkeSocket.releaseReference(this);
}
private void closeAllSaRecords(boolean expectSaClosed) {
closeIkeSaRecord(mCurrentIkeSaRecord, expectSaClosed);
closeIkeSaRecord(mLocalInitNewIkeSaRecord, expectSaClosed);
closeIkeSaRecord(mRemoteInitNewIkeSaRecord, expectSaClosed);
mCurrentIkeSaRecord = null;
mLocalInitNewIkeSaRecord = null;
mRemoteInitNewIkeSaRecord = null;
}
private void closeIkeSaRecord(IkeSaRecord ikeSaRecord, boolean expectSaClosed) {
if (ikeSaRecord == null) return;
removeIkeSaRecord(ikeSaRecord);
ikeSaRecord.close();
if (!expectSaClosed) return;
logWtf(
"IkeSaRecord with local SPI: "
+ ikeSaRecord.getLocalSpi()
+ " is not correctly closed.");
}
private void handleIkeFatalError(Exception error) {
IkeException ikeException =
error instanceof IkeException
? (IkeException) error
: new IkeInternalException(error);
// Clean up all SaRecords.
closeAllSaRecords(false /*expectSaClosed*/);
mUserCbExecutor.execute(
() -> {
mIkeSessionCallback.onClosedExceptionally(ikeException);
});
loge("IKE Session fatal error in " + getCurrentState().getName(), ikeException);
quitNow();
}
/** Initial state of IkeSessionStateMachine. */
class Initial extends ExceptionHandler {
@Override
public void enterState() {
try {
mRemoteAddress = mIkeSessionOptions.getServerAddress();
boolean isIpv4 = mRemoteAddress instanceof Inet4Address;
FileDescriptor sock =
Os.socket(
isIpv4 ? OsConstants.AF_INET : OsConstants.AF_INET6,
OsConstants.SOCK_DGRAM,
OsConstants.IPPROTO_UDP);
Os.connect(sock, mRemoteAddress, IkeSocket.IKE_SERVER_PORT);
InetSocketAddress localAddr = (InetSocketAddress) Os.getsockname(sock);
mLocalAddress = localAddr.getAddress();
mLocalPort = localAddr.getPort();
Os.close(sock);
mIkeSocket =
IkeSocket.getIkeSocket(
mIkeSessionOptions.getUdpEncapsulationSocket(),
IkeSessionStateMachine.this);
} catch (ErrnoException | SocketException e) {
handleIkeFatalError(e);
}
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_LOCAL_REQUEST_CREATE_IKE:
transitionTo(mCreateIkeLocalIkeInit);
return HANDLED;
case CMD_FORCE_TRANSITION:
transitionTo((State) message.obj);
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
/**
* Idle represents a state when there is no ongoing IKE exchange affecting established IKE SA.
*/
class Idle extends LocalRequestQueuer {
@Override
public void enterState() {
mScheduler.readyForNextProcedure();
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
deferMessage(message);
transitionTo(mReceiving);
return HANDLED;
case CMD_FORCE_TRANSITION: // Testing command
transitionTo((State) message.obj);
return HANDLED;
case CMD_EXECUTE_LOCAL_REQ:
executeLocalRequest((LocalRequest) message.obj, message);
return HANDLED;
default:
// Queue local requests, and trigger next procedure
if (isLocalRequest(message.what)) {
handleLocalRequest(message.what, (LocalRequest) message.obj);
// Synchronously calls through to the scheduler callback, which will
// post the CMD_EXECUTE_LOCAL_REQ to the front of the queue, ensuring
// it is always the next request processed.
mScheduler.readyForNextProcedure();
return HANDLED;
}
return NOT_HANDLED;
}
}
private void executeLocalRequest(LocalRequest req, Message message) {
switch (req.procedureType) {
case CMD_LOCAL_REQUEST_REKEY_IKE:
transitionTo(mRekeyIkeLocalCreate);
break;
case CMD_LOCAL_REQUEST_DELETE_IKE:
transitionTo(mDeleteIkeLocalDelete);
break;
case CMD_LOCAL_REQUEST_CREATE_CHILD: // fallthrough
case CMD_LOCAL_REQUEST_REKEY_CHILD: // fallthrough
case CMD_LOCAL_REQUEST_DELETE_CHILD:
deferMessage(message);
transitionTo(mChildProcedureOngoing);
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Invalid local request procedure type: " + req.procedureType));
}
}
}
/**
* Gets IKE exchange subtype of a inbound IKE request message.
*
* <p>Knowing IKE exchange subtype of a inbound IKE request message helps IkeSessionStateMachine
* to validate this request using the specific rule.
*
* <p>It is not allowed to obtain exchange subtype from a inbound response message for two
* reasons. Firstly, the exchange subtype of a response message is the same with its
* corresponding request message. Secondly, trying to get the exchange subtype from a response
* message will easily fail when the response message contains only error notification payloads.
*
* @param ikeMessage inbound request IKE message to check.
* @return IKE exchange subtype.
*/
@IkeExchangeSubType
private static int getIkeExchangeSubType(IkeMessage ikeMessage) {
IkeHeader ikeHeader = ikeMessage.ikeHeader;
if (ikeHeader.isResponseMsg) {
throw new IllegalStateException("IKE Exchange subtype invalid for response messages.");
}
switch (ikeHeader.exchangeType) {
// DPD omitted - should never be handled via handleRequestIkeMessage()
case IkeHeader.EXCHANGE_TYPE_IKE_SA_INIT:
return IKE_EXCHANGE_SUBTYPE_IKE_INIT;
case IkeHeader.EXCHANGE_TYPE_IKE_AUTH:
return IKE_EXCHANGE_SUBTYPE_IKE_AUTH;
case IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA:
// It is guaranteed in the decoding process that SA Payload has at least one SA
// Proposal. Since Rekey IKE and Create Child (both initial creation and rekey
// creation) will cause a collision, although the RFC 7296 does not prohibit one SA
// Payload to contain both IKE proposals and Child proposals, containing two types
// does not make sense. IKE libary will reply according to the first SA Proposal
// type and ignore the other type.
IkeSaPayload saPayload =
ikeMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
if (saPayload == null) {
return IKE_EXCHANGE_SUBTYPE_INVALID;
}
// If the received message has both SA(IKE) Payload and Notify-Rekey Payload, IKE
// library will treat it as a Rekey IKE request and ignore the Notify-Rekey
// Payload to provide better interoperability.
if (saPayload.proposalList.get(0).protocolId == IkePayload.PROTOCOL_ID_IKE) {
return IKE_EXCHANGE_SUBTYPE_REKEY_IKE;
}
// If a Notify-Rekey Payload is found, this message is for rekeying a Child SA.
List<IkeNotifyPayload> notifyPayloads =
ikeMessage.getPayloadListForType(
IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
// It is checked during decoding that there is at most one Rekey notification
// payload.
for (IkeNotifyPayload notifyPayload : notifyPayloads) {
if (notifyPayload.notifyType == IkeNotifyPayload.NOTIFY_TYPE_REKEY_SA) {
return IKE_EXCHANGE_SUBTYPE_REKEY_CHILD;
}
}
return IKE_EXCHANGE_SUBTYPE_CREATE_CHILD;
case IkeHeader.EXCHANGE_TYPE_INFORMATIONAL:
List<IkeDeletePayload> deletePayloads =
ikeMessage.getPayloadListForType(
IkePayload.PAYLOAD_TYPE_DELETE, IkeDeletePayload.class);
// If no Delete payload was found, this request is a generic informational request.
if (deletePayloads.isEmpty()) return IKE_EXCHANGE_SUBTYPE_GENERIC_INFO;
// IKEv2 protocol does not clearly disallow to have both a Delete IKE payload and a
// Delete Child payload in one IKE message. In this case, IKE library will only
// respond to the Delete IKE payload.
for (IkeDeletePayload deletePayload : deletePayloads) {
if (deletePayload.protocolId == IkePayload.PROTOCOL_ID_IKE) {
return IKE_EXCHANGE_SUBTYPE_DELETE_IKE;
}
}
return IKE_EXCHANGE_SUBTYPE_DELETE_CHILD;
default:
throw new IllegalStateException(
"Unrecognized exchange type in the validated IKE header: "
+ ikeHeader.exchangeType);
}
}
// Sends the provided IkeMessage using the current IKE SA record
@VisibleForTesting
void sendEncryptedIkeMessage(IkeMessage msg) {
sendEncryptedIkeMessage(mCurrentIkeSaRecord, msg);
}
// Sends the provided IkeMessage using the provided IKE SA record
@VisibleForTesting
void sendEncryptedIkeMessage(IkeSaRecord ikeSaRecord, IkeMessage msg) {
byte[][] packetList =
msg.encryptAndEncode(
mIkeIntegrity,
mIkeCipher,
ikeSaRecord,
mSupportFragment,
DEFAULT_FRAGMENT_SIZE);
for (byte[] packet : packetList) {
mIkeSocket.sendIkePacket(packet, mRemoteAddress);
}
if (msg.ikeHeader.isResponseMsg) {
ikeSaRecord.updateLastSentRespAllPackets(Arrays.asList(packetList));
}
}
// Builds and sends IKE-level error notification response on the provided IKE SA record
@VisibleForTesting
void buildAndSendErrorNotificationResponse(
IkeSaRecord ikeSaRecord, int messageId, @ErrorType int errorType) {
IkeNotifyPayload error = new IkeNotifyPayload(errorType);
buildAndSendNotificationResponse(ikeSaRecord, messageId, error);
}
// Builds and sends error notification response on the provided IKE SA record
@VisibleForTesting
void buildAndSendNotificationResponse(
IkeSaRecord ikeSaRecord, int messageId, IkeNotifyPayload notifyPayload) {
IkeMessage msg =
buildEncryptedNotificationMessage(
ikeSaRecord,
new IkeInformationalPayload[] {notifyPayload},
EXCHANGE_TYPE_INFORMATIONAL,
true /*isResponse*/,
messageId);
sendEncryptedIkeMessage(ikeSaRecord, msg);
}
// Builds an Encrypted IKE Informational Message for the given IkeInformationalPayload using the
// current IKE SA record.
@VisibleForTesting
IkeMessage buildEncryptedInformationalMessage(
IkeInformationalPayload[] payloads, boolean isResponse, int messageId) {
return buildEncryptedInformationalMessage(
mCurrentIkeSaRecord, payloads, isResponse, messageId);
}
// Builds an Encrypted IKE Informational Message for the given IkeInformationalPayload using the
// provided IKE SA record.
@VisibleForTesting
IkeMessage buildEncryptedInformationalMessage(
IkeSaRecord saRecord,
IkeInformationalPayload[] payloads,
boolean isResponse,
int messageId) {
return buildEncryptedNotificationMessage(
saRecord, payloads, IkeHeader.EXCHANGE_TYPE_INFORMATIONAL, isResponse, messageId);
}
// Builds an Encrypted IKE Message for the given IkeInformationalPayload using the provided IKE
// SA record and exchange type.
@VisibleForTesting
IkeMessage buildEncryptedNotificationMessage(
IkeSaRecord saRecord,
IkeInformationalPayload[] payloads,
@ExchangeType int exchangeType,
boolean isResponse,
int messageId) {
IkeHeader header =
new IkeHeader(
saRecord.getInitiatorSpi(),
saRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
exchangeType,
isResponse /*isResponseMsg*/,
saRecord.isLocalInit /*fromIkeInitiator*/,
messageId);
return new IkeMessage(header, Arrays.asList(payloads));
}
private abstract class LocalRequestQueuer extends ExceptionHandler {
/**
* Reroutes all local requests to the scheduler
*
* @param requestVal The command value of the request
* @param req The instance of the LocalRequest to be queued.
*/
protected void handleLocalRequest(int requestVal, LocalRequest req) {
if (req.isCancelled()) return;
switch (requestVal) {
case CMD_LOCAL_REQUEST_DELETE_IKE:
mScheduler.addRequestAtFront(req);
return;
case CMD_LOCAL_REQUEST_REKEY_IKE: // Fallthrough
case CMD_LOCAL_REQUEST_INFO:
mScheduler.addRequest(req);
return;
case CMD_LOCAL_REQUEST_CREATE_CHILD: // Fallthrough
case CMD_LOCAL_REQUEST_REKEY_CHILD: // Fallthrough
case CMD_LOCAL_REQUEST_DELETE_CHILD:
ChildLocalRequest childReq = (ChildLocalRequest) req;
if (childReq.procedureType != requestVal) {
cleanUpAndQuit(
new IllegalArgumentException(
"ChildLocalRequest procedure type was invalid"));
}
mScheduler.addRequest(childReq);
return;
default:
cleanUpAndQuit(
new IllegalStateException(
"Unknown local request passed to handleLocalRequest"));
}
}
/** Check if received signal is a local request. */
protected boolean isLocalRequest(int msgWhat) {
if ((msgWhat >= CMD_IKE_LOCAL_REQUEST_BASE
&& msgWhat < CMD_IKE_LOCAL_REQUEST_BASE + CMD_CATEGORY_SIZE)
|| (msgWhat >= CMD_CHILD_LOCAL_REQUEST_BASE
&& msgWhat < CMD_CHILD_LOCAL_REQUEST_BASE + CMD_CATEGORY_SIZE)) {
return true;
}
return false;
}
}
/**
* Base state defines common behaviours when receiving an IKE packet.
*
* <p>State that represents an ongoing IKE procedure MUST extend BusyState to handle received
* IKE packet. Idle state will defer the received packet to a BusyState to process it.
*/
private abstract class BusyState extends LocalRequestQueuer {
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
handleReceivedIkePacket(message);
return HANDLED;
case CMD_FORCE_TRANSITION:
transitionTo((State) message.obj);
return HANDLED;
case CMD_EXECUTE_LOCAL_REQ:
logWtf("Invalid execute local request command in non-idle state");
return NOT_HANDLED;
case CMD_RETRANSMIT:
triggerRetransmit();
return HANDLED;
default:
// Queue local requests, and trigger next procedure
if (isLocalRequest(message.what)) {
handleLocalRequest(message.what, (LocalRequest) message.obj);
return HANDLED;
}
return NOT_HANDLED;
}
}
/**
* Handler for retransmission timer firing
*
* <p>By default, the trigger is logged and dropped. States that have a retransmitter should
* override this function, and proxy the call to Retransmitter.retransmit()
*/
protected void triggerRetransmit() {
logWtf("Retransmission trigger dropped in state: " + this.getClass().getSimpleName());
}
protected IkeSaRecord getIkeSaRecordForPacket(IkeHeader ikeHeader) {
if (ikeHeader.fromIkeInitiator) {
return mLocalSpiToIkeSaRecordMap.get(ikeHeader.ikeResponderSpi);
} else {
return mLocalSpiToIkeSaRecordMap.get(ikeHeader.ikeInitiatorSpi);
}
}
protected void handleReceivedIkePacket(Message message) {
// TODO: b/138411550 Notify subclasses when discarding a received packet. Receiving MUST
// go back to Idle state in this case.
String methodTag = "handleReceivedIkePacket: ";
ReceivedIkePacket receivedIkePacket = (ReceivedIkePacket) message.obj;
IkeHeader ikeHeader = receivedIkePacket.ikeHeader;
byte[] ikePacketBytes = receivedIkePacket.ikePacketBytes;
IkeSaRecord ikeSaRecord = getIkeSaRecordForPacket(ikeHeader);
String msgDirection = ikeHeader.isResponseMsg ? "response" : "request";
// Drop packets that we don't have an SA for:
if (ikeSaRecord == null) {
// TODO: Print a summary of the IKE message (perhaps the IKE header)
cleanUpAndQuit(
new IllegalStateException(
"Received an IKE "
+ msgDirection
+ "but found no matching SA for it"));
return;
}
logd(
methodTag
+ "Received an "
+ ikeHeader.getBasicInfoString()
+ " on IKE SA with local SPI: "
+ ikeSaRecord.getLocalSpi()
+ ". Packet size: "
+ ikePacketBytes.length);
if (ikeHeader.isResponseMsg) {
int expectedMsgId = ikeSaRecord.getLocalRequestMessageId();
if (expectedMsgId - 1 == ikeHeader.messageId) {
logd(methodTag + "Received re-transmitted response. Discard it.");
return;
}
DecodeResult decodeResult =
IkeMessage.decode(
expectedMsgId,
mIkeIntegrity,
mIkeCipher,
ikeSaRecord,
ikeHeader,
ikePacketBytes,
ikeSaRecord.getCollectedFragments(true /*isResp*/));
switch (decodeResult.status) {
case DECODE_STATUS_OK:
ikeSaRecord.incrementLocalRequestMessageId();
ikeSaRecord.resetCollectedFragments(true /*isResp*/);
DecodeResultOk resultOk = (DecodeResultOk) decodeResult;
if (isTempFailure(resultOk.ikeMessage)) {
handleTempFailure();
} else {
mTempFailHandler.reset();
}
handleResponseIkeMessage(resultOk.ikeMessage);
break;
case DECODE_STATUS_PARTIAL:
ikeSaRecord.updateCollectedFragments(
(DecodeResultPartial) decodeResult, true /*isResp*/);
break;
case DECODE_STATUS_PROTECTED_ERROR:
IkeException ikeException = ((DecodeResultError) decodeResult).ikeException;
logi(methodTag + "Protected error", ikeException);
ikeSaRecord.incrementLocalRequestMessageId();
ikeSaRecord.resetCollectedFragments(true /*isResp*/);
handleResponseGenericProcessError(
ikeSaRecord,
new InvalidSyntaxException(
"Generic processing error in the received response",
ikeException));
break;
case DECODE_STATUS_UNPROTECTED_ERROR:
logi(
methodTag
+ "Message authentication or decryption failed on received"
+ " response. Discard it",
((DecodeResultError) decodeResult).ikeException);
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Unrecognized decoding status: " + decodeResult.status));
}
} else {
int expectedMsgId = ikeSaRecord.getRemoteRequestMessageId();
if (expectedMsgId - 1 == ikeHeader.messageId) {
if (ikeSaRecord.isRetransmittedRequest(ikePacketBytes)) {
logd("Received re-transmitted request. Retransmitting response");
for (byte[] packet : ikeSaRecord.getLastSentRespAllPackets()) {
mIkeSocket.sendIkePacket(packet, mRemoteAddress);
}
// TODO:Support resetting remote rekey delete timer.
} else {
logi(methodTag + "Received response with invalid message ID. Discard it.");
}
} else {
DecodeResult decodeResult =
IkeMessage.decode(
expectedMsgId,
mIkeIntegrity,
mIkeCipher,
ikeSaRecord,
ikeHeader,
ikePacketBytes,
ikeSaRecord.getCollectedFragments(false /*isResp*/));
switch (decodeResult.status) {
case DECODE_STATUS_OK:
ikeSaRecord.incrementRemoteRequestMessageId();
ikeSaRecord.resetCollectedFragments(false /*isResp*/);
DecodeResultOk resultOk = (DecodeResultOk) decodeResult;
IkeMessage ikeMessage = resultOk.ikeMessage;
ikeSaRecord.updateLastReceivedReqFirstPacket(resultOk.firstPacket);
// Handle DPD here.
if (ikeMessage.isDpdRequest()) {
logd(methodTag + "Received DPD request");
IkeMessage dpdResponse =
buildEncryptedInformationalMessage(
ikeSaRecord,
new IkeInformationalPayload[] {},
true,
ikeHeader.messageId);
sendEncryptedIkeMessage(ikeSaRecord, dpdResponse);
break;
}
int ikeExchangeSubType = getIkeExchangeSubType(ikeMessage);
logd(
methodTag
+ "Request exchange subtype: "
+ EXCHANGE_SUBTYPE_TO_STRING.get(ikeExchangeSubType));
if (ikeExchangeSubType == IKE_EXCHANGE_SUBTYPE_INVALID
|| ikeExchangeSubType == IKE_EXCHANGE_SUBTYPE_IKE_INIT
|| ikeExchangeSubType == IKE_EXCHANGE_SUBTYPE_IKE_AUTH) {
// Reply with INVALID_SYNTAX and close IKE Session.
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeHeader.messageId,
ERROR_TYPE_INVALID_SYNTAX);
handleIkeFatalError(
new InvalidSyntaxException(
"Cannot handle message with invalid or unexpected"
+ " IkeExchangeSubType: "
+ ikeExchangeSubType));
return;
}
handleRequestIkeMessage(ikeMessage, ikeExchangeSubType, message);
break;
case DECODE_STATUS_PARTIAL:
ikeSaRecord.updateCollectedFragments(
(DecodeResultPartial) decodeResult, false /*isResp*/);
break;
case DECODE_STATUS_PROTECTED_ERROR:
DecodeResultProtectedError resultError =
(DecodeResultProtectedError) decodeResult;
IkeException ikeException = resultError.ikeException;
logi(methodTag + "Protected error", resultError.ikeException);
ikeSaRecord.incrementRemoteRequestMessageId();
ikeSaRecord.resetCollectedFragments(false /*isResp*/);
ikeSaRecord.updateLastReceivedReqFirstPacket(resultError.firstPacket);
// IkeException MUST be already wrapped into an IkeProtocolException
handleRequestGenericProcessError(
ikeSaRecord,
ikeHeader.messageId,
(IkeProtocolException) ikeException);
break;
case DECODE_STATUS_UNPROTECTED_ERROR:
logi(
methodTag
+ "Message authentication or decryption failed on"
+ " received request. Discard it",
((DecodeResultError) decodeResult).ikeException);
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Unrecognized decoding status: "
+ decodeResult.status));
}
}
}
}
private boolean isTempFailure(IkeMessage message) {
List<IkeNotifyPayload> notifyPayloads =
message.getPayloadListForType(PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
for (IkeNotifyPayload notify : notifyPayloads) {
if (notify.notifyType == ERROR_TYPE_TEMPORARY_FAILURE) {
return true;
}
}
return false;
}
protected void handleTempFailure() {
// Log and close IKE Session due to unexpected TEMPORARY_FAILURE. This error should
// only occur during CREATE_CHILD_SA exchange.
handleIkeFatalError(
new InvalidSyntaxException("Received unexpected TEMPORARY_FAILURE"));
// States that accept a TEMPORARY MUST override this method to schedule a retry.
}
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
// Subclasses MUST override it if they care
cleanUpAndQuit(
new IllegalStateException(
"Do not support handling an encrypted request: " + ikeExchangeSubType));
}
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
// Subclasses MUST override it if they care
cleanUpAndQuit(
new IllegalStateException("Do not support handling an encrypted response"));
}
/**
* Method for handling generic processing error of a request.
*
* <p>A generic processing error is usally syntax error, unsupported critical payload error
* and major version error. IKE SA that should reply with corresponding error notifications
*/
protected void handleRequestGenericProcessError(
IkeSaRecord ikeSaRecord, int messageId, IkeProtocolException exception) {
IkeNotifyPayload errNotify = exception.buildNotifyPayload();
sendEncryptedIkeMessage(
ikeSaRecord,
buildEncryptedInformationalMessage(
ikeSaRecord,
new IkeInformationalPayload[] {errNotify},
true /*isResponse*/,
messageId));
// Receiver of INVALID_SYNTAX error notification should delete the IKE SA
if (exception.getErrorType() == ERROR_TYPE_INVALID_SYNTAX) {
handleIkeFatalError(exception);
}
}
/**
* Method for handling generic processing error of a response.
*
* <p>Detailed error is wrapped in the InvalidSyntaxException, which is usally syntax error,
* unsupported critical payload error and major version error. IKE SA that receives a
* response with these errors should be closed.
*/
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
// Subclasses MUST override it if they care
cleanUpAndQuit(
new IllegalStateException(
"Do not support handling generic processing error of encrypted"
+ " response"));
}
}
/**
* Retransmitter represents a RAII class to send the initial request, and retransmit as needed.
*
* <p>The Retransmitter class will automatically start transmission upon creation.
*/
@VisibleForTesting
class EncryptedRetransmitter extends Retransmitter {
private final IkeSaRecord mIkeSaRecord;
@VisibleForTesting
EncryptedRetransmitter(IkeMessage msg) {
this(mCurrentIkeSaRecord, msg);
}
private EncryptedRetransmitter(IkeSaRecord ikeSaRecord, IkeMessage msg) {
super(getHandler(), msg);
mIkeSaRecord = ikeSaRecord;
retransmit();
}
@Override
public void send(IkeMessage msg) {
sendEncryptedIkeMessage(mIkeSaRecord, msg);
}
@Override
public void handleRetransmissionFailure() {
handleIkeFatalError(new IOException("Retransmitting failure"));
}
}
/**
* DeleteResponderBase represents all states after IKE_INIT and IKE_AUTH.
*
* <p>All post-init states share common functionality of being able to respond to IKE_DELETE
* requests.
*/
private abstract class DeleteResponderBase extends BusyState {
/** Builds a IKE Delete Response for the given IKE SA and request. */
protected IkeMessage buildIkeDeleteResp(IkeMessage req, IkeSaRecord ikeSaRecord) {
IkeInformationalPayload[] payloads = new IkeInformationalPayload[] {};
return buildEncryptedInformationalMessage(
ikeSaRecord, payloads, true /* isResp */, req.ikeHeader.messageId);
}
/**
* Validates that the delete request is acceptable.
*
* <p>The request message must be guaranteed by previous checks to be of SUBTYPE_DELETE_IKE,
* and therefore contains an IkeDeletePayload. This is checked in getIkeExchangeSubType.
*/
protected void validateIkeDeleteReq(IkeMessage req, IkeSaRecord expectedRecord)
throws InvalidSyntaxException {
if (expectedRecord != getIkeSaRecordForPacket(req.ikeHeader)) {
throw new InvalidSyntaxException("Delete request received in wrong SA");
}
}
/**
* Helper method for responding to a session deletion request
*
* <p>Note that this method expects that the session is keyed on the current IKE SA session,
* and closing the IKE SA indicates that the remote wishes to end the session as a whole. As
* such, this should not be used in rekey cases where there is any ambiguity as to which IKE
* SA the session is reliant upon.
*
* <p>Note that this method will also quit the state machine.
*
* @param ikeMessage The received session deletion request
*/
protected void handleDeleteSessionRequest(IkeMessage ikeMessage) {
try {
validateIkeDeleteReq(ikeMessage, mCurrentIkeSaRecord);
IkeMessage resp = buildIkeDeleteResp(ikeMessage, mCurrentIkeSaRecord);
mUserCbExecutor.execute(
() -> {
mIkeSessionCallback.onClosed();
});
sendEncryptedIkeMessage(mCurrentIkeSaRecord, resp);
removeIkeSaRecord(mCurrentIkeSaRecord);
mCurrentIkeSaRecord.close();
mCurrentIkeSaRecord = null;
quitNow();
} catch (InvalidSyntaxException e) {
// Got deletion of a non-Current IKE SA. Program error.
cleanUpAndQuit(new IllegalStateException(e));
}
}
}
/**
* DeleteBase abstracts deletion handling for all states initiating a delete exchange
*
* <p>All subclasses of this state share common functionality that a deletion request is sent,
* and the response is received.
*/
private abstract class DeleteBase extends DeleteResponderBase {
/** Builds a IKE Delete Request for the given IKE SA. */
protected IkeMessage buildIkeDeleteReq(IkeSaRecord ikeSaRecord) {
IkeInformationalPayload[] payloads =
new IkeInformationalPayload[] {new IkeDeletePayload()};
return buildEncryptedInformationalMessage(
ikeSaRecord,
payloads,
false /* isResp */,
ikeSaRecord.getLocalRequestMessageId());
}
protected void validateIkeDeleteResp(IkeMessage resp, IkeSaRecord expectedSaRecord)
throws InvalidSyntaxException {
if (expectedSaRecord != getIkeSaRecordForPacket(resp.ikeHeader)) {
throw new IllegalStateException("Response received on incorrect SA");
}
if (resp.ikeHeader.exchangeType != IkeHeader.EXCHANGE_TYPE_INFORMATIONAL) {
throw new InvalidSyntaxException(
"Invalid exchange type; expected INFORMATIONAL, but got: "
+ resp.ikeHeader.exchangeType);
}
if (!resp.ikePayloadList.isEmpty()) {
throw new InvalidSyntaxException(
"Unexpected payloads - IKE Delete response should be empty.");
}
}
}
/**
* Receiving represents a state when idle IkeSessionStateMachine receives an incoming packet.
*
* <p>If this incoming packet is fully handled by Receiving state and does not trigger any
* further state transition or deletion of whole IKE Session, IkeSessionStateMachine MUST
* transition back to Idle.
*/
class Receiving extends RekeyIkeHandlerBase {
private boolean mProcedureFinished = true;
@Override
public void enterState() {
mProcedureFinished = true;
}
@Override
protected void handleReceivedIkePacket(Message message) {
super.handleReceivedIkePacket(message);
// If the received packet does not trigger a state transition or the packet causes this
// state machine to quit, transition back to Idle State. In the second case, state
// machine will first go back to Idle and then quit.
if (mProcedureFinished) transitionTo(mIdle);
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_REKEY_IKE:
// Errors in this exchange with no specific protocol error code will all be
// classified to use NO_PROPOSAL_CHOSEN. The reason that we don't use
// NO_ADDITIONAL_SAS is because it indicates "responder is unwilling to accept
// any more Child SAs on this IKE SA.", according to RFC 7296. Sending this
// error may mislead the remote peer.
try {
validateIkeRekeyReq(ikeMessage);
// TODO: Add support for limited re-negotiation of parameters
// Build a rekey response payload with our previously selected proposal,
// against which we will validate the received proposals.
IkeSaPayload reqSaPayload =
ikeMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
byte respProposalNumber =
reqSaPayload.getNegotiatedProposalNumber(mSaProposal);
List<IkePayload> payloadList =
CreateIkeSaHelper.getRekeyIkeSaResponsePayloads(
respProposalNumber, mSaProposal, mLocalAddress);
// Build IKE header
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA,
true /*isResponseMsg*/,
mCurrentIkeSaRecord.isLocalInit,
ikeMessage.ikeHeader.messageId);
IkeMessage responseIkeMessage = new IkeMessage(ikeHeader, payloadList);
// Build new SA first to ensure that we can find a valid proposal.
mRemoteInitNewIkeSaRecord =
validateAndBuildIkeSa(
ikeMessage, responseIkeMessage, false /*isLocalInit*/);
sendEncryptedIkeMessage(responseIkeMessage);
transitionTo(mRekeyIkeRemoteDelete);
mProcedureFinished = false;
} catch (IkeProtocolException e) {
handleRekeyCreationFailure(ikeMessage.ikeHeader.messageId, e);
} catch (GeneralSecurityException e) {
handleRekeyCreationFailure(
ikeMessage.ikeHeader.messageId,
new NoValidProposalChosenException(
"Error in building new IKE SA", e));
} catch (IOException e) {
handleRekeyCreationFailure(
ikeMessage.ikeHeader.messageId,
new NoValidProposalChosenException(
"IKE SPI allocation collided - they reused an SPI.", e));
}
return;
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
handleDeleteSessionRequest(ikeMessage);
return;
case IKE_EXCHANGE_SUBTYPE_CREATE_CHILD: // Fall through
case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD: // Fall through
case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD:
deferMessage(
obtainMessage(
CMD_RECEIVE_REQUEST_FOR_CHILD,
ikeExchangeSubType,
0 /*placeHolder*/,
ikeMessage));
transitionTo(mChildProcedureOngoing);
mProcedureFinished = false;
return;
default:
// TODO: Add support for generic INFORMATIONAL request
}
}
private void handleRekeyCreationFailure(int messageId, IkeProtocolException e) {
loge("Received invalid Rekey IKE request. Reject with error notification", e);
buildAndSendNotificationResponse(
mCurrentIkeSaRecord, messageId, e.buildNotifyPayload());
}
}
/**
* This class represents a state when there is at least one ongoing Child procedure
* (Create/Rekey/Delete Child)
*
* <p>For a locally initiated Child procedure, this state is responsible for notifying Child
* Session to initiate the exchange, building outbound request IkeMessage with Child Session
* provided payload list and redirecting the inbound response to Child Session for validation.
*
* <p>For a remotely initiated Child procedure, this state is responsible for redirecting the
* inbound request to Child Session(s) and building outbound response IkeMessage with Child
* Session provided payload list. Exchange collision on a Child Session will be resolved inside
* the Child Session.
*
* <p>For a remotely initiated IKE procedure, this state will only accept a Delete IKE request
* and reject other types with TEMPORARY_FAILURE, since it causes conflict with the ongoing
* Child procedure.
*
* <p>For most inbound request/response, this state will first pick out and handle IKE related
* payloads and then send the rest of the payloads to Child Session for further validation. It
* is the Child Session's responsibility to check required payloads (and verify the exchange
* type) according to its procedure type. Only when receiving an inbound delete Child request,
* as the only case where multiple Child Sessions will be affected by one IkeMessage, this state
* will only send Delete Payload(s) to Child Session.
*/
class ChildProcedureOngoing extends DeleteBase {
// It is possible that mChildInLocalProcedure is also in mChildInRemoteProcedures when both
// sides initiated exchange for the same Child Session.
private ChildSessionStateMachine mChildInLocalProcedure;
private Set<ChildSessionStateMachine> mChildInRemoteProcedures;
private ChildLocalRequest mLocalRequestOngoing;
private int mLastInboundRequestMsgId;
private List<IkePayload> mOutboundRespPayloads;
private Set<ChildSessionStateMachine> mAwaitingChildResponse;
private EncryptedRetransmitter mRetransmitter;
@Override
public void enterState() {
mChildInLocalProcedure = null;
mChildInRemoteProcedures = new HashSet<>();
mLocalRequestOngoing = null;
mLastInboundRequestMsgId = 0;
mOutboundRespPayloads = new LinkedList<>();
mAwaitingChildResponse = new HashSet<>();
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_REQUEST_FOR_CHILD:
// Handle remote request (and do state transition)
handleRequestIkeMessage(
(IkeMessage) message.obj,
message.arg1 /*ikeExchangeSubType*/,
null /*ReceivedIkePacket*/);
return HANDLED;
case CMD_OUTBOUND_CHILD_PAYLOADS_READY:
ChildOutboundData outboundData = (ChildOutboundData) message.obj;
int exchangeType = outboundData.exchangeType;
List<IkePayload> outboundPayloads = outboundData.payloadList;
if (outboundData.isResp) {
handleOutboundResponse(
exchangeType, outboundPayloads, outboundData.childSession);
} else {
handleOutboundRequest(exchangeType, outboundPayloads);
}
return HANDLED;
case CMD_CHILD_PROCEDURE_FINISHED:
ChildSessionStateMachine childSession = (ChildSessionStateMachine) message.obj;
if (mChildInLocalProcedure == childSession) {
mChildInLocalProcedure = null;
mLocalRequestOngoing = null;
}
mChildInRemoteProcedures.remove(childSession);
transitionToIdleIfAllProceduresDone();
return HANDLED;
case CMD_HANDLE_FIRST_CHILD_NEGOTIATION:
FirstChildNegotiationData childData = (FirstChildNegotiationData) message.obj;
mChildInLocalProcedure = getChildSession(childData.childSessionCallback);
if (mChildInLocalProcedure == null) {
cleanUpAndQuit(new IllegalStateException("First child not found."));
return HANDLED;
}
mChildInLocalProcedure.handleFirstChildExchange(
childData.reqPayloads,
childData.respPayloads,
mLocalAddress,
mRemoteAddress,
getEncapSocketIfNeeded(),
mIkePrf,
mCurrentIkeSaRecord.getSkD());
return HANDLED;
case CMD_EXECUTE_LOCAL_REQ:
executeLocalRequest((ChildLocalRequest) message.obj);
return HANDLED;
default:
return super.processStateMessage(message);
}
}
@Override
protected void handleTempFailure() {
mTempFailHandler.handleTempFailure(mLocalRequestOngoing);
}
private void transitionToIdleIfAllProceduresDone() {
if (mChildInLocalProcedure == null && mChildInRemoteProcedures.isEmpty()) {
transitionTo(mIdle);
}
}
private ChildSessionStateMachine getChildSession(ChildSessionCallback callbacks) {
synchronized (mChildCbToSessions) {
return mChildCbToSessions.get(callbacks);
}
}
private UdpEncapsulationSocket getEncapSocketIfNeeded() {
boolean isNatDetected = mIsLocalBehindNat || mIsRemoteBehindNat;
return (isNatDetected ? mIkeSessionOptions.getUdpEncapsulationSocket() : null);
}
private void executeLocalRequest(ChildLocalRequest req) {
mChildInLocalProcedure = getChildSession(req.childSessionCallback);
mLocalRequestOngoing = req;
if (mChildInLocalProcedure == null) {
// This request has been validated to have a recognized target Child Session when
// it was sent to IKE Session at the begginnig. Failing to find this Child Session
// now means the Child creation has failed.
logd(
"Child state machine not found for local request: "
+ req.procedureType
+ " Creation of Child Session may have been failed.");
transitionToIdleIfAllProceduresDone();
return;
}
switch (req.procedureType) {
case CMD_LOCAL_REQUEST_CREATE_CHILD:
mChildInLocalProcedure.createChildSession(
mLocalAddress,
mRemoteAddress,
getEncapSocketIfNeeded(),
mIkePrf,
mCurrentIkeSaRecord.getSkD());
break;
case CMD_LOCAL_REQUEST_REKEY_CHILD:
mChildInLocalProcedure.rekeyChildSession();
break;
case CMD_LOCAL_REQUEST_DELETE_CHILD:
mChildInLocalProcedure.deleteChildSession();
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Invalid Child procedure type: " + req.procedureType));
break;
}
}
/**
* This method is called when this state receives an inbound request or when mReceiving
* received an inbound Child request and deferred it to this state.
*/
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
// TODO: Grab a remote lock and hand payloads to the Child Session
mLastInboundRequestMsgId = ikeMessage.ikeHeader.messageId;
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_CREATE_CHILD:
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_NO_ADDITIONAL_SAS);
break;
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
// Send response and quit state machine
handleDeleteSessionRequest(ikeMessage);
// Return immediately to avoid transitioning to mIdle
return;
case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD:
handleInboundDeleteChildRequest(ikeMessage);
break;
case IKE_EXCHANGE_SUBTYPE_REKEY_IKE:
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
break;
case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD:
handleInboundRekeyChildRequest(ikeMessage);
break;
case IKE_EXCHANGE_SUBTYPE_GENERIC_INFO:
// TODO:b/139943757 Handle general informational request
default:
cleanUpAndQuit(
new IllegalStateException(
"Invalid IKE exchange subtype: " + ikeExchangeSubType));
return;
}
transitionToIdleIfAllProceduresDone();
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
mRetransmitter.stopRetransmitting();
List<IkePayload> handledPayloads = new LinkedList<>();
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case PAYLOAD_TYPE_NOTIFY:
// TODO: Handle fatal IKE error notification and IKE status notification.
break;
case PAYLOAD_TYPE_VENDOR:
// TODO: Handle Vendor ID Payload
handledPayloads.add(payload);
break;
case PAYLOAD_TYPE_CP:
// TODO: Handle IKE related configuration attributes and pass the payload to
// Child to further handle internal IP address attributes.
break;
default:
break;
}
}
List<IkePayload> payloads = new LinkedList<>();
payloads.addAll(ikeMessage.ikePayloadList);
payloads.removeAll(handledPayloads);
mChildInLocalProcedure.receiveResponse(ikeMessage.ikeHeader.exchangeType, payloads);
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
mRetransmitter.stopRetransmitting();
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
handleIkeFatalError(ikeException);
}
private void handleInboundDeleteChildRequest(IkeMessage ikeMessage) {
// It is guaranteed in #getIkeExchangeSubType that at least one Delete Child Payload
// exists.
HashMap<ChildSessionStateMachine, List<IkePayload>> childToDelPayloadsMap =
new HashMap<>();
Set<Integer> spiHandled = new HashSet<>();
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case PAYLOAD_TYPE_VENDOR:
// TODO: Investigate if Vendor ID Payload can be in an INFORMATIONAL
// message.
break;
case PAYLOAD_TYPE_NOTIFY:
logw(
"Unexpected or unknown notification: "
+ ((IkeNotifyPayload) payload).notifyType);
break;
case PAYLOAD_TYPE_DELETE:
IkeDeletePayload delPayload = (IkeDeletePayload) payload;
for (int spi : delPayload.spisToDelete) {
ChildSessionStateMachine child = mRemoteSpiToChildSessionMap.get(spi);
if (child == null) {
// TODO: Investigate how other implementations handle that.
logw("Child SA not found with received SPI: " + spi);
} else if (!spiHandled.add(spi)) {
logw("Received repeated Child SPI: " + spi);
} else {
// Store Delete Payload with its target ChildSession
if (!childToDelPayloadsMap.containsKey(child)) {
childToDelPayloadsMap.put(child, new LinkedList<>());
}
List<IkePayload> delPayloads = childToDelPayloadsMap.get(child);
// Avoid storing repeated Delete Payload
if (!delPayloads.contains(delPayload)) delPayloads.add(delPayload);
}
}
break;
case PAYLOAD_TYPE_CP:
// TODO: Handle it
break;
default:
logw("Unexpected payload types found: " + payload.payloadType);
}
}
// If no Child SA is found, only reply with IKE related payloads or an empty
// message
if (childToDelPayloadsMap.isEmpty()) {
logd("No Child SA is found for this request.");
sendEncryptedIkeMessage(
buildEncryptedInformationalMessage(
new IkeInformationalPayload[0],
true /*isResp*/,
ikeMessage.ikeHeader.messageId));
return;
}
// Send Delete Payloads to Child Sessions
for (ChildSessionStateMachine child : childToDelPayloadsMap.keySet()) {
child.receiveRequest(
IKE_EXCHANGE_SUBTYPE_DELETE_CHILD,
EXCHANGE_TYPE_INFORMATIONAL,
childToDelPayloadsMap.get(child));
mAwaitingChildResponse.add(child);
mChildInRemoteProcedures.add(child);
}
}
private void handleInboundRekeyChildRequest(IkeMessage ikeMessage) {
// It is guaranteed in #getIkeExchangeSubType that at least one Notify-Rekey Child
// Payload exists.
List<IkePayload> handledPayloads = new LinkedList<>();
ChildSessionStateMachine targetChild = null;
Set<Integer> unrecognizedSpis = new HashSet<>();
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case PAYLOAD_TYPE_VENDOR:
// TODO: Handle it.
handledPayloads.add(payload);
break;
case PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (NOTIFY_TYPE_REKEY_SA != notifyPayload.notifyType) break;
int childSpi = notifyPayload.spi;
ChildSessionStateMachine child = mRemoteSpiToChildSessionMap.get(childSpi);
if (child == null) {
// Remember unrecognized SPIs and reply error notification if no
// recognized SPI found.
unrecognizedSpis.add(childSpi);
logw("Child SA not found with received SPI: " + childSpi);
} else if (targetChild == null) {
// Each message should have only one Notify-Rekey Payload. If there are
// multiple of them, we only process the first valid one and ignore
// others.
targetChild = mRemoteSpiToChildSessionMap.get(childSpi);
} else {
logw("More than one Notify-Rekey Payload found with SPI: " + childSpi);
handledPayloads.add(notifyPayload);
}
break;
case PAYLOAD_TYPE_CP:
// TODO: Handle IKE related configuration attributes and pass the payload to
// Child to further handle internal IP address attributes.
break;
default:
break;
}
}
// Reject request with error notification.
if (targetChild == null) {
IkeInformationalPayload[] errorPayloads =
new IkeInformationalPayload[unrecognizedSpis.size()];
int i = 0;
for (Integer spi : unrecognizedSpis) {
errorPayloads[i++] =
new IkeNotifyPayload(
IkePayload.PROTOCOL_ID_ESP,
spi,
ERROR_TYPE_CHILD_SA_NOT_FOUND,
new byte[0]);
}
IkeMessage msg =
buildEncryptedNotificationMessage(
mCurrentIkeSaRecord,
errorPayloads,
EXCHANGE_TYPE_INFORMATIONAL,
true /*isResponse*/,
ikeMessage.ikeHeader.messageId);
sendEncryptedIkeMessage(mCurrentIkeSaRecord, msg);
return;
}
// Normal path
List<IkePayload> payloads = new LinkedList<>();
payloads.addAll(ikeMessage.ikePayloadList);
payloads.removeAll(handledPayloads);
mAwaitingChildResponse.add(targetChild);
mChildInRemoteProcedures.add(targetChild);
targetChild.receiveRequest(
IKE_EXCHANGE_SUBTYPE_REKEY_CHILD, ikeMessage.ikeHeader.exchangeType, payloads);
}
private void handleOutboundRequest(int exchangeType, List<IkePayload> outboundPayloads) {
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
exchangeType,
false /*isResp*/,
mCurrentIkeSaRecord.isLocalInit,
mCurrentIkeSaRecord.getLocalRequestMessageId());
IkeMessage ikeMessage = new IkeMessage(ikeHeader, outboundPayloads);
mRetransmitter = new EncryptedRetransmitter(ikeMessage);
}
private void handleOutboundResponse(
int exchangeType,
List<IkePayload> outboundPayloads,
ChildSessionStateMachine childSession) {
// For each request IKE passed to Child, Child will send back to IKE a response. Even
// if the Child Sesison is under simultaneous deletion, it will send back an empty
// payload list.
mOutboundRespPayloads.addAll(outboundPayloads);
mAwaitingChildResponse.remove(childSession);
if (!mAwaitingChildResponse.isEmpty()) return;
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
exchangeType,
true /*isResp*/,
mCurrentIkeSaRecord.isLocalInit,
mLastInboundRequestMsgId);
IkeMessage ikeMessage = new IkeMessage(ikeHeader, mOutboundRespPayloads);
sendEncryptedIkeMessage(ikeMessage);
}
}
/** CreateIkeLocalIkeInit represents state when IKE library initiates IKE_INIT exchange. */
@VisibleForTesting
public class CreateIkeLocalIkeInit extends BusyState {
private IkeSecurityParameterIndex mLocalIkeSpiResource;
private IkeSecurityParameterIndex mRemoteIkeSpiResource;
private Retransmitter mRetransmitter;
// TODO: Support negotiating IKE fragmentation
@Override
public void enterState() {
try {
IkeMessage request = buildIkeInitReq();
// Register local SPI to receive the IKE INIT response.
mIkeSocket.registerIke(
request.ikeHeader.ikeInitiatorSpi, IkeSessionStateMachine.this);
mIkeInitRequestBytes = request.encode();
mIkeInitNoncePayload =
request.getPayloadForType(
IkePayload.PAYLOAD_TYPE_NONCE, IkeNoncePayload.class);
mRetransmitter = new UnencryptedRetransmitter(request);
} catch (IOException e) {
// Fail to assign IKE SPI
handleIkeFatalError(e);
}
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
handleReceivedIkePacket(message);
return HANDLED;
default:
return super.processStateMessage(message);
}
}
protected void handleReceivedIkePacket(Message message) {
String methodTag = "handleReceivedIkePacket: ";
ReceivedIkePacket receivedIkePacket = (ReceivedIkePacket) message.obj;
IkeHeader ikeHeader = receivedIkePacket.ikeHeader;
byte[] ikePacketBytes = receivedIkePacket.ikePacketBytes;
logd(
methodTag
+ "Received an "
+ ikeHeader.getBasicInfoString()
+ ". Packet size: "
+ ikePacketBytes.length);
if (ikeHeader.isResponseMsg) {
DecodeResult decodeResult = IkeMessage.decode(0, ikeHeader, ikePacketBytes);
switch (decodeResult.status) {
case DECODE_STATUS_OK:
handleResponseIkeMessage(((DecodeResultOk) decodeResult).ikeMessage);
mIkeInitResponseBytes = ikePacketBytes;
// SA negotiation failed
if (mCurrentIkeSaRecord == null) break;
mCurrentIkeSaRecord.incrementLocalRequestMessageId();
break;
case DECODE_STATUS_PARTIAL:
// Fall through. We don't support IKE fragmentation here. We should never
// get this status.
case DECODE_STATUS_PROTECTED_ERROR:
// IKE INIT response is not protected. So we should never get this status
cleanUpAndQuit(
new IllegalStateException(
"Unexpected decoding status: " + decodeResult.status));
break;
case DECODE_STATUS_UNPROTECTED_ERROR:
logi(
"Discard unencrypted response with syntax error",
((DecodeResultError) decodeResult).ikeException);
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Invalid decoding status: " + decodeResult.status));
}
} else {
// TODO: Also prettyprint IKE header in the log.
logi("Received a request while waiting for IKE_INIT response. Discard it.");
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
boolean ikeInitSuccess = false;
try {
validateIkeInitResp(mRetransmitter.getMessage(), ikeMessage);
mCurrentIkeSaRecord =
IkeSaRecord.makeFirstIkeSaRecord(
mRetransmitter.getMessage(),
ikeMessage,
mLocalIkeSpiResource,
mRemoteIkeSpiResource,
mIkePrf,
mIkeIntegrity == null ? 0 : mIkeIntegrity.getKeyLength(),
mIkeCipher.getKeyLength(),
new LocalRequest(CMD_LOCAL_REQUEST_REKEY_IKE));
addIkeSaRecord(mCurrentIkeSaRecord);
ikeInitSuccess = true;
transitionTo(mCreateIkeLocalIkeAuth);
} catch (IkeProtocolException | GeneralSecurityException | IOException e) {
// TODO: Try another DH group to buld KE Payload if receiving InvalidKeException
handleIkeFatalError(e);
} finally {
if (!ikeInitSuccess) {
if (mLocalIkeSpiResource != null) {
mLocalIkeSpiResource.close();
mLocalIkeSpiResource = null;
}
if (mRemoteIkeSpiResource != null) {
mRemoteIkeSpiResource.close();
mRemoteIkeSpiResource = null;
}
}
}
}
private IkeMessage buildIkeInitReq() throws IOException {
// Generate IKE SPI
mLocalIkeSpiResource =
IkeSecurityParameterIndex.allocateSecurityParameterIndex(mLocalAddress);
long initSpi = mLocalIkeSpiResource.getSpi();
long respSpi = 0;
// It is validated in IkeSessionOptions.Builder to ensure IkeSessionOptions has at least
// one IkeSaProposal and all SaProposals are valid for IKE SA negotiation.
IkeSaProposal[] saProposals = mIkeSessionOptions.getSaProposals();
List<IkePayload> payloadList =
CreateIkeSaHelper.getIkeInitSaRequestPayloads(
saProposals,
initSpi,
respSpi,
mLocalAddress,
mRemoteAddress,
mLocalPort,
IkeSocket.IKE_SERVER_PORT);
payloadList.add(
new IkeNotifyPayload(
IkeNotifyPayload.NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED));
// TODO: Add Notification Payloads according to user configurations.
// Build IKE header
IkeHeader ikeHeader =
new IkeHeader(
initSpi,
respSpi,
IkePayload.PAYLOAD_TYPE_SA,
IkeHeader.EXCHANGE_TYPE_IKE_SA_INIT,
false /*isResponseMsg*/,
true /*fromIkeInitiator*/,
0 /*messageId*/);
return new IkeMessage(ikeHeader, payloadList);
}
private void validateIkeInitResp(IkeMessage reqMsg, IkeMessage respMsg)
throws IkeProtocolException, IOException {
IkeHeader respIkeHeader = respMsg.ikeHeader;
mRemoteIkeSpiResource =
IkeSecurityParameterIndex.allocateSecurityParameterIndex(
mIkeSessionOptions.getServerAddress(), respIkeHeader.ikeResponderSpi);
int exchangeType = respIkeHeader.exchangeType;
if (exchangeType != IkeHeader.EXCHANGE_TYPE_IKE_SA_INIT) {
throw new InvalidSyntaxException(
"Expected EXCHANGE_TYPE_IKE_SA_INIT but received: " + exchangeType);
}
IkeSaPayload respSaPayload = null;
IkeKePayload respKePayload = null;
/**
* There MAY be multiple NAT_DETECTION_SOURCE_IP payloads in a message if the sender
* does not know which of several network attachments will be used to send the packet.
*/
List<IkeNotifyPayload> natSourcePayloads = new LinkedList<>();
IkeNotifyPayload natDestPayload = null;
boolean hasNoncePayload = false;
for (IkePayload payload : respMsg.ikePayloadList) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_SA:
respSaPayload = (IkeSaPayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_KE:
respKePayload = (IkeKePayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_CERT_REQUEST:
throw new UnsupportedOperationException(
"Do not support handling Cert Request Payload.");
// TODO: Handle it when using certificate based authentication. Otherwise,
// ignore it.
case IkePayload.PAYLOAD_TYPE_NONCE:
hasNoncePayload = true;
mIkeRespNoncePayload = (IkeNoncePayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_VENDOR:
// Do not support any vendor defined protocol extensions. Ignore
// all Vendor ID Payloads.
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (notifyPayload.isErrorNotify()) {
throw notifyPayload.validateAndBuildIkeException();
}
switch (notifyPayload.notifyType) {
case NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP:
natSourcePayloads.add(notifyPayload);
break;
case NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP:
if (natDestPayload != null) {
throw new InvalidSyntaxException(
"More than one"
+ " NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP"
+ " found");
}
natDestPayload = notifyPayload;
break;
case NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED:
mSupportFragment = true;
break;
default:
// Unknown and unexpected status notifications are ignored as per
// RFC7296.
logw(
"Received unknown or unexpected status notifications with"
+ " notify type: "
+ notifyPayload.notifyType);
}
break;
default:
logw(
"Received unexpected payload in IKE INIT response. Payload type: "
+ payload.payloadType);
}
}
if (respSaPayload == null
|| respKePayload == null
|| natSourcePayloads.isEmpty()
|| natDestPayload == null
|| !hasNoncePayload) {
throw new InvalidSyntaxException(
"SA, KE, Nonce, Notify-NAT-Detection-Source, or"
+ " Notify-NAT-Detection-Destination payload missing.");
}
IkeSaPayload reqSaPayload =
reqMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
mSaProposal =
IkeSaPayload.getVerifiedNegotiatedIkeProposalPair(
reqSaPayload, respSaPayload, mRemoteAddress)
.second
.saProposal;
// Build IKE crypto tools using mSaProposal. It is ensured that mSaProposal is valid and
// has exactly one Transform for each Transform type. Only exception is when
// combined-mode cipher is used, there will be either no integrity algorithm or an
// INTEGRITY_ALGORITHM_NONE type algorithm.
Provider provider = IkeMessage.getSecurityProvider();
mIkeCipher = IkeCipher.create(mSaProposal.getEncryptionTransforms()[0], provider);
if (!mIkeCipher.isAead()) {
mIkeIntegrity =
IkeMacIntegrity.create(mSaProposal.getIntegrityTransforms()[0], provider);
}
mIkePrf = IkeMacPrf.create(mSaProposal.getPrfTransforms()[0], provider);
IkeKePayload reqKePayload =
reqMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class);
if (reqKePayload.dhGroup != respKePayload.dhGroup
&& respKePayload.dhGroup != mSaProposal.getDhGroupTransforms()[0].id) {
throw new InvalidSyntaxException("Received KE payload with mismatched DH group.");
}
// NAT detection
long initIkeSpi = respMsg.ikeHeader.ikeInitiatorSpi;
long respIkeSpi = respMsg.ikeHeader.ikeResponderSpi;
mIsLocalBehindNat = true;
mIsRemoteBehindNat = true;
// Check if local node is behind NAT
byte[] expectedLocalNatData =
IkeNotifyPayload.generateNatDetectionData(
initIkeSpi, respIkeSpi, mLocalAddress, mLocalPort);
mIsLocalBehindNat = !Arrays.equals(expectedLocalNatData, natDestPayload.notifyData);
// Check if the remote node is behind NAT
byte[] expectedRemoteNatData =
IkeNotifyPayload.generateNatDetectionData(
initIkeSpi, respIkeSpi, mRemoteAddress, IkeSocket.IKE_SERVER_PORT);
for (IkeNotifyPayload natPayload : natSourcePayloads) {
// If none of the received hash matches the expected value, the remote node is
// behind NAT.
if (Arrays.equals(expectedRemoteNatData, natPayload.notifyData)) {
mIsRemoteBehindNat = false;
}
}
}
@Override
public void exitState() {
super.exitState();
mRetransmitter.stopRetransmitting();
}
private class UnencryptedRetransmitter extends Retransmitter {
private UnencryptedRetransmitter(IkeMessage msg) {
super(getHandler(), msg);
retransmit();
}
@Override
public void send(IkeMessage msg) {
// Sends unencrypted
mIkeSocket.sendIkePacket(msg.encode(), mRemoteAddress);
}
@Override
public void handleRetransmissionFailure() {
handleIkeFatalError(new IOException("Retransmitting IKE INIT request failure"));
}
}
}
/**
* CreateIkeLocalIkeAuthBase represents the common state and functionality required to perform
* IKE AUTH exchanges in both the EAP and non-EAP flows.
*/
abstract class CreateIkeLocalIkeAuthBase extends DeleteBase {
protected Retransmitter mRetransmitter;
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
// TODO: b/139482382 If receiving a remote request while waiting for the last IKE AUTH
// response, defer it to next state.
protected IkeMessage buildIkeAuthReqMessage(List<IkePayload> payloadList) {
// Build IKE header
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
IkeHeader.EXCHANGE_TYPE_IKE_AUTH,
false /*isResponseMsg*/,
true /*fromIkeInitiator*/,
mCurrentIkeSaRecord.getLocalRequestMessageId());
return new IkeMessage(ikeHeader, payloadList);
}
protected void authenticatePsk(
byte[] psk, IkeAuthPayload authPayload, IkeIdPayload respIdPayload)
throws AuthenticationFailedException {
if (authPayload.authMethod != IkeAuthPayload.AUTH_METHOD_PRE_SHARED_KEY) {
throw new AuthenticationFailedException(
"Expected the remote/server to use PSK-based authentication but"
+ " they used: "
+ authPayload.authMethod);
}
IkeAuthPskPayload pskPayload = (IkeAuthPskPayload) authPayload;
pskPayload.verifyInboundSignature(
psk,
mIkeInitResponseBytes,
mCurrentIkeSaRecord.nonceInitiator,
respIdPayload.getEncodedPayloadBody(),
mIkePrf,
mCurrentIkeSaRecord.getSkPr());
}
protected List<IkePayload> extractChildPayloadsFromMessage(IkeMessage ikeMessage)
throws InvalidSyntaxException {
IkeSaPayload saPayload =
ikeMessage.getPayloadForType(IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
IkeTsPayload tsInitPayload =
ikeMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_TS_INITIATOR, IkeTsPayload.class);
IkeTsPayload tsRespPayload =
ikeMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_TS_RESPONDER, IkeTsPayload.class);
List<IkeNotifyPayload> notifyPayloads =
ikeMessage.getPayloadListForType(
IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
boolean hasErrorNotify = false;
List<IkePayload> list = new LinkedList<>();
for (IkeNotifyPayload payload : notifyPayloads) {
if (payload.isNewChildSaNotify()) {
list.add(payload);
if (payload.isErrorNotify()) {
hasErrorNotify = true;
}
}
}
// If there is no error notification, SA, TS-initiator and TS-responder MUST all be
// included in this message.
if (!hasErrorNotify
&& (saPayload == null || tsInitPayload == null || tsRespPayload == null)) {
throw new InvalidSyntaxException(
"SA, TS-Initiator or TS-Responder payload is missing.");
}
list.add(saPayload);
list.add(tsInitPayload);
list.add(tsRespPayload);
return list;
}
protected void performFirstChildNegotiation(
List<IkePayload> childReqList, List<IkePayload> childRespList) {
childReqList.add(mIkeInitNoncePayload);
childRespList.add(mIkeRespNoncePayload);
deferMessage(
obtainMessage(
CMD_HANDLE_FIRST_CHILD_NEGOTIATION,
new FirstChildNegotiationData(
mFirstChildSessionOptions,
mFirstChildCallbacks,
childReqList,
childRespList)));
mUserCbExecutor.execute(
() -> {
mIkeSessionCallback.onOpened(null /*sessionConfiguration*/);
// TODO: Construct and pass a real IkeSessionConfiguration
});
transitionTo(mChildProcedureOngoing);
}
}
/**
* CreateIkeLocalIkeAuth represents state when IKE library initiates IKE_AUTH exchange.
*
* <p>If using EAP, CreateIkeLocalIkeAuth will transition to CreateIkeLocalIkeAuthInEap state
* after validating the IKE AUTH response.
*/
class CreateIkeLocalIkeAuth extends CreateIkeLocalIkeAuthBase {
private boolean mUseEap;
@Override
public void enterState() {
try {
super.enterState();
mRetransmitter = new EncryptedRetransmitter(buildIkeAuthReq());
mUseEap =
(IkeSessionOptions.IKE_AUTH_METHOD_EAP
== mIkeSessionOptions.getLocalAuthConfig().mAuthMethod);
} catch (ResourceUnavailableException e) {
// Handle IPsec SPI assigning failure.
handleIkeFatalError(e);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
int exchangeType = ikeMessage.ikeHeader.exchangeType;
if (exchangeType != IkeHeader.EXCHANGE_TYPE_IKE_AUTH) {
throw new InvalidSyntaxException(
"Expected EXCHANGE_TYPE_IKE_AUTH but received: " + exchangeType);
}
List<IkePayload> childReqList =
extractChildPayloadsFromMessage(mRetransmitter.getMessage());
if (mUseEap) {
validateIkeAuthRespWithEapPayload(ikeMessage);
// childReqList needed after EAP completed, so persist to IkeSessionStateMachine
// state.
mFirstChildReqList = childReqList;
IkeEapPayload ikeEapPayload =
ikeMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_EAP, IkeEapPayload.class);
deferMessage(obtainMessage(CMD_EAP_START_EAP_AUTH, ikeEapPayload));
transitionTo(mCreateIkeLocalIkeAuthInEap);
} else {
validateIkeAuthRespWithChildPayloads(ikeMessage);
performFirstChildNegotiation(
childReqList, extractChildPayloadsFromMessage(ikeMessage));
}
} catch (IkeProtocolException e) {
if (!mUseEap) {
// Notify the remote because they may have set up the IKE SA.
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
}
handleIkeFatalError(e);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
mRetransmitter.stopRetransmitting();
if (!mUseEap) {
// Notify the remote because they may have set up the IKE SA.
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
}
handleIkeFatalError(ikeException);
}
private IkeMessage buildIkeAuthReq() throws ResourceUnavailableException {
List<IkePayload> payloadList = new LinkedList<>();
// Build Identification payloads
mInitIdPayload =
new IkeIdPayload(
true /*isInitiator*/, mIkeSessionOptions.getLocalIdentification());
IkeIdPayload respIdPayload =
new IkeIdPayload(
false /*isInitiator*/, mIkeSessionOptions.getRemoteIdentification());
payloadList.add(mInitIdPayload);
payloadList.add(respIdPayload);
// Build Authentication payload
IkeAuthConfig authConfig = mIkeSessionOptions.getLocalAuthConfig();
switch (authConfig.mAuthMethod) {
case IkeSessionOptions.IKE_AUTH_METHOD_PSK:
IkeAuthPskPayload pskPayload =
new IkeAuthPskPayload(
((IkeAuthPskConfig) authConfig).mPsk,
mIkeInitRequestBytes,
mCurrentIkeSaRecord.nonceResponder,
mInitIdPayload.getEncodedPayloadBody(),
mIkePrf,
mCurrentIkeSaRecord.getSkPi());
payloadList.add(pskPayload);
break;
case IkeSessionOptions.IKE_AUTH_METHOD_PUB_KEY_SIGNATURE:
// TODO: Support authentication based on public key signature.
throw new UnsupportedOperationException(
"Do not support public-key based authentication.");
case IkeSessionOptions.IKE_AUTH_METHOD_EAP:
// Do not include AUTH payload when using EAP.
break;
default:
cleanUpAndQuit(
new IllegalArgumentException(
"Unrecognized authentication method: "
+ authConfig.mAuthMethod));
}
payloadList.addAll(
CreateChildSaHelper.getInitChildCreateReqPayloads(
mIpSecManager,
mLocalAddress,
mFirstChildSessionOptions,
true /*isFirstChild*/));
return buildIkeAuthReqMessage(payloadList);
}
private void validateIkeAuthRespWithEapPayload(IkeMessage respMsg)
throws IkeProtocolException {
IkeEapPayload ikeEapPayload =
respMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_EAP, IkeEapPayload.class);
if (ikeEapPayload == null) {
throw new AuthenticationFailedException("Missing EAP payload");
}
// TODO: check that we don't receive any ChildSaRespPayloads here
List<IkePayload> nonEapPayloads = new LinkedList<>();
nonEapPayloads.addAll(respMsg.ikePayloadList);
nonEapPayloads.remove(ikeEapPayload);
validateIkeAuthResp(nonEapPayloads);
}
private void validateIkeAuthRespWithChildPayloads(IkeMessage respMsg)
throws IkeProtocolException {
// Extract and validate existence of payloads for first Child SA setup.
List<IkePayload> childSaRespPayloads = extractChildPayloadsFromMessage(respMsg);
List<IkePayload> nonChildPayloads = new LinkedList<>();
nonChildPayloads.addAll(respMsg.ikePayloadList);
nonChildPayloads.removeAll(childSaRespPayloads);
validateIkeAuthResp(nonChildPayloads);
}
private void validateIkeAuthResp(List<IkePayload> payloadList) throws IkeProtocolException {
// Validate IKE Authentication
IkeAuthPayload authPayload = null;
List<IkeCertPayload> certPayloads = new LinkedList<>();
for (IkePayload payload : payloadList) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_ID_RESPONDER:
mRespIdPayload = (IkeIdPayload) payload;
if (!mIkeSessionOptions
.getRemoteIdentification()
.equals(mRespIdPayload.ikeId)) {
throw new AuthenticationFailedException(
"Unrecognized Responder Identification.");
}
break;
case IkePayload.PAYLOAD_TYPE_AUTH:
authPayload = (IkeAuthPayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_CERT:
certPayloads.add((IkeCertPayload) payload);
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (notifyPayload.isErrorNotify()) {
throw notifyPayload.validateAndBuildIkeException();
} else {
// Unknown and unexpected status notifications are ignored as per
// RFC7296.
logw(
"Received unknown or unexpected status notifications with"
+ " notify type: "
+ notifyPayload.notifyType);
}
break;
default:
logw(
"Received unexpected payload in IKE AUTH response. Payload"
+ " type: "
+ payload.payloadType);
}
}
// Verify existence of payloads
if (mRespIdPayload == null || authPayload == null) {
throw new AuthenticationFailedException("ID-Responder or Auth payload is missing.");
}
// Authenticate the remote peer.
authenticate(authPayload, mRespIdPayload, certPayloads);
}
private void authenticate(
IkeAuthPayload authPayload,
IkeIdPayload respIdPayload,
List<IkeCertPayload> certPayloads)
throws AuthenticationFailedException {
switch (mIkeSessionOptions.getRemoteAuthConfig().mAuthMethod) {
case IkeSessionOptions.IKE_AUTH_METHOD_PSK:
authenticatePsk(
((IkeAuthPskConfig) mIkeSessionOptions.getRemoteAuthConfig()).mPsk,
authPayload,
respIdPayload);
break;
case IkeSessionOptions.IKE_AUTH_METHOD_PUB_KEY_SIGNATURE:
authenticateDigitalSignature(
certPayloads,
((IkeAuthDigitalSignRemoteConfig)
mIkeSessionOptions.getRemoteAuthConfig())
.mTrustAnchor,
authPayload,
respIdPayload);
break;
default:
cleanUpAndQuit(
new IllegalArgumentException(
"Unrecognized auth method: " + authPayload.authMethod));
}
}
private void authenticateDigitalSignature(
List<IkeCertPayload> certPayloads,
TrustAnchor trustAnchor,
IkeAuthPayload authPayload,
IkeIdPayload respIdPayload)
throws AuthenticationFailedException {
if (authPayload.authMethod != IkeAuthPayload.AUTH_METHOD_RSA_DIGITAL_SIGN
&& authPayload.authMethod != IkeAuthPayload.AUTH_METHOD_GENERIC_DIGITAL_SIGN) {
throw new AuthenticationFailedException(
"Expected the remote/server to use digital-signature-based authentication"
+ " but they used: "
+ authPayload.authMethod);
}
X509Certificate endCert = null;
List<X509Certificate> certList = new LinkedList<>();
// TODO: b/122676944 Extract CRL from IkeCrlPayload when we support IkeCrlPayload
for (IkeCertPayload certPayload : certPayloads) {
X509Certificate cert = ((IkeCertX509CertPayload) certPayload).certificate;
// The first certificate MUST be the end entity certificate.
if (endCert == null) endCert = cert;
certList.add(cert);
}
if (endCert == null) {
throw new AuthenticationFailedException(
"The remote/server failed to provide a end certificate");
}
Set<TrustAnchor> trustAnchorSet = new HashSet<>();
trustAnchorSet.add(trustAnchor);
IkeCertPayload.validateCertificates(
endCert, certList, null /*crlList*/, trustAnchorSet);
IkeAuthDigitalSignPayload signPayload = (IkeAuthDigitalSignPayload) authPayload;
signPayload.verifyInboundSignature(
endCert,
mIkeInitResponseBytes,
mCurrentIkeSaRecord.nonceInitiator,
respIdPayload.getEncodedPayloadBody(),
mIkePrf,
mCurrentIkeSaRecord.getSkPr());
}
@Override
public void exitState() {
mRetransmitter.stopRetransmitting();
}
}
/**
* CreateIkeLocalIkeAuthInEap represents the state when the IKE library authenticates the client
* with an EAP session.
*/
class CreateIkeLocalIkeAuthInEap extends CreateIkeLocalIkeAuthBase {
private EapAuthenticator mEapAuthenticator;
@Override
public void enterState() {
IkeSessionOptions.IkeAuthEapConfig ikeAuthEapConfig =
(IkeSessionOptions.IkeAuthEapConfig) mIkeSessionOptions.getLocalAuthConfig();
mEapAuthenticator =
mEapAuthenticatorFactory.newEapAuthenticator(
getHandler().getLooper(),
new IkeEapCallback(),
mContext,
ikeAuthEapConfig.mEapConfig);
}
@Override
public boolean processStateMessage(Message msg) {
switch (msg.what) {
case CMD_EAP_START_EAP_AUTH:
IkeEapPayload ikeEapPayload = (IkeEapPayload) msg.obj;
mEapAuthenticator.processEapMessage(ikeEapPayload.eapMessage);
return HANDLED;
case CMD_EAP_OUTBOUND_MSG_READY:
byte[] eapMsgBytes = (byte[]) msg.obj;
IkeEapPayload eapPayload = new IkeEapPayload(eapMsgBytes);
// Setup new retransmitter with EAP response
mRetransmitter =
new EncryptedRetransmitter(
buildIkeAuthReqMessage(Arrays.asList(eapPayload)));
return HANDLED;
case CMD_EAP_ERRORED:
handleIkeFatalError(new AuthenticationFailedException((Throwable) msg.obj));
return HANDLED;
case CMD_EAP_FAILED:
AuthenticationFailedException exception =
new AuthenticationFailedException("EAP Authentication Failed");
handleIkeFatalError(exception);
return HANDLED;
case CMD_EAP_FINISH_EAP_AUTH:
deferMessage(msg);
transitionTo(mCreateIkeLocalIkeAuthPostEap);
return HANDLED;
default:
return super.processStateMessage(msg);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
mRetransmitter.stopRetransmitting();
int exchangeType = ikeMessage.ikeHeader.exchangeType;
if (exchangeType != IkeHeader.EXCHANGE_TYPE_IKE_AUTH) {
throw new InvalidSyntaxException(
"Expected EXCHANGE_TYPE_IKE_AUTH but received: " + exchangeType);
}
IkeEapPayload eapPayload = null;
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_EAP:
eapPayload = (IkeEapPayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (notifyPayload.isErrorNotify()) {
throw notifyPayload.validateAndBuildIkeException();
} else {
// Unknown and unexpected status notifications are ignored as per
// RFC7296.
logw(
"Received unknown or unexpected status notifications with"
+ " notify type: "
+ notifyPayload.notifyType);
}
break;
default:
logw(
"Received unexpected payload in IKE AUTH response. Payload"
+ " type: "
+ payload.payloadType);
}
}
if (eapPayload == null) {
throw new AuthenticationFailedException("EAP Payload is missing.");
}
mEapAuthenticator.processEapMessage(eapPayload.eapMessage);
} catch (IkeProtocolException exception) {
handleIkeFatalError(exception);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
mRetransmitter.stopRetransmitting();
handleIkeFatalError(ikeException);
}
private class IkeEapCallback implements IEapCallback {
@Override
public void onSuccess(byte[] msk, byte[] emsk) {
// Extended MSK not used in IKEv2, drop.
sendMessage(CMD_EAP_FINISH_EAP_AUTH, msk);
}
@Override
public void onFail() {
sendMessage(CMD_EAP_FAILED);
}
@Override
public void onResponse(byte[] eapMsg) {
sendMessage(CMD_EAP_OUTBOUND_MSG_READY, eapMsg);
}
@Override
public void onError(Throwable cause) {
sendMessage(CMD_EAP_ERRORED, cause);
}
}
}
/**
* CreateIkeLocalIkeAuthPostEap represents the state when the IKE library is performing the
* post-EAP PSK-base authentication run.
*/
class CreateIkeLocalIkeAuthPostEap extends CreateIkeLocalIkeAuthBase {
private byte[] mEapMsk = new byte[0];
@Override
public boolean processStateMessage(Message msg) {
switch (msg.what) {
case CMD_EAP_FINISH_EAP_AUTH:
mEapMsk = (byte[]) msg.obj;
IkeAuthPskPayload pskPayload =
new IkeAuthPskPayload(
mEapMsk,
mIkeInitRequestBytes,
mCurrentIkeSaRecord.nonceResponder,
mInitIdPayload.getEncodedPayloadBody(),
mIkePrf,
mCurrentIkeSaRecord.getSkPi());
IkeMessage postEapAuthMsg = buildIkeAuthReqMessage(Arrays.asList(pskPayload));
mRetransmitter = new EncryptedRetransmitter(postEapAuthMsg);
return HANDLED;
default:
return super.processStateMessage(msg);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
int exchangeType = ikeMessage.ikeHeader.exchangeType;
if (exchangeType != IkeHeader.EXCHANGE_TYPE_IKE_AUTH) {
throw new InvalidSyntaxException(
"Expected EXCHANGE_TYPE_IKE_AUTH but received: " + exchangeType);
}
// Extract and validate existence of payloads for first Child SA setup.
List<IkePayload> childSaRespPayloads = extractChildPayloadsFromMessage(ikeMessage);
List<IkePayload> nonChildPayloads = new LinkedList<>();
nonChildPayloads.addAll(ikeMessage.ikePayloadList);
nonChildPayloads.removeAll(childSaRespPayloads);
validateIkeAuthRespPostEap(nonChildPayloads);
performFirstChildNegotiation(mFirstChildReqList, childSaRespPayloads);
} catch (IkeProtocolException e) {
// Notify the remote because they may have set up the IKE SA.
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
handleIkeFatalError(e);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
mRetransmitter.stopRetransmitting();
// Notify the remote because they may have set up the IKE SA.
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
handleIkeFatalError(ikeException);
}
private void validateIkeAuthRespPostEap(List<IkePayload> payloadList)
throws IkeProtocolException {
IkeAuthPayload authPayload = null;
for (IkePayload payload : payloadList) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_AUTH:
authPayload = (IkeAuthPayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (notifyPayload.isErrorNotify()) {
throw notifyPayload.validateAndBuildIkeException();
} else {
// Unknown and unexpected status notifications are ignored as per
// RFC7296.
logw(
"Received unknown or unexpected status notifications with"
+ " notify type: "
+ notifyPayload.notifyType);
}
break;
default:
logw(
"Received unexpected payload in IKE AUTH response. Payload"
+ " type: "
+ payload.payloadType);
}
}
// Verify existence of payloads
if (authPayload == null) {
throw new AuthenticationFailedException("Post-EAP Auth payload missing.");
}
authenticatePsk(mEapMsk, authPayload, mRespIdPayload);
}
@Override
public void exitState() {
mRetransmitter.stopRetransmitting();
}
}
private abstract class RekeyIkeHandlerBase extends DeleteBase {
private void validateIkeRekeyCommon(IkeMessage ikeMessage) throws InvalidSyntaxException {
boolean hasSaPayload = false;
boolean hasKePayload = false;
boolean hasNoncePayload = false;
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_SA:
hasSaPayload = true;
break;
case IkePayload.PAYLOAD_TYPE_KE:
hasKePayload = true;
break;
case IkePayload.PAYLOAD_TYPE_NONCE:
hasNoncePayload = true;
break;
case IkePayload.PAYLOAD_TYPE_VENDOR:
// Vendor payloads allowed, but not verified
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
// Notification payloads allowed, but left to handler methods to process.
break;
default:
logw(
"Received unexpected payload in IKE REKEY request. Payload type: "
+ payload.payloadType);
}
}
if (!hasSaPayload || !hasKePayload || !hasNoncePayload) {
throw new InvalidSyntaxException("SA, KE or Nonce payload missing.");
}
}
@VisibleForTesting
void validateIkeRekeyReq(IkeMessage ikeMessage) throws InvalidSyntaxException {
// Skip validation of exchange type since it has been done during decoding request.
List<IkeNotifyPayload> notificationPayloads =
ikeMessage.getPayloadListForType(
IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
for (IkeNotifyPayload notifyPayload : notificationPayloads) {
if (notifyPayload.isErrorNotify()) {
logw("Error notifications invalid in request: " + notifyPayload.notifyType);
}
}
validateIkeRekeyCommon(ikeMessage);
}
@VisibleForTesting
void validateIkeRekeyResp(IkeMessage reqMsg, IkeMessage respMsg)
throws InvalidSyntaxException {
int exchangeType = respMsg.ikeHeader.exchangeType;
if (exchangeType != IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA
&& exchangeType != IkeHeader.EXCHANGE_TYPE_INFORMATIONAL) {
throw new InvalidSyntaxException(
"Expected Rekey response (CREATE_CHILD_SA or INFORMATIONAL) but received: "
+ exchangeType);
}
List<IkeNotifyPayload> notificationPayloads =
respMsg.getPayloadListForType(
IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
for (IkeNotifyPayload notifyPayload : notificationPayloads) {
if (notifyPayload.isErrorNotify()) {
// Error notifications found. Stop validation for SA negotiation.
return;
}
}
validateIkeRekeyCommon(respMsg);
// Verify DH groups matching
IkeKePayload reqKePayload =
reqMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class);
IkeKePayload respKePayload =
respMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class);
if (reqKePayload.dhGroup != respKePayload.dhGroup) {
throw new InvalidSyntaxException("Received KE payload with mismatched DH group.");
}
}
// It doesn't make sense to include multiple error notify payloads in one response. If it
// happens, IKE Session will only handle the most severe one.
protected boolean handleErrorNotifyIfExists(IkeMessage respMsg, boolean isSimulRekey) {
IkeNotifyPayload invalidSyntaxNotifyPayload = null;
IkeNotifyPayload tempFailureNotifyPayload = null;
IkeNotifyPayload firstErrorNotifyPayload = null;
List<IkeNotifyPayload> notificationPayloads =
respMsg.getPayloadListForType(
IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
for (IkeNotifyPayload notifyPayload : notificationPayloads) {
if (!notifyPayload.isErrorNotify()) continue;
if (firstErrorNotifyPayload == null) firstErrorNotifyPayload = notifyPayload;
if (ERROR_TYPE_INVALID_SYNTAX == notifyPayload.notifyType) {
invalidSyntaxNotifyPayload = notifyPayload;
} else if (ERROR_TYPE_TEMPORARY_FAILURE == notifyPayload.notifyType) {
tempFailureNotifyPayload = notifyPayload;
}
}
// No error Notify Payload included in this response.
if (firstErrorNotifyPayload == null) return NOT_HANDLED;
// Handle Invalid Syntax if it exists
if (invalidSyntaxNotifyPayload != null) {
try {
IkeProtocolException exception =
invalidSyntaxNotifyPayload.validateAndBuildIkeException();
handleIkeFatalError(exception);
} catch (InvalidSyntaxException e) {
// Error notify payload has invalid syntax
handleIkeFatalError(e);
}
return HANDLED;
}
if (tempFailureNotifyPayload != null) {
// Handle Temporary Failure if exists
loge("Received TEMPORARY_FAILURE for rekey IKE. Already handled during decoding.");
} else {
// Handle other errors
loge(
"Received error notification: "
+ firstErrorNotifyPayload.notifyType
+ " for rekey IKE. Schedule a retry");
if (!isSimulRekey) {
scheduleRetry(mCurrentIkeSaRecord.getFutureRekeyEvent());
}
}
if (isSimulRekey) {
transitionTo(mRekeyIkeRemoteDelete);
} else {
transitionTo(mIdle);
}
return HANDLED;
}
protected IkeSaRecord validateAndBuildIkeSa(
IkeMessage reqMsg, IkeMessage respMessage, boolean isLocalInit)
throws IkeProtocolException, GeneralSecurityException, IOException {
InetAddress initAddr = isLocalInit ? mLocalAddress : mRemoteAddress;
InetAddress respAddr = isLocalInit ? mRemoteAddress : mLocalAddress;
Pair<IkeProposal, IkeProposal> negotiatedProposals = null;
try {
IkeSaPayload reqSaPayload =
reqMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
IkeSaPayload respSaPayload =
respMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
// Throw exception or return valid negotiated proposal with allocated SPIs
negotiatedProposals =
IkeSaPayload.getVerifiedNegotiatedIkeProposalPair(
reqSaPayload, respSaPayload, mRemoteAddress);
IkeProposal reqProposal = negotiatedProposals.first;
IkeProposal respProposal = negotiatedProposals.second;
Provider provider = IkeMessage.getSecurityProvider();
IkeMacPrf newPrf;
IkeCipher newCipher;
IkeMacIntegrity newIntegrity = null;
newCipher =
IkeCipher.create(
respProposal.saProposal.getEncryptionTransforms()[0], provider);
if (!newCipher.isAead()) {
newIntegrity =
IkeMacIntegrity.create(
respProposal.saProposal.getIntegrityTransforms()[0], provider);
}
newPrf = IkeMacPrf.create(respProposal.saProposal.getPrfTransforms()[0], provider);
// Build new SaRecord
IkeSaRecord newSaRecord =
IkeSaRecord.makeRekeyedIkeSaRecord(
mCurrentIkeSaRecord,
mIkePrf,
reqMsg,
respMessage,
reqProposal.getIkeSpiResource(),
respProposal.getIkeSpiResource(),
newPrf,
newIntegrity == null ? 0 : newIntegrity.getKeyLength(),
newCipher.getKeyLength(),
isLocalInit,
new LocalRequest(CMD_LOCAL_REQUEST_REKEY_IKE));
addIkeSaRecord(newSaRecord);
mIkeCipher = newCipher;
mIkePrf = newPrf;
mIkeIntegrity = newIntegrity;
return newSaRecord;
} catch (IkeProtocolException | GeneralSecurityException | IOException e) {
if (negotiatedProposals != null) {
negotiatedProposals.first.getIkeSpiResource().close();
negotiatedProposals.second.getIkeSpiResource().close();
}
throw e;
}
}
}
/** RekeyIkeLocalCreate represents state when IKE library initiates Rekey IKE exchange. */
class RekeyIkeLocalCreate extends RekeyIkeHandlerBase {
protected Retransmitter mRetransmitter;
@Override
public void enterState() {
try {
mRetransmitter = new EncryptedRetransmitter(buildIkeRekeyReq());
} catch (IOException e) {
loge("Fail to assign IKE SPI for rekey. Schedule a retry.", e);
scheduleRetry(mCurrentIkeSaRecord.getFutureRekeyEvent());
transitionTo(mIdle);
}
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
protected void handleTempFailure() {
mTempFailHandler.handleTempFailure(mCurrentIkeSaRecord.getFutureRekeyEvent());
}
/**
* Builds a IKE Rekey request, reusing the current proposal
*
* <p>As per RFC 7296, rekey messages are of format: { HDR { SK { SA, Ni, KEi } } }
*
* <p>This method currently reuses agreed upon proposal.
*/
private IkeMessage buildIkeRekeyReq() throws IOException {
// TODO: Evaluate if we need to support different proposals for rekeys
IkeSaProposal[] saProposals = new IkeSaProposal[] {mSaProposal};
// No need to allocate SPIs; they will be allocated as part of the
// getRekeyIkeSaRequestPayloads
List<IkePayload> payloadList =
CreateIkeSaHelper.getRekeyIkeSaRequestPayloads(saProposals, mLocalAddress);
// Build IKE header
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA,
false /*isResponseMsg*/,
mCurrentIkeSaRecord.isLocalInit,
mCurrentIkeSaRecord.getLocalRequestMessageId());
return new IkeMessage(ikeHeader, payloadList);
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
handleDeleteSessionRequest(ikeMessage);
break;
default:
// TODO: Implement simultaneous rekey
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
// Validate syntax
validateIkeRekeyResp(mRetransmitter.getMessage(), ikeMessage);
// Handle error notifications if they exist
if (handleErrorNotifyIfExists(ikeMessage, false /*isSimulRekey*/) == NOT_HANDLED) {
// No error notifications included. Negotiate new SA
mLocalInitNewIkeSaRecord =
validateAndBuildIkeSa(
mRetransmitter.getMessage(), ikeMessage, true /*isLocalInit*/);
transitionTo(mRekeyIkeLocalDelete);
}
// Stop retransmissions
mRetransmitter.stopRetransmitting();
} catch (IkeProtocolException e) {
if (e instanceof InvalidSyntaxException) {
handleProcessRespOrSaCreationFailureAndQuit(e);
} else {
handleProcessRespOrSaCreationFailureAndQuit(
new InvalidSyntaxException(
"Error in processing IKE Rekey-Create response", e));
}
} catch (GeneralSecurityException | IOException e) {
handleProcessRespOrSaCreationFailureAndQuit(
new IkeInternalException("Error in creating a new IKE SA during rekey", e));
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
handleProcessRespOrSaCreationFailureAndQuit(ikeException);
}
private void handleProcessRespOrSaCreationFailureAndQuit(IkeException exception) {
// We don't retry rekey if failure was caused by invalid response or SA creation error.
// Reason is there is no way to notify the remote side the old SA is still alive but the
// new one has failed.
mRetransmitter.stopRetransmitting();
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
handleIkeFatalError(exception);
}
}
/**
* SimulRekeyIkeLocalCreate represents the state where IKE library has replied to rekey request
* sent from the remote and is waiting for a rekey response for a locally initiated rekey
* request.
*
* <p>SimulRekeyIkeLocalCreate extends RekeyIkeLocalCreate so that it can call super class to
* validate incoming rekey response against locally initiated rekey request.
*/
class SimulRekeyIkeLocalCreate extends RekeyIkeLocalCreate {
@Override
public void enterState() {
mRetransmitter = new EncryptedRetransmitter(null);
// TODO: Populate super.mRetransmitter from state initialization data
// Do not send request.
}
public IkeMessage buildRequest() {
throw new UnsupportedOperationException(
"Do not support sending request in " + getCurrentState().getName());
}
@Override
public void exitState() {
// Do nothing.
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
ReceivedIkePacket receivedIkePacket = (ReceivedIkePacket) message.obj;
IkeHeader ikeHeader = receivedIkePacket.ikeHeader;
if (mRemoteInitNewIkeSaRecord == getIkeSaRecordForPacket(ikeHeader)) {
deferMessage(message);
} else {
handleReceivedIkePacket(message);
}
return HANDLED;
default:
return super.processStateMessage(message);
}
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
deferMessage(message);
return;
default:
// TODO: Add more cases for other types of request.
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
validateIkeRekeyResp(mRetransmitter.getMessage(), ikeMessage);
// TODO: Check and handle error notifications before SA negotiation
mLocalInitNewIkeSaRecord =
validateAndBuildIkeSa(
mRetransmitter.getMessage(), ikeMessage, true /*isLocalInit*/);
transitionTo(mSimulRekeyIkeLocalDeleteRemoteDelete);
} catch (IkeProtocolException e) {
// TODO: Handle processing errors.
} catch (GeneralSecurityException e) {
// TODO: Fatal - kill session.
} catch (IOException e) {
// TODO: SPI allocation collided - delete new IKE SA, retry rekey.
}
}
}
/** RekeyIkeDeleteBase represents common behaviours of deleting stage during rekeying IKE SA. */
private abstract class RekeyIkeDeleteBase extends DeleteBase {
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
ReceivedIkePacket receivedIkePacket = (ReceivedIkePacket) message.obj;
IkeHeader ikeHeader = receivedIkePacket.ikeHeader;
// Verify that this message is correctly authenticated and encrypted:
IkeSaRecord ikeSaRecord = getIkeSaRecordForPacket(ikeHeader);
boolean isMessageOnNewSa = false;
if (ikeSaRecord != null && mIkeSaRecordSurviving == ikeSaRecord) {
DecodeResult decodeResult =
IkeMessage.decode(
ikeHeader.isResponseMsg
? ikeSaRecord.getLocalRequestMessageId()
: ikeSaRecord.getRemoteRequestMessageId(),
mIkeIntegrity,
mIkeCipher,
ikeSaRecord,
ikeHeader,
receivedIkePacket.ikePacketBytes,
ikeSaRecord.getCollectedFragments(ikeHeader.isResponseMsg));
isMessageOnNewSa =
(decodeResult.status == DECODE_STATUS_PROTECTED_ERROR)
|| (decodeResult.status == DECODE_STATUS_OK)
|| (decodeResult.status == DECODE_STATUS_PARTIAL);
}
// Authenticated request received on the new/surviving SA; treat it as
// an acknowledgement that the remote has successfully rekeyed.
if (isMessageOnNewSa) {
State nextState = mIdle;
// This is the first IkeMessage seen on the new SA. It cannot be a response.
// Likewise, if it a request, it must not be a retransmission. Verify msgId.
// If either condition happens, consider rekey a success, but immediately
// kill the session.
if (ikeHeader.isResponseMsg
|| ikeSaRecord.getRemoteRequestMessageId() - ikeHeader.messageId
!= 0) {
nextState = mDeleteIkeLocalDelete;
} else {
deferMessage(message);
}
// Locally close old (and losing) IKE SAs. As a result of not waiting for
// delete responses, the old SA can be left in a state where the stored ID
// is no longer correct. However, this finishRekey() call will remove that
// SA, so it doesn't matter.
finishRekey();
transitionTo(nextState);
} else {
handleReceivedIkePacket(message);
}
return HANDLED;
default:
return super.processStateMessage(message);
// TODO: Add more cases for other packet types.
}
}
// Rekey timer for old (and losing) SAs will be cancelled as part of the closing of the SA.
protected void finishRekey() {
mCurrentIkeSaRecord = mIkeSaRecordSurviving;
mLocalInitNewIkeSaRecord = null;
mRemoteInitNewIkeSaRecord = null;
mIkeSaRecordSurviving = null;
if (mIkeSaRecordAwaitingLocalDel != null) {
removeIkeSaRecord(mIkeSaRecordAwaitingLocalDel);
mIkeSaRecordAwaitingLocalDel.close();
mIkeSaRecordAwaitingLocalDel = null;
}
if (mIkeSaRecordAwaitingRemoteDel != null) {
removeIkeSaRecord(mIkeSaRecordAwaitingRemoteDel);
mIkeSaRecordAwaitingRemoteDel.close();
mIkeSaRecordAwaitingRemoteDel = null;
}
synchronized (mChildCbToSessions) {
for (ChildSessionStateMachine child : mChildCbToSessions.values()) {
child.setSkD(mCurrentIkeSaRecord.getSkD());
}
}
// TODO: Update prf of all child sessions
}
}
/**
* SimulRekeyIkeLocalDeleteRemoteDelete represents the deleting stage during simultaneous
* rekeying when IKE library is waiting for both a Delete request and a Delete response.
*/
class SimulRekeyIkeLocalDeleteRemoteDelete extends RekeyIkeDeleteBase {
private Retransmitter mRetransmitter;
@Override
public void enterState() {
// Detemine surviving IKE SA. According to RFC 7296: "The new IKE SA containing the
// lowest nonce SHOULD be deleted by the node that created it, and the other surviving
// new IKE SA MUST inherit all the Child SAs."
if (mLocalInitNewIkeSaRecord.compareTo(mRemoteInitNewIkeSaRecord) > 0) {
mIkeSaRecordSurviving = mLocalInitNewIkeSaRecord;
mIkeSaRecordAwaitingLocalDel = mCurrentIkeSaRecord;
mIkeSaRecordAwaitingRemoteDel = mRemoteInitNewIkeSaRecord;
} else {
mIkeSaRecordSurviving = mRemoteInitNewIkeSaRecord;
mIkeSaRecordAwaitingLocalDel = mLocalInitNewIkeSaRecord;
mIkeSaRecordAwaitingRemoteDel = mCurrentIkeSaRecord;
}
mRetransmitter =
new EncryptedRetransmitter(
mIkeSaRecordAwaitingLocalDel,
buildIkeDeleteReq(mIkeSaRecordAwaitingLocalDel));
// TODO: Set timer awaiting for delete request.
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
IkeSaRecord ikeSaRecordForPacket = getIkeSaRecordForPacket(ikeMessage.ikeHeader);
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
try {
validateIkeDeleteReq(ikeMessage, mIkeSaRecordAwaitingRemoteDel);
IkeMessage respMsg =
buildIkeDeleteResp(ikeMessage, mIkeSaRecordAwaitingRemoteDel);
removeIkeSaRecord(mIkeSaRecordAwaitingRemoteDel);
// TODO: Encode and send response and close
// mIkeSaRecordAwaitingRemoteDel.
// TODO: Stop timer awating delete request.
transitionTo(mSimulRekeyIkeLocalDelete);
} catch (InvalidSyntaxException e) {
logd("Validation failed for delete request", e);
// TODO: Shutdown - fatal error
}
return;
default:
// TODO: Reply with TEMPORARY_FAILURE
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
validateIkeDeleteResp(ikeMessage, mIkeSaRecordAwaitingLocalDel);
finishDeleteIkeSaAwaitingLocalDel();
} catch (InvalidSyntaxException e) {
loge("Invalid syntax on IKE Delete response. Shutting down anyways", e);
finishDeleteIkeSaAwaitingLocalDel();
} catch (IllegalStateException e) {
// Response received on incorrect SA
cleanUpAndQuit(e);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException exception) {
if (mIkeSaRecordAwaitingLocalDel == ikeSaRecord) {
loge("Invalid syntax on IKE Delete response. Shutting down anyways", exception);
finishDeleteIkeSaAwaitingLocalDel();
} else {
cleanUpAndQuit(
new IllegalStateException("Delete response received on incorrect SA"));
}
}
private void finishDeleteIkeSaAwaitingLocalDel() {
mRetransmitter.stopRetransmitting();
removeIkeSaRecord(mIkeSaRecordAwaitingLocalDel);
mIkeSaRecordAwaitingLocalDel.close();
mIkeSaRecordAwaitingLocalDel = null;
transitionTo(mSimulRekeyIkeRemoteDelete);
}
@Override
public void exitState() {
finishRekey();
mRetransmitter.stopRetransmitting();
// TODO: Stop awaiting delete request timer.
}
}
/**
* SimulRekeyIkeLocalDelete represents the state when IKE library is waiting for a Delete
* response during simultaneous rekeying.
*/
class SimulRekeyIkeLocalDelete extends RekeyIkeDeleteBase {
private Retransmitter mRetransmitter;
@Override
public void enterState() {
mRetransmitter = new EncryptedRetransmitter(mIkeSaRecordAwaitingLocalDel, null);
// TODO: Populate mRetransmitter from state initialization data.
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
// Always return a TEMPORARY_FAILURE. In no case should we accept a message on an SA
// that is going away. All messages on the new SA is caught in RekeyIkeDeleteBase
buildAndSendErrorNotificationResponse(
mIkeSaRecordAwaitingLocalDel,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
validateIkeDeleteResp(ikeMessage, mIkeSaRecordAwaitingLocalDel);
finishRekey();
transitionTo(mIdle);
} catch (InvalidSyntaxException e) {
loge(
"Invalid syntax on IKE Delete response. Shutting down old IKE SA and"
+ " finishing rekey",
e);
finishRekey();
transitionTo(mIdle);
} catch (IllegalStateException e) {
// Response received on incorrect SA
cleanUpAndQuit(e);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException exception) {
if (mIkeSaRecordAwaitingLocalDel == ikeSaRecord) {
loge(
"Invalid syntax on IKE Delete response. Shutting down old IKE SA and"
+ " finishing rekey",
exception);
finishRekey();
transitionTo(mIdle);
} else {
cleanUpAndQuit(
new IllegalStateException("Delete response received on incorrect SA"));
}
}
}
/**
* SimulRekeyIkeRemoteDelete represents the state that waiting for a Delete request during
* simultaneous rekeying.
*/
class SimulRekeyIkeRemoteDelete extends RekeyIkeDeleteBase {
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
// At this point, the incoming request can ONLY be on mIkeSaRecordAwaitingRemoteDel - if
// it was on the surviving SA, it is deferred and the rekey is finished. It is likewise
// impossible to have this on the local-deleted SA, since the delete has already been
// acknowledged in the SimulRekeyIkeLocalDeleteRemoteDelete state.
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
try {
validateIkeDeleteReq(ikeMessage, mIkeSaRecordAwaitingRemoteDel);
IkeMessage respMsg =
buildIkeDeleteResp(ikeMessage, mIkeSaRecordAwaitingRemoteDel);
sendEncryptedIkeMessage(mIkeSaRecordAwaitingRemoteDel, respMsg);
finishRekey();
transitionTo(mIdle);
} catch (InvalidSyntaxException e) {
// Program error.
cleanUpAndQuit(new IllegalStateException(e));
}
return;
default:
buildAndSendErrorNotificationResponse(
mIkeSaRecordAwaitingRemoteDel,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
}
}
}
/**
* RekeyIkeLocalDelete represents the deleting stage when IKE library is initiating a Rekey
* procedure.
*
* <p>RekeyIkeLocalDelete and SimulRekeyIkeLocalDelete have same behaviours in
* processStateMessage(). While RekeyIkeLocalDelete overrides enterState() and exitState()
* methods for initiating and finishing the deleting stage for IKE rekeying.
*/
class RekeyIkeLocalDelete extends SimulRekeyIkeLocalDelete {
private Retransmitter mRetransmitter;
@Override
public void enterState() {
mIkeSaRecordSurviving = mLocalInitNewIkeSaRecord;
mIkeSaRecordAwaitingLocalDel = mCurrentIkeSaRecord;
mRetransmitter =
new EncryptedRetransmitter(
mIkeSaRecordAwaitingLocalDel,
buildIkeDeleteReq(mIkeSaRecordAwaitingLocalDel));
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
public void exitState() {
mRetransmitter.stopRetransmitting();
}
}
/**
* RekeyIkeRemoteDelete represents the deleting stage when responding to a Rekey procedure.
*
* <p>RekeyIkeRemoteDelete and SimulRekeyIkeRemoteDelete have same behaviours in
* processStateMessage(). While RekeyIkeLocalDelete overrides enterState() and exitState()
* methods for waiting incoming delete request and for finishing the deleting stage for IKE
* rekeying.
*/
class RekeyIkeRemoteDelete extends SimulRekeyIkeRemoteDelete {
@Override
public void enterState() {
mIkeSaRecordSurviving = mRemoteInitNewIkeSaRecord;
mIkeSaRecordAwaitingRemoteDel = mCurrentIkeSaRecord;
sendMessageDelayed(TIMEOUT_REKEY_REMOTE_DELETE, REKEY_DELETE_TIMEOUT_MS);
}
@Override
public boolean processStateMessage(Message message) {
// Intercept rekey delete timeout. Assume rekey succeeded since no retransmissions
// were received.
if (message.what == TIMEOUT_REKEY_REMOTE_DELETE) {
finishRekey();
transitionTo(mIdle);
return HANDLED;
} else {
return super.processStateMessage(message);
}
}
@Override
public void exitState() {
removeMessages(TIMEOUT_REKEY_REMOTE_DELETE);
}
}
/** DeleteIkeLocalDelete initiates a deletion request of the current IKE Session. */
class DeleteIkeLocalDelete extends DeleteBase {
private Retransmitter mRetransmitter;
@Override
public void enterState() {
mRetransmitter = new EncryptedRetransmitter(buildIkeDeleteReq(mCurrentIkeSaRecord));
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
handleDeleteSessionRequest(ikeMessage);
return;
default:
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
validateIkeDeleteResp(ikeMessage, mCurrentIkeSaRecord);
mUserCbExecutor.execute(
() -> {
mIkeSessionCallback.onClosed();
});
removeIkeSaRecord(mCurrentIkeSaRecord);
mCurrentIkeSaRecord.close();
mCurrentIkeSaRecord = null;
quitNow();
} catch (InvalidSyntaxException e) {
handleResponseGenericProcessError(mCurrentIkeSaRecord, e);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException exception) {
loge("Invalid syntax on IKE Delete response. Shutting down anyways", exception);
handleIkeFatalError(exception);
quitNow();
}
@Override
public void exitState() {
mRetransmitter.stopRetransmitting();
}
}
/**
* Helper class to generate IKE SA creation payloads, in both request and response directions.
*/
private static class CreateIkeSaHelper {
public static List<IkePayload> getIkeInitSaRequestPayloads(
IkeSaProposal[] saProposals,
long initIkeSpi,
long respIkeSpi,
InetAddress localAddr,
InetAddress remoteAddr,
int localPort,
int remotePort)
throws IOException {
List<IkePayload> payloadList =
getCreateIkeSaPayloads(IkeSaPayload.createInitialIkeSaPayload(saProposals));
// Though RFC says Notify-NAT payload is "just after the Ni and Nr payloads (before the
// optional CERTREQ payload)", it also says recipient MUST NOT reject " messages in
// which the payloads were not in the "right" order" due to the lack of clarity of the
// payload order.
payloadList.add(
new IkeNotifyPayload(
NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP,
IkeNotifyPayload.generateNatDetectionData(
initIkeSpi, respIkeSpi, localAddr, localPort)));
payloadList.add(
new IkeNotifyPayload(
NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP,
IkeNotifyPayload.generateNatDetectionData(
initIkeSpi, respIkeSpi, remoteAddr, remotePort)));
return payloadList;
}
public static List<IkePayload> getRekeyIkeSaRequestPayloads(
IkeSaProposal[] saProposals, InetAddress localAddr) throws IOException {
if (localAddr == null) {
throw new IllegalArgumentException("Local address was null for rekey");
}
return getCreateIkeSaPayloads(
IkeSaPayload.createRekeyIkeSaRequestPayload(saProposals, localAddr));
}
public static List<IkePayload> getRekeyIkeSaResponsePayloads(
byte respProposalNumber, IkeSaProposal saProposal, InetAddress localAddr)
throws IOException {
if (localAddr == null) {
throw new IllegalArgumentException("Local address was null for rekey");
}
return getCreateIkeSaPayloads(
IkeSaPayload.createRekeyIkeSaResponsePayload(
respProposalNumber, saProposal, localAddr));
}
/**
* Builds the initial or rekey IKE creation payloads.
*
* <p>Will return a non-empty list of IkePayloads, the first of which WILL be the SA payload
*/
private static List<IkePayload> getCreateIkeSaPayloads(IkeSaPayload saPayload)
throws IOException {
if (saPayload.proposalList.size() == 0) {
throw new IllegalArgumentException("Invalid SA proposal list - was empty");
}
List<IkePayload> payloadList = new ArrayList<>(3);
payloadList.add(saPayload);
payloadList.add(new IkeNoncePayload());
// SaPropoals.Builder guarantees that each SA proposal has at least one DH group.
DhGroupTransform dhGroupTransform =
((IkeProposal) saPayload.proposalList.get(0))
.saProposal
.getDhGroupTransforms()[0];
payloadList.add(new IkeKePayload(dhGroupTransform.id));
return payloadList;
}
}
}