diff options
Diffstat (limited to 'src/java/com/android/internal/net/ipsec')
53 files changed, 17942 insertions, 0 deletions
diff --git a/src/java/com/android/internal/net/ipsec/ike/AbstractSessionStateMachine.java b/src/java/com/android/internal/net/ipsec/ike/AbstractSessionStateMachine.java new file mode 100644 index 00000000..82122847 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/AbstractSessionStateMachine.java @@ -0,0 +1,199 @@ +/* + * 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.IkeManager.getIkeLog; + +import android.os.Looper; +import android.os.Message; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; + +import java.util.concurrent.TimeUnit; + +/** + * This class represents the common information of both IkeSessionStateMachine and + * ChildSessionStateMachine + */ +abstract class AbstractSessionStateMachine extends StateMachine { + private static final int CMD_SHARED_BASE = 0; + protected static final int CMD_CATEGORY_SIZE = 100; + + /** + * Commands of Child local request that will be used in both IkeSessionStateMachine and + * ChildSessionStateMachine. + */ + protected static final int CMD_CHILD_LOCAL_REQUEST_BASE = CMD_SHARED_BASE; + + @VisibleForTesting + static final int CMD_LOCAL_REQUEST_CREATE_CHILD = CMD_CHILD_LOCAL_REQUEST_BASE + 1; + + @VisibleForTesting + static final int CMD_LOCAL_REQUEST_DELETE_CHILD = CMD_CHILD_LOCAL_REQUEST_BASE + 2; + + @VisibleForTesting + static final int CMD_LOCAL_REQUEST_REKEY_CHILD = CMD_CHILD_LOCAL_REQUEST_BASE + 3; + + /** Timeout commands. */ + protected static final int CMD_TIMEOUT_BASE = CMD_SHARED_BASE + CMD_CATEGORY_SIZE; + /** Timeout when the remote side fails to send a Rekey-Delete request. */ + @VisibleForTesting static final int TIMEOUT_REKEY_REMOTE_DELETE = CMD_TIMEOUT_BASE + 1; + + /** Commands for testing only */ + protected static final int CMD_TEST_BASE = CMD_SHARED_BASE + 2 * CMD_CATEGORY_SIZE; + /** Force state machine to a target state for testing purposes. */ + @VisibleForTesting static final int CMD_FORCE_TRANSITION = CMD_TEST_BASE + 1; + + /** Private commands for subclasses */ + protected static final int CMD_PRIVATE_BASE = CMD_SHARED_BASE + 3 * CMD_CATEGORY_SIZE; + + protected static final SparseArray<String> SHARED_CMD_TO_STR; + + static { + SHARED_CMD_TO_STR = new SparseArray<>(); + SHARED_CMD_TO_STR.put(CMD_LOCAL_REQUEST_CREATE_CHILD, "Create Child"); + SHARED_CMD_TO_STR.put(CMD_LOCAL_REQUEST_DELETE_CHILD, "Delete Child"); + SHARED_CMD_TO_STR.put(CMD_LOCAL_REQUEST_REKEY_CHILD, "Rekey Child"); + SHARED_CMD_TO_STR.put(TIMEOUT_REKEY_REMOTE_DELETE, "Timout rekey remote delete"); + SHARED_CMD_TO_STR.put(CMD_FORCE_TRANSITION, "Force transition"); + } + + // Use a value greater than the retransmit-failure timeout. + static final long REKEY_DELETE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(180L); + + private final String mLogTag; + + protected AbstractSessionStateMachine(String name, Looper looper) { + super(name, looper); + mLogTag = name; + } + + /** + * Top level state for handling uncaught exceptions for all subclasses. + * + * <p>All other state in SessionStateMachine MUST extend this state. + * + * <p>Only errors this state should catch are unexpected internal failures. Since this may be + * run in critical processes, it must never take down the process if it fails + */ + protected abstract class ExceptionHandlerBase extends State { + @Override + public final void enter() { + try { + enterState(); + } catch (RuntimeException e) { + cleanUpAndQuit(e); + } + } + + @Override + public final boolean processMessage(Message message) { + try { + String cmdName = SHARED_CMD_TO_STR.get(message.what); + if (cmdName == null) { + cmdName = getCmdString(message.what); + } + + // Unrecognized message will be logged by super class(Android StateMachine) + if (cmdName != null) logd("processStateMessage: " + cmdName); + + return processStateMessage(message); + } catch (RuntimeException e) { + cleanUpAndQuit(e); + return HANDLED; + } + } + + @Override + public final void exit() { + try { + exitState(); + } catch (RuntimeException e) { + cleanUpAndQuit(e); + } + } + + protected void enterState() { + // Do nothing. Subclasses MUST override it if they care. + } + + protected boolean processStateMessage(Message message) { + return NOT_HANDLED; + } + + protected void exitState() { + // Do nothing. Subclasses MUST override it if they care. + } + + protected abstract void cleanUpAndQuit(RuntimeException e); + + protected abstract String getCmdString(int cmd); + } + + @Override + protected void log(String s) { + getIkeLog().d(mLogTag, s); + } + + @Override + protected void logd(String s) { + getIkeLog().d(mLogTag, s); + } + + protected void logd(String s, Throwable e) { + getIkeLog().d(mLogTag, s, e); + } + + @Override + protected void logv(String s) { + getIkeLog().v(mLogTag, s); + } + + @Override + protected void logi(String s) { + getIkeLog().i(mLogTag, s); + } + + protected void logi(String s, Throwable cause) { + getIkeLog().i(mLogTag, s, cause); + } + + @Override + protected void logw(String s) { + getIkeLog().w(mLogTag, s); + } + + @Override + protected void loge(String s) { + getIkeLog().e(mLogTag, s); + } + + @Override + protected void loge(String s, Throwable e) { + getIkeLog().e(mLogTag, s, e); + } + + protected void logWtf(String s) { + getIkeLog().wtf(mLogTag, s); + } + + protected void logWtf(String s, Throwable e) { + getIkeLog().wtf(mLogTag, s, e); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/ChildSessionStateMachine.java b/src/java/com/android/internal/net/ipsec/ike/ChildSessionStateMachine.java new file mode 100644 index 00000000..889d8c12 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/ChildSessionStateMachine.java @@ -0,0 +1,2268 @@ +/* + * 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.IkeManager.getIkeLog; +import static android.net.ipsec.ike.SaProposal.DH_GROUP_NONE; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE; + +import static com.android.internal.net.ipsec.ike.IkeSessionStateMachine.IKE_EXCHANGE_SUBTYPE_DELETE_CHILD; +import static com.android.internal.net.ipsec.ike.IkeSessionStateMachine.IKE_EXCHANGE_SUBTYPE_REKEY_CHILD; +import static com.android.internal.net.ipsec.ike.message.IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA; +import static com.android.internal.net.ipsec.ike.message.IkeHeader.EXCHANGE_TYPE_IKE_AUTH; +import static com.android.internal.net.ipsec.ike.message.IkeHeader.EXCHANGE_TYPE_INFORMATIONAL; +import static com.android.internal.net.ipsec.ike.message.IkeHeader.ExchangeType; +import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_REKEY_SA; +import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_USE_TRANSPORT_MODE; +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_KE; +import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_NONCE; +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_SA; +import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_TS_INITIATOR; +import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_TS_RESPONDER; +import static com.android.internal.net.ipsec.ike.message.IkePayload.PROTOCOL_ID_ESP; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.content.Context; +import android.net.IpSecManager; +import android.net.IpSecManager.ResourceUnavailableException; +import android.net.IpSecManager.SecurityParameterIndex; +import android.net.IpSecManager.SpiUnavailableException; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.ipsec.ike.ChildSaProposal; +import android.net.ipsec.ike.ChildSessionCallback; +import android.net.ipsec.ike.ChildSessionConfiguration; +import android.net.ipsec.ike.ChildSessionOptions; +import android.net.ipsec.ike.IkeTrafficSelector; +import android.net.ipsec.ike.SaProposal; +import android.net.ipsec.ike.TunnelModeChildSessionOptions; +import android.net.ipsec.ike.exceptions.IkeException; +import android.net.ipsec.ike.exceptions.IkeInternalException; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.os.Looper; +import android.os.Message; +import android.util.Pair; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.ipsec.ike.IkeLocalRequestScheduler.ChildLocalRequest; +import com.android.internal.net.ipsec.ike.IkeSessionStateMachine.IkeExchangeSubType; +import com.android.internal.net.ipsec.ike.SaRecord.ChildSaRecord; +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.InvalidKeException; +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.exceptions.TemporaryFailureException; +import com.android.internal.net.ipsec.ike.exceptions.TsUnacceptableException; +import com.android.internal.net.ipsec.ike.message.IkeConfigPayload; +import com.android.internal.net.ipsec.ike.message.IkeConfigPayload.ConfigAttribute; +import com.android.internal.net.ipsec.ike.message.IkeDeletePayload; +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.IkeNoncePayload; +import com.android.internal.net.ipsec.ike.message.IkeNotifyPayload; +import com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NotifyType; +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.ChildProposal; +import com.android.internal.net.ipsec.ike.message.IkeSaPayload.DhGroupTransform; +import com.android.internal.net.ipsec.ike.message.IkeTsPayload; +import com.android.internal.util.State; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.InetAddress; +import java.security.GeneralSecurityException; +import java.security.Provider; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * ChildSessionStateMachine tracks states and manages exchanges of this Child Session. + * + * <p>ChildSessionStateMachine has two types of states. One type are states where there is no + * ongoing procedure affecting Child 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 = {CreateChild | DeleteChild | Info | RekeyChild | SimulRekeyChild}. + * - Exchange Initiator indicates whether local or remote peer is the exchange initiator: + * Exchange Initiator = {Local | Remote} + * - Exchange type defines the function of this exchange. + * Exchange Type = {Create | Delete} + * </pre> + */ +public class ChildSessionStateMachine extends AbstractSessionStateMachine { + private static final String TAG = "ChildSessionStateMachine"; + + private static final int SPI_NOT_REGISTERED = 0; + + // Time after which Child SA needs to be rekeyed + @VisibleForTesting static final long SA_SOFT_LIFETIME_MS = TimeUnit.HOURS.toMillis(2L); + + private static final int CMD_GENERAL_BASE = CMD_PRIVATE_BASE; + + /** Receive request for negotiating first Child SA. */ + private static final int CMD_HANDLE_FIRST_CHILD_EXCHANGE = CMD_GENERAL_BASE + 1; + /** Receive a request from the remote. */ + private static final int CMD_HANDLE_RECEIVED_REQUEST = CMD_GENERAL_BASE + 2; + /** Receive a reponse from the remote. */ + private static final int CMD_HANDLE_RECEIVED_RESPONSE = CMD_GENERAL_BASE + 3; + /** Kill Session and close all alive Child SAs immediately. */ + private static final int CMD_KILL_SESSION = CMD_GENERAL_BASE + 4; + + private static final SparseArray<String> CMD_TO_STR; + + static { + CMD_TO_STR = new SparseArray<>(); + CMD_TO_STR.put(CMD_HANDLE_FIRST_CHILD_EXCHANGE, "Handle First Child"); + CMD_TO_STR.put(CMD_HANDLE_RECEIVED_REQUEST, "Rcv request"); + CMD_TO_STR.put(CMD_HANDLE_RECEIVED_RESPONSE, "Rcv response"); + CMD_TO_STR.put(CMD_KILL_SESSION, "Kill session"); + } + + private final Context mContext; + private final IpSecManager mIpSecManager; + + /** User provided configurations. */ + private final ChildSessionOptions mChildSessionOptions; + + private final Executor mUserCbExecutor; + private final ChildSessionCallback mUserCallback; + + /** Callback to notify IKE Session the state changes. */ + private final IChildSessionSmCallback mChildSmCallback; + + // TODO: Also store ChildSessionCallback for notifying users. + + /** Local address assigned on device. */ + @VisibleForTesting InetAddress mLocalAddress; + /** Remote address configured by users. */ + @VisibleForTesting InetAddress mRemoteAddress; + + /** + * UDP-Encapsulated socket that allows IPsec traffic to pass through a NAT. Null if UDP + * encapsulation is not needed. + */ + @VisibleForTesting @Nullable UdpEncapsulationSocket mUdpEncapSocket; + + /** Crypto parameters. Updated upon initial negotiation or IKE SA rekey. */ + @VisibleForTesting IkeMacPrf mIkePrf; + + @VisibleForTesting byte[] mSkD; + + /** Package private ChildSaProposal that represents the negotiated Child SA proposal. */ + @VisibleForTesting ChildSaProposal mSaProposal; + + /** Negotiated local Traffic Selector. */ + @VisibleForTesting IkeTrafficSelector[] mLocalTs; + /** Negotiated remote Traffic Selector. */ + @VisibleForTesting IkeTrafficSelector[] mRemoteTs; + + @VisibleForTesting IkeCipher mChildCipher; + @VisibleForTesting IkeMacIntegrity mChildIntegrity; + + /** Package private */ + @VisibleForTesting ChildSaRecord mCurrentChildSaRecord; + /** Package private */ + @VisibleForTesting ChildSaRecord mLocalInitNewChildSaRecord; + /** Package private */ + @VisibleForTesting ChildSaRecord mRemoteInitNewChildSaRecord; + + /** Package private */ + @VisibleForTesting ChildSaRecord mChildSaRecordSurviving; + + @VisibleForTesting final State mKillChildSessionParent = new KillChildSessionParent(); + + @VisibleForTesting final State mInitial = new Initial(); + @VisibleForTesting final State mCreateChildLocalCreate = new CreateChildLocalCreate(); + @VisibleForTesting final State mIdle = new Idle(); + @VisibleForTesting final State mDeleteChildLocalDelete = new DeleteChildLocalDelete(); + @VisibleForTesting final State mDeleteChildRemoteDelete = new DeleteChildRemoteDelete(); + @VisibleForTesting final State mRekeyChildLocalCreate = new RekeyChildLocalCreate(); + @VisibleForTesting final State mRekeyChildRemoteCreate = new RekeyChildRemoteCreate(); + @VisibleForTesting final State mRekeyChildLocalDelete = new RekeyChildLocalDelete(); + @VisibleForTesting final State mRekeyChildRemoteDelete = new RekeyChildRemoteDelete(); + + /** + * Builds a new uninitialized ChildSessionStateMachine + * + * <p>Upon creation, this state machine will await either the handleFirstChildExchange + * (IKE_AUTH), or the createChildSession (Additional child creation beyond the first child) to + * be called, both of which must pass keying and SA information. + * + * <p>This two-stage initialization is required to allow race-free user interaction with the IKE + * Session keyed on the child state machine callbacks. + * + * <p>Package private + */ + ChildSessionStateMachine( + Looper looper, + Context context, + IpSecManager ipSecManager, + ChildSessionOptions sessionOptions, + Executor userCbExecutor, + ChildSessionCallback userCallback, + IChildSessionSmCallback childSmCallback) { + super(TAG, looper); + + mContext = context; + mIpSecManager = ipSecManager; + mChildSessionOptions = sessionOptions; + + mUserCbExecutor = userCbExecutor; + mUserCallback = userCallback; + mChildSmCallback = childSmCallback; + + addState(mKillChildSessionParent); + + addState(mInitial, mKillChildSessionParent); + addState(mCreateChildLocalCreate, mKillChildSessionParent); + addState(mIdle, mKillChildSessionParent); + addState(mDeleteChildLocalDelete, mKillChildSessionParent); + addState(mDeleteChildRemoteDelete, mKillChildSessionParent); + addState(mRekeyChildLocalCreate, mKillChildSessionParent); + addState(mRekeyChildRemoteCreate, mKillChildSessionParent); + addState(mRekeyChildLocalDelete, mKillChildSessionParent); + addState(mRekeyChildRemoteDelete, mKillChildSessionParent); + + setInitialState(mInitial); + } + + /** + * Interface for ChildSessionStateMachine to notify IkeSessionStateMachine of state changes. + * + * <p>Child Session may encounter an IKE Session fatal error in three cases with different + * handling rules: + * + * <pre> + * - When there is a fatal error in an inbound request, onOutboundPayloadsReady will be + * called first to send out an error notification and then onFatalIkeSessionError(false) + * will be called to locally close the IKE Session. + * - When there is a fatal error in an inbound response, only onFatalIkeSessionError(true) + * will be called to notify the remote with a Delete request and then close the IKE Session. + * - When there is an fatal error notification in an inbound response, only + * onFatalIkeSessionError(false) is called to close the IKE Session locally. + * </pre> + * + * <p>Package private. + */ + interface IChildSessionSmCallback { + /** Notify that new Child SA is created. */ + void onChildSaCreated(int remoteSpi, ChildSessionStateMachine childSession); + + /** Notify that a Child SA is deleted. */ + void onChildSaDeleted(int remoteSpi); + + /** Schedule a future Child Rekey Request on the LocalRequestScheduler. */ + void scheduleLocalRequest(ChildLocalRequest futureRequest, long delayedTime); + + /** Schedule retry for a Child Rekey Request on the LocalRequestScheduler. */ + void scheduleRetryLocalRequest(ChildLocalRequest futureRequest); + + /** Notify the IKE Session to send out IKE message for this Child Session. */ + void onOutboundPayloadsReady( + @ExchangeType int exchangeType, + boolean isResp, + List<IkePayload> payloadList, + ChildSessionStateMachine childSession); + + /** Notify that a Child procedure has been finished. */ + void onProcedureFinished(ChildSessionStateMachine childSession); + + /** + * Notify the IKE Session State Machine that this Child has been fully shut down. + * + * <p>This method MUST be called after the user callbacks have been fired, and MUST always + * be called before the state machine can shut down. + */ + void onChildSessionClosed(ChildSessionCallback userCallbacks); + + /** + * Notify that a Child procedure has been finished and the IKE Session should close itself + * because of a fatal error. + * + * <p>The IKE Session should send a Delete IKE request before closing when needsNotifyRemote + * is true. + */ + void onFatalIkeSessionError(boolean needsNotifyRemote); + } + + /** + * Receive requesting and responding payloads for negotiating first Child SA. + * + * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call + * as an asynchronous job to the ChildStateMachine handler. + * + * @param reqPayloads SA negotiation related payloads in IKE_AUTH request. + * @param respPayloads SA negotiation related payloads in IKE_AUTH response. + * @param localAddress The local (outer) address of the Child Session. + * @param remoteAddress The remote (outer) address of the Child Session. + * @param udpEncapSocket The socket to use for UDP encapsulation, or NULL if no encap needed. + * @param ikePrf The pseudo-random function to use for key derivation + * @param skD The key for which to derive new keying information from. + */ + public void handleFirstChildExchange( + List<IkePayload> reqPayloads, + List<IkePayload> respPayloads, + InetAddress localAddress, + InetAddress remoteAddress, + UdpEncapsulationSocket udpEncapSocket, + IkeMacPrf ikePrf, + byte[] skD) { + + this.mLocalAddress = localAddress; + this.mRemoteAddress = remoteAddress; + this.mUdpEncapSocket = udpEncapSocket; + this.mIkePrf = ikePrf; + this.mSkD = skD; + + int spi = registerProvisionalChildAndGetSpi(respPayloads); + sendMessage( + CMD_HANDLE_FIRST_CHILD_EXCHANGE, + new FirstChildNegotiationData(reqPayloads, respPayloads, spi)); + } + + /** + * Initiate Create Child procedure. + * + * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call + * as an asynchronous job to the ChildStateMachine handler. + * + * @param localAddress The local (outer) address from which traffic will originate. + * @param remoteAddress The remote (outer) address to which traffic will be sent. + * @param udpEncapSocket The socket to use for UDP encapsulation, or NULL if no encap needed. + * @param ikePrf The pseudo-random function to use for key derivation + * @param skD The key for which to derive new keying information from. + */ + public void createChildSession( + InetAddress localAddress, + InetAddress remoteAddress, + UdpEncapsulationSocket udpEncapSocket, + IkeMacPrf ikePrf, + byte[] skD) { + this.mLocalAddress = localAddress; + this.mRemoteAddress = remoteAddress; + this.mUdpEncapSocket = udpEncapSocket; + this.mIkePrf = ikePrf; + this.mSkD = skD; + + sendMessage(CMD_LOCAL_REQUEST_CREATE_CHILD); + } + + /** + * Initiate Delete Child procedure. + * + * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call + * as an asynchronous job to the ChildStateMachine handler. + */ + public void deleteChildSession() { + sendMessage(CMD_LOCAL_REQUEST_DELETE_CHILD); + } + + /** + * Initiate Rekey Child procedure. + * + * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call + * as an asynchronous job to the ChildStateMachine handler. + */ + public void rekeyChildSession() { + sendMessage(CMD_LOCAL_REQUEST_REKEY_CHILD); + } + + /** + * Kill Child Session and all alive Child SAs without doing IKE exchange. + * + * <p>It is usually called when IKE Session is being closed. + */ + public void killSession() { + sendMessage(CMD_KILL_SESSION); + } + + private ChildLocalRequest makeRekeyLocalRequest() { + return new ChildLocalRequest( + CMD_LOCAL_REQUEST_REKEY_CHILD, mUserCallback, null /*childOptions*/); + } + + private long getRekeyTimeout() { + // TODO: Make rekey timout fuzzy + return SA_SOFT_LIFETIME_MS; + } + + /** + * Receive a request + * + * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call + * as an asynchronous job to the ChildStateMachine handler. + * + * @param exchangeSubtype the exchange subtype of this inbound request. + * @param exchangeType the exchange type in the request message. + * @param payloadList the Child-procedure-related payload list in the request message that needs + * validation. + */ + public void receiveRequest( + @IkeExchangeSubType int exchangeSubtype, + @ExchangeType int exchangeType, + List<IkePayload> payloadList) { + sendMessage( + CMD_HANDLE_RECEIVED_REQUEST, + new ReceivedRequest(exchangeSubtype, exchangeType, payloadList)); + } + + /** + * Receive a response. + * + * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call + * as an asynchronous job to the ChildStateMachine handler. + * + * @param exchangeType the exchange type in the response message that needs validation. + * @param payloadList the Child-procedure-related payload list in the response message that + * needs validation. + */ + public void receiveResponse(@ExchangeType int exchangeType, List<IkePayload> payloadList) { + if (!isAwaitingCreateResp()) { + sendMessage( + CMD_HANDLE_RECEIVED_RESPONSE, new ReceivedResponse(exchangeType, payloadList)); + } + + // If we are waiting for a Create/RekeyCreate response and the received message contains SA + // payload we need to register for this provisional Child. + int spi = registerProvisionalChildAndGetSpi(payloadList); + sendMessage( + CMD_HANDLE_RECEIVED_RESPONSE, + new ReceivedCreateResponse(exchangeType, payloadList, spi)); + } + + private boolean isAwaitingCreateResp() { + return (getCurrentState() == mCreateChildLocalCreate + || getCurrentState() == mRekeyChildLocalCreate); + } + + /** + * Update SK_d with provided value when IKE SA is rekeyed. + * + * <p>It MUST be only called at the end of Rekey IKE procedure, which guarantees this Child + * Session is not in Create Child or Rekey Child procedure. + * + * @param skD the new skD in byte array. + */ + public void setSkD(byte[] skD) { + mSkD = skD; + } + + /** + * Register provisioning ChildSessionStateMachine in IChildSessionSmCallback + * + * <p>This method is for avoiding CHILD_SA_NOT_FOUND error in IkeSessionStateMachine when remote + * peer sends request for delete/rekey this Child SA before ChildSessionStateMachine sends + * FirstChildNegotiationData or Create response to itself. + */ + private int registerProvisionalChildAndGetSpi(List<IkePayload> respPayloads) { + IkeSaPayload saPayload = + IkePayload.getPayloadForTypeInProvidedList( + PAYLOAD_TYPE_SA, IkeSaPayload.class, respPayloads); + + if (saPayload == null) return SPI_NOT_REGISTERED; + + // IkeSaPayload.Proposal stores SPI in long type so as to be applied to both 8-byte IKE SPI + // and 4-byte Child SPI. Here we cast the stored SPI to int to represent a Child SPI. + int remoteGenSpi = (int) (saPayload.proposalList.get(0).spi); + mChildSmCallback.onChildSaCreated(remoteGenSpi, this); + return remoteGenSpi; + } + + private void replyErrorNotification(@NotifyType int notifyType) { + replyErrorNotification(notifyType, new byte[0]); + } + + private void replyErrorNotification(@NotifyType int notifyType, byte[] notifyData) { + List<IkePayload> outPayloads = new ArrayList<>(1); + IkeNotifyPayload notifyPayload = new IkeNotifyPayload(notifyType, notifyData); + outPayloads.add(notifyPayload); + + mChildSmCallback.onOutboundPayloadsReady( + EXCHANGE_TYPE_INFORMATIONAL, true /*isResp*/, outPayloads, this); + } + + /** Notify users the deletion of a Child SA. MUST be called through mUserCbExecutor */ + private void onIpSecTransformPairDeleted(ChildSaRecord childSaRecord) { + mUserCallback.onIpSecTransformDeleted( + childSaRecord.getOutboundIpSecTransform(), IpSecManager.DIRECTION_OUT); + mUserCallback.onIpSecTransformDeleted( + childSaRecord.getInboundIpSecTransform(), IpSecManager.DIRECTION_IN); + } + + /** + * ReceivedRequest contains exchange subtype and payloads that are extracted from a request + * message to the current Child procedure. + */ + private static class ReceivedRequest { + @IkeExchangeSubType public final int exchangeSubtype; + @ExchangeType public final int exchangeType; + public final List<IkePayload> requestPayloads; + + ReceivedRequest( + @IkeExchangeSubType int eSubtype, + @ExchangeType int eType, + List<IkePayload> reqPayloads) { + exchangeSubtype = eSubtype; + exchangeType = eType; + requestPayloads = reqPayloads; + } + } + + /** + * ReceivedResponse contains exchange type and payloads that are extracted from a response + * message to the current Child procedure. + */ + private static class ReceivedResponse { + @ExchangeType public final int exchangeType; + public final List<IkePayload> responsePayloads; + + ReceivedResponse(@ExchangeType int eType, List<IkePayload> respPayloads) { + exchangeType = eType; + responsePayloads = respPayloads; + } + } + + private static class ReceivedCreateResponse extends ReceivedResponse { + public final int registeredSpi; + + ReceivedCreateResponse(@ExchangeType int eType, List<IkePayload> respPayloads, int spi) { + super(eType, respPayloads); + registeredSpi = spi; + } + } + + /** + * FirstChildNegotiationData contains payloads for negotiating first Child SA in IKE_AUTH + * request and IKE_AUTH response and callback to notify IkeSessionStateMachine the SA + * negotiation result. + */ + private static class FirstChildNegotiationData extends ReceivedCreateResponse { + public final List<IkePayload> requestPayloads; + + FirstChildNegotiationData( + List<IkePayload> reqPayloads, List<IkePayload> respPayloads, int spi) { + super(EXCHANGE_TYPE_IKE_AUTH, respPayloads, spi); + requestPayloads = reqPayloads; + } + } + + /** Top level state for handling uncaught exceptions for all subclasses. */ + abstract class ExceptionHandler extends ExceptionHandlerBase { + @Override + protected void cleanUpAndQuit(RuntimeException e) { + // TODO: b/140123526 Send a response if exception was caught when processing a request. + + // Clean up all SaRecords. + closeAllSaRecords(false /*expectSaClosed*/); + + mUserCbExecutor.execute( + () -> { + mUserCallback.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*/); + + mChildSmCallback.onProcedureFinished(this); + mChildSmCallback.onChildSessionClosed(mUserCallback); + } + + private void closeAllSaRecords(boolean expectSaClosed) { + closeChildSaRecord(mCurrentChildSaRecord, expectSaClosed); + closeChildSaRecord(mLocalInitNewChildSaRecord, expectSaClosed); + closeChildSaRecord(mRemoteInitNewChildSaRecord, expectSaClosed); + + mCurrentChildSaRecord = null; + mLocalInitNewChildSaRecord = null; + mRemoteInitNewChildSaRecord = null; + } + + private void closeChildSaRecord(ChildSaRecord childSaRecord, boolean expectSaClosed) { + if (childSaRecord == null) return; + + mUserCbExecutor.execute( + () -> { + onIpSecTransformPairDeleted(childSaRecord); + }); + + mChildSmCallback.onChildSaDeleted(childSaRecord.getRemoteSpi()); + childSaRecord.close(); + + if (!expectSaClosed) return; + + logWtf( + "ChildSaRecord with local SPI: " + + childSaRecord.getLocalSpi() + + " is not correctly closed."); + } + + private void handleChildFatalError(Exception error) { + IkeException ikeException = + error instanceof IkeException + ? (IkeException) error + : new IkeInternalException(error); + + mUserCbExecutor.execute( + () -> { + mUserCallback.onClosedExceptionally(ikeException); + }); + loge("Child Session fatal error", ikeException); + + // Clean up all SaRecords and quit + closeAllSaRecords(false /*expectSaClosed*/); + quitNow(); + } + + /** + * This state handles the request to close Child Session immediately without initiating any + * exchange. + * + * <p>Request for closing Child Session immediately is usually caused by the closing of IKE + * Session. All states MUST be a child state of KillChildSessionParent to handle the closing + * request. + */ + private class KillChildSessionParent extends ExceptionHandler { + @Override + public boolean processStateMessage(Message message) { + switch (message.what) { + case CMD_KILL_SESSION: + mUserCbExecutor.execute( + () -> { + mUserCallback.onClosed(); + }); + + closeAllSaRecords(false /*expectSaClosed*/); + + quitNow(); + return HANDLED; + default: + return NOT_HANDLED; + } + } + } + + /** + * CreateChildLocalCreateBase represents the common information for a locally-initiated initial + * Child SA negotiation for setting up this Child Session. + */ + private abstract class CreateChildLocalCreateBase extends ExceptionHandler { + protected void validateAndBuildChild( + List<IkePayload> reqPayloads, + List<IkePayload> respPayloads, + @ExchangeType int exchangeType, + @ExchangeType int expectedExchangeType, + int registeredSpi) { + CreateChildResult createChildResult = + CreateChildSaHelper.validateAndNegotiateInitChild( + reqPayloads, + respPayloads, + exchangeType, + expectedExchangeType, + mChildSessionOptions.isTransportMode(), + mIpSecManager, + mRemoteAddress); + switch (createChildResult.status) { + case CREATE_STATUS_OK: + try { + setUpNegotiatedResult(createChildResult); + + ChildLocalRequest rekeyLocalRequest = makeRekeyLocalRequest(); + + mCurrentChildSaRecord = + ChildSaRecord.makeChildSaRecord( + mContext, + reqPayloads, + respPayloads, + createChildResult.initSpi, + createChildResult.respSpi, + mLocalAddress, + mRemoteAddress, + mUdpEncapSocket, + mIkePrf, + mChildIntegrity, + mChildCipher, + mSkD, + mChildSessionOptions.isTransportMode(), + true /*isLocalInit*/, + rekeyLocalRequest); + + mChildSmCallback.scheduleLocalRequest(rekeyLocalRequest, getRekeyTimeout()); + + ChildSessionConfiguration sessionConfig = + buildChildSessionConfigFromResp(createChildResult, respPayloads); + mUserCbExecutor.execute( + () -> { + mUserCallback.onIpSecTransformCreated( + mCurrentChildSaRecord.getInboundIpSecTransform(), + IpSecManager.DIRECTION_IN); + mUserCallback.onIpSecTransformCreated( + mCurrentChildSaRecord.getOutboundIpSecTransform(), + IpSecManager.DIRECTION_OUT); + mUserCallback.onOpened(sessionConfig); + }); + + transitionTo(mIdle); + } catch (GeneralSecurityException + | ResourceUnavailableException + | SpiUnavailableException + | IOException e) { + // #makeChildSaRecord failed. + + // TODO: Initiate deletion + mChildSmCallback.onChildSaDeleted(createChildResult.respSpi.getSpi()); + createChildResult.initSpi.close(); + createChildResult.respSpi.close(); + handleChildFatalError(e); + } + break; + case CREATE_STATUS_CHILD_ERROR_INVALID_MSG: + // TODO: Initiate deletion + handleCreationFailAndQuit(registeredSpi, createChildResult.exception); + break; + case CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY: + handleCreationFailAndQuit(registeredSpi, createChildResult.exception); + break; + default: + cleanUpAndQuit( + new IllegalStateException( + "Unrecognized status: " + createChildResult.status)); + } + } + + private void setUpNegotiatedResult(CreateChildResult createChildResult) { + // Build crypto tools using negotiated ChildSaProposal. It is ensured by {@link + // IkeSaPayload#getVerifiedNegotiatedChildProposalPair} that the negotiated + // ChildSaProposal is valid. The negotiated ChildSaProposal has exactly one encryption + // algorithm. When it has a combined-mode encryption algorithm, it either does not have + // integrity algorithm or only has one NONE value integrity algorithm. When the + // negotiated ChildSaProposal has a normal encryption algorithm, it either does not have + // integrity algorithm or has one integrity algorithm with any supported value. + + mSaProposal = createChildResult.negotiatedProposal; + Provider provider = IkeMessage.getSecurityProvider(); + mChildCipher = IkeCipher.create(mSaProposal.getEncryptionTransforms()[0], provider); + if (mSaProposal.getIntegrityTransforms().length != 0 + && mSaProposal.getIntegrityTransforms()[0].id + != SaProposal.INTEGRITY_ALGORITHM_NONE) { + mChildIntegrity = + IkeMacIntegrity.create(mSaProposal.getIntegrityTransforms()[0], provider); + } + + mLocalTs = createChildResult.initTs; + mRemoteTs = createChildResult.respTs; + } + + private ChildSessionConfiguration buildChildSessionConfigFromResp( + CreateChildResult createChildResult, List<IkePayload> respPayloads) { + IkeConfigPayload configPayload = + IkePayload.getPayloadForTypeInProvidedList( + PAYLOAD_TYPE_CP, IkeConfigPayload.class, respPayloads); + + if (mChildSessionOptions.isTransportMode() + || configPayload == null + || configPayload.configType != IkeConfigPayload.CONFIG_TYPE_REPLY) { + if (configPayload != null) { + logw("Unexpected config payload. Config Type: " + configPayload.configType); + } + + return new ChildSessionConfiguration( + Arrays.asList(createChildResult.initTs), + Arrays.asList(createChildResult.respTs)); + } else { + return new ChildSessionConfiguration( + Arrays.asList(createChildResult.initTs), + Arrays.asList(createChildResult.respTs), + configPayload); + } + } + + private void handleCreationFailAndQuit(int registeredSpi, IkeException exception) { + if (registeredSpi != SPI_NOT_REGISTERED) { + mChildSmCallback.onChildSaDeleted(registeredSpi); + } + handleChildFatalError(exception); + } + } + + /** Initial state of ChildSessionStateMachine. */ + class Initial extends CreateChildLocalCreateBase { + @Override + public boolean processStateMessage(Message message) { + switch (message.what) { + case CMD_HANDLE_FIRST_CHILD_EXCHANGE: + FirstChildNegotiationData childNegotiationData = + (FirstChildNegotiationData) message.obj; + List<IkePayload> reqPayloads = childNegotiationData.requestPayloads; + List<IkePayload> respPayloads = childNegotiationData.responsePayloads; + + // Negotiate Child SA. The exchangeType has been validated in + // IkeSessionStateMachine. Won't validate it again here. + validateAndBuildChild( + reqPayloads, + respPayloads, + EXCHANGE_TYPE_IKE_AUTH, + EXCHANGE_TYPE_IKE_AUTH, + childNegotiationData.registeredSpi); + + return HANDLED; + case CMD_LOCAL_REQUEST_CREATE_CHILD: + transitionTo(mCreateChildLocalCreate); + return HANDLED; + case CMD_LOCAL_REQUEST_DELETE_CHILD: + // This may happen when creation has been rescheduled to be after deletion. + mUserCbExecutor.execute( + () -> { + mUserCallback.onClosed(); + }); + quitNow(); + return HANDLED; + case CMD_FORCE_TRANSITION: + transitionTo((State) message.obj); + return HANDLED; + default: + return NOT_HANDLED; + } + } + } + + /** + * CreateChildLocalCreate represents the state where Child Session initiates the Create Child + * exchange. + */ + class CreateChildLocalCreate extends CreateChildLocalCreateBase { + private List<IkePayload> mRequestPayloads; + + @Override + public void enterState() { + try { + mRequestPayloads = + CreateChildSaHelper.getInitChildCreateReqPayloads( + mIpSecManager, + mLocalAddress, + mChildSessionOptions, + false /*isFirstChild*/); + mChildSmCallback.onOutboundPayloadsReady( + EXCHANGE_TYPE_CREATE_CHILD_SA, + false /*isResp*/, + mRequestPayloads, + ChildSessionStateMachine.this); + } catch (ResourceUnavailableException e) { + // Fail to assign SPI + handleChildFatalError(e); + } + } + + @Override + public boolean processStateMessage(Message message) { + switch (message.what) { + case CMD_HANDLE_RECEIVED_RESPONSE: + ReceivedCreateResponse rcvResp = (ReceivedCreateResponse) message.obj; + validateAndBuildChild( + mRequestPayloads, + rcvResp.responsePayloads, + rcvResp.exchangeType, + EXCHANGE_TYPE_CREATE_CHILD_SA, + rcvResp.registeredSpi); + return HANDLED; + default: + return NOT_HANDLED; + } + } + } + + /** + * Idle represents a state when there is no ongoing IKE exchange affecting established Child SA. + */ + class Idle extends ExceptionHandler { + @Override + public void enterState() { + mChildSmCallback.onProcedureFinished(ChildSessionStateMachine.this); + } + + @Override + public boolean processStateMessage(Message message) { + switch (message.what) { + case CMD_LOCAL_REQUEST_DELETE_CHILD: + transitionTo(mDeleteChildLocalDelete); + return HANDLED; + case CMD_LOCAL_REQUEST_REKEY_CHILD: + transitionTo(mRekeyChildLocalCreate); + return HANDLED; + case CMD_HANDLE_RECEIVED_REQUEST: + ReceivedRequest req = (ReceivedRequest) message.obj; + switch (req.exchangeSubtype) { + case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD: + deferMessage(message); + transitionTo(mDeleteChildRemoteDelete); + return HANDLED; + case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD: + deferMessage(message); + transitionTo(mRekeyChildRemoteCreate); + return HANDLED; + default: + return NOT_HANDLED; + } + case CMD_FORCE_TRANSITION: // Testing command + transitionTo((State) message.obj); + return HANDLED; + default: + return NOT_HANDLED; + } + } + } + + /** + * DeleteResponderBase represents all states after Child Session is established + * + * <p>All post-init states share common functionality of being able to respond to Delete Child + * requests. + */ + private abstract class DeleteResponderBase extends ExceptionHandler { + /** + * Check if the payload list has a Delete Payload that includes the remote SPI of the input + * ChildSaRecord. + */ + protected boolean hasRemoteChildSpiForDelete( + List<IkePayload> payloads, ChildSaRecord expectedRecord) { + List<IkeDeletePayload> delPayloads = + IkePayload.getPayloadListForTypeInProvidedList( + PAYLOAD_TYPE_DELETE, IkeDeletePayload.class, payloads); + + for (IkeDeletePayload delPayload : delPayloads) { + for (int spi : delPayload.spisToDelete) { + if (spi == expectedRecord.getRemoteSpi()) return true; + } + } + return false; + } + + /** + * Build and send payload list that has a Delete Payload that includes the local SPI of the + * input ChildSaRecord. + */ + protected void sendDeleteChild(ChildSaRecord childSaRecord, boolean isResp) { + List<IkePayload> outIkePayloads = new ArrayList<>(1); + outIkePayloads.add(new IkeDeletePayload(new int[] {childSaRecord.getLocalSpi()})); + + mChildSmCallback.onOutboundPayloadsReady( + EXCHANGE_TYPE_INFORMATIONAL, + isResp, + outIkePayloads, + ChildSessionStateMachine.this); + } + + /** + * Helper method for responding to a session deletion request + * + * <p>Note that this method expects that the session is keyed on the mCurrentChildSaRecord + * and closing this Child 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 + * Child SA the session is reliant upon. + * + * <p>Note that this method will also quit the state machine + */ + protected void handleDeleteSessionRequest(List<IkePayload> payloads) { + if (!hasRemoteChildSpiForDelete(payloads, mCurrentChildSaRecord)) { + cleanUpAndQuit( + new IllegalStateException( + "Found no remote SPI for mCurrentChildSaRecord in a Delete Child" + + " request.")); + } else { + + mUserCbExecutor.execute( + () -> { + mUserCallback.onClosed(); + onIpSecTransformPairDeleted(mCurrentChildSaRecord); + }); + + sendDeleteChild(mCurrentChildSaRecord, true /*isResp*/); + + mChildSmCallback.onChildSaDeleted(mCurrentChildSaRecord.getRemoteSpi()); + mCurrentChildSaRecord.close(); + mCurrentChildSaRecord = null; + + quitNow(); + } + } + } + + /** + * DeleteBase abstracts deletion handling for all states initiating and responding to a Delete + * Child 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 { + /** Validate payload types in Delete Child response. */ + protected void validateDeleteRespPayloadAndExchangeType( + List<IkePayload> respPayloads, @ExchangeType int exchangeType) + throws IkeProtocolException { + + if (exchangeType != EXCHANGE_TYPE_INFORMATIONAL) { + throw new InvalidSyntaxException( + "Unexpected exchange type in Delete Child response: " + exchangeType); + } + + for (IkePayload payload : respPayloads) { + handlePayload: + switch (payload.payloadType) { + case PAYLOAD_TYPE_DELETE: + // A Delete Payload is only required when it is not simultaneous deletion. + // Included Child SPIs are verified in the subclass to make sure the remote + // side is deleting the right SAs. + break handlePayload; + case PAYLOAD_TYPE_NOTIFY: + IkeNotifyPayload notify = (IkeNotifyPayload) payload; + if (!notify.isErrorNotify()) { + logw( + "Unexpected or unknown status notification in Delete Child" + + " response: " + + notify.notifyType); + break handlePayload; + } + + throw notify.validateAndBuildIkeException(); + default: + logw( + "Unexpected payload type in Delete Child response: " + + payload.payloadType); + } + } + } + } + + /** + * DeleteChildLocalDelete represents the state where Child Session initiates the Delete Child + * exchange. + */ + class DeleteChildLocalDelete extends DeleteBase { + private boolean mSimulDeleteDetected = false; + + @Override + public void enterState() { + mSimulDeleteDetected = false; + sendDeleteChild(mCurrentChildSaRecord, false /*isResp*/); + } + + @Override + public boolean processStateMessage(Message message) { + switch (message.what) { + case CMD_HANDLE_RECEIVED_RESPONSE: + try { + ReceivedResponse resp = (ReceivedResponse) message.obj; + validateDeleteRespPayloadAndExchangeType( + resp.responsePayloads, resp.exchangeType); + + boolean currentSaSpiFound = + hasRemoteChildSpiForDelete( + resp.responsePayloads, mCurrentChildSaRecord); + if (!currentSaSpiFound && !mSimulDeleteDetected) { + throw new InvalidSyntaxException( + "Found no remote SPI in received Delete response."); + } else if (currentSaSpiFound && mSimulDeleteDetected) { + // As required by the RFC 7296, in simultaneous delete case, the remote + // side MUST NOT include SPI of mCurrentChildSaRecord. However, to + // provide better interoperatibility, IKE library will keep IKE Session + // alive and continue the deleting process. + logw( + "Found remote SPI in the Delete response in a simultaneous" + + " deletion case"); + } + + mUserCbExecutor.execute( + () -> { + mUserCallback.onClosed(); + onIpSecTransformPairDeleted(mCurrentChildSaRecord); + }); + + mChildSmCallback.onChildSaDeleted(mCurrentChildSaRecord.getRemoteSpi()); + mCurrentChildSaRecord.close(); + mCurrentChildSaRecord = null; + + quitNow(); + } catch (IkeProtocolException e) { + // Shut down Child Session and notify users the error. + handleChildFatalError(e); + } + return HANDLED; + case CMD_HANDLE_RECEIVED_REQUEST: + ReceivedRequest req = (ReceivedRequest) message.obj; + switch (req.exchangeSubtype) { + case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD: + // It has been verified in IkeSessionStateMachine that the incoming + // request can be ONLY for mCurrentChildSaRecord at this point. + if (!hasRemoteChildSpiForDelete( + req.requestPayloads, mCurrentChildSaRecord)) { + // Program error + cleanUpAndQuit( + new IllegalStateException( + "Found no remote SPI for mCurrentChildSaRecord in" + + " a Delete request")); + + } else { + mChildSmCallback.onOutboundPayloadsReady( + EXCHANGE_TYPE_INFORMATIONAL, + true /*isResp*/, + new LinkedList<>(), + ChildSessionStateMachine.this); + mSimulDeleteDetected = true; + } + return HANDLED; + case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD: + replyErrorNotification(ERROR_TYPE_TEMPORARY_FAILURE); + return HANDLED; + default: + cleanUpAndQuit( + new IllegalStateException( + "Invalid exchange subtype for Child Session: " + + req.exchangeSubtype)); + return HANDLED; + } + default: + return NOT_HANDLED; + } + } + } + + /** + * DeleteChildRemoteDelete represents the state where Child Session receives the Delete Child + * request. + */ + class DeleteChildRemoteDelete extends DeleteResponderBase { + @Override + public boolean processStateMessage(Message message) { + switch (message.what) { + case CMD_HANDLE_RECEIVED_REQUEST: + ReceivedRequest req = (ReceivedRequest) message.obj; + if (req.exchangeSubtype == IKE_EXCHANGE_SUBTYPE_DELETE_CHILD) { + handleDeleteSessionRequest(req.requestPayloads); + return HANDLED; + } + return NOT_HANDLED; + default: + return NOT_HANDLED; + } + } + } + + /** + * RekeyChildLocalCreate represents the state where Child Session initiates the Rekey Child + * exchange. + * + * <p>As indicated in RFC 7296 section 2.8, "when rekeying, the new Child SA SHOULD NOT have + * different Traffic Selectors and algorithms than the old one." + */ + class RekeyChildLocalCreate extends DeleteResponderBase { + private List<IkePayload> mRequestPayloads; + + @Override + public void enterState() { + try { + // Build request with negotiated proposal and TS. + mRequestPayloads = + CreateChildSaHelper.getRekeyChildCreateReqPayloads( + mIpSecManager, + mLocalAddress, + mSaProposal, + mLocalTs, + mRemoteTs, + mCurrentChildSaRecord.getLocalSpi(), + mChildSessionOptions.isTransportMode()); + mChildSmCallback.onOutboundPayloadsReady( + EXCHANGE_TYPE_CREATE_CHILD_SA, + false /*isResp*/, + mRequestPayloads, + ChildSessionStateMachine.this); + } catch (ResourceUnavailableException e) { + loge("Fail to assign Child SPI. Schedule a retry for rekey Child"); + mChildSmCallback.scheduleRetryLocalRequest( + (ChildLocalRequest) mCurrentChildSaRecord.getFutureRekeyEvent()); + transitionTo(mIdle); + } + } + + @Override + public boolean processStateMessage(Message message) { + switch (message.what) { + case CMD_HANDLE_RECEIVED_RESPONSE: + ReceivedCreateResponse resp = (ReceivedCreateResponse) message.obj; + CreateChildResult createChildResult = + CreateChildSaHelper.validateAndNegotiateRekeyChildResp( + mRequestPayloads, + resp.responsePayloads, + resp.exchangeType, + EXCHANGE_TYPE_CREATE_CHILD_SA, + mChildSessionOptions.isTransportMode(), + mCurrentChildSaRecord, + mIpSecManager, + mRemoteAddress); + + switch (createChildResult.status) { + case CREATE_STATUS_OK: + try { + // Do not need to update the negotiated proposal and TS because they + // are not changed. + + ChildLocalRequest rekeyLocalRequest = makeRekeyLocalRequest(); + + mLocalInitNewChildSaRecord = + ChildSaRecord.makeChildSaRecord( + mContext, + mRequestPayloads, + resp.responsePayloads, + createChildResult.initSpi, + createChildResult.respSpi, + mLocalAddress, + mRemoteAddress, + mUdpEncapSocket, + mIkePrf, + mChildIntegrity, + mChildCipher, + mSkD, + mChildSessionOptions.isTransportMode(), + true /*isLocalInit*/, + rekeyLocalRequest); + + mChildSmCallback.scheduleLocalRequest( + rekeyLocalRequest, getRekeyTimeout()); + + mUserCbExecutor.execute( + () -> { + mUserCallback.onIpSecTransformCreated( + mLocalInitNewChildSaRecord + .getInboundIpSecTransform(), + IpSecManager.DIRECTION_IN); + mUserCallback.onIpSecTransformCreated( + mLocalInitNewChildSaRecord + .getOutboundIpSecTransform(), + IpSecManager.DIRECTION_OUT); + }); + + transitionTo(mRekeyChildLocalDelete); + } catch (GeneralSecurityException + | ResourceUnavailableException + | SpiUnavailableException + | IOException e) { + // #makeChildSaRecord failed + handleProcessRespOrSaCreationFailAndQuit(resp.registeredSpi, e); + createChildResult.initSpi.close(); + createChildResult.respSpi.close(); + } + break; + case CREATE_STATUS_CHILD_ERROR_INVALID_MSG: + handleProcessRespOrSaCreationFailAndQuit( + resp.registeredSpi, createChildResult.exception); + break; + case CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY: + if (createChildResult.exception instanceof TemporaryFailureException) { + loge( + "Received TEMPORARY_FAILURE for rekey Child. Retry has" + + "already been scheduled by IKE Session."); + } else { + loge( + "Received error notification for rekey Child. Schedule a" + + " retry"); + mChildSmCallback.scheduleRetryLocalRequest( + (ChildLocalRequest) + mCurrentChildSaRecord.getFutureRekeyEvent()); + } + + transitionTo(mIdle); + break; + default: + cleanUpAndQuit( + new IllegalStateException( + "Unrecognized status: " + createChildResult.status)); + } + return HANDLED; + default: + // TODO: Handle rekey and delete request + return NOT_HANDLED; + } + } + + private void handleProcessRespOrSaCreationFailAndQuit( + int registeredSpi, Exception 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. Sending delete request for new SA indicates the rekey has + // finished and the new SA has died. + + // TODO: Initiate deletion on newly created SA + if (registeredSpi != SPI_NOT_REGISTERED) { + mChildSmCallback.onChildSaDeleted(registeredSpi); + } + handleChildFatalError(exception); + } + } + + /** + * RekeyChildRemoteCreate represents the state where Child Session receives a Rekey Child + * request. + * + * <p>As indicated in RFC 7296 section 2.8, "when rekeying, the new Child SA SHOULD NOT have + * different Traffic Selectors and algorithms than the old one." + * + * <p>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. + */ + class RekeyChildRemoteCreate extends ExceptionHandler { + @Override + public boolean processStateMessage(Message message) { + switch (message.what) { + case CMD_HANDLE_RECEIVED_REQUEST: + ReceivedRequest req = (ReceivedRequest) message.obj; + + if (req.exchangeSubtype == IKE_EXCHANGE_SUBTYPE_REKEY_CHILD) { + handleCreateChildRequest(req); + return HANDLED; + } + + return NOT_HANDLED; + default: + return NOT_HANDLED; + } + } + + private void handleCreateChildRequest(ReceivedRequest req) { + List<IkePayload> reqPayloads = null; + List<IkePayload> respPayloads = null; + try { + reqPayloads = req.requestPayloads; + + // Build a rekey response payload list with our previously selected proposal, + // against which we will validate the received request. It is guaranteed in + // IkeSessionStateMachine#getIkeExchangeSubType that a SA Payload is included in the + // inbound request payload list. + IkeSaPayload reqSaPayload = + IkePayload.getPayloadForTypeInProvidedList( + PAYLOAD_TYPE_SA, IkeSaPayload.class, reqPayloads); + byte respProposalNumber = reqSaPayload.getNegotiatedProposalNumber(mSaProposal); + + respPayloads = + CreateChildSaHelper.getRekeyChildCreateRespPayloads( + mIpSecManager, + mLocalAddress, + respProposalNumber, + mSaProposal, + mLocalTs, + mRemoteTs, + mCurrentChildSaRecord.getLocalSpi(), + mChildSessionOptions.isTransportMode()); + } catch (NoValidProposalChosenException e) { + handleCreationFailureAndBackToIdle(e); + return; + } catch (ResourceUnavailableException e) { + handleCreationFailureAndBackToIdle( + new NoValidProposalChosenException("Fail to assign inbound SPI", e)); + return; + } + + CreateChildResult createChildResult = + CreateChildSaHelper.validateAndNegotiateRekeyChildRequest( + reqPayloads, + respPayloads, + req.exchangeType /*exchangeType*/, + EXCHANGE_TYPE_CREATE_CHILD_SA /*expectedExchangeType*/, + mChildSessionOptions.isTransportMode(), + mIpSecManager, + mRemoteAddress); + + switch (createChildResult.status) { + case CREATE_STATUS_OK: + try { + // Do not need to update the negotiated proposal and TS + // because they are not changed. + + ChildLocalRequest rekeyLocalRequest = makeRekeyLocalRequest(); + + mRemoteInitNewChildSaRecord = + ChildSaRecord.makeChildSaRecord( + mContext, + reqPayloads, + respPayloads, + createChildResult.initSpi, + createChildResult.respSpi, + mLocalAddress, + mRemoteAddress, + mUdpEncapSocket, + mIkePrf, + mChildIntegrity, + mChildCipher, + mSkD, + mChildSessionOptions.isTransportMode(), + false /*isLocalInit*/, + rekeyLocalRequest); + + mChildSmCallback.scheduleLocalRequest(rekeyLocalRequest, getRekeyTimeout()); + + mChildSmCallback.onChildSaCreated( + mRemoteInitNewChildSaRecord.getRemoteSpi(), + ChildSessionStateMachine.this); + + // To avoid traffic loss, outbound transform should only be applied once + // the remote has (implicitly) acknowledged our response via the + // delete-old-SA request. This will be performed in the finishRekey() + // method. + mUserCbExecutor.execute( + () -> { + mUserCallback.onIpSecTransformCreated( + mRemoteInitNewChildSaRecord.getInboundIpSecTransform(), + IpSecManager.DIRECTION_IN); + }); + + mChildSmCallback.onOutboundPayloadsReady( + EXCHANGE_TYPE_CREATE_CHILD_SA, + true /*isResp*/, + respPayloads, + ChildSessionStateMachine.this); + + transitionTo(mRekeyChildRemoteDelete); + } catch (GeneralSecurityException + | ResourceUnavailableException + | SpiUnavailableException + | IOException e) { + // #makeChildSaRecord failed. + createChildResult.initSpi.close(); + createChildResult.respSpi.close(); + + handleCreationFailureAndBackToIdle( + new NoValidProposalChosenException( + "Error in Child SA creation", e)); + } + break; + case CREATE_STATUS_CHILD_ERROR_INVALID_MSG: + IkeException error = createChildResult.exception; + if (error instanceof IkeProtocolException) { + handleCreationFailureAndBackToIdle((IkeProtocolException) error); + } else { + handleCreationFailureAndBackToIdle( + new NoValidProposalChosenException( + "Error in validating Create Child request", error)); + } + break; + case CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY: + cleanUpAndQuit( + new IllegalStateException( + "Unexpected processing status in Create Child request: " + + createChildResult.status)); + break; + default: + cleanUpAndQuit( + new IllegalStateException( + "Unrecognized status: " + createChildResult.status)); + } + } + + private void handleCreationFailureAndBackToIdle(IkeProtocolException e) { + loge("Received invalid Rekey Child request. Reject with error notification", e); + + ArrayList<IkePayload> payloads = new ArrayList<>(1); + payloads.add(e.buildNotifyPayload()); + mChildSmCallback.onOutboundPayloadsReady( + EXCHANGE_TYPE_CREATE_CHILD_SA, + true /*isResp*/, + payloads, + ChildSessionStateMachine.this); + + transitionTo(mIdle); + } + } + + /** + * RekeyChildDeleteBase represents common behaviours of deleting stage during rekeying Child SA. + */ + abstract class RekeyChildDeleteBase extends DeleteBase { + @Override + public boolean processStateMessage(Message message) { + switch (message.what) { + case CMD_HANDLE_RECEIVED_REQUEST: + try { + if (isOnNewSa((ReceivedRequest) message.obj)) { + finishRekey(); + deferMessage(message); + transitionTo(mIdle); + return HANDLED; + } + return NOT_HANDLED; + } catch (IllegalStateException e) { + cleanUpAndQuit(e); + return HANDLED; + } + default: + return NOT_HANDLED; + } + } + + private boolean isOnNewSa(ReceivedRequest req) { + switch (req.exchangeSubtype) { + case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD: + return hasRemoteChildSpiForDelete(req.requestPayloads, mChildSaRecordSurviving); + case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD: + return CreateChildSaHelper.hasRemoteChildSpiForRekey( + req.requestPayloads, mChildSaRecordSurviving); + default: + throw new IllegalStateException( + "Invalid exchange subtype for Child Session: " + req.exchangeSubtype); + } + } + + // Rekey timer for old SA will be cancelled as part of the closing of the SA. + protected void finishRekey() { + mUserCbExecutor.execute( + () -> { + onIpSecTransformPairDeleted(mCurrentChildSaRecord); + }); + + mChildSmCallback.onChildSaDeleted(mCurrentChildSaRecord.getRemoteSpi()); + mCurrentChildSaRecord.close(); + + mCurrentChildSaRecord = mChildSaRecordSurviving; + + mLocalInitNewChildSaRecord = null; + mRemoteInitNewChildSaRecord = null; + mChildSaRecordSurviving = null; + } + } + + /** + * RekeyChildLocalDelete represents the deleting stage of a locally-initiated Rekey Child + * procedure. + */ + class RekeyChildLocalDelete extends RekeyChildDeleteBase { + @Override + public void enterState() { + mChildSaRecordSurviving = mLocalInitNewChildSaRecord; + sendDeleteChild(mCurrentChildSaRecord, false /*isResp*/); + } + + @Override + public boolean processStateMessage(Message message) { + if (super.processStateMessage(message) == HANDLED) { + return HANDLED; + } + + switch (message.what) { + case CMD_HANDLE_RECEIVED_RESPONSE: + try { + ReceivedResponse resp = (ReceivedResponse) message.obj; + validateDeleteRespPayloadAndExchangeType( + resp.responsePayloads, resp.exchangeType); + + boolean currentSaSpiFound = + hasRemoteChildSpiForDelete( + resp.responsePayloads, mCurrentChildSaRecord); + if (!currentSaSpiFound) { + loge( + "Found no remote SPI for current SA in received Delete" + + " response. Shutting down old SA and finishing rekey."); + } + } catch (IkeProtocolException e) { + loge( + "Received Delete response with invalid syntax or error" + + " notifications. Shutting down old SA and finishing rekey.", + e); + } + finishRekey(); + transitionTo(mIdle); + return HANDLED; + default: + // TODO: Handle requests on mCurrentChildSaRecord: Reply TEMPORARY_FAILURE to + // a rekey request and reply empty INFORMATIONAL message to a delete request. + return NOT_HANDLED; + } + } + } + + /** + * RekeyChildRemoteDelete represents the deleting stage of a remotely-initiated Rekey Child + * procedure. + */ + class RekeyChildRemoteDelete extends RekeyChildDeleteBase { + @Override + public void enterState() { + mChildSaRecordSurviving = mRemoteInitNewChildSaRecord; + sendMessageDelayed(TIMEOUT_REKEY_REMOTE_DELETE, REKEY_DELETE_TIMEOUT_MS); + } + + @Override + public boolean processStateMessage(Message message) { + if (super.processStateMessage(message) == HANDLED) { + return HANDLED; + } + + switch (message.what) { + case CMD_HANDLE_RECEIVED_REQUEST: + ReceivedRequest req = (ReceivedRequest) message.obj; + + if (req.exchangeSubtype == IKE_EXCHANGE_SUBTYPE_DELETE_CHILD) { + handleDeleteRequest(req.requestPayloads); + + } else { + replyErrorNotification(ERROR_TYPE_TEMPORARY_FAILURE); + } + return HANDLED; + case TIMEOUT_REKEY_REMOTE_DELETE: + // Receiving this signal means the remote side has received the outbound + // Rekey-Create response since no retransmissions were received during the + // waiting time. IKE library will assume the remote side has set up the new + // Child SA and finish the rekey procedure. Users should be warned there is + // a risk that the remote side failed to set up the new Child SA and all + // outbound IPsec traffic protected by new Child SA will be dropped. + + // TODO:Consider finishing rekey procedure if the IKE Session receives a new + // request. Since window size is one, receiving a new request indicates the + // remote side has received the outbound Rekey-Create response + + finishRekey(); + transitionTo(mIdle); + return HANDLED; + default: + return NOT_HANDLED; + } + } + + private void handleDeleteRequest(List<IkePayload> payloads) { + if (!hasRemoteChildSpiForDelete(payloads, mCurrentChildSaRecord)) { + // Request received on incorrect SA + cleanUpAndQuit( + new IllegalStateException( + "Found no remote SPI for current SA in received Delete" + + " response.")); + } else { + sendDeleteChild(mCurrentChildSaRecord, true /*isResp*/); + finishRekey(); + transitionTo(mIdle); + } + } + + @Override + protected void finishRekey() { + mUserCbExecutor.execute( + () -> { + mUserCallback.onIpSecTransformCreated( + mRemoteInitNewChildSaRecord.getOutboundIpSecTransform(), + IpSecManager.DIRECTION_OUT); + }); + + super.finishRekey(); + } + + @Override + public void exitState() { + removeMessages(TIMEOUT_REKEY_REMOTE_DELETE); + } + } + + /** + * Package private helper class to generate IKE SA creation payloads, in both request and + * response directions. + */ + static class CreateChildSaHelper { + /** Create payload list for creating the initial Child SA for this Child Session. */ + public static List<IkePayload> getInitChildCreateReqPayloads( + IpSecManager ipSecManager, + InetAddress localAddress, + ChildSessionOptions childSessionOptions, + boolean isFirstChild) + throws ResourceUnavailableException { + + ChildSaProposal[] saProposals = childSessionOptions.getSaProposals(); + + if (isFirstChild) { + for (int i = 0; i < saProposals.length; i++) { + saProposals[i] = + childSessionOptions.getSaProposals()[i].getCopyWithoutDhTransform(); + } + } + + List<IkePayload> payloadList = + getChildCreatePayloads( + IkeSaPayload.createChildSaRequestPayload( + saProposals, ipSecManager, localAddress), + childSessionOptions.getLocalTrafficSelectors(), + childSessionOptions.getRemoteTrafficSelectors(), + childSessionOptions.isTransportMode()); + + if (!childSessionOptions.isTransportMode()) { + ConfigAttribute[] attributes = + ((TunnelModeChildSessionOptions) childSessionOptions) + .getConfigurationRequests(); + IkeConfigPayload configPayload = + new IkeConfigPayload(false /*isReply*/, Arrays.asList(attributes)); + payloadList.add(configPayload); + } + + return payloadList; + } + + /** Create payload list as a rekey Child Session request. */ + public static List<IkePayload> getRekeyChildCreateReqPayloads( + IpSecManager ipSecManager, + InetAddress localAddress, + ChildSaProposal currentProposal, + IkeTrafficSelector[] currentLocalTs, + IkeTrafficSelector[] currentRemoteTs, + int localSpi, + boolean isTransport) + throws ResourceUnavailableException { + List<IkePayload> payloads = + getChildCreatePayloads( + IkeSaPayload.createChildSaRequestPayload( + new ChildSaProposal[] {currentProposal}, + ipSecManager, + localAddress), + currentLocalTs, + currentRemoteTs, + isTransport); + + payloads.add( + new IkeNotifyPayload( + PROTOCOL_ID_ESP, localSpi, NOTIFY_TYPE_REKEY_SA, new byte[0])); + return payloads; + } + + /** Create payload list as a rekey Child Session response. */ + public static List<IkePayload> getRekeyChildCreateRespPayloads( + IpSecManager ipSecManager, + InetAddress localAddress, + byte proposalNumber, + ChildSaProposal currentProposal, + IkeTrafficSelector[] currentLocalTs, + IkeTrafficSelector[] currentRemoteTs, + int localSpi, + boolean isTransport) + throws ResourceUnavailableException { + List<IkePayload> payloads = + getChildCreatePayloads( + IkeSaPayload.createChildSaResponsePayload( + proposalNumber, currentProposal, ipSecManager, localAddress), + currentRemoteTs /*initTs*/, + currentLocalTs /*respTs*/, + isTransport); + + payloads.add( + new IkeNotifyPayload( + PROTOCOL_ID_ESP, localSpi, NOTIFY_TYPE_REKEY_SA, new byte[0])); + return payloads; + } + + /** Create payload list for creating a new Child SA. */ + private static List<IkePayload> getChildCreatePayloads( + IkeSaPayload saPayload, + IkeTrafficSelector[] initTs, + IkeTrafficSelector[] respTs, + boolean isTransport) + throws ResourceUnavailableException { + List<IkePayload> payloadList = new ArrayList<>(5); + + payloadList.add(saPayload); + payloadList.add(new IkeTsPayload(true /*isInitiator*/, initTs)); + payloadList.add(new IkeTsPayload(false /*isInitiator*/, respTs)); + payloadList.add(new IkeNoncePayload()); + + DhGroupTransform[] dhGroups = + ((ChildProposal) saPayload.proposalList.get(0)) + .saProposal.getDhGroupTransforms(); + if (dhGroups.length != 0 && dhGroups[0].id != DH_GROUP_NONE) { + payloadList.add(new IkeKePayload(dhGroups[0].id)); + } + + if (isTransport) payloadList.add(new IkeNotifyPayload(NOTIFY_TYPE_USE_TRANSPORT_MODE)); + + return payloadList; + } + + /** + * Validate the received response of initial Create Child SA exchange and return the + * negotiation result. + */ + public static CreateChildResult validateAndNegotiateInitChild( + List<IkePayload> reqPayloads, + List<IkePayload> respPayloads, + @ExchangeType int exchangeType, + @ExchangeType int expectedExchangeType, + boolean expectTransport, + IpSecManager ipSecManager, + InetAddress remoteAddress) { + + return validateAndNegotiateChild( + reqPayloads, + respPayloads, + exchangeType, + expectedExchangeType, + true /*isLocalInit*/, + expectTransport, + ipSecManager, + remoteAddress); + } + + /** + * Validate the received rekey-create request against locally built response (based on + * previously negotiated Child SA) and return the negotiation result. + */ + public static CreateChildResult validateAndNegotiateRekeyChildRequest( + List<IkePayload> reqPayloads, + List<IkePayload> respPayloads, + @ExchangeType int exchangeType, + @ExchangeType int expectedExchangeType, + boolean expectTransport, + IpSecManager ipSecManager, + InetAddress remoteAddress) { + + // It is guaranteed that a Rekey-Notify Payload with remote SPI of current Child SA is + // included in the reqPayloads. So we won't validate it again here. + return validateAndNegotiateChild( + reqPayloads, + respPayloads, + exchangeType, + expectedExchangeType, + false /*isLocalInit*/, + expectTransport, + ipSecManager, + remoteAddress); + } + + /** + * Validate the received rekey-create response against locally built request and previously + * negotiated Child SA, and return the negotiation result. + */ + public static CreateChildResult validateAndNegotiateRekeyChildResp( + List<IkePayload> reqPayloads, + List<IkePayload> respPayloads, + @ExchangeType int exchangeType, + @ExchangeType int expectedExchangeType, + boolean expectTransport, + ChildSaRecord expectedChildRecord, + IpSecManager ipSecManager, + InetAddress remoteAddress) { + // Validate rest of payloads and negotiate Child SA. + CreateChildResult childResult = + validateAndNegotiateChild( + reqPayloads, + respPayloads, + exchangeType, + expectedExchangeType, + true /*isLocalInit*/, + expectTransport, + ipSecManager, + remoteAddress); + + // TODO: Validate new Child SA does not have different Traffic Selectors + + return childResult; + } + + /** + * Check if SPI of Child SA that is expected to be rekeyed is included in the provided + * payload list. + */ + public static boolean hasRemoteChildSpiForRekey( + List<IkePayload> payloads, ChildSaRecord expectedRecord) { + List<IkeNotifyPayload> notifyPayloads = + IkePayload.getPayloadListForTypeInProvidedList( + IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class, payloads); + + boolean hasExpectedRekeyNotify = false; + for (IkeNotifyPayload notifyPayload : notifyPayloads) { + if (notifyPayload.notifyType == NOTIFY_TYPE_REKEY_SA + && notifyPayload.spi == expectedRecord.getRemoteSpi()) { + hasExpectedRekeyNotify = true; + break; + } + } + + return hasExpectedRekeyNotify; + } + + /** Validate the received payload list and negotiate Child SA. */ + private static CreateChildResult validateAndNegotiateChild( + List<IkePayload> reqPayloads, + List<IkePayload> respPayloads, + @ExchangeType int exchangeType, + @ExchangeType int expectedExchangeType, + boolean isLocalInit, + boolean expectTransport, + IpSecManager ipSecManager, + InetAddress remoteAddress) { + List<IkePayload> inboundPayloads = isLocalInit ? respPayloads : reqPayloads; + + try { + validatePayloadAndExchangeType( + inboundPayloads, + isLocalInit /*isResp*/, + exchangeType, + expectedExchangeType); + } catch (InvalidSyntaxException e) { + return new CreateChildResult(CREATE_STATUS_CHILD_ERROR_INVALID_MSG, e); + } + + List<IkeNotifyPayload> notifyPayloads = + IkePayload.getPayloadListForTypeInProvidedList( + IkePayload.PAYLOAD_TYPE_NOTIFY, + IkeNotifyPayload.class, + inboundPayloads); + + boolean hasTransportNotify = false; + for (IkeNotifyPayload notify : notifyPayloads) { + if (notify.isErrorNotify()) { + try { + IkeProtocolException exception = notify.validateAndBuildIkeException(); + if (isLocalInit) { + return new CreateChildResult( + CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY, exception); + } else { + logw("Received unexpected error notification: " + notify.notifyType); + } + } catch (InvalidSyntaxException e) { + return new CreateChildResult(CREATE_STATUS_CHILD_ERROR_INVALID_MSG, e); + } + } + + switch (notify.notifyType) { + case IkeNotifyPayload.NOTIFY_TYPE_ADDITIONAL_TS_POSSIBLE: + // TODO: Store it as part of negotiation results that can be retrieved + // by users. + break; + case IkeNotifyPayload.NOTIFY_TYPE_IPCOMP_SUPPORTED: + // Ignore + break; + case IkeNotifyPayload.NOTIFY_TYPE_USE_TRANSPORT_MODE: + hasTransportNotify = true; + break; + case IkeNotifyPayload.NOTIFY_TYPE_ESP_TFC_PADDING_NOT_SUPPORTED: + // Ignore + break; + default: + // Unknown and unexpected status notifications are ignored as per RFC7296. + logw( + "Received unknown or unexpected status notifications with notify" + + " type: " + + notify.notifyType); + } + } + + Pair<ChildProposal, ChildProposal> childProposalPair = null; + try { + IkeSaPayload reqSaPayload = + IkePayload.getPayloadForTypeInProvidedList( + IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class, reqPayloads); + IkeSaPayload respSaPayload = + IkePayload.getPayloadForTypeInProvidedList( + IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class, respPayloads); + + // This method either throws exception or returns non-null pair that contains two + // valid {@link ChildProposal} both with a {@link SecurityParameterIndex} allocated + // inside. + childProposalPair = + IkeSaPayload.getVerifiedNegotiatedChildProposalPair( + reqSaPayload, respSaPayload, ipSecManager, remoteAddress); + ChildSaProposal saProposal = childProposalPair.second.saProposal; + + validateKePayloads(inboundPayloads, isLocalInit /*isResp*/, saProposal); + + if (expectTransport != hasTransportNotify) { + throw new NoValidProposalChosenException( + "Failed the negotiation on Child SA mode (conflicting modes chosen)."); + } + + Pair<IkeTrafficSelector[], IkeTrafficSelector[]> tsPair = + validateAndGetNegotiatedTsPair(reqPayloads, respPayloads); + + return new CreateChildResult( + childProposalPair.first.getChildSpiResource(), + childProposalPair.second.getChildSpiResource(), + saProposal, + tsPair.first, + tsPair.second); + } catch (IkeProtocolException + | ResourceUnavailableException + | SpiUnavailableException e) { + if (childProposalPair != null) { + childProposalPair.first.getChildSpiResource().close(); + childProposalPair.second.getChildSpiResource().close(); + } + + if (e instanceof InvalidSyntaxException) { + return new CreateChildResult( + CREATE_STATUS_CHILD_ERROR_INVALID_MSG, (InvalidSyntaxException) e); + } else if (e instanceof IkeProtocolException) { + return new CreateChildResult( + CREATE_STATUS_CHILD_ERROR_INVALID_MSG, + new InvalidSyntaxException( + "Processing error in received Create Child response", e)); + } else { + return new CreateChildResult( + CREATE_STATUS_CHILD_ERROR_INVALID_MSG, new IkeInternalException(e)); + } + } + } + + // Validate syntax to make sure all necessary payloads exist and exchange type is correct. + private static void validatePayloadAndExchangeType( + List<IkePayload> inboundPayloads, + boolean isResp, + @ExchangeType int exchangeType, + @ExchangeType int expectedExchangeType) + throws InvalidSyntaxException { + boolean hasSaPayload = false; + boolean hasKePayload = false; + boolean hasNoncePayload = false; + boolean hasTsInitPayload = false; + boolean hasTsRespPayload = false; + boolean hasErrorNotify = false; + + for (IkePayload payload : inboundPayloads) { + switch (payload.payloadType) { + case PAYLOAD_TYPE_SA: + hasSaPayload = true; + break; + case PAYLOAD_TYPE_KE: + // Could not decide if KE Payload MUST or MUST NOT be included until SA + // negotiation is done. + hasKePayload = true; + break; + case PAYLOAD_TYPE_NONCE: + hasNoncePayload = true; + break; + case PAYLOAD_TYPE_TS_INITIATOR: + hasTsInitPayload = true; + break; + case PAYLOAD_TYPE_TS_RESPONDER: + hasTsRespPayload = true; + break; + case PAYLOAD_TYPE_NOTIFY: + if (((IkeNotifyPayload) payload).isErrorNotify()) hasErrorNotify = true; + // Do not have enough context to handle all notifications. Handle them + // together in higher layer. + break; + case PAYLOAD_TYPE_CP: + // Handled in child creation state. Note Child Session can only handle + // Config Payload in initial creation and can only handle a Config Reply. + // For interoperability, Config Payloads received in rekey creation + // or with other config types will be ignored. + break; + default: + logw( + "Received unexpected payload in Create Child SA message. Payload" + + " type: " + + payload.payloadType); + } + } + + // Do not need to check exchange type of a request because it has been already verified + // in IkeSessionStateMachine + if (isResp + && exchangeType != expectedExchangeType + && exchangeType != EXCHANGE_TYPE_INFORMATIONAL) { + throw new InvalidSyntaxException("Received invalid exchange type: " + exchangeType); + } + + if (exchangeType == EXCHANGE_TYPE_INFORMATIONAL + && (hasSaPayload + || hasKePayload + || hasNoncePayload + || hasTsInitPayload + || hasTsRespPayload)) { + logw( + "Unexpected payload found in an INFORMATIONAL message: SA, KE, Nonce," + + " TS-Initiator or TS-Responder"); + } + + if (isResp + && !hasErrorNotify + && (!hasSaPayload + || !hasNoncePayload + || !hasTsInitPayload + || !hasTsRespPayload)) { + throw new InvalidSyntaxException( + "SA, Nonce, TS-Initiator or TS-Responder missing."); + } + } + + private static Pair<IkeTrafficSelector[], IkeTrafficSelector[]> + validateAndGetNegotiatedTsPair( + List<IkePayload> reqPayloads, List<IkePayload> respPayloads) + throws TsUnacceptableException { + IkeTrafficSelector[] initTs = + validateAndGetNegotiatedTs(reqPayloads, respPayloads, true /*isInitTs*/); + IkeTrafficSelector[] respTs = + validateAndGetNegotiatedTs(reqPayloads, respPayloads, false /*isInitTs*/); + + return new Pair<IkeTrafficSelector[], IkeTrafficSelector[]>(initTs, respTs); + } + + private static IkeTrafficSelector[] validateAndGetNegotiatedTs( + List<IkePayload> reqPayloads, List<IkePayload> respPayloads, boolean isInitTs) + throws TsUnacceptableException { + int tsType = isInitTs ? PAYLOAD_TYPE_TS_INITIATOR : PAYLOAD_TYPE_TS_RESPONDER; + IkeTsPayload reqPayload = + IkePayload.getPayloadForTypeInProvidedList( + tsType, IkeTsPayload.class, reqPayloads); + IkeTsPayload respPayload = + IkePayload.getPayloadForTypeInProvidedList( + tsType, IkeTsPayload.class, respPayloads); + + if (!reqPayload.contains(respPayload)) { + throw new TsUnacceptableException(); + } + + // It is guaranteed by decoding inbound TS Payload and constructing outbound TS Payload + // that each TS Payload has at least one IkeTrafficSelector. + return respPayload.trafficSelectors; + } + + @VisibleForTesting + static void validateKePayloads( + List<IkePayload> inboundPayloads, + boolean isResp, + ChildSaProposal negotiatedProposal) + throws IkeProtocolException { + DhGroupTransform[] dhTransforms = negotiatedProposal.getDhGroupTransforms(); + + if (dhTransforms.length > 1) { + throw new IllegalArgumentException( + "Found multiple DH Group Transforms in the negotiated SA proposal"); + } + boolean expectKePayload = + dhTransforms.length == 1 && dhTransforms[0].id != DH_GROUP_NONE; + + IkeKePayload kePayload = + IkePayload.getPayloadForTypeInProvidedList( + PAYLOAD_TYPE_KE, IkeKePayload.class, inboundPayloads); + + if (expectKePayload && (kePayload == null || dhTransforms[0].id != kePayload.dhGroup)) { + if (isResp) { + throw new InvalidSyntaxException( + "KE Payload missing or has mismatched DH Group with the negotiated" + + " proposal."); + } else { + throw new InvalidKeException(dhTransforms[0].id); + } + + } else if (!expectKePayload && kePayload != null && isResp) { + // It is valid when the remote request proposed multiple DH Groups with a KE + // payload, and the responder chose DH_GROUP_NONE. + throw new InvalidSyntaxException("Received unexpected KE Payload."); + } + } + + private static void logw(String s) { + getIkeLog().w(TAG, s); + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + CREATE_STATUS_OK, + CREATE_STATUS_CHILD_ERROR_INVALID_MSG, + CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY + }) + @interface CreateStatus {} + + /** The Child SA negotiation succeeds. */ + private static final int CREATE_STATUS_OK = 0; + /** The inbound message is invalid in Child negotiation but is non-fatal for IKE Session. */ + private static final int CREATE_STATUS_CHILD_ERROR_INVALID_MSG = 1; + /** The inbound message includes error notification that failed the Child negotiation. */ + private static final int CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY = 2; + + private static class CreateChildResult { + @CreateStatus public final int status; + public final SecurityParameterIndex initSpi; + public final SecurityParameterIndex respSpi; + public final ChildSaProposal negotiatedProposal; + public final IkeTrafficSelector[] initTs; + public final IkeTrafficSelector[] respTs; + public final IkeException exception; + + private CreateChildResult( + @CreateStatus int status, + SecurityParameterIndex initSpi, + SecurityParameterIndex respSpi, + ChildSaProposal negotiatedProposal, + IkeTrafficSelector[] initTs, + IkeTrafficSelector[] respTs, + IkeException exception) { + this.status = status; + this.initSpi = initSpi; + this.respSpi = respSpi; + this.negotiatedProposal = negotiatedProposal; + this.initTs = initTs; + this.respTs = respTs; + this.exception = exception; + } + + /* Construct a CreateChildResult instance for a successful case. */ + CreateChildResult( + SecurityParameterIndex initSpi, + SecurityParameterIndex respSpi, + ChildSaProposal negotiatedProposal, + IkeTrafficSelector[] initTs, + IkeTrafficSelector[] respTs) { + this( + CREATE_STATUS_OK, + initSpi, + respSpi, + negotiatedProposal, + initTs, + respTs, + null /*exception*/); + } + + /** Construct a CreateChildResult instance for an error case. */ + CreateChildResult(@CreateStatus int status, IkeException exception) { + this( + status, + null /*initSpi*/, + null /*respSpi*/, + null /*negotiatedProposal*/, + null /*initTs*/, + null /*respTs*/, + exception); + } + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/ChildSessionStateMachineFactory.java b/src/java/com/android/internal/net/ipsec/ike/ChildSessionStateMachineFactory.java new file mode 100644 index 00000000..5282d022 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/ChildSessionStateMachineFactory.java @@ -0,0 +1,95 @@ +/* + * 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 android.content.Context; +import android.net.IpSecManager; +import android.net.ipsec.ike.ChildSessionCallback; +import android.net.ipsec.ike.ChildSessionOptions; +import android.os.Looper; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.ipsec.ike.ChildSessionStateMachine.IChildSessionSmCallback; + +import java.util.concurrent.Executor; + +/** Package private factory for making ChildSessionStateMachine. */ +// TODO: Make it a inner Creator class of ChildSessionStateMachine +final class ChildSessionStateMachineFactory { + + private static IChildSessionFactoryHelper sChildSessionHelper = new ChildSessionFactoryHelper(); + + /** Package private. */ + static ChildSessionStateMachine makeChildSessionStateMachine( + Looper looper, + Context context, + ChildSessionOptions sessionOptions, + Executor userCbExecutor, + ChildSessionCallback userCallbacks, + IChildSessionSmCallback childSmCallback) { + return sChildSessionHelper.makeChildSessionStateMachine( + looper, context, sessionOptions, userCbExecutor, userCallbacks, childSmCallback); + } + + @VisibleForTesting + static void setChildSessionFactoryHelper(IChildSessionFactoryHelper helper) { + sChildSessionHelper = helper; + } + + /** + * IChildSessionFactoryHelper provides a package private interface for constructing + * ChildSessionStateMachine. + * + * <p>IChildSessionFactoryHelper exists so that the interface is injectable for testing. + */ + interface IChildSessionFactoryHelper { + ChildSessionStateMachine makeChildSessionStateMachine( + Looper looper, + Context context, + ChildSessionOptions sessionOptions, + Executor userCbExecutor, + ChildSessionCallback userCallbacks, + IChildSessionSmCallback childSmCallback); + } + + /** + * ChildSessionFactoryHelper implements a method for constructing ChildSessionStateMachine. + * + * <p>Package private. + */ + static class ChildSessionFactoryHelper implements IChildSessionFactoryHelper { + public ChildSessionStateMachine makeChildSessionStateMachine( + Looper looper, + Context context, + ChildSessionOptions sessionOptions, + Executor userCbExecutor, + ChildSessionCallback userCallbacks, + IChildSessionSmCallback childSmCallback) { + ChildSessionStateMachine childSession = + new ChildSessionStateMachine( + looper, + context, + (IpSecManager) context.getSystemService(Context.IPSEC_SERVICE), + sessionOptions, + userCbExecutor, + userCallbacks, + childSmCallback); + childSession.start(); + return childSession; + } + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/IkeDhParams.java b/src/java/com/android/internal/net/ipsec/ike/IkeDhParams.java new file mode 100644 index 00000000..44373fc9 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/IkeDhParams.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 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; + +/** IkeDhParams contains Diffie-Hellman constants for IKEv2 supported DH Groups */ +public class IkeDhParams { + + public static final int BASE_GENERATOR_MODP = 2; + + public static final String PRIME_1024_BIT_MODP = + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381" + + "FFFFFFFFFFFFFFFF"; + public static final String PRIME_2048_BIT_MODP = + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + + "15728E5A8AACAA68FFFFFFFFFFFFFFFF"; +} diff --git a/src/java/com/android/internal/net/ipsec/ike/IkeEapAuthenticatorFactory.java b/src/java/com/android/internal/net/ipsec/ike/IkeEapAuthenticatorFactory.java new file mode 100644 index 00000000..b0d6f127 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/IkeEapAuthenticatorFactory.java @@ -0,0 +1,40 @@ +/* + * 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 android.content.Context; +import android.net.eap.EapSessionConfig; +import android.os.Looper; + +import com.android.internal.net.eap.EapAuthenticator; +import com.android.internal.net.eap.IEapCallback; + +/** Package private factory for building EapAuthenticator instances. */ +final class IkeEapAuthenticatorFactory { + /** + * Builds and returns a new EapAuthenticator + * + * @param looper Looper for running a message loop + * @param cbHandler Handler for posting callbacks to the given IEapCallback + * @param cb IEapCallback for callbacks to the client + * @param context Context for the EapAuthenticator + */ + public EapAuthenticator newEapAuthenticator( + Looper looper, IEapCallback cb, Context context, EapSessionConfig eapSessionConfig) { + return new EapAuthenticator(looper, cb, context, eapSessionConfig); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/IkeLocalRequestScheduler.java b/src/java/com/android/internal/net/ipsec/ike/IkeLocalRequestScheduler.java new file mode 100644 index 00000000..b7bf8701 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/IkeLocalRequestScheduler.java @@ -0,0 +1,119 @@ +/* + * 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 android.net.ipsec.ike.ChildSessionCallback; +import android.net.ipsec.ike.ChildSessionOptions; + +import java.util.LinkedList; + +/** + * IkeLocalRequestScheduler caches all local requests scheduled by an IKE Session and notify the IKE + * Session to process the request when it is allowed. + * + * <p>LocalRequestScheduler is running on the IkeSessionStateMachine thread. + */ +public final class IkeLocalRequestScheduler { + private final LinkedList<LocalRequest> mRequestQueue = new LinkedList<>(); + + private final IProcedureConsumer mConsumer; + + private boolean mLocalProcedureOngoing; + private boolean mRemoteProcedureOngoing; + + /** + * Construct an instance of IkeLocalRequestScheduler + * + * @param consumer the interface to initiate new procedure. + */ + public IkeLocalRequestScheduler(IProcedureConsumer consumer) { + mConsumer = consumer; + } + + /** Add a new local request to the queue. */ + public void addRequest(LocalRequest request) { + mRequestQueue.offer(request); + } + + /** Add a new local request to the front of the queue. */ + public void addRequestAtFront(LocalRequest request) { + mRequestQueue.offerFirst(request); + } + + /** + * Notifies the scheduler that the caller is ready for a new procedure + * + * <p>Synchronously triggers the call to onNewProcedureReady. + */ + public void readyForNextProcedure() { + while (!mRequestQueue.isEmpty()) { + LocalRequest request = mRequestQueue.poll(); + if (!request.isCancelled()) { + mConsumer.onNewProcedureReady(request); + return; + } + } + } + + /** + * This class represents a user requested or internally scheduled IKE procedure that will be + * initiated locally. + */ + public static class LocalRequest { + public final int procedureType; + // TODO: Also store specific payloads for INFO exchange. + private boolean mIsCancelled; + + LocalRequest(int type) { + procedureType = type; + mIsCancelled = false; + } + + boolean isCancelled() { + return mIsCancelled; + } + + void cancel() { + mIsCancelled = true; + } + } + + /** + * This class represents a user requested or internally scheduled Child procedure that will be + * initiated locally. + */ + public static class ChildLocalRequest extends LocalRequest { + public final ChildSessionCallback childSessionCallback; + public final ChildSessionOptions childSessionOptions; + + ChildLocalRequest( + int type, ChildSessionCallback childCallback, ChildSessionOptions childOptions) { + super(type); + childSessionOptions = childOptions; + childSessionCallback = childCallback; + } + } + + /** Interface to initiate a new IKE procedure */ + public interface IProcedureConsumer { + /** + * Called when a new IKE procedure can be initiated. + * + * @param localRequest the request to be initiated. + */ + void onNewProcedureReady(LocalRequest localRequest); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachine.java b/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachine.java new file mode 100644 index 00000000..03c419db --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachine.java @@ -0,0 +1,4235 @@ +/* + * 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; + } + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/IkeSocket.java b/src/java/com/android/internal/net/ipsec/ike/IkeSocket.java new file mode 100644 index 00000000..2f853c7f --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/IkeSocket.java @@ -0,0 +1,281 @@ +/* + * 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.IkeManager.getIkeLog; +import static android.system.OsConstants.F_SETFL; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOCK_NONBLOCK; + +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.os.Handler; +import android.system.ErrnoException; +import android.system.Os; +import android.util.LongSparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.ipsec.ike.message.IkeHeader; +import com.android.internal.net.ipsec.ike.utils.PacketReader; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * IkeSocket sends and receives IKE packets via the user provided {@link UdpEncapsulationSocket}. + * + * <p>One UdpEncapsulationSocket instance can only be bound to one IkeSocket instance. IkeSocket + * maintains a static map to cache all bound UdpEncapsulationSockets and their IkeSocket instances. + * It returns the existing IkeSocket when it has been bound with user provided {@link + * UdpEncapsulationSocket}. + * + * <p>As a packet receiver, IkeSocket registers a file descriptor with a thread's Looper and handles + * read events (and errors). Users can expect a call life-cycle like the following: + * + * <pre> + * [1] when user gets a new initiated IkeSocket, start() is called and followed by createFd(). + * [2] yield, waiting for a read event which will invoke handlePacket() + * [3] when user closes this IkeSocket, its reference count decreases. Then stop() is called when + * there is no reference of this instance. + * </pre> + * + * <p>IkeSocket is constructed and called only on a single IKE working thread by {@link + * IkeSessionStateMachine}. Since all {@link IkeSessionStateMachine}s run on the same working + * thread, there will not be concurrent modification problems. + */ +public final class IkeSocket extends PacketReader implements AutoCloseable { + private static final String TAG = "IkeSocket"; + + // TODO: b/129358324 Consider supporting IKE exchange without UDP Encapsulation. + // UDP-encapsulated IKE packets MUST be sent to 4500. + @VisibleForTesting static final int IKE_SERVER_PORT = 4500; + + // A Non-ESP marker helps the recipient to distinguish IKE packets from ESP packets. + @VisibleForTesting static final int NON_ESP_MARKER_LEN = 4; + @VisibleForTesting static final byte[] NON_ESP_MARKER = new byte[NON_ESP_MARKER_LEN]; + + // Map from UdpEncapsulationSocket to IkeSocket instances. + private static Map<UdpEncapsulationSocket, IkeSocket> sFdToIkeSocketMap = new HashMap<>(); + + private static IPacketReceiver sPacketReceiver = new PacketReceiver(); + + // Package private map from locally generated IKE SPI to IkeSessionStateMachine instances. + @VisibleForTesting + final LongSparseArray<IkeSessionStateMachine> mSpiToIkeSession = new LongSparseArray<>(); + + // Package private set to store all running IKE Sessions that are using this IkeSocket instance. + @VisibleForTesting final Set<IkeSessionStateMachine> mAliveIkeSessions = new HashSet<>(); + + // UdpEncapsulationSocket for sending and receving IKE packet. + private final UdpEncapsulationSocket mUdpEncapSocket; + + private IkeSocket(UdpEncapsulationSocket udpEncapSocket, Handler handler) { + super(handler); + mUdpEncapSocket = udpEncapSocket; + } + + /** + * Get an IkeSocket instance. + * + * <p>Return the existing IkeSocket instance if it has been created for the input + * udpEncapSocket. Otherwise, create and return a new IkeSocket instance. + * + * @param udpEncapSocket user provided UdpEncapsulationSocket + * @param ikeSession the IkeSessionStateMachine that is requesting an IkeSocket. + * @return an IkeSocket instance + */ + public static IkeSocket getIkeSocket( + UdpEncapsulationSocket udpEncapSocket, IkeSessionStateMachine ikeSession) + throws ErrnoException { + FileDescriptor fd = udpEncapSocket.getFileDescriptor(); + // All created IkeSocket has modified its FileDescriptor to non-blocking type for handling + // read events in a non-blocking way. + Os.fcntlInt(fd, F_SETFL, SOCK_DGRAM | SOCK_NONBLOCK); + + IkeSocket ikeSocket = null; + if (sFdToIkeSocketMap.containsKey(udpEncapSocket)) { + ikeSocket = sFdToIkeSocketMap.get(udpEncapSocket); + + } else { + ikeSocket = new IkeSocket(udpEncapSocket, new Handler()); + // Create and register FileDescriptor for receiving IKE packet on current thread. + ikeSocket.start(); + + sFdToIkeSocketMap.put(udpEncapSocket, ikeSocket); + } + + ikeSocket.mAliveIkeSessions.add(ikeSession); + return ikeSocket; + } + + /** + * Get FileDecriptor of mUdpEncapSocket. + * + * <p>PacketReader registers a listener for this file descriptor on the thread where IkeSocket + * is constructed. When there is a read event, this listener is invoked and then calls {@link + * handlePacket} to handle the received packet. + */ + @Override + protected FileDescriptor createFd() { + return mUdpEncapSocket.getFileDescriptor(); + } + + /** + * IPacketReceiver provides a package private interface for handling received packet. + * + * <p>IPacketReceiver exists so that the interface is injectable for testing. + */ + interface IPacketReceiver { + void handlePacket(byte[] recvbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession); + } + + /** Package private */ + @VisibleForTesting + static final class PacketReceiver implements IPacketReceiver { + public void handlePacket( + byte[] recvbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession) { + ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf); + + // Check the existence of the Non-ESP Marker. A received packet can be either an IKE + // packet starts with 4 zero-valued bytes Non-ESP Marker or an ESP packet starts with 4 + // bytes ESP SPI. ESP SPI value can never be zero. + byte[] espMarker = new byte[NON_ESP_MARKER_LEN]; + byteBuffer.get(espMarker); + if (!Arrays.equals(NON_ESP_MARKER, espMarker)) { + // Drop the received ESP packet. + getIkeLog().e(TAG, "Receive an ESP packet."); + return; + } + + try { + // Re-direct IKE packet to IkeSessionStateMachine according to the locally generated + // IKE SPI. + byte[] ikePacketBytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(ikePacketBytes); + + // TODO: Retrieve and log the source address + getIkeLog().d(TAG, "Receive packet of " + ikePacketBytes.length + " bytes)"); + getIkeLog().d(TAG, getIkeLog().pii(ikePacketBytes)); + + IkeHeader ikeHeader = new IkeHeader(ikePacketBytes); + + long localGeneratedSpi = + ikeHeader.fromIkeInitiator + ? ikeHeader.ikeResponderSpi + : ikeHeader.ikeInitiatorSpi; + + IkeSessionStateMachine ikeStateMachine = spiToIkeSession.get(localGeneratedSpi); + if (ikeStateMachine == null) { + getIkeLog().w(TAG, "Unrecognized IKE SPI."); + // TODO: Handle invalid IKE SPI error + } else { + ikeStateMachine.receiveIkePacket(ikeHeader, ikePacketBytes); + } + } catch (IkeProtocolException e) { + // Handle invalid IKE header + getIkeLog().i(TAG, "Can't parse malformed IKE packet header."); + } + } + } + + /** Package private */ + @VisibleForTesting + static void setPacketReceiver(IPacketReceiver receiver) { + sPacketReceiver = receiver; + } + + /** + * Handle received IKE packet. Invoked when there is a read event. Any desired copies of + * |recvbuf| should be made in here, as the underlying byte array is reused across all reads. + */ + @Override + protected void handlePacket(byte[] recvbuf, int length) { + sPacketReceiver.handlePacket(Arrays.copyOfRange(recvbuf, 0, length), mSpiToIkeSession); + } + + /** + * Send encoded IKE packet to destination address + * + * @param ikePacket encoded IKE packet + * @param serverAddress IP address of remote server + */ + public void sendIkePacket(byte[] ikePacket, InetAddress serverAddress) { + getIkeLog() + .d( + TAG, + "Send packet to " + + serverAddress.getHostAddress() + + "( " + + ikePacket.length + + " bytes)"); + try { + ByteBuffer buffer = ByteBuffer.allocate(NON_ESP_MARKER_LEN + ikePacket.length); + + // Build outbound UDP Encapsulation packet body for sending IKE message. + buffer.put(NON_ESP_MARKER).put(ikePacket); + buffer.rewind(); + + // Use unconnected UDP socket because one {@UdpEncapsulationSocket} may be shared by + // multiple IKE sessions that send messages to different destinations. + Os.sendto( + mUdpEncapSocket.getFileDescriptor(), buffer, 0, serverAddress, IKE_SERVER_PORT); + } catch (ErrnoException | IOException e) { + // TODO: Handle exception + } + } + + /** + * Register new created IKE SA + * + * @param spi the locally generated IKE SPI + * @param ikeSession the IKE session this IKE SA belongs to + */ + public void registerIke(long spi, IkeSessionStateMachine ikeSession) { + mSpiToIkeSession.put(spi, ikeSession); + } + + /** + * Unregister a deleted IKE SA + * + * @param spi the locally generated IKE SPI + */ + public void unregisterIke(long spi) { + mSpiToIkeSession.remove(spi); + } + + /** Release reference of current IkeSocket when the IKE session is closed. */ + public void releaseReference(IkeSessionStateMachine ikeSession) { + mAliveIkeSessions.remove(ikeSession); + if (mAliveIkeSessions.isEmpty()) close(); + } + + /** Implement {@link AutoCloseable#close()} */ + @Override + public void close() { + sFdToIkeSocketMap.remove(mUdpEncapSocket); + // PackeReader unregisters file descriptor on thread with which the Handler constructor + // argument is associated. + stop(); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/SaRecord.java b/src/java/com/android/internal/net/ipsec/ike/SaRecord.java new file mode 100644 index 00000000..035af175 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/SaRecord.java @@ -0,0 +1,1098 @@ +/* + * 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.IkeManager.getIkeLog; + +import android.annotation.Nullable; +import android.content.Context; +import android.net.IpSecManager; +import android.net.IpSecManager.ResourceUnavailableException; +import android.net.IpSecManager.SecurityParameterIndex; +import android.net.IpSecManager.SpiUnavailableException; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.IpSecTransform; + +import com.android.internal.annotations.VisibleForTesting; +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.IkeSessionStateMachine.IkeSecurityParameterIndex; +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.message.IkeKePayload; +import com.android.internal.net.ipsec.ike.message.IkeMessage; +import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResultPartial; +import com.android.internal.net.ipsec.ike.message.IkeNoncePayload; +import com.android.internal.net.ipsec.ike.message.IkePayload; + +import dalvik.system.CloseGuard; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.List; + +/** + * SaRecord represents common information of an IKE SA and a Child SA. + * + * <p>When doing rekey, there can be multiple SAs in the same IkeSessionStateMachine or + * ChildSessionStateMachine, where they use same cryptographic algorithms but with different keys. + * We store cryptographic algorithms and unchanged SA configurations in IkeSessionOptions or + * ChildSessionOptions and store changed information including keys, SPIs, and nonces in SaRecord. + * + * <p>All keys are named by the key type plus the source of the traffic this key is protecting. For + * example, "mSkAi" represents the integrity key that protects traffic from the SA initiator to the + * SA responder. + * + * <p>Except for keys, all other paramters (SPIs, nonces and messages) are named by the creator. For + * example, "initSPI" represents a SPI that is created by the SA initiator. + */ +public abstract class SaRecord implements AutoCloseable { + private static ISaRecordHelper sSaRecordHelper = new SaRecordHelper(); + private static IIpSecTransformHelper sIpSecTransformHelper = new IpSecTransformHelper(); + + /** Flag indicates if this SA is locally initiated */ + public final boolean isLocalInit; + + public final byte[] nonceInitiator; + public final byte[] nonceResponder; + + private final byte[] mSkAi; + private final byte[] mSkAr; + private final byte[] mSkEi; + private final byte[] mSkEr; + + private final LocalRequest mFutureRekeyEvent; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + /** Package private */ + SaRecord( + boolean localInit, + byte[] nonceInit, + byte[] nonceResp, + byte[] skAi, + byte[] skAr, + byte[] skEi, + byte[] skEr, + LocalRequest futureRekeyEvent) { + isLocalInit = localInit; + nonceInitiator = nonceInit; + nonceResponder = nonceResp; + + mSkAi = skAi; + mSkAr = skAr; + mSkEi = skEi; + mSkEr = skEr; + + logKey("SK_ai", skAi); + logKey("SK_ar", skAr); + logKey("SK_ei", skEi); + logKey("SK_er", skEr); + + mFutureRekeyEvent = futureRekeyEvent; + + mCloseGuard.open("close"); + } + + private void logKey(String type, byte[] key) { + getIkeLog().d(getTag(), type + ": " + getIkeLog().pii(key)); + } + + protected abstract String getTag(); + + /** + * Get the integrity key for calculate integrity checksum for an outbound packet. + * + * @return the integrity key in a byte array, which will be empty if integrity algorithm is not + * used in this SA. + */ + public byte[] getOutboundIntegrityKey() { + return isLocalInit ? mSkAi : mSkAr; + } + + /** + * Get the integrity key to authenticate an inbound packet. + * + * @return the integrity key in a byte array, which will be empty if integrity algorithm is not + * used in this SA. + */ + public byte[] getInboundIntegrityKey() { + return isLocalInit ? mSkAr : mSkAi; + } + + /** + * Get the encryption key for protecting an outbound packet. + * + * @return the encryption key in a byte array. + */ + public byte[] getOutboundEncryptionKey() { + return isLocalInit ? mSkEi : mSkEr; + } + + /** + * Get the decryption key for an inbound packet. + * + * @return the decryption key in a byte array. + */ + public byte[] getInboundDecryptionKey() { + return isLocalInit ? mSkEr : mSkEi; + } + + /** Check that the SaRecord was closed properly. */ + @Override + protected void finalize() throws Throwable { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } + + @Override + public void close() { + mFutureRekeyEvent.cancel(); + } + + /** Package private */ + LocalRequest getFutureRekeyEvent() { + return mFutureRekeyEvent; + } + + /** Package private */ + @VisibleForTesting + static void setSaRecordHelper(ISaRecordHelper helper) { + sSaRecordHelper = helper; + } + + /** Package private */ + @VisibleForTesting + static void setIpSecTransformHelper(IIpSecTransformHelper helper) { + sIpSecTransformHelper = helper; + } + + /** + * SaRecordHelper implements methods for constructing SaRecord. + * + * <p>Package private + */ + static class SaRecordHelper implements ISaRecordHelper { + @Override + public IkeSaRecord makeFirstIkeSaRecord( + IkeMessage initRequest, + IkeMessage initResponse, + IkeSaRecordConfig ikeSaRecordConfig) + throws GeneralSecurityException { + // Extract nonces + byte[] nonceInit = + initRequest.getPayloadForType( + IkePayload.PAYLOAD_TYPE_NONCE, IkeNoncePayload.class) + .nonceData; + byte[] nonceResp = + initResponse.getPayloadForType( + IkePayload.PAYLOAD_TYPE_NONCE, IkeNoncePayload.class) + .nonceData; + + // Get SKEYSEED + byte[] sharedDhKey = getSharedKey(initRequest, initResponse); + byte[] sKeySeed = + ikeSaRecordConfig.prf.generateSKeySeed(nonceInit, nonceResp, sharedDhKey); + + return makeIkeSaRecord(sKeySeed, nonceInit, nonceResp, ikeSaRecordConfig); + } + + @Override + public IkeSaRecord makeRekeyedIkeSaRecord( + IkeSaRecord oldSaRecord, + IkeMacPrf oldPrf, + IkeMessage rekeyRequest, + IkeMessage rekeyResponse, + IkeSaRecordConfig ikeSaRecordConfig) + throws GeneralSecurityException { + // Extract nonces + byte[] nonceInit = + rekeyRequest.getPayloadForType( + IkePayload.PAYLOAD_TYPE_NONCE, IkeNoncePayload.class) + .nonceData; + byte[] nonceResp = + rekeyResponse.getPayloadForType( + IkePayload.PAYLOAD_TYPE_NONCE, IkeNoncePayload.class) + .nonceData; + + // Get SKEYSEED + IkeMessage localMsg = ikeSaRecordConfig.isLocalInit ? rekeyRequest : rekeyResponse; + IkeMessage remoteMsg = ikeSaRecordConfig.isLocalInit ? rekeyResponse : rekeyRequest; + + byte[] sharedDhKey = getSharedKey(localMsg, remoteMsg); + byte[] sKeySeed = + oldPrf.generateRekeyedSKeySeed( + oldSaRecord.mSkD, nonceInit, nonceResp, sharedDhKey); + + return makeIkeSaRecord(sKeySeed, nonceInit, nonceResp, ikeSaRecordConfig); + } + + private byte[] getSharedKey(IkeMessage keLocalMessage, IkeMessage keRemoteMessage) + throws GeneralSecurityException { + IkeKePayload keLocalPayload = + keLocalMessage.getPayloadForType( + IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class); + IkeKePayload keRemotePayload = + keRemoteMessage.getPayloadForType( + IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class); + + return IkeKePayload.getSharedKey( + keLocalPayload.localPrivateKey, keRemotePayload.keyExchangeData); + } + + /** + * Package private method for calculating keys and construct IkeSaRecord. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.13">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2), Generating Keying Material</a> + */ + @VisibleForTesting + IkeSaRecord makeIkeSaRecord( + byte[] sKeySeed, + byte[] nonceInit, + byte[] nonceResp, + IkeSaRecordConfig ikeSaRecordConfig) { + // Build data to sign for generating the keying material. + ByteBuffer bufferToSign = + ByteBuffer.allocate( + nonceInit.length + nonceResp.length + 2 * IkePayload.SPI_LEN_IKE); + + IkeSecurityParameterIndex initSpi = ikeSaRecordConfig.initSpi; + IkeSecurityParameterIndex respSpi = ikeSaRecordConfig.respSpi; + IkeMacPrf prf = ikeSaRecordConfig.prf; + int integrityKeyLength = ikeSaRecordConfig.integrityKeyLength; + int encryptionKeyLength = ikeSaRecordConfig.encryptionKeyLength; + + bufferToSign + .put(nonceInit) + .put(nonceResp) + .putLong(initSpi.getSpi()) + .putLong(respSpi.getSpi()); + + // Get length of the keying material according to RFC 7296, 2.13 and 2.14. The length of + // SK_D is always equal to the length of PRF key. + int skDLength = prf.getKeyLength(); + int keyMaterialLen = + skDLength + + 2 * integrityKeyLength + + 2 * encryptionKeyLength + + 2 * prf.getKeyLength(); + byte[] keyMat = prf.generateKeyMat(sKeySeed, bufferToSign.array(), keyMaterialLen); + + // Extract keys. + byte[] skD = new byte[skDLength]; + byte[] skAi = new byte[integrityKeyLength]; + byte[] skAr = new byte[integrityKeyLength]; + byte[] skEi = new byte[encryptionKeyLength]; + byte[] skEr = new byte[encryptionKeyLength]; + byte[] skPi = new byte[prf.getKeyLength()]; + byte[] skPr = new byte[prf.getKeyLength()]; + + ByteBuffer keyMatBuffer = ByteBuffer.wrap(keyMat); + keyMatBuffer.get(skD).get(skAi).get(skAr).get(skEi).get(skEr).get(skPi).get(skPr); + return new IkeSaRecord( + initSpi, + respSpi, + ikeSaRecordConfig.isLocalInit, + nonceInit, + nonceResp, + skD, + skAi, + skAr, + skEi, + skEr, + skPi, + skPr, + ikeSaRecordConfig.futureRekeyEvent); + } + + @Override + public ChildSaRecord makeChildSaRecord( + List<IkePayload> reqPayloads, + List<IkePayload> respPayloads, + ChildSaRecordConfig childSaRecordConfig) + throws GeneralSecurityException, ResourceUnavailableException, + SpiUnavailableException, IOException { + // Extract nonces. Encoding/Decoding of payload list guarantees that there is only one + // nonce payload in the reqPayloads and respPayloads lists + byte[] nonceInit = + IkePayload.getPayloadForTypeInProvidedList( + IkePayload.PAYLOAD_TYPE_NONCE, + IkeNoncePayload.class, + reqPayloads) + .nonceData; + byte[] nonceResp = + IkePayload.getPayloadForTypeInProvidedList( + IkePayload.PAYLOAD_TYPE_NONCE, + IkeNoncePayload.class, + respPayloads) + .nonceData; + + // Check if KE Payload exists and get DH shared key. Encoding/Decoding of payload list + // guarantees that there is either no KE payload in the reqPayloads and respPayloads + // lists, or only one KE payload in each list. + byte[] sharedDhKey = new byte[0]; + IkeKePayload keInitPayload = + IkePayload.getPayloadForTypeInProvidedList( + IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class, reqPayloads); + if (keInitPayload != null) { + IkeKePayload keRespPayload = + IkePayload.getPayloadForTypeInProvidedList( + IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class, respPayloads); + sharedDhKey = + IkeKePayload.getSharedKey( + keInitPayload.localPrivateKey, keRespPayload.keyExchangeData); + } + + return makeChildSaRecord(sharedDhKey, nonceInit, nonceResp, childSaRecordConfig); + } + /** + * Package private method for calculating keys, build IpSecTransforms and construct + * ChildSaRecord. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.17">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2), Generating Keying Material for Child SAs</a> + */ + @VisibleForTesting + ChildSaRecord makeChildSaRecord( + byte[] sharedKey, + byte[] nonceInit, + byte[] nonceResp, + ChildSaRecordConfig childSaRecordConfig) + throws ResourceUnavailableException, SpiUnavailableException, IOException { + // Build data to sign for generating the keying material. + ByteBuffer bufferToSign = + ByteBuffer.allocate(sharedKey.length + nonceInit.length + nonceResp.length); + bufferToSign.put(sharedKey).put(nonceInit).put(nonceResp); + + // Get length of the keying material according to RFC 7296, 2.17. + int encryptionKeyLength = childSaRecordConfig.encryptionAlgo.getKeyLength(); + int integrityKeyLength = + childSaRecordConfig.hasIntegrityAlgo + ? childSaRecordConfig.integrityAlgo.getKeyLength() + : 0; + int keyMaterialLen = 2 * encryptionKeyLength + 2 * integrityKeyLength; + byte[] keyMat = + childSaRecordConfig.ikePrf.generateKeyMat( + childSaRecordConfig.skD, bufferToSign.array(), keyMaterialLen); + + // Extract keys according to the order that keys carrying data from initiator to + // responder are taken before keys for the other direction and encryption keys are taken + // before integrity keys. + byte[] skEi = new byte[encryptionKeyLength]; + byte[] skAi = new byte[integrityKeyLength]; + byte[] skEr = new byte[encryptionKeyLength]; + byte[] skAr = new byte[integrityKeyLength]; + + ByteBuffer keyMatBuffer = ByteBuffer.wrap(keyMat); + keyMatBuffer.get(skEi).get(skAi).get(skEr).get(skAr); + + // IpSecTransform for traffic from the initiator + IpSecTransform initTransform = null; + // IpSecTransform for traffic from the responder + IpSecTransform respTransform = null; + try { + // Build IpSecTransform + initTransform = + sIpSecTransformHelper.makeIpSecTransform( + childSaRecordConfig.context, + childSaRecordConfig.initAddress /*source address*/, + childSaRecordConfig.udpEncapSocket, + childSaRecordConfig.respSpi /*destination SPI*/, + childSaRecordConfig.integrityAlgo, + childSaRecordConfig.encryptionAlgo, + skAi, + skEi, + childSaRecordConfig.isTransport); + respTransform = + sIpSecTransformHelper.makeIpSecTransform( + childSaRecordConfig.context, + childSaRecordConfig.respAddress /*source address*/, + childSaRecordConfig.udpEncapSocket, + childSaRecordConfig.initSpi /*destination SPI*/, + childSaRecordConfig.integrityAlgo, + childSaRecordConfig.encryptionAlgo, + skAr, + skEr, + childSaRecordConfig.isTransport); + + int initSpi = childSaRecordConfig.initSpi.getSpi(); + int respSpi = childSaRecordConfig.respSpi.getSpi(); + + boolean isLocalInit = childSaRecordConfig.isLocalInit; + int inSpi = isLocalInit ? initSpi : respSpi; + int outSpi = isLocalInit ? respSpi : initSpi; + IpSecTransform inTransform = isLocalInit ? respTransform : initTransform; + IpSecTransform outTransform = isLocalInit ? initTransform : respTransform; + + return new ChildSaRecord( + inSpi, + outSpi, + isLocalInit, + nonceInit, + nonceResp, + skAi, + skAr, + skEi, + skEr, + inTransform, + outTransform, + childSaRecordConfig.futureRekeyEvent); + + } catch (Exception e) { + if (initTransform != null) initTransform.close(); + if (respTransform != null) respTransform.close(); + throw e; + } + } + } + + /** + * IpSecTransformHelper implements the IIpSecTransformHelper interface for constructing {@link + * IpSecTransform}}. + * + * <p>Package private + */ + static class IpSecTransformHelper implements IIpSecTransformHelper { + private static final String TAG = "IpSecTransformHelper"; + + @Override + public IpSecTransform makeIpSecTransform( + Context context, + InetAddress sourceAddress, + UdpEncapsulationSocket udpEncapSocket, + IpSecManager.SecurityParameterIndex spi, + @Nullable IkeMacIntegrity integrityAlgo, + IkeCipher encryptionAlgo, + byte[] integrityKey, + byte[] encryptionKey, + boolean isTransport) + throws ResourceUnavailableException, SpiUnavailableException, IOException { + IpSecTransform.Builder builder = new IpSecTransform.Builder(context); + + if (encryptionAlgo.isAead()) { + builder.setAuthenticatedEncryption( + encryptionAlgo.buildIpSecAlgorithmWithKey(encryptionKey)); + } else { + builder.setEncryption(encryptionAlgo.buildIpSecAlgorithmWithKey(encryptionKey)); + builder.setAuthentication(integrityAlgo.buildIpSecAlgorithmWithKey(integrityKey)); + } + + if (udpEncapSocket != null && sourceAddress instanceof Inet6Address) { + getIkeLog().wtf(TAG, "Kernel does not support UDP encapsulation for IPv6 SAs"); + } + if (udpEncapSocket != null && sourceAddress instanceof Inet4Address) { + builder.setIpv4Encapsulation(udpEncapSocket, IkeSocket.IKE_SERVER_PORT); + } + + if (isTransport) { + return builder.buildTransportModeTransform(sourceAddress, spi); + } else { + return builder.buildTunnelModeTransform(sourceAddress, spi); + } + } + } + + /** Package private class to group parameters for building a ChildSaRecord. */ + @VisibleForTesting + static final class ChildSaRecordConfig { + public final Context context; + public final SecurityParameterIndex initSpi; + public final SecurityParameterIndex respSpi; + public final InetAddress initAddress; + public final InetAddress respAddress; + @Nullable public final UdpEncapsulationSocket udpEncapSocket; + public final IkeMacPrf ikePrf; + @Nullable public final IkeMacIntegrity integrityAlgo; + public final IkeCipher encryptionAlgo; + public final byte[] skD; + public final boolean isTransport; + public final boolean isLocalInit; + public final boolean hasIntegrityAlgo; + public final ChildLocalRequest futureRekeyEvent; + + ChildSaRecordConfig( + Context context, + SecurityParameterIndex initSpi, + SecurityParameterIndex respSpi, + InetAddress localAddress, + InetAddress remoteAddress, + @Nullable UdpEncapsulationSocket udpEncapSocket, + IkeMacPrf ikePrf, + @Nullable IkeMacIntegrity integrityAlgo, + IkeCipher encryptionAlgo, + byte[] skD, + boolean isTransport, + boolean isLocalInit, + ChildLocalRequest futureRekeyEvent) { + this.context = context; + this.initSpi = initSpi; + this.respSpi = respSpi; + this.initAddress = isLocalInit ? localAddress : remoteAddress; + this.respAddress = isLocalInit ? remoteAddress : localAddress; + this.udpEncapSocket = udpEncapSocket; + this.ikePrf = ikePrf; + this.integrityAlgo = integrityAlgo; + this.encryptionAlgo = encryptionAlgo; + this.skD = skD; + this.isTransport = isTransport; + this.isLocalInit = isLocalInit; + hasIntegrityAlgo = (integrityAlgo != null); + this.futureRekeyEvent = futureRekeyEvent; + } + } + + /** IkeSaRecord represents an IKE SA. */ + public static class IkeSaRecord extends SaRecord implements Comparable<IkeSaRecord> { + private static final String TAG = "IkeSaRecord"; + + /** SPI of IKE SA initiator */ + private final IkeSecurityParameterIndex mInitiatorSpiResource; + /** SPI of IKE SA responder */ + private final IkeSecurityParameterIndex mResponderSpiResource; + + private final byte[] mSkD; + private final byte[] mSkPi; + private final byte[] mSkPr; + + private int mLocalRequestMessageId; + private int mRemoteRequestMessageId; + + private DecodeResultPartial mCollectedReqFragments; + private DecodeResultPartial mCollectedRespFragments; + + private byte[] mLastRecivedReqFirstPacket; + private List<byte[]> mLastSentRespAllPackets; + + /** Package private */ + IkeSaRecord( + IkeSecurityParameterIndex initSpi, + IkeSecurityParameterIndex respSpi, + boolean localInit, + byte[] nonceInit, + byte[] nonceResp, + byte[] skD, + byte[] skAi, + byte[] skAr, + byte[] skEi, + byte[] skEr, + byte[] skPi, + byte[] skPr, + LocalRequest futureRekeyEvent) { + super(localInit, nonceInit, nonceResp, skAi, skAr, skEi, skEr, futureRekeyEvent); + + mInitiatorSpiResource = initSpi; + mResponderSpiResource = respSpi; + + mSkD = skD; + mSkPi = skPi; + mSkPr = skPr; + + mLocalRequestMessageId = 0; + mRemoteRequestMessageId = 0; + + mCollectedReqFragments = null; + mCollectedRespFragments = null; + + logKey("SK_d", skD); + logKey("SK_pi", skPi); + logKey("SK_pr", skPr); + } + + /** + * Package private interface for IkeSessionStateMachien to construct an IkeSaRecord + * instance. + */ + static IkeSaRecord makeFirstIkeSaRecord( + IkeMessage initRequest, + IkeMessage initResponse, + IkeSecurityParameterIndex initSpi, + IkeSecurityParameterIndex respSpi, + IkeMacPrf prf, + int integrityKeyLength, + int encryptionKeyLength, + LocalRequest futureRekeyEvent) + throws GeneralSecurityException { + return sSaRecordHelper.makeFirstIkeSaRecord( + initRequest, + initResponse, + new IkeSaRecordConfig( + initSpi, + respSpi, + prf, + integrityKeyLength, + encryptionKeyLength, + true /*isLocalInit*/, + futureRekeyEvent)); + } + + /** Package private */ + static IkeSaRecord makeRekeyedIkeSaRecord( + IkeSaRecord oldSaRecord, + IkeMacPrf oldPrf, + IkeMessage rekeyRequest, + IkeMessage rekeyResponse, + IkeSecurityParameterIndex initSpi, + IkeSecurityParameterIndex respSpi, + IkeMacPrf prf, + int integrityKeyLength, + int encryptionKeyLength, + boolean isLocalInit, + LocalRequest futureRekeyEvent) + throws GeneralSecurityException { + return sSaRecordHelper.makeRekeyedIkeSaRecord( + oldSaRecord, + oldPrf, + rekeyRequest, + rekeyResponse, + new IkeSaRecordConfig( + initSpi, + respSpi, + prf, + integrityKeyLength, + encryptionKeyLength, + isLocalInit, + futureRekeyEvent)); + } + + private void logKey(String type, byte[] key) { + getIkeLog().d(TAG, type + ": " + getIkeLog().pii(key)); + } + + @Override + protected String getTag() { + return TAG; + } + + /** Package private */ + long getInitiatorSpi() { + return mInitiatorSpiResource.getSpi(); + } + + /** Package private */ + long getResponderSpi() { + return mResponderSpiResource.getSpi(); + } + + /** Package private */ + long getLocalSpi() { + return isLocalInit ? mInitiatorSpiResource.getSpi() : mResponderSpiResource.getSpi(); + } + + /** Package private */ + long getRemoteSpi() { + return isLocalInit ? mResponderSpiResource.getSpi() : mInitiatorSpiResource.getSpi(); + } + + /** Package private */ + byte[] getSkD() { + return mSkD; + } + + /** + * Get the PRF key of IKE initiator for building an outbound Auth Payload. + * + * @return the PRF key in a byte array. + */ + public byte[] getSkPi() { + return mSkPi; + } + + /** + * Get the PRF key of IKE responder for validating an inbound Auth Payload. + * + * @return the PRF key in a byte array. + */ + public byte[] getSkPr() { + return mSkPr; + } + + /** + * Compare with a specific IkeSaRecord + * + * @param record IkeSaRecord to be compared. + * @return a negative integer if input IkeSaRecord contains lowest nonce; a positive integer + * if this IkeSaRecord has lowest nonce; return zero if lowest nonces of two + * IkeSaRecords match. + */ + public int compareTo(IkeSaRecord record) { + // TODO: Implement it b/122924815. + return 1; + } + + /** + * Get current message ID for the local requesting window. + * + * <p>Called for building an outbound request or for validating the message ID of an inbound + * response. + * + * @return the local request message ID. + */ + public int getLocalRequestMessageId() { + return mLocalRequestMessageId; + } + + /** + * Get current message ID for the remote requesting window. + * + * <p>Called for validating the message ID of an inbound request. If the message ID of the + * inbound request is smaller than the current remote message ID by one, it means the + * message is a retransmitted request. + * + * @return the remote request message ID + */ + public int getRemoteRequestMessageId() { + return mRemoteRequestMessageId; + } + + /** + * Increment the local request message ID by one. + * + * <p>It should be called when IKE library has received an authenticated and protected + * response with the correct local request message ID. + */ + public void incrementLocalRequestMessageId() { + mLocalRequestMessageId++; + } + + /** + * Increment the remote request message ID by one. + * + * <p>It should be called when IKE library has received an authenticated and protected + * request with the correct remote request message ID. + */ + public void incrementRemoteRequestMessageId() { + mRemoteRequestMessageId++; + } + + /** Return all collected IKE fragments that have been collected. */ + public DecodeResultPartial getCollectedFragments(boolean isResp) { + return isResp ? mCollectedRespFragments : mCollectedReqFragments; + } + + /** + * Update collected IKE fragments when receiving new IKE fragment. + * + * <p>TODO: b/140264067 Investigate if we need to support reassembling timeout. It is safe + * to do not support it because as an initiator, we will re-transmit the request anyway. As + * a responder, caching these fragments until getting a complete message won't affect + * anything. + */ + public void updateCollectedFragments( + DecodeResultPartial updatedFragments, boolean isResp) { + if (isResp) { + mCollectedRespFragments = updatedFragments; + } else { + mCollectedReqFragments = updatedFragments; + } + } + + /** Reset collected IKE fragemnts */ + public void resetCollectedFragments(boolean isResp) { + updateCollectedFragments(null, isResp); + } + + /** Update first packet of last received request. */ + public void updateLastReceivedReqFirstPacket(byte[] reqPacket) { + mLastRecivedReqFirstPacket = reqPacket; + } + + /** Update all packets of last sent response. */ + public void updateLastSentRespAllPackets(List<byte[]> respPacketList) { + mLastSentRespAllPackets = respPacketList; + } + + /** Returns if received IKE packet is the first packet of a re-transmistted request. */ + public boolean isRetransmittedRequest(byte[] request) { + return Arrays.equals(mLastRecivedReqFirstPacket, request); + } + + /** Get all encoded packets of last sent response. */ + public List<byte[]> getLastSentRespAllPackets() { + return mLastSentRespAllPackets; + } + + /** Release IKE SPI resource. */ + @Override + public void close() { + super.close(); + mInitiatorSpiResource.close(); + mResponderSpiResource.close(); + } + } + + /** Package private class that groups parameters to construct an IkeSaRecord instance. */ + @VisibleForTesting + static class IkeSaRecordConfig { + public final IkeSecurityParameterIndex initSpi; + public final IkeSecurityParameterIndex respSpi; + public final IkeMacPrf prf; + public final int integrityKeyLength; + public final int encryptionKeyLength; + public final boolean isLocalInit; + public final LocalRequest futureRekeyEvent; + + IkeSaRecordConfig( + IkeSecurityParameterIndex initSpi, + IkeSecurityParameterIndex respSpi, + IkeMacPrf prf, + int integrityKeyLength, + int encryptionKeyLength, + boolean isLocalInit, + LocalRequest futureRekeyEvent) { + this.initSpi = initSpi; + this.respSpi = respSpi; + this.prf = prf; + this.integrityKeyLength = integrityKeyLength; + this.encryptionKeyLength = encryptionKeyLength; + this.isLocalInit = isLocalInit; + this.futureRekeyEvent = futureRekeyEvent; + } + } + + /** ChildSaRecord represents an Child SA. */ + public static class ChildSaRecord extends SaRecord implements Comparable<ChildSaRecord> { + private static final String TAG = "ChildSaRecord"; + + /** Locally generated SPI for receiving IPsec Packet. */ + private final int mInboundSpi; + /** Remotely generated SPI for sending IPsec Packet. */ + private final int mOutboundSpi; + + /** IPsec Transform applied to traffic towards the host. */ + private final IpSecTransform mInboundTransform; + /** IPsec Transform applied to traffic from the host. */ + private final IpSecTransform mOutboundTransform; + + /** Package private */ + ChildSaRecord( + int inSpi, + int outSpi, + boolean localInit, + byte[] nonceInit, + byte[] nonceResp, + byte[] skAi, + byte[] skAr, + byte[] skEi, + byte[] skEr, + IpSecTransform inTransform, + IpSecTransform outTransform, + ChildLocalRequest futureRekeyEvent) { + super(localInit, nonceInit, nonceResp, skAi, skAr, skEi, skEr, futureRekeyEvent); + + mInboundSpi = inSpi; + mOutboundSpi = outSpi; + mInboundTransform = inTransform; + mOutboundTransform = outTransform; + } + + /** + * Package private interface for ChildSessionStateMachine to construct a ChildSaRecord + * instance. + */ + static ChildSaRecord makeChildSaRecord( + Context context, + List<IkePayload> reqPayloads, + List<IkePayload> respPayloads, + SecurityParameterIndex initSpi, + SecurityParameterIndex respSpi, + InetAddress localAddress, + InetAddress remoteAddress, + @Nullable UdpEncapsulationSocket udpEncapSocket, + IkeMacPrf prf, + @Nullable IkeMacIntegrity integrityAlgo, + IkeCipher encryptionAlgo, + byte[] skD, + boolean isTransport, + boolean isLocalInit, + ChildLocalRequest futureRekeyEvent) + throws GeneralSecurityException, ResourceUnavailableException, + SpiUnavailableException, IOException { + return sSaRecordHelper.makeChildSaRecord( + reqPayloads, + respPayloads, + new ChildSaRecordConfig( + context, + initSpi, + respSpi, + localAddress, + remoteAddress, + udpEncapSocket, + prf, + integrityAlgo, + encryptionAlgo, + skD, + isTransport, + isLocalInit, + futureRekeyEvent)); + } + + @Override + protected String getTag() { + return TAG; + } + + /** Package private */ + int getLocalSpi() { + return mInboundSpi; + } + + /** Package private */ + int getRemoteSpi() { + return mOutboundSpi; + } + + /** Package private */ + IpSecTransform getInboundIpSecTransform() { + return mInboundTransform; + } + + /** Package private */ + IpSecTransform getOutboundIpSecTransform() { + return mOutboundTransform; + } + + /** + * Compare with a specific ChildSaRecord + * + * @param record ChildSaRecord to be compared. + * @return a negative integer if input ChildSaRecord contains lowest nonce; a positive + * integer if this ChildSaRecord has lowest nonce; return zero if lowest nonces of two + * ChildSaRecord match. + */ + public int compareTo(ChildSaRecord record) { + // TODO: Implement it b/122924815 + return 1; + } + + /** Release IpSecTransform pair. */ + @Override + public void close() { + super.close(); + mInboundTransform.close(); + mOutboundTransform.close(); + } + } + + /** + * ISaRecordHelper provides a package private interface for constructing SaRecord. + * + * <p>ISaRecordHelper exists so that the interface is injectable for testing. + */ + interface ISaRecordHelper { + /** + * Construct IkeSaRecord as results of IKE initial exchange. + * + * @param initRequest IKE_INIT request. + * @param initResponse IKE_INIT request. + * @param ikeSaRecordConfig that contains IKE SPI resources and negotiated algorithm + * information for constructing an IkeSaRecord instance. + * @return ikeSaRecord for initial IKE SA. + * @throws GeneralSecurityException if the DH public key in the response is invalid. + */ + IkeSaRecord makeFirstIkeSaRecord( + IkeMessage initRequest, + IkeMessage initResponse, + IkeSaRecordConfig ikeSaRecordConfig) + throws GeneralSecurityException; + + /** + * Construct new IkeSaRecord when doing rekey. + * + * @param oldSaRecord old IKE SA + * @param oldPrf the PRF function from the old SA + * @param rekeyRequest Rekey IKE request. + * @param rekeyResponse Rekey IKE response. + * @param ikeSaRecordConfig that contains IKE SPI resources and negotiated algorithm + * information for constructing an IkeSaRecord instance. + * @return ikeSaRecord for new IKE SA. + */ + IkeSaRecord makeRekeyedIkeSaRecord( + IkeSaRecord oldSaRecord, + IkeMacPrf oldPrf, + IkeMessage rekeyRequest, + IkeMessage rekeyResponse, + IkeSaRecordConfig ikeSaRecordConfig) + throws GeneralSecurityException; + + /** + * Construct ChildSaRecord and generate IpSecTransform pairs. + * + * @param reqPayloads payload list in request. + * @param respPayloads payload list in response. + * @param childSaRecordConfig the grouped parameters for constructing ChildSaRecord. + * @return new Child SA. + */ + ChildSaRecord makeChildSaRecord( + List<IkePayload> reqPayloads, + List<IkePayload> respPayloads, + ChildSaRecordConfig childSaRecordConfig) + throws GeneralSecurityException, ResourceUnavailableException, + SpiUnavailableException, IOException; + } + + /** + * IIpSecTransformHelper provides a package private interface to construct {@link + * IpSecTransform} + * + * <p>IIpSecTransformHelper exists so that the interface is injectable for testing. + */ + @VisibleForTesting + interface IIpSecTransformHelper { + /** + * Construct an instance of {@link IpSecTransform} + * + * @param context current context + * @param sourceAddress the source {@code InetAddress} of traffic on sockets of interfaces + * that will use this transform + * @param udpEncapSocket the UDP-Encap socket that allows IpSec traffic to pass through a + * NAT. Null if no NAT exists. + * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed + * traffic + * @param integrityAlgo specifying the authentication algorithm to be applied. + * @param encryptionAlgo specifying the encryption algorithm or authenticated encryption + * algorithm to be applied. + * @param integrityKey the negotiated authentication key to be applied. + * @param encryptionKey the negotiated encryption key to be applied. + * @param isTransport the flag indicates if a transport or a tunnel mode transform will be + * built. + * @return an instance of {@link IpSecTransform} + * @throws ResourceUnavailableException indicating that too many transforms are active + * @throws SpiUnavailableException indicating the rare case where an SPI collides with an + * existing transform + * @throws IOException indicating other errors + */ + IpSecTransform makeIpSecTransform( + Context context, + InetAddress sourceAddress, + UdpEncapsulationSocket udpEncapSocket, + IpSecManager.SecurityParameterIndex spi, + @Nullable IkeMacIntegrity integrityAlgo, + IkeCipher encryptionAlgo, + byte[] integrityKey, + byte[] encryptionKey, + boolean isTransport) + throws ResourceUnavailableException, SpiUnavailableException, IOException; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/crypto/IkeCipher.java b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeCipher.java new file mode 100644 index 00000000..33a8b37c --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeCipher.java @@ -0,0 +1,178 @@ +/* + * 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.crypto; + +import android.net.IpSecAlgorithm; +import android.net.ipsec.ike.SaProposal; + +import com.android.internal.net.ipsec.ike.message.IkeSaPayload.EncryptionTransform; + +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; + +/** + * IkeCipher contains common information of normal and combined mode encryption algorithms. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public abstract class IkeCipher extends IkeCrypto { + private static final int KEY_LEN_3DES = 24; + + private static final int IV_LEN_3DES = 8; + private static final int IV_LEN_AES_CBC = 16; + private static final int IV_LEN_AES_GCM = 8; + + private final boolean mIsAead; + private final int mIvLen; + + protected final Cipher mCipher; + + protected IkeCipher( + int algorithmId, + int keyLength, + int ivLength, + String algorithmName, + boolean isAead, + Provider provider) { + super(algorithmId, keyLength, algorithmName); + mIvLen = ivLength; + mIsAead = isAead; + + try { + mCipher = Cipher.getInstance(getAlgorithmName(), provider); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("Failed to construct " + getTypeString(), e); + } + } + + /** + * Contruct an instance of IkeCipher. + * + * @param encryptionTransform the valid negotiated EncryptionTransform. + * @param provider the security provider. + * @return an instance of IkeCipher. + */ + public static IkeCipher create(EncryptionTransform encryptionTransform, Provider provider) { + int algorithmId = encryptionTransform.id; + + // Use specifiedKeyLength for algorithms with variable key length. Since + // specifiedKeyLength are encoded in bits, it needs to be converted to bytes. + switch (algorithmId) { + case SaProposal.ENCRYPTION_ALGORITHM_3DES: + return new IkeNormalModeCipher( + algorithmId, KEY_LEN_3DES, IV_LEN_3DES, "DESede/CBC/NoPadding", provider); + case SaProposal.ENCRYPTION_ALGORITHM_AES_CBC: + return new IkeNormalModeCipher( + algorithmId, + encryptionTransform.getSpecifiedKeyLength() / 8, + IV_LEN_AES_CBC, + "AES/CBC/NoPadding", + provider); + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8: + // Fall through + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12: + // Fall through + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16: + // Fall through + return new IkeCombinedModeCipher( + algorithmId, + encryptionTransform.getSpecifiedKeyLength() / 8, + IV_LEN_AES_GCM, + "AES/GCM/NoPadding", + provider); + default: + throw new IllegalArgumentException( + "Unrecognized Encryption Algorithm ID: " + algorithmId); + } + } + + /** + * Check if this encryption algorithm is a combined-mode/AEAD algorithm. + * + * @return if this encryption algorithm is a combined-mode/AEAD algorithm. + */ + public boolean isAead() { + return mIsAead; + } + + /** + * Get the block size (in bytes). + * + * @return the block size (in bytes). + */ + public int getBlockSize() { + // Currently all supported encryption algorithms are block ciphers. So the return value will + // not be zero. + return mCipher.getBlockSize(); + } + + /** + * Get initialization vector (IV) length. + * + * @return the IV length. + */ + public int getIvLen() { + return mIvLen; + } + + /** + * Generate initialization vector (IV). + * + * @return the initialization vector (IV). + */ + public byte[] generateIv() { + byte[] iv = new byte[getIvLen()]; + new SecureRandom().nextBytes(iv); + return iv; + } + + protected void validateKeyLenOrThrow(byte[] key) { + if (key.length != getKeyLength()) { + throw new IllegalArgumentException( + "Expected key with length of : " + + getKeyLength() + + " Received key with length of : " + + key.length); + } + } + + /** + * Build IpSecAlgorithm from this IkeCipher. + * + * <p>Build IpSecAlgorithm that represents the same encryption algorithm with this IkeCipher + * instance with provided encryption key. + * + * @param key the encryption key in byte array. + * @return the IpSecAlgorithm. + */ + public abstract IpSecAlgorithm buildIpSecAlgorithmWithKey(byte[] key); + + /** + * Returns algorithm type as a String. + * + * @return the algorithm type as a String. + */ + @Override + public String getTypeString() { + return "Encryption Algorithm"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/crypto/IkeCombinedModeCipher.java b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeCombinedModeCipher.java new file mode 100644 index 00000000..4bb1d34b --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeCombinedModeCipher.java @@ -0,0 +1,209 @@ +/* + * 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.crypto; + +import android.net.IpSecAlgorithm; +import android.net.ipsec.ike.SaProposal; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Provider; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +import javax.crypto.AEADBadTagException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * IkeCipher represents a negotiated combined-mode cipher(AEAD) encryption algorithm. + * + * <p>Checksum mentioned in this class is also known as authentication tag or Integrity Checksum + * Vector(ICV) + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + * @see <a href="https://tools.ietf.org/html/rfc5282">RFC 5282,Using Authenticated Encryption + * Algorithms with the Encrypted Payload of the Internet Key Exchange version 2 (IKEv2) + * Protocol</a> + */ +public final class IkeCombinedModeCipher extends IkeCipher { + private static final int SALT_LEN_GCM = 4; + + private final int mChecksumLen; + private final int mSaltLen; + + /** Package private */ + IkeCombinedModeCipher( + int algorithmId, int keyLength, int ivLength, String algorithmName, Provider provider) { + super(algorithmId, keyLength, ivLength, algorithmName, true /*isAead*/, provider); + switch (algorithmId) { + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8: + mSaltLen = SALT_LEN_GCM; + mChecksumLen = 8; + break; + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12: + mSaltLen = SALT_LEN_GCM; + mChecksumLen = 12; + break; + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16: + mSaltLen = SALT_LEN_GCM; + mChecksumLen = 16; + break; + default: + throw new IllegalArgumentException( + "Unrecognized Encryption Algorithm ID: " + algorithmId); + } + } + + private byte[] doCipherAction( + byte[] data, byte[] additionalAuthData, byte[] keyBytes, byte[] ivBytes, int opmode) + throws AEADBadTagException { + try { + // Provided key consists of encryption/decryption key plus 4-byte salt. Salt is used + // with IV to build the nonce. + ByteBuffer secretKeyAndSaltBuffer = ByteBuffer.wrap(keyBytes); + byte[] secretKeyBytes = new byte[keyBytes.length - mSaltLen]; + byte[] salt = new byte[mSaltLen]; + secretKeyAndSaltBuffer.get(secretKeyBytes); + secretKeyAndSaltBuffer.get(salt); + + SecretKeySpec key = new SecretKeySpec(secretKeyBytes, getAlgorithmName()); + + ByteBuffer nonceBuffer = ByteBuffer.allocate(mSaltLen + ivBytes.length); + nonceBuffer.put(salt); + nonceBuffer.put(ivBytes); + + mCipher.init(opmode, key, getParamSpec(nonceBuffer.array())); + mCipher.updateAAD(additionalAuthData); + + ByteBuffer inputBuffer = ByteBuffer.wrap(data); + + int outputLen = data.length; + if (opmode == Cipher.ENCRYPT_MODE) outputLen += mChecksumLen; + ByteBuffer outputBuffer = ByteBuffer.allocate(outputLen); + + mCipher.doFinal(inputBuffer, outputBuffer); + return outputBuffer.array(); + } catch (AEADBadTagException e) { + // Checksum failed in decryption + throw (AEADBadTagException) e; + } catch (InvalidKeyException + | InvalidAlgorithmParameterException + | IllegalBlockSizeException + | BadPaddingException + | ShortBufferException e) { + String errorMessage = + Cipher.ENCRYPT_MODE == opmode + ? "Failed to encrypt data: " + : "Failed to decrypt data: "; + throw new IllegalArgumentException(errorMessage, e); + } + } + + private AlgorithmParameterSpec getParamSpec(byte[] nonce) { + switch (getAlgorithmId()) { + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8: + // Fall through + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12: + // Fall through + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16: + return new GCMParameterSpec(mChecksumLen * 8, nonce); + default: + throw new IllegalArgumentException( + "Unrecognized Encryption Algorithm ID: " + getAlgorithmId()); + } + } + + /** + * Encrypt padded data and calculate checksum for it. + * + * @param paddedData the padded data to encrypt. + * @param additionalAuthData additional data to authenticate (also known as associated data). + * @param keyBytes the encryption key. + * @param ivBytes the initialization vector (IV). + * @return the encrypted and padded data with checksum. + */ + public byte[] encrypt( + byte[] paddedData, byte[] additionalAuthData, byte[] keyBytes, byte[] ivBytes) { + try { + return doCipherAction( + paddedData, additionalAuthData, keyBytes, ivBytes, Cipher.ENCRYPT_MODE); + } catch (AEADBadTagException e) { + throw new IllegalArgumentException("Failed to encrypt data: ", e); + } + } + + /** + * Authenticate and decrypt the padded data with checksum. + * + * @param paddedDataWithChecksum the padded data with checksum. + * @param additionalAuthData additional data to authenticate (also known as associated data). + * @param keyBytes the decryption key. + * @param ivBytes the initialization vector (IV). + * @return the decrypted and padded data + * @throws AEADBadTagException if authentication or decryption fails + */ + public byte[] decrypt( + byte[] paddedDataWithChecksum, + byte[] additionalAuthData, + byte[] keyBytes, + byte[] ivBytes) + throws AEADBadTagException { + + byte[] decryptPaddedDataAndAuthTag = + doCipherAction( + paddedDataWithChecksum, + additionalAuthData, + keyBytes, + ivBytes, + Cipher.DECRYPT_MODE); + + int decryptPaddedDataLen = decryptPaddedDataAndAuthTag.length - mChecksumLen; + return Arrays.copyOf(decryptPaddedDataAndAuthTag, decryptPaddedDataLen); + } + + /** + * Gets key length of this algorithm (in bytes). + * + * @return the key length (in bytes). + */ + @Override + public int getKeyLength() { + return super.getKeyLength() + mSaltLen; + } + + /** + * Returns length of checksum. + * + * @return the length of checksum in bytes. + */ + public int getChecksumLen() { + return mChecksumLen; + } + + @Override + public IpSecAlgorithm buildIpSecAlgorithmWithKey(byte[] key) { + validateKeyLenOrThrow(key); + return new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, key, mChecksumLen * 8); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/crypto/IkeCrypto.java b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeCrypto.java new file mode 100644 index 00000000..65a676b5 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeCrypto.java @@ -0,0 +1,57 @@ +/* + * 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.crypto; + +/** + * IkeCrypto is an abstract class that represents common information for all negotiated + * cryptographic algorithms that are used to build IKE SA and protect IKE message. + */ +abstract class IkeCrypto { + private final int mAlgorithmId; + private final int mKeyLength; + private final String mAlgorithmName; + + protected IkeCrypto(int algorithmId, int keyLength, String algorithmName) { + mAlgorithmId = algorithmId; + mKeyLength = keyLength; + mAlgorithmName = algorithmName; + } + + protected int getAlgorithmId() { + return mAlgorithmId; + } + + protected String getAlgorithmName() { + return mAlgorithmName; + } + + /** + * Gets key length of this algorithm (in bytes). + * + * @return the key length (in bytes). + */ + public int getKeyLength() { + return mKeyLength; + } + + /** + * Returns algorithm type as a String. + * + * @return the algorithm type as a String. + */ + public abstract String getTypeString(); +} diff --git a/src/java/com/android/internal/net/ipsec/ike/crypto/IkeMac.java b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeMac.java new file mode 100644 index 00000000..ee45cc9e --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeMac.java @@ -0,0 +1,93 @@ +/* + * 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.crypto; + +import com.android.internal.net.crypto.KeyGenerationUtils.ByteSigner; + +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +/** + * IkeMac is an abstract class that represents common information for all negotiated algorithms that + * generates Message Authentication Code (MAC), e.g. PRF and integrity algorithm. + */ +abstract class IkeMac extends IkeCrypto implements ByteSigner { + // STOPSHIP: b/130190639 Catch unchecked exceptions, notify users and close the IKE session. + private final boolean mIsEncryptAlgo; + private final Mac mMac; + private final Cipher mCipher; + + protected IkeMac( + int algorithmId, + int keyLength, + String algorithmName, + boolean isEncryptAlgo, + Provider provider) { + super(algorithmId, keyLength, algorithmName); + + mIsEncryptAlgo = isEncryptAlgo; + + try { + if (mIsEncryptAlgo) { + mMac = null; + mCipher = Cipher.getInstance(getAlgorithmName(), provider); + } else { + mMac = Mac.getInstance(getAlgorithmName(), provider); + mCipher = null; + } + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("Failed to construct " + getTypeString(), e); + } + } + + /** + * Signs the bytes to generate a Message Authentication Code (MAC). + * + * <p>Caller is responsible for providing valid key according to their use cases (e.g. PSK, + * SK_p, SK_d ...). + * + * @param keyBytes the key to sign data. + * @param dataToSign the data to be signed. + * @return the calculated MAC. + */ + @Override + public byte[] signBytes(byte[] keyBytes, byte[] dataToSign) { + try { + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, getAlgorithmName()); + + if (mIsEncryptAlgo) { + throw new UnsupportedOperationException( + "Do not support " + getTypeString() + " using encryption algorithm."); + } else { + ByteBuffer inputBuffer = ByteBuffer.wrap(dataToSign); + mMac.init(secretKey); + mMac.update(inputBuffer); + + return mMac.doFinal(); + } + } catch (InvalidKeyException | IllegalStateException e) { + throw new IllegalArgumentException("Failed to generate MAC: ", e); + } + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/crypto/IkeMacIntegrity.java b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeMacIntegrity.java new file mode 100644 index 00000000..8f2173f6 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeMacIntegrity.java @@ -0,0 +1,189 @@ +/* + * 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.crypto; + +import android.net.IpSecAlgorithm; +import android.net.ipsec.ike.SaProposal; + +import com.android.internal.net.ipsec.ike.message.IkeSaPayload.IntegrityTransform; + +import java.security.Provider; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.Mac; + +/** + * IkeMacIntegrity represents a negotiated integrity algorithm. + * + * <p>For integrity algorithms based on encryption algorithm, all operations will be done by a + * {@link Cipher}. Otherwise, all operations will be done by a {@link Mac}. + * + * <p>@see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2)</a> + */ +public class IkeMacIntegrity extends IkeMac { + // STOPSHIP: b/130190639 Catch unchecked exceptions, notify users and close the IKE session. + private final int mChecksumLength; + + private IkeMacIntegrity( + @SaProposal.IntegrityAlgorithm int algorithmId, + int keyLength, + String algorithmName, + boolean isEncryptAlgo, + Provider provider, + int checksumLength) { + super(algorithmId, keyLength, algorithmName, isEncryptAlgo, provider); + mChecksumLength = checksumLength; + } + + /** + * Construct an instance of IkeMacIntegrity. + * + * @param integrityTransform the valid negotiated IntegrityTransform. + * @param provider the security provider. + * @return an instance of IkeMacIntegrity. + */ + public static IkeMacIntegrity create(IntegrityTransform integrityTransform, Provider provider) { + int algorithmId = integrityTransform.id; + + int keyLength = 0; + String algorithmName = ""; + boolean isEncryptAlgo = false; + int checksumLength = 0; + + switch (algorithmId) { + case SaProposal.INTEGRITY_ALGORITHM_NONE: + throw new IllegalArgumentException("Integrity algorithm is not found."); + case SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96: + keyLength = 20; + algorithmName = "HmacSHA1"; + checksumLength = 12; + break; + case SaProposal.INTEGRITY_ALGORITHM_AES_XCBC_96: + keyLength = 16; + isEncryptAlgo = true; + checksumLength = 12; + + // TODO:Set mAlgorithmName + throw new UnsupportedOperationException( + "Do not support INTEGRITY_ALGORITHM_AES_XCBC_96."); + case SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128: + keyLength = 32; + algorithmName = "HmacSHA256"; + checksumLength = 16; + break; + case SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_384_192: + keyLength = 48; + algorithmName = "HmacSHA384"; + checksumLength = 24; + break; + case SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_512_256: + keyLength = 64; + algorithmName = "HmacSHA512"; + checksumLength = 32; + break; + default: + throw new IllegalArgumentException( + "Unrecognized Integrity Algorithm ID: " + algorithmId); + } + + return new IkeMacIntegrity( + algorithmId, keyLength, algorithmName, isEncryptAlgo, provider, checksumLength); + } + + /** + * Gets integrity checksum length (in bytes). + * + * <p>IKE defines a fixed truncation length for each integirty algorithm as its checksum length. + * + * @return the integrity checksum length (in bytes). + */ + public int getChecksumLen() { + return mChecksumLength; + } + + /** + * Signs the bytes to generate an integrity checksum. + * + * @param keyBytes the negotiated integrity key. + * @param dataToAuthenticate the data to authenticate. + * @return the integrity checksum. + */ + public byte[] generateChecksum(byte[] keyBytes, byte[] dataToAuthenticate) { + if (getKeyLength() != keyBytes.length) { + throw new IllegalArgumentException( + "Expected key length: " + + getKeyLength() + + " Received key length: " + + keyBytes.length); + } + + byte[] signedBytes = signBytes(keyBytes, dataToAuthenticate); + return Arrays.copyOfRange(signedBytes, 0, mChecksumLength); + } + + /** + * Build IpSecAlgorithm from this IkeMacIntegrity. + * + * <p>Build IpSecAlgorithm that represents the same integrity algorithm with this + * IkeMacIntegrity instance with provided integrity key. + * + * @param key the integrity key in byte array. + * @return the IpSecAlgorithm. + */ + public IpSecAlgorithm buildIpSecAlgorithmWithKey(byte[] key) { + if (key.length != getKeyLength()) { + throw new IllegalArgumentException( + "Expected key with length of : " + + getKeyLength() + + " Received key with length of : " + + key.length); + } + + switch (getAlgorithmId()) { + case SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96: + return new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, key, mChecksumLength * 8); + case SaProposal.INTEGRITY_ALGORITHM_AES_XCBC_96: + // TODO:Consider supporting AES128_XCBC in IpSecTransform. + throw new IllegalArgumentException( + "Do not support IpSecAlgorithm with AES128_XCBC."); + case SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128: + return new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA256, key, mChecksumLength * 8); + case SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_384_192: + return new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA384, key, mChecksumLength * 8); + case SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_512_256: + return new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA512, key, mChecksumLength * 8); + default: + throw new IllegalArgumentException( + "Unrecognized Integrity Algorithm ID: " + getAlgorithmId()); + } + } + + /** + * Returns algorithm type as a String. + * + * @return the algorithm type as a String. + */ + @Override + public String getTypeString() { + return "Integrity Algorithm."; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/crypto/IkeMacPrf.java b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeMacPrf.java new file mode 100644 index 00000000..1d81aaed --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeMacPrf.java @@ -0,0 +1,152 @@ +/* + * 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.crypto; + +import android.net.ipsec.ike.SaProposal; + +import com.android.internal.net.crypto.KeyGenerationUtils; +import com.android.internal.net.ipsec.ike.message.IkeSaPayload.PrfTransform; + +import java.nio.ByteBuffer; +import java.security.Provider; + +import javax.crypto.Cipher; +import javax.crypto.Mac; + +/** + * IkeMacPrf represents a negotiated pseudorandom function. + * + * <p>Pseudorandom function is usually used for IKE SA authentication and generating keying + * materials. + * + * <p>For pseudorandom functions based on integrity algorithms, all operations will be done by a + * {@link Mac}. For pseudorandom functions based on encryption algorithms, all operations will be + * done by a {@link Cipher}. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public class IkeMacPrf extends IkeMac { + // STOPSHIP: b/130190639 Catch unchecked exceptions, notify users and close the IKE session. + + private IkeMacPrf( + @SaProposal.PseudorandomFunction int algorithmId, + int keyLength, + String algorithmName, + boolean isEncryptAlgo, + Provider provider) { + super(algorithmId, keyLength, algorithmName, isEncryptAlgo, provider); + } + + /** + * Construct an instance of IkeMacPrf. + * + * @param prfTransform the valid negotiated PrfTransform. + * @param provider the security provider. + * @return an instance of IkeMacPrf. + */ + public static IkeMacPrf create(PrfTransform prfTransform, Provider provider) { + int algorithmId = prfTransform.id; + + int keyLength = 0; + String algorithmName = ""; + boolean isEncryptAlgo = false; + + switch (algorithmId) { + case SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1: + keyLength = 20; + algorithmName = "HmacSHA1"; + break; + case SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC: + keyLength = 16; + isEncryptAlgo = true; + + // TODO:Set mAlgorithmName + throw new UnsupportedOperationException( + "Do not support PSEUDORANDOM_FUNCTION_AES128_XCBC."); + default: + throw new IllegalArgumentException("Unrecognized PRF ID: " + algorithmId); + } + + return new IkeMacPrf(algorithmId, keyLength, algorithmName, isEncryptAlgo, provider); + } + + /** + * Generates SKEYSEED based on the nonces and shared DH secret. + * + * @param nonceInit the IKE initiator nonce. + * @param nonceResp the IKE responder nonce. + * @param sharedDhKey the DH shared key. + * @return the byte array of SKEYSEED. + */ + public byte[] generateSKeySeed(byte[] nonceInit, byte[] nonceResp, byte[] sharedDhKey) { + // TODO: If it is PSEUDORANDOM_FUNCTION_AES128_XCBC, only use first 8 bytes of each nonce. + + ByteBuffer keyBuffer = ByteBuffer.allocate(nonceInit.length + nonceResp.length); + keyBuffer.put(nonceInit).put(nonceResp); + + return signBytes(keyBuffer.array(), sharedDhKey); + } + + /** + * Generates a rekey SKEYSEED based on the nonces and shared DH secret. + * + * @param skD the secret for deriving new keys + * @param nonceInit the IKE initiator nonce. + * @param nonceResp the IKE responder nonce. + * @param sharedDhKey the DH shared key. + * @return the byte array of SKEYSEED. + */ + public byte[] generateRekeyedSKeySeed( + byte[] skD, byte[] nonceInit, byte[] nonceResp, byte[] sharedDhKey) { + // TODO: If it is PSEUDORANDOM_FUNCTION_AES128_XCBC, only use first 8 bytes of each nonce. + + ByteBuffer dataToSign = + ByteBuffer.allocate(sharedDhKey.length + nonceInit.length + nonceResp.length); + dataToSign.put(sharedDhKey).put(nonceInit).put(nonceResp); + + return signBytes(skD, dataToSign.array()); + } + + /** + * Derives keying materials from IKE/Child SA negotiation. + * + * <p>prf+(K, S) outputs a pseudorandom stream by using negotiated PRF iteratively. In this way + * it can generate long enough keying material containing all the keys for this IKE/Child SA. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.13">RFC 7296 Internet Key + * Exchange Protocol Version 2 (IKEv2) 2.13. Generating Keying Material </a> + * @param keyBytes the key to sign data. SKEYSEED is used for generating KEYMAT for IKE SA. SK_d + * is used for generating KEYMAT for Child SA. + * @param dataToSign the data to be signed. + * @param keyMaterialLen the length of keying materials. + * @return the byte array of keying materials + */ + public byte[] generateKeyMat(byte[] keyBytes, byte[] dataToSign, int keyMaterialLen) { + return KeyGenerationUtils.prfPlus(this, keyBytes, dataToSign, keyMaterialLen); + } + + /** + * Returns algorithm type as a String. + * + * @return the algorithm type as a String. + */ + @Override + public String getTypeString() { + return "Pseudorandom Function"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/crypto/IkeNormalModeCipher.java b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeNormalModeCipher.java new file mode 100644 index 00000000..e4904d4f --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/crypto/IkeNormalModeCipher.java @@ -0,0 +1,124 @@ +/* + * 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.crypto; + +import android.net.IpSecAlgorithm; +import android.net.ipsec.ike.SaProposal; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Provider; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * IkeCipher represents a negotiated normal mode encryption algorithm. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeNormalModeCipher extends IkeCipher { + /** Package private */ + IkeNormalModeCipher( + int algorithmId, int keyLength, int ivLength, String algorithmName, Provider provider) { + super(algorithmId, keyLength, ivLength, algorithmName, false /*isAead*/, provider); + } + + private byte[] doCipherAction(byte[] data, byte[] keyBytes, byte[] ivBytes, int opmode) + throws IllegalBlockSizeException { + if (getKeyLength() != keyBytes.length) { + throw new IllegalArgumentException( + "Expected key length: " + + getKeyLength() + + " Received key length: " + + keyBytes.length); + } + try { + SecretKeySpec key = new SecretKeySpec(keyBytes, getAlgorithmName()); + IvParameterSpec iv = new IvParameterSpec(ivBytes); + mCipher.init(opmode, key, iv); + + ByteBuffer inputBuffer = ByteBuffer.wrap(data); + ByteBuffer outputBuffer = ByteBuffer.allocate(data.length); + + mCipher.doFinal(inputBuffer, outputBuffer); + return outputBuffer.array(); + } catch (InvalidKeyException + | InvalidAlgorithmParameterException + | BadPaddingException + | ShortBufferException e) { + String errorMessage = + Cipher.ENCRYPT_MODE == opmode + ? "Failed to encrypt data: " + : "Failed to decrypt data: "; + throw new IllegalArgumentException(errorMessage, e); + } + } + + /** + * Encrypt padded data. + * + * @param paddedData the padded data to encrypt. + * @param keyBytes the encryption key. + * @param ivBytes the initialization vector (IV). + * @return the encrypted and padded data. + */ + public byte[] encrypt(byte[] paddedData, byte[] keyBytes, byte[] ivBytes) { + try { + return doCipherAction(paddedData, keyBytes, ivBytes, Cipher.ENCRYPT_MODE); + } catch (IllegalBlockSizeException e) { + throw new IllegalArgumentException("Failed to encrypt data: ", e); + } + } + + /** + * Decrypt the encrypted and padded data. + * + * @param encryptedData the encrypted and padded data. + * @param keyBytes the decryption key. + * @param ivBytes the initialization vector (IV). + * @return the decrypted and padded data. + * @throws IllegalBlockSizeException if the total encryptedData length is not a multiple of + * block size. + */ + public byte[] decrypt(byte[] encryptedData, byte[] keyBytes, byte[] ivBytes) + throws IllegalBlockSizeException { + return doCipherAction(encryptedData, keyBytes, ivBytes, Cipher.DECRYPT_MODE); + } + + @Override + public IpSecAlgorithm buildIpSecAlgorithmWithKey(byte[] key) { + validateKeyLenOrThrow(key); + + switch (getAlgorithmId()) { + case SaProposal.ENCRYPTION_ALGORITHM_3DES: + // TODO: Consider supporting 3DES in IpSecTransform. + throw new UnsupportedOperationException("Do not support 3Des encryption."); + case SaProposal.ENCRYPTION_ALGORITHM_AES_CBC: + return new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, key); + default: + throw new IllegalArgumentException( + "Unrecognized Encryption Algorithm ID: " + getAlgorithmId()); + } + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/AuthenticationFailedException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/AuthenticationFailedException.java new file mode 100644 index 00000000..e5873648 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/AuthenticationFailedException.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 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.exceptions; + +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +/** + * This exception is thrown when IKE authentication fails. + * + * <p>Contains an exception message detailing the failure cause. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.21.2">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class AuthenticationFailedException extends IkeProtocolException { + private static final int EXPECTED_ERROR_DATA_LEN = 0; + + /** + * Construct a instance of AuthenticationFailedException. + * + * @param message the detail message. + */ + public AuthenticationFailedException(String message) { + super(ERROR_TYPE_AUTHENTICATION_FAILED, message); + } + + /** + * Construct a instance of AuthenticationFailedExcepion. + * + * @param cause the cause. + */ + public AuthenticationFailedException(Throwable cause) { + super(ERROR_TYPE_AUTHENTICATION_FAILED, cause); + } + + /** + * Construct a instance of AuthenticationFailedExcepion from a notify payload. + * + * @param notifyData the notify data included in the payload. + */ + public AuthenticationFailedException(byte[] notifyData) { + super(ERROR_TYPE_AUTHENTICATION_FAILED, notifyData); + } + + @Override + protected boolean isValidDataLength(int dataLen) { + return EXPECTED_ERROR_DATA_LEN == dataLen; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidKeException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidKeException.java new file mode 100644 index 00000000..ae2330c7 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidKeException.java @@ -0,0 +1,66 @@ +/* + * 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.exceptions; + +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_KE_PAYLOAD; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +/** + * This exception is thrown when the received KE payload in the request is different from accepted + * Diffie-Hellman group. + * + * <p>Responder should include an INVALID_KE_PAYLOAD Notify payload in a response message for both + * IKE INI exchange and other SA negotiation exchanges after IKE is setup.. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-1.3">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class InvalidKeException extends IkeProtocolException { + private static final int EXPECTED_ERROR_DATA_LEN = 2; + + /** + * Construct an instance of InvalidKeException + * + * @param dhGroup the expected DH group + */ + public InvalidKeException(int dhGroup) { + super(ERROR_TYPE_INVALID_KE_PAYLOAD, integerToByteArray(dhGroup, EXPECTED_ERROR_DATA_LEN)); + } + + /** + * Construct a instance of InvalidKeException from a notify payload. + * + * @param notifyData the notify data included in the payload. + */ + public InvalidKeException(byte[] notifyData) { + super(ERROR_TYPE_INVALID_KE_PAYLOAD, notifyData); + } + + /** + * Return the expected DH Group included in this exception. + * + * @return the expected DH Group. + */ + public int getDhGroup() { + return byteArrayToInteger(getErrorData()); + } + + @Override + protected boolean isValidDataLength(int dataLen) { + return EXPECTED_ERROR_DATA_LEN == dataLen; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidMajorVersionException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidMajorVersionException.java new file mode 100644 index 00000000..dc0357ee --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidMajorVersionException.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 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.exceptions; + +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_MAJOR_VERSION; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +/** + * This exception is thrown when major version is higher than 2. + * + * <p>Include INVALID_MAJOR_VERSION Notify payload in an unencrypted response message containing + * version number 2. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.5">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class InvalidMajorVersionException extends IkeProtocolException { + private static final int EXPECTED_ERROR_DATA_LEN = 1; + + /** + * Construct a instance of InvalidMajorVersionException + * + * @param version the major version in received packet + */ + public InvalidMajorVersionException(byte version) { + super(ERROR_TYPE_INVALID_MAJOR_VERSION, new byte[] {version}); + } + + /** + * Construct a instance of InvalidMajorVersionException from a notify payload. + * + * @param notifyData the notify data included in the payload. + */ + public InvalidMajorVersionException(byte[] notifyData) { + super(ERROR_TYPE_INVALID_MAJOR_VERSION, notifyData); + } + + /** + * Return the major verion included in this exception. + * + * @return the major verion + */ + public int getMajorVerion() { + return byteArrayToInteger(getErrorData()); + } + + @Override + protected boolean isValidDataLength(int dataLen) { + return EXPECTED_ERROR_DATA_LEN == dataLen; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidMessageIdException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidMessageIdException.java new file mode 100644 index 00000000..9c5cffa3 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidMessageIdException.java @@ -0,0 +1,68 @@ +/* + * 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.exceptions; + +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_MESSAGE_ID; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +/** + * This exception is thrown when the message ID is out of window size. + * + * <p>Notifications based on this exception contains the four-octet invalid message ID. It MUST only + * ever be sent in an INFORMATIONAL request. Sending this notification is OPTIONAL, and + * notifications of this type MUST be rate limited. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.3">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class InvalidMessageIdException extends IkeProtocolException { + private static final int EXPECTED_ERROR_DATA_LEN = 4; + + /** + * Construct a instance of InvalidMessageIdException + * + * @param messageId the invalid Message ID. + */ + public InvalidMessageIdException(int messageId) { + super( + ERROR_TYPE_INVALID_MESSAGE_ID, + integerToByteArray(messageId, EXPECTED_ERROR_DATA_LEN)); + } + + /** + * Construct a instance of InvalidMessageIdException from a notify payload. + * + * @param notifyData the notify data included in the payload. + */ + public InvalidMessageIdException(byte[] notifyData) { + super(ERROR_TYPE_INVALID_MESSAGE_ID, notifyData); + } + + /** + * Return the invalid message ID included in this exception. + * + * @return the message ID. + */ + public int getMessageId() { + return byteArrayToInteger(getErrorData()); + } + + @Override + protected boolean isValidDataLength(int dataLen) { + return EXPECTED_ERROR_DATA_LEN == dataLen; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidSyntaxException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidSyntaxException.java new file mode 100644 index 00000000..fd73f2f1 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidSyntaxException.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 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.exceptions; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +/** + * This exception is thrown if any IKE message field is invalid. + * + * <p>Include INVALID_SYNTAX Notify payload in an encrypted response message if current message is + * an encrypted request and cryptographic checksum is valid. Fatal error. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.10.1">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class InvalidSyntaxException extends IkeProtocolException { + private static final int EXPECTED_ERROR_DATA_LEN = 0; + + /** + * Construct an instance of InvalidSyntaxException. + * + * @param message the descriptive message. + */ + public InvalidSyntaxException(String message) { + super(ERROR_TYPE_INVALID_SYNTAX, message); + } + + /** + * Construct a instance of InvalidSyntaxException. + * + * @param cause the reason of exception. + */ + public InvalidSyntaxException(Throwable cause) { + super(ERROR_TYPE_INVALID_SYNTAX, cause); + } + + /** + * Construct a instance of InvalidSyntaxException. + * + * @param message the descriptive message. + * @param cause the reason of exception. + */ + public InvalidSyntaxException(String message, Throwable cause) { + super(ERROR_TYPE_INVALID_SYNTAX, message, cause); + } + + /** + * Construct a instance of InvalidSyntaxException from a notify payload. + * + * @param notifyData the notify data included in the payload. + */ + public InvalidSyntaxException(byte[] notifyData) { + super(ERROR_TYPE_INVALID_SYNTAX, notifyData); + } + + @Override + protected boolean isValidDataLength(int dataLen) { + return EXPECTED_ERROR_DATA_LEN == dataLen; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/NoValidProposalChosenException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/NoValidProposalChosenException.java new file mode 100644 index 00000000..4514c65a --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/NoValidProposalChosenException.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 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.exceptions; + +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_NO_PROPOSAL_CHOSEN; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +/** + * This exception is thrown if either none of SA proposals from SA initiator is acceptable or the + * negotiated SA proposal from SA responder is invalid. + * + * <p>Include the NO_PROPOSAL_CHOSEN Notify payload in an encrypted response message if received + * message is an encrypted request from SA initiator. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.7">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class NoValidProposalChosenException extends IkeProtocolException { + private static final int EXPECTED_ERROR_DATA_LEN = 0; + + /** + * Construct an instance of NoValidProposalChosenException. + * + * @param message the descriptive message. + */ + public NoValidProposalChosenException(String message) { + super(ERROR_TYPE_NO_PROPOSAL_CHOSEN, message); + } + + /** + * Construct an instance of NoValidProposalChosenException. + * + * @param message the descriptive message. + * @param cause the reason of exception. + */ + public NoValidProposalChosenException(String message, Throwable cause) { + super(ERROR_TYPE_NO_PROPOSAL_CHOSEN, cause); + } + + /** + * Construct a instance of NoValidProposalChosenException from a notify payload. + * + * @param notifyData the notify data included in the payload. + */ + public NoValidProposalChosenException(byte[] notifyData) { + super(ERROR_TYPE_NO_PROPOSAL_CHOSEN, notifyData); + } + + @Override + protected boolean isValidDataLength(int dataLen) { + return EXPECTED_ERROR_DATA_LEN == dataLen; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/TemporaryFailureException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/TemporaryFailureException.java new file mode 100644 index 00000000..57ef8cda --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/TemporaryFailureException.java @@ -0,0 +1,54 @@ +/* + * 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.exceptions; + +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +/** + * This exception is thrown when local node or remote peer receives a request that cannot be + * completed due to a temporary condition such as a rekeying operation. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.7">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class TemporaryFailureException extends IkeProtocolException { + private static final int EXPECTED_ERROR_DATA_LEN = 0; + + /** + * Construct an instance of TemporaryFailureException. + * + * @param message the descriptive message. + */ + public TemporaryFailureException(String message) { + super(ERROR_TYPE_TEMPORARY_FAILURE, message); + } + + /** + * Construct a instance of TemporaryFailureException from a notify payload. + * + * @param notifyData the notify data included in the payload. + */ + public TemporaryFailureException(byte[] notifyData) { + super(ERROR_TYPE_TEMPORARY_FAILURE, notifyData); + } + + @Override + protected boolean isValidDataLength(int dataLen) { + return EXPECTED_ERROR_DATA_LEN == dataLen; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/TsUnacceptableException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/TsUnacceptableException.java new file mode 100644 index 00000000..ef1152a0 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/TsUnacceptableException.java @@ -0,0 +1,50 @@ +/* + * 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.exceptions; + +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_TS_UNACCEPTABLE; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +/** + * This exception is thrown if the remote sever proposed unacceptable TS. + * + * <p>If remote server is the exchange initiator, IKE library should respond with a TS_UNACCEPTABLE + * Notify message. If the remote server is the exchange responder, IKE library should initiate a + * Delete IKE exchange and close the IKE Session. + */ +public final class TsUnacceptableException extends IkeProtocolException { + private static final int EXPECTED_ERROR_DATA_LEN = 0; + + /** Construct an instance of TsUnacceptableException. */ + public TsUnacceptableException() { + super(ERROR_TYPE_TS_UNACCEPTABLE); + } + + /** + * Construct a instance of TsUnacceptableException from a notify payload. + * + * @param notifyData the notify data included in the payload. + */ + public TsUnacceptableException(byte[] notifyData) { + super(ERROR_TYPE_TS_UNACCEPTABLE, notifyData); + } + + @Override + protected boolean isValidDataLength(int dataLen) { + return EXPECTED_ERROR_DATA_LEN == dataLen; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/UnrecognizedIkeProtocolException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/UnrecognizedIkeProtocolException.java new file mode 100644 index 00000000..3d1d5087 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/UnrecognizedIkeProtocolException.java @@ -0,0 +1,41 @@ +/* + * 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.exceptions; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +/** + * This exception represents an unrecognized error notification in a received response. + * + * <p>When receiving an unrecognized error notification in a response, IKE Session MUST assume that + * the corresponding request has failed entirely. If it is in a request, IKE Session MUST ignore it. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.10.1">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class UnrecognizedIkeProtocolException extends IkeProtocolException { + /** Constructs an instance of UnrecognizedIkeProtocolException */ + public UnrecognizedIkeProtocolException(int errorType, byte[] notifyData) { + super(errorType, notifyData); + } + + @Override + protected boolean isValidDataLength(int dataLen) { + // Unrecognized error does not have an expected error data length. Any non-negative length + // is valid + return dataLen >= 0; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/exceptions/UnsupportedCriticalPayloadException.java b/src/java/com/android/internal/net/ipsec/ike/exceptions/UnsupportedCriticalPayloadException.java new file mode 100644 index 00000000..ab1f75e4 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/exceptions/UnsupportedCriticalPayloadException.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018 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.exceptions; + +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_UNSUPPORTED_CRITICAL_PAYLOAD; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import java.util.ArrayList; +import java.util.List; + +/** + * This exception is thrown when payload type is not supported and critical bit is set + * + * <p>Include UNSUPPORTED_CRITICAL_PAYLOAD Notify payloads in a response message. Each payload + * contains only one payload type. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.5">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class UnsupportedCriticalPayloadException extends IkeProtocolException { + private static final int EXPECTED_ERROR_DATA_LEN = 1; + + public final List<Integer> payloadTypeList; + + /** + * Construct an instance of UnsupportedCriticalPayloadException. + * + * <p>To keep IkeProtocolException simpler, we only pass the first payload type to the + * superclass which can be retrieved by users. + * + * @param payloadList the list of all unsupported critical payload types. + */ + public UnsupportedCriticalPayloadException(List<Integer> payloadList) { + super( + ERROR_TYPE_UNSUPPORTED_CRITICAL_PAYLOAD, + integerToByteArray(payloadList.get(0), EXPECTED_ERROR_DATA_LEN)); + payloadTypeList = payloadList; + } + + /** + * Construct a instance of UnsupportedCriticalPayloadException from a notify payload. + * + * @param notifyData the notify data included in the payload. + */ + public UnsupportedCriticalPayloadException(byte[] notifyData) { + super(ERROR_TYPE_UNSUPPORTED_CRITICAL_PAYLOAD, notifyData); + payloadTypeList = new ArrayList<>(1); + payloadTypeList.add(byteArrayToInteger(notifyData)); + } + + /** + * Return the all the unsupported critical payloads included in this exception. + * + * @return the unsupported critical payload list. + */ + public List<Integer> getUnsupportedCriticalPayloadList() { + return payloadTypeList; + } + + @Override + protected boolean isValidDataLength(int dataLen) { + return EXPECTED_ERROR_DATA_LEN == dataLen; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeAuthDigitalSignPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeAuthDigitalSignPayload.java new file mode 100644 index 00000000..a4803af4 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeAuthDigitalSignPayload.java @@ -0,0 +1,265 @@ +/* + * 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.message; + +import android.annotation.StringDef; +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.annotations.VisibleForTesting; +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.message.IkeAuthPayload.AuthMethod; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +/** + * IkeAuthDigitalSignPayload represents Authentication Payload using a specific or generic digital + * signature authentication method. + * + * <p>If AUTH_METHOD_RSA_DIGITAL_SIGN is used, then the hash algorithm is SHA1. If + * AUTH_METHOD_GENERIC_DIGITAL_SIGN is used, the signature algorihtm and hash algorithm are + * extracted from authentication data. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.8">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + * @see <a href="https://tools.ietf.org/html/rfc7427">RFC 7427, Signature Authentication in the + * Internet Key Exchange Version 2 (IKEv2)</a> + */ +public class IkeAuthDigitalSignPayload extends IkeAuthPayload { + private static final String KEY_ALGO_NAME = "RSA"; + + // Byte arrays of DER encoded identifier ASN.1 objects that indicates the algorithm used to + // generate the signature, extracted from + // <a href="https://tools.ietf.org/html/rfc7427#appendix-A"> RFC 7427. There is no need to + // understand the encoding process. They are just constants to indicate the algorithm type. + private static final byte[] PKI_ALGO_ID_DER_BYTES_RSA_SHA1 = { + (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09, + (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0x86, + (byte) 0xf7, (byte) 0x0d, (byte) 0x01, (byte) 0x01, + (byte) 0x05, (byte) 0x05, (byte) 0x00 + }; + private static final byte[] PKI_ALGO_ID_DER_BYTES_RSA_SHA2_256 = { + (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09, + (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0x86, + (byte) 0xf7, (byte) 0x0d, (byte) 0x01, (byte) 0x01, + (byte) 0x0b, (byte) 0x05, (byte) 0x00 + }; + private static final byte[] PKI_ALGO_ID_DER_BYTES_RSA_SHA2_384 = { + (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09, + (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0x86, + (byte) 0xf7, (byte) 0x0d, (byte) 0x01, (byte) 0x01, + (byte) 0x0c, (byte) 0x05, (byte) 0x00 + }; + private static final byte[] PKI_ALGO_ID_DER_BYTES_RSA_SHA2_512 = { + (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09, + (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0x86, + (byte) 0xf7, (byte) 0x0d, (byte) 0x01, (byte) 0x01, + (byte) 0x0d, (byte) 0x05, (byte) 0x00 + }; + + // Length of ASN.1 object length field. + private static final int SIGNATURE_ALGO_ASN1_LEN_LEN = 1; + + // Currently we only support RSA for signature algorithm. + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + SIGNATURE_ALGO_RSA_SHA1, + SIGNATURE_ALGO_RSA_SHA2_256, + SIGNATURE_ALGO_RSA_SHA2_384, + SIGNATURE_ALGO_RSA_SHA2_512 + }) + @VisibleForTesting + @interface SignatureAlgo {} + + @VisibleForTesting static final String SIGNATURE_ALGO_RSA_SHA1 = "SHA1withRSA"; + @VisibleForTesting static final String SIGNATURE_ALGO_RSA_SHA2_256 = "SHA256withRSA"; + @VisibleForTesting static final String SIGNATURE_ALGO_RSA_SHA2_384 = "SHA384withRSA"; + @VisibleForTesting static final String SIGNATURE_ALGO_RSA_SHA2_512 = "SHA512withRSA"; + + public final String signatureAlgoAndHash; + public final byte[] signature; + + protected IkeAuthDigitalSignPayload( + boolean critical, @AuthMethod int authMethod, byte[] authData) + throws IkeProtocolException { + super(critical, authMethod); + switch (authMethod) { + case AUTH_METHOD_RSA_DIGITAL_SIGN: + signatureAlgoAndHash = SIGNATURE_ALGO_RSA_SHA1; + signature = authData; + break; + case AUTH_METHOD_GENERIC_DIGITAL_SIGN: + ByteBuffer inputBuffer = ByteBuffer.wrap(authData); + + // Get signature algorithm. + int signAlgoLen = Byte.toUnsignedInt(inputBuffer.get()); + byte[] signAlgoBytes = new byte[signAlgoLen]; + inputBuffer.get(signAlgoBytes); + signatureAlgoAndHash = bytesToJavaStandardSignAlgoName(signAlgoBytes); + + // Get signature. + signature = new byte[authData.length - SIGNATURE_ALGO_ASN1_LEN_LEN - signAlgoLen]; + inputBuffer.get(signature); + break; + default: + throw new IllegalArgumentException("Unrecognized authentication method."); + } + } + + /** + * Construct IkeAuthDigitalSignPayload for an outbound IKE packet. + * + * <p>Since IKE library is always a client, outbound IkeAuthDigitalSignPayload always signs IKE + * initiator's SignedOctets, which is concatenation of the IKE_INIT request message, the Nonce + * of IKE responder and the signed ID-Initiator payload body. + * + * <p>Caller MUST validate that the signatureAlgoName is supported by IKE library. + * + * @param signatureAlgoName the name of the algorithm requested. See the Signature section in + * the <a href= "{@docRoot}/../technotes/guides/security/StandardNames.html#Signature"> Java + * Cryptography Architecture Standard Algorithm Name Documentation</a> for information about + * standard algorithm names. + * @param privateKey the private key of the identity whose signature is going to be generated. + * @param ikeInitBytes IKE_INIT request for calculating IKE initiator's SignedOctets. + * @param nonce nonce of IKE responder for calculating IKE initiator's SignedOctets. + * @param idPayloadBodyBytes ID-Initiator payload body for calculating IKE initiator's + * SignedOctets. + * @param ikePrf the negotiated PRF. + * @param prfKeyBytes the negotiated PRF initiator key. + */ + public IkeAuthDigitalSignPayload( + String signatureAlgoName, + PrivateKey privateKey, + byte[] ikeInitBytes, + byte[] nonce, + byte[] idPayloadBodyBytes, + IkeMacPrf ikePrf, + byte[] prfKeyBytes) { + super(false, IkeAuthPayload.AUTH_METHOD_GENERIC_DIGITAL_SIGN); + byte[] dataToSignBytes = + getSignedOctets(ikeInitBytes, nonce, idPayloadBodyBytes, ikePrf, prfKeyBytes); + + try { + Signature signGen = + Signature.getInstance(signatureAlgoName, IkeMessage.getSecurityProvider()); + signGen.initSign(privateKey); + signGen.update(dataToSignBytes); + + signature = signGen.sign(); + signatureAlgoAndHash = signatureAlgoName; + } catch (SignatureException | InvalidKeyException e) { + throw new IllegalArgumentException("Signature generation failed", e); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Security Provider does not support " + + KEY_ALGO_NAME + + " or " + + signatureAlgoName); + } + } + + private String bytesToJavaStandardSignAlgoName(byte[] signAlgoBytes) + throws AuthenticationFailedException { + if (Arrays.equals(PKI_ALGO_ID_DER_BYTES_RSA_SHA1, signAlgoBytes)) { + return SIGNATURE_ALGO_RSA_SHA1; + } else if (Arrays.equals(PKI_ALGO_ID_DER_BYTES_RSA_SHA2_256, signAlgoBytes)) { + return SIGNATURE_ALGO_RSA_SHA2_256; + } else if (Arrays.equals(PKI_ALGO_ID_DER_BYTES_RSA_SHA2_384, signAlgoBytes)) { + return SIGNATURE_ALGO_RSA_SHA2_384; + } else if (Arrays.equals(PKI_ALGO_ID_DER_BYTES_RSA_SHA2_512, signAlgoBytes)) { + return SIGNATURE_ALGO_RSA_SHA2_512; + } else { + throw new AuthenticationFailedException( + "Unrecognized ASN.1 objects for Signature algorithm and Hash"); + } + } + + /** + * Verify received signature in an inbound IKE packet. + * + * <p>Since IKE library is always a client, inbound IkeAuthDigitalSignPayload always signs IKE + * responder's SignedOctets, which is concatenation of the IKE_INIT response message, the Nonce + * of IKE initiator and the signed ID-Responder payload body. + * + * @param certificate received end certificate to verify the signature. + * @param ikeInitBytes IKE_INIT response for calculating IKE responder's SignedOctets. + * @param nonce nonce of IKE initiator for calculating IKE responder's SignedOctets. + * @param idPayloadBodyBytes ID-Responder payload body for calculating IKE responder's + * SignedOctets. + * @param ikePrf the negotiated PRF. + * @param prfKeyBytes the negotiated PRF responder key. + * @throws AuthenticationFailedException if received signature verification failed. + */ + public void verifyInboundSignature( + X509Certificate certificate, + byte[] ikeInitBytes, + byte[] nonce, + byte[] idPayloadBodyBytes, + IkeMacPrf ikePrf, + byte[] prfKeyBytes) + throws AuthenticationFailedException { + byte[] dataToSignBytes = + getSignedOctets(ikeInitBytes, nonce, idPayloadBodyBytes, ikePrf, prfKeyBytes); + + try { + Signature signValidator = + Signature.getInstance(signatureAlgoAndHash, IkeMessage.getSecurityProvider()); + signValidator.initVerify(certificate); + signValidator.update(dataToSignBytes); + + if (!signValidator.verify(signature)) { + throw new AuthenticationFailedException("Signature verification failed."); + } + } catch (SignatureException | InvalidKeyException e) { + throw new AuthenticationFailedException(e); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Security Provider does not support " + signatureAlgoAndHash); + } + } + + // TODO: Add methods for generating signature. + + @Override + protected void encodeAuthDataToByteBuffer(ByteBuffer byteBuffer) { + // TODO: Implement it. + throw new UnsupportedOperationException( + "It is not supported to encode a " + getTypeString()); + } + + @Override + protected int getAuthDataLength() { + // TODO: Implement it. + throw new UnsupportedOperationException( + "It is not supported to get payload length of " + getTypeString()); + } + + @Override + public String getTypeString() { + return "Auth(Digital Sign)"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeAuthPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeAuthPayload.java new file mode 100644 index 00000000..53a3f65f --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeAuthPayload.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2018 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.message; + +import android.annotation.IntDef; +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.net.ipsec.ike.crypto.IkeMacPrf; +import com.android.internal.net.ipsec.ike.exceptions.AuthenticationFailedException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; + +/** + * IkeAuthPayload is an abstract class that represents the common information for all Authentication + * Payload with different authentication methods. + * + * <p>Authentication Payload using different authentication method should implement its own + * subclasses with its own logic for signing data, decoding auth data and verifying signature. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.8">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public abstract class IkeAuthPayload extends IkePayload { + // Length of header of Authentication Payload in octets + private static final int AUTH_HEADER_LEN = 4; + // Length of reserved field in octets + private static final int AUTH_RESERVED_FIELD_LEN = 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AUTH_METHOD_RSA_DIGITAL_SIGN, + AUTH_METHOD_PRE_SHARED_KEY, + AUTH_METHOD_GENERIC_DIGITAL_SIGN + }) + public @interface AuthMethod {} + + // RSA signature-based authentication. Use SHA-1 for hash algorithm. + public static final int AUTH_METHOD_RSA_DIGITAL_SIGN = 1; + // PSK-based authentication. + public static final int AUTH_METHOD_PRE_SHARED_KEY = 2; + // Generic method for all types of signature-based authentication. + public static final int AUTH_METHOD_GENERIC_DIGITAL_SIGN = 14; + + @AuthMethod public final int authMethod; + + protected IkeAuthPayload(boolean critical, int authMethod) { + super(PAYLOAD_TYPE_AUTH, critical); + this.authMethod = authMethod; + } + + protected static IkeAuthPayload getIkeAuthPayload(boolean critical, byte[] payloadBody) + throws IkeProtocolException { + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + + int authMethod = Byte.toUnsignedInt(inputBuffer.get()); + // Skip reserved field + byte[] reservedField = new byte[AUTH_RESERVED_FIELD_LEN]; + inputBuffer.get(reservedField); + + byte[] authData = new byte[payloadBody.length - AUTH_HEADER_LEN]; + inputBuffer.get(authData); + switch (authMethod) { + case AUTH_METHOD_PRE_SHARED_KEY: + return new IkeAuthPskPayload(critical, authData); + case AUTH_METHOD_RSA_DIGITAL_SIGN: + return new IkeAuthDigitalSignPayload( + critical, AUTH_METHOD_RSA_DIGITAL_SIGN, authData); + case AUTH_METHOD_GENERIC_DIGITAL_SIGN: + return new IkeAuthDigitalSignPayload( + critical, AUTH_METHOD_GENERIC_DIGITAL_SIGN, authData); + default: + throw new AuthenticationFailedException("Unsupported authentication method"); + } + } + + // When not using EAP, the peers are authenticated by having each sign a block of data named as + // SignedOctets. IKE initiator's SignedOctets are the concatenation of the IKE_INIT request + // message, the Nonce of IKE responder and the signed ID-Initiator payload body. Similarly, IKE + // responder's SignedOctets are the concatenation of the IKE_INIT response message, the Nonce of + // IKE initiator and the signed ID-Responder payload body. + protected static byte[] getSignedOctets( + byte[] ikeInitBytes, + byte[] nonce, + byte[] idPayloadBodyBytes, + IkeMacPrf ikePrf, + byte[] prfKeyBytes) { + byte[] signedidPayloadBodyBytes = ikePrf.signBytes(prfKeyBytes, idPayloadBodyBytes); + + ByteBuffer buffer = + ByteBuffer.allocate( + ikeInitBytes.length + nonce.length + signedidPayloadBodyBytes.length); + buffer.put(ikeInitBytes).put(nonce).put(signedidPayloadBodyBytes); + + return buffer.array(); + } + + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + byteBuffer.put((byte) authMethod).put(new byte[AUTH_RESERVED_FIELD_LEN]); + encodeAuthDataToByteBuffer(byteBuffer); + } + + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + AUTH_HEADER_LEN + getAuthDataLength(); + } + + protected abstract void encodeAuthDataToByteBuffer(ByteBuffer byteBuffer); + + protected abstract int getAuthDataLength(); +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeAuthPskPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeAuthPskPayload.java new file mode 100644 index 00000000..93bef170 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeAuthPskPayload.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2018 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.message; + +import com.android.internal.net.ipsec.ike.crypto.IkeMacPrf; +import com.android.internal.net.ipsec.ike.exceptions.AuthenticationFailedException; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * IkeAuthPskPayload represents an Authentication Payload using Pre-Shared Key to do authentication. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.8">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeAuthPskPayload extends IkeAuthPayload { + // Hex of ASCII characters "Key Pad for IKEv2" for calculating PSK signature. + private static final byte[] IKE_KEY_PAD_STRING_ASCII_HEX_BYTES = { + (byte) 0x4b, (byte) 0x65, (byte) 0x79, (byte) 0x20, + (byte) 0x50, (byte) 0x61, (byte) 0x64, (byte) 0x20, + (byte) 0x66, (byte) 0x6f, (byte) 0x72, (byte) 0x20, + (byte) 0x49, (byte) 0x4b, (byte) 0x45, (byte) 0x76, + (byte) 0x32 + }; + + public final byte[] signature; + + /** + * Construct IkeAuthPskPayload for received IKE packet in the context of {@link + * IkePayloadFactory}. + */ + protected IkeAuthPskPayload(boolean critical, byte[] authData) { + super(critical, IkeAuthPayload.AUTH_METHOD_PRE_SHARED_KEY); + signature = authData; + } + + /** + * Construct IkeAuthPskPayload for an outbound IKE packet. + * + * <p>Since IKE library is always a client, outbound IkeAuthPskPayload always signs IKE + * initiator's SignedOctets, which is concatenation of the IKE_INIT request message, the Nonce + * of IKE responder and the signed ID-Initiator payload body. + * + * @param psk locally stored pre-shared key + * @param ikeInitBytes IKE_INIT request for calculating IKE initiator's SignedOctets. + * @param nonce nonce of IKE responder for calculating IKE initiator's SignedOctets. + * @param idPayloadBodyBytes ID-Initiator payload body for calculating IKE initiator's + * SignedOctets. + * @param ikePrf the negotiated PRF. + * @param prfKeyBytes the negotiated PRF key. + */ + public IkeAuthPskPayload( + byte[] psk, + byte[] ikeInitBytes, + byte[] nonce, + byte[] idPayloadBodyBytes, + IkeMacPrf ikePrf, + byte[] prfKeyBytes) { + super(false, IkeAuthPayload.AUTH_METHOD_PRE_SHARED_KEY); + signature = + calculatePskSignature( + psk, ikeInitBytes, nonce, idPayloadBodyBytes, ikePrf, prfKeyBytes); + } + + private static byte[] calculatePskSignature( + byte[] psk, + byte[] ikeInitBytes, + byte[] nonce, + byte[] idPayloadBodyBytes, + IkeMacPrf ikePrf, + byte[] prfKeyBytes) { + byte[] signingKeyBytes = ikePrf.signBytes(psk, IKE_KEY_PAD_STRING_ASCII_HEX_BYTES); + byte[] dataToSignBytes = + getSignedOctets(ikeInitBytes, nonce, idPayloadBodyBytes, ikePrf, prfKeyBytes); + + return ikePrf.signBytes(signingKeyBytes, dataToSignBytes); + } + + /** + * Verify received signature in inbound IKE packet. + * + * <p>Since IKE library is always a client, inbound IkeAuthPskPayload always signs IKE + * responder's SignedOctets, which is concatenation of the IKE_INIT response message, the Nonce + * of IKE initiator and the signed ID-Responder payload body. + * + * @param psk locally stored pre-shared key + * @param ikeInitBytes IKE_INIT response for calculating IKE responder's SignedOctets. + * @param nonce nonce of IKE initiator for calculating IKE responder's SignedOctets. + * @param idPayloadBodyBytes ID-Responder payload body for calculating IKE responder's + * SignedOctets. + * @param ikePrf the negotiated PRF. + * @param prfKeyBytes the negotiated PRF key. + * @throws AuthenticationFailedException if received signature is not equal to calculated + * signature. + */ + public void verifyInboundSignature( + byte[] psk, + byte[] ikeInitBytes, + byte[] nonce, + byte[] idPayloadBodyBytes, + IkeMacPrf ikePrf, + byte[] prfKeyBytes) + throws AuthenticationFailedException { + byte[] calculatedSignature = + calculatePskSignature( + psk, ikeInitBytes, nonce, idPayloadBodyBytes, ikePrf, prfKeyBytes); + if (!Arrays.equals(signature, calculatedSignature)) { + throw new AuthenticationFailedException("Signature verification failed."); + } + } + + @Override + protected void encodeAuthDataToByteBuffer(ByteBuffer byteBuffer) { + byteBuffer.put(signature); + } + + @Override + protected int getAuthDataLength() { + return signature.length; + } + + @Override + public String getTypeString() { + return "Auth(PSK)"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeCertPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeCertPayload.java new file mode 100644 index 00000000..9227dda9 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeCertPayload.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 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.message; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.net.ipsec.ike.exceptions.AuthenticationFailedException; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.ProviderException; +import java.security.cert.CertificateException; +import java.security.cert.TrustAnchor; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * IkeCertPayload is an abstract class that represents the common information for all Certificate + * Payload carrying different types of certifciate-related data and static methods related to + * certificate validation. + * + * <p>Certificate Payload is only sent in IKE_AUTH exchange. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.6">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public abstract class IkeCertPayload extends IkePayload { + // Length of certificate encoding type field in octets. + private static final int CERT_ENCODING_LEN = 1; + + private static final String KEY_STORE_TYPE_PKCS12 = "PKCS12"; + private static final String CERT_PATH_ALGO_PKIX = "PKIX"; + private static final String CERT_AUTH_TYPE_RSA = "RSA"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + CERTIFICATE_ENCODING_X509_CERT_SIGNATURE, + CERTIFICATE_ENCODING_CRL, + CERTIFICATE_ENCODING_X509_CERT_HASH_URL, + }) + public @interface CertificateEncoding {} + + public static final int CERTIFICATE_ENCODING_X509_CERT_SIGNATURE = 4; + public static final int CERTIFICATE_ENCODING_CRL = 7; + public static final int CERTIFICATE_ENCODING_X509_CERT_HASH_URL = 12; + + @CertificateEncoding public final int certEncodingType; + + protected IkeCertPayload(@CertificateEncoding int encodingType) { + this(false /*critical*/, encodingType); + } + + protected IkeCertPayload(boolean critical, @CertificateEncoding int encodingType) { + super(PAYLOAD_TYPE_CERT, critical); + certEncodingType = encodingType; + } + + protected static IkeCertPayload getIkeCertPayload(boolean critical, byte[] payloadBody) + throws IkeProtocolException { + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + + int certEncodingType = Byte.toUnsignedInt(inputBuffer.get()); + byte[] certData = new byte[payloadBody.length - CERT_ENCODING_LEN]; + inputBuffer.get(certData); + switch (certEncodingType) { + case CERTIFICATE_ENCODING_X509_CERT_SIGNATURE: + return new IkeCertX509CertPayload(critical, certData); + // TODO: Support decoding CRL and "Hash and URL". + case CERTIFICATE_ENCODING_CRL: + throw new AuthenticationFailedException( + "CERTIFICATE_ENCODING_CRL decoding is unsupported."); + case CERTIFICATE_ENCODING_X509_CERT_HASH_URL: + throw new AuthenticationFailedException( + "CERTIFICATE_ENCODING_X509_CERT_HASH_URL decoding is unsupported"); + default: + throw new AuthenticationFailedException("Unrecognized certificate encoding type."); + } + } + + /** + * Validate an end certificate against the received chain and trust anchors. + * + * <p>Validation is done by checking if there is a valid certificate path from end certificate + * to provided trust anchors. + * + * <p>TrustManager implementation used in this method MUST conforms RFC 4158 and RFC 5280. As + * indicated in RFC 4158, Key Identifiers(KIDs) are not required to match during certification + * path validation and cannot be used to eliminate certificates. + * + * <p>Validation will fail if any certficate in the certificate chain is using RSA public key + * whose RSA modulus is smaller than 1024 bits. + * + * @param endCert the end certificate that will be used to verify AUTH payload + * @param certList all the received certificates (include the end certificate) + * @param crlList the certificate revocation lists + * @param trustAnchorSet the certificate authority set to validate the end certificate + * @throws AuthenticationFailedException if there is no valid certificate path + */ + public static void validateCertificates( + X509Certificate endCert, + List<X509Certificate> certList, + @Nullable List<X509CRL> crlList, + Set<TrustAnchor> trustAnchorSet) + throws AuthenticationFailedException { + try { + // TODO: b/122676944 Support CRL checking + + // Create a new keyStore with all trusted anchors + KeyStore keyStore = + KeyStore.getInstance(KEY_STORE_TYPE_PKCS12, IkeMessage.getSecurityProvider()); + keyStore.load(null); + for (TrustAnchor t : trustAnchorSet) { + X509Certificate trustedCert = t.getTrustedCert(); + String alias = + trustedCert.getSubjectX500Principal().getName() + trustedCert.hashCode(); + keyStore.setCertificateEntry(alias, trustedCert); + } + + // Build X509TrustManager with all keystore + TrustManagerFactory tmFactory = + TrustManagerFactory.getInstance( + CERT_PATH_ALGO_PKIX, IkeMessage.getTrustManagerProvider()); + tmFactory.init(keyStore); + + X509TrustManager trustManager = null; + for (TrustManager tm : tmFactory.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + trustManager = (X509TrustManager) tm; + } + } + if (trustManager == null) { + throw new ProviderException( + "X509TrustManager is not supported by " + + IkeMessage.getTrustManagerProvider().getName()); + } + + // Build and validate certificate path + trustManager.checkServerTrusted( + certList.toArray(new X509Certificate[certList.size()]), CERT_AUTH_TYPE_RSA); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException("Algorithm is not supported by the provider", e); + } catch (IOException | KeyStoreException e) { + throw new IllegalStateException(e); + } catch (CertificateException e) { + throw new AuthenticationFailedException(e); + } + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeCertX509CertPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeCertX509CertPayload.java new file mode 100644 index 00000000..1804b9b9 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeCertX509CertPayload.java @@ -0,0 +1,105 @@ +/* + * 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.message; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.net.ipsec.ike.exceptions.AuthenticationFailedException; + +import java.io.ByteArrayInputStream; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +/** + * IkeCertX509CertPayload represents a Certificate Payload carrying a DER-encoded X.509 certificate. + * + * <p>When sending multiple certificates, IKE library should put certificates in order starting with + * the target certificate and ending with a certificate issued by the trust anchor. When receiving + * an inbound packet, IKE library should take first certificate as the target certificate but treat + * the rest unordered. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.6">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeCertX509CertPayload extends IkeCertPayload { + public final X509Certificate certificate; + + /** Construct IkeCertX509CertPayload for an outbound packet. */ + public IkeCertX509CertPayload(X509Certificate x509Certificate) { + super(CERTIFICATE_ENCODING_X509_CERT_SIGNATURE); + certificate = x509Certificate; + } + + protected IkeCertX509CertPayload(boolean critical, byte[] certData) + throws IkeProtocolException { + super(critical, CERTIFICATE_ENCODING_X509_CERT_SIGNATURE); + try { + CertificateFactory factory = + CertificateFactory.getInstance("X.509", IkeMessage.getSecurityProvider()); + certificate = + (X509Certificate) + factory.generateCertificate(new ByteArrayInputStream(certData)); + // Parsing InputStream error + if (certificate == null) { + throw new AuthenticationFailedException( + "No certificate parsed from received data."); + } + if (certificate.getEncoded().length < certData.length) { + throw new AuthenticationFailedException("Unexpected trailing bytes."); + } + } catch (GeneralSecurityException e) { + throw new AuthenticationFailedException(e); + } + } + + /** + * Encode IkeCertX509CertPayload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + // TODO: Implement it. + throw new UnsupportedOperationException( + "It is not supported to encode a " + getTypeString()); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + // TODO: Implement it. + throw new UnsupportedOperationException( + "It is not supported to get payload length of " + getTypeString()); + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "Cert(X509)"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeConfigPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeConfigPayload.java new file mode 100644 index 00000000..ddd1b3ed --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeConfigPayload.java @@ -0,0 +1,767 @@ +/* + * 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.message; + +import android.annotation.IntDef; +import android.net.LinkAddress; +import android.net.ipsec.ike.IkeManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; + +/** + * This class represents Configuration payload. + * + * <p>Configuration payload is used to exchange configuration information between IKE peers. + * + * <p>Configuration type should be consistent with the IKE message direction (e.g. a request Config + * Payload should be in a request IKE message). IKE library will ignore Config Payload with + * inconsistent type or with unrecognized type. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.6">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeConfigPayload extends IkePayload { + private static final int CONFIG_HEADER_RESERVED_LEN = 3; + private static final int CONFIG_HEADER_LEN = 4; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + CONFIG_ATTR_INTERNAL_IP4_ADDRESS, + CONFIG_ATTR_INTERNAL_IP4_NETMASK, + CONFIG_ATTR_INTERNAL_IP4_DNS, + CONFIG_ATTR_INTERNAL_IP4_DHCP, + CONFIG_ATTR_APPLICATION_VERSION, + CONFIG_ATTR_INTERNAL_IP6_ADDRESS, + CONFIG_ATTR_INTERNAL_IP6_DNS, + CONFIG_ATTR_INTERNAL_IP4_SUBNET, + CONFIG_ATTR_SUPPORTED_ATTRIBUTES, + CONFIG_ATTR_INTERNAL_IP6_SUBNET + }) + public @interface ConfigAttr {} + + public static final int CONFIG_ATTR_INTERNAL_IP4_ADDRESS = 1; + public static final int CONFIG_ATTR_INTERNAL_IP4_NETMASK = 2; + public static final int CONFIG_ATTR_INTERNAL_IP4_DNS = 3; + public static final int CONFIG_ATTR_INTERNAL_IP4_DHCP = 6; + public static final int CONFIG_ATTR_APPLICATION_VERSION = 7; + public static final int CONFIG_ATTR_INTERNAL_IP6_ADDRESS = 8; + public static final int CONFIG_ATTR_INTERNAL_IP6_DNS = 10; + public static final int CONFIG_ATTR_INTERNAL_IP4_SUBNET = 13; + public static final int CONFIG_ATTR_SUPPORTED_ATTRIBUTES = 14; + public static final int CONFIG_ATTR_INTERNAL_IP6_SUBNET = 15; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({CONFIG_TYPE_REQUEST, CONFIG_TYPE_REPLY}) + public @interface ConfigType {} + + // We don't support CONFIG_TYPE_SET and CONFIG_TYPE_ACK + public static final int CONFIG_TYPE_REQUEST = 1; + public static final int CONFIG_TYPE_REPLY = 2; + + @ConfigType public final int configType; + public final List<ConfigAttribute> recognizedAttributeList; + + /** Build an IkeConfigPayload from a decoded inbound IKE packet. */ + IkeConfigPayload(boolean critical, byte[] payloadBody) throws InvalidSyntaxException { + super(PAYLOAD_TYPE_CP, critical); + + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + configType = Byte.toUnsignedInt(inputBuffer.get()); + inputBuffer.get(new byte[CONFIG_HEADER_RESERVED_LEN]); + + recognizedAttributeList = ConfigAttribute.decodeAttributeFrom(inputBuffer); + + // For an inbound Config Payload, IKE library is only able to handle a Config Reply or IKE + // Session attribute requests in a Config Request. For interoperability, netmask validation + // will be skipped for Config(Request) and config payloads with unsupported config types. + if (configType == CONFIG_TYPE_REPLY) { + validateNetmaskInReply(); + } + } + + /** Build an IkeConfigPayload instance for an outbound IKE packet. */ + public IkeConfigPayload(boolean isReply, List<ConfigAttribute> attributeList) { + super(PAYLOAD_TYPE_CP, false); + this.configType = isReply ? CONFIG_TYPE_REPLY : CONFIG_TYPE_REQUEST; + this.recognizedAttributeList = attributeList; + } + + private void validateNetmaskInReply() throws InvalidSyntaxException { + boolean hasIpv4Address = false; + int numNetmask = 0; + + for (ConfigAttribute attr : recognizedAttributeList) { + if (attr.isEmptyValue()) { + IkeManager.getIkeLog() + .d( + "IkeConfigPayload", + "Found empty attribute in a Config Payload reply " + + attr.attributeType); + } + switch (attr.attributeType) { + case CONFIG_ATTR_INTERNAL_IP4_ADDRESS: + if (!attr.isEmptyValue()) hasIpv4Address = true; + break; + case CONFIG_ATTR_INTERNAL_IP4_NETMASK: + if (!attr.isEmptyValue()) numNetmask++; + break; + default: + continue; + } + } + + if (!hasIpv4Address && numNetmask > 0) { + throw new InvalidSyntaxException( + "Found INTERNAL_IP4_NETMASK attribute but no INTERNAL_IP4_ADDRESS attribute"); + } + + if (numNetmask > 1) { + throw new InvalidSyntaxException("Found more than one INTERNAL_IP4_NETMASK"); + } + } + + // TODO: Create ConfigAttribute subclasses for each attribute. + + /** This class represents common information of all Configuration Attributes. */ + public abstract static class ConfigAttribute { + private static final int ATTRIBUTE_TYPE_MASK = 0x7fff; + + private static final int ATTRIBUTE_HEADER_LEN = 4; + private static final int IPV4_PREFIX_LEN_MAX = 32; + + protected static final int VALUE_LEN_NOT_INCLUDED = 0; + + protected static final int IPV4_ADDRESS_LEN = 4; + protected static final int IPV6_ADDRESS_LEN = 16; + protected static final int PREFIX_LEN_LEN = 1; + + public final int attributeType; + + protected ConfigAttribute(int attributeType) { + this.attributeType = attributeType; + } + + protected ConfigAttribute(int attributeType, int len) throws InvalidSyntaxException { + this(attributeType); + + if (!isLengthValid(len)) { + throw new InvalidSyntaxException("Invalid configuration length"); + } + } + + /** + * Package private method to decode ConfigAttribute list from an inbound packet + * + * <p>NegativeArraySizeException and BufferUnderflowException will be caught in {@link + * IkeMessage} + */ + static List<ConfigAttribute> decodeAttributeFrom(ByteBuffer inputBuffer) + throws InvalidSyntaxException { + List<ConfigAttribute> configList = new LinkedList(); + + while (inputBuffer.hasRemaining()) { + int attributeType = Short.toUnsignedInt(inputBuffer.getShort()); + int length = Short.toUnsignedInt(inputBuffer.getShort()); + byte[] value = new byte[length]; + inputBuffer.get(value); + + switch (attributeType) { + case CONFIG_ATTR_INTERNAL_IP4_ADDRESS: + configList.add(new ConfigAttributeIpv4Address(value)); + break; + case CONFIG_ATTR_INTERNAL_IP4_NETMASK: + configList.add(new ConfigAttributeIpv4Netmask(value)); + break; + case CONFIG_ATTR_INTERNAL_IP4_DNS: + configList.add(new ConfigAttributeIpv4Dns(value)); + break; + case CONFIG_ATTR_INTERNAL_IP4_DHCP: + configList.add(new ConfigAttributeIpv4Dhcp(value)); + break; + case CONFIG_ATTR_INTERNAL_IP6_ADDRESS: + configList.add(new ConfigAttributeIpv6Address(value)); + break; + case CONFIG_ATTR_INTERNAL_IP6_DNS: + configList.add(new ConfigAttributeIpv6Dns(value)); + break; + case CONFIG_ATTR_INTERNAL_IP4_SUBNET: + configList.add(new ConfigAttributeIpv4Subnet(value)); + break; + case CONFIG_ATTR_INTERNAL_IP6_SUBNET: + configList.add(new ConfigAttributeIpv6Subnet(value)); + break; + default: + IkeManager.getIkeLog() + .i( + "IkeConfigPayload", + "Unrecognized attribute type: " + attributeType); + } + + // TODO: Support App version and supported attribute list + } + + return configList; + } + + /** Encode attribute to ByteBuffer. */ + public void encodeAttributeToByteBuffer(ByteBuffer buffer) { + buffer.putShort((short) (attributeType & ATTRIBUTE_TYPE_MASK)) + .putShort((short) getValueLength()); + encodeValueToByteBuffer(buffer); + } + + /** Get attribute length. */ + public int getAttributeLen() { + return ATTRIBUTE_HEADER_LEN + getValueLength(); + } + + /** Returns if this attribute value is empty. */ + public boolean isEmptyValue() { + return getValueLength() == VALUE_LEN_NOT_INCLUDED; + } + + protected static int netmaskToPrefixLen(Inet4Address address) { + byte[] bytes = address.getAddress(); + + int netmaskInt = ByteBuffer.wrap(bytes).getInt(); + int leftmostBitMask = 0x80000000; + + int prefixLen = 0; + while ((netmaskInt & leftmostBitMask) == leftmostBitMask) { + prefixLen++; + netmaskInt <<= 1; + } + + if (netmaskInt != 0) { + throw new IllegalArgumentException("Invalid netmask address"); + } + + return prefixLen; + } + + protected static byte[] prefixToNetmaskBytes(int prefixLen) { + if (prefixLen > IPV4_PREFIX_LEN_MAX || prefixLen < 0) { + throw new IllegalArgumentException("Invalid IPv4 prefix length."); + } + + int netmaskInt = (int) (((long) 0xffffffff) << (IPV4_PREFIX_LEN_MAX - prefixLen)); + byte[] netmask = new byte[IPV4_ADDRESS_LEN]; + + ByteBuffer buffer = ByteBuffer.allocate(IPV4_ADDRESS_LEN); + buffer.putInt(netmaskInt); + return buffer.array(); + } + + protected abstract void encodeValueToByteBuffer(ByteBuffer buffer); + + protected abstract int getValueLength(); + + protected abstract boolean isLengthValid(int length); + } + + /** + * This class represents common information of all Configuration Attributes whoses value is one + * IPv4 address or empty. + */ + abstract static class ConfigAttrIpv4AddressBase extends ConfigAttribute { + public final Inet4Address address; + + protected ConfigAttrIpv4AddressBase(int attributeType, Inet4Address address) { + super(attributeType); + this.address = address; + } + + protected ConfigAttrIpv4AddressBase(int attributeType) { + super(attributeType); + this.address = null; + } + + protected ConfigAttrIpv4AddressBase(int attributeType, byte[] value) + throws InvalidSyntaxException { + super(attributeType, value.length); + + if (value.length == VALUE_LEN_NOT_INCLUDED) { + address = null; + return; + } + + try { + address = (Inet4Address) Inet4Address.getByAddress(value); + } catch (UnknownHostException e) { + throw new InvalidSyntaxException("Invalid attribute value", e); + } + } + + @Override + protected void encodeValueToByteBuffer(ByteBuffer buffer) { + if (address == null) { + buffer.put(new byte[0]); + return; + } + + buffer.put(address.getAddress()); + } + + @Override + protected int getValueLength() { + return address == null ? 0 : IPV4_ADDRESS_LEN; + } + + @Override + protected boolean isLengthValid(int length) { + return length == IPV4_ADDRESS_LEN || length == VALUE_LEN_NOT_INCLUDED; + } + } + + /** This class represents Configuration Attribute for IPv4 internal address. */ + public static class ConfigAttributeIpv4Address extends ConfigAttrIpv4AddressBase { + /** Construct an instance with specified address for an outbound packet. */ + public ConfigAttributeIpv4Address(Inet4Address ipv4Address) { + super(CONFIG_ATTR_INTERNAL_IP4_ADDRESS, ipv4Address); + } + + /** + * Construct an instance without a specified address for an outbound packet. + * + * <p>It must be only used in a configuration request. + */ + public ConfigAttributeIpv4Address() { + super(CONFIG_ATTR_INTERNAL_IP4_ADDRESS); + } + + /** Construct an instance with a decoded inbound packet. */ + @VisibleForTesting + ConfigAttributeIpv4Address(byte[] value) throws InvalidSyntaxException { + super(CONFIG_ATTR_INTERNAL_IP4_ADDRESS, value); + } + } + + /** + * This class represents Configuration Attribute for IPv4 netmask. + * + * <p>Non-empty values for this attribute in a CFG_REQUEST do not make sense and thus MUST NOT + * be included + */ + public static class ConfigAttributeIpv4Netmask extends ConfigAttrIpv4AddressBase { + /** + * Construct an instance without a specified netmask for an outbound packet. + * + * <p>It must be only used in a configuration request. + */ + public ConfigAttributeIpv4Netmask() { + super(CONFIG_ATTR_INTERNAL_IP4_NETMASK); + } + + /** Construct an instance with a decoded inbound packet. */ + @VisibleForTesting + public ConfigAttributeIpv4Netmask(byte[] value) throws InvalidSyntaxException { + super(CONFIG_ATTR_INTERNAL_IP4_NETMASK, value); + + if (address == null) return; + try { + netmaskToPrefixLen(address); + } catch (IllegalArgumentException e) { + throw new InvalidSyntaxException("Invalid attribute value", e); + } + } + + /** Convert netmask to prefix length. */ + public int getPrefixLen() { + return netmaskToPrefixLen(address); + } + } + + /** This class represents Configuration Attribute for IPv4 DHCP server. */ + public static class ConfigAttributeIpv4Dhcp extends ConfigAttrIpv4AddressBase { + /** Construct an instance with specified DHCP server address for an outbound packet. */ + public ConfigAttributeIpv4Dhcp(Inet4Address ipv4Address) { + super(CONFIG_ATTR_INTERNAL_IP4_DHCP, ipv4Address); + } + + /** + * Construct an instance without a specified DHCP server address for an outbound packet. + * + * <p>It must be only used in a configuration request. + */ + public ConfigAttributeIpv4Dhcp() { + super(CONFIG_ATTR_INTERNAL_IP4_DHCP); + } + + /** Construct an instance with a decoded inbound packet. */ + @VisibleForTesting + ConfigAttributeIpv4Dhcp(byte[] value) throws InvalidSyntaxException { + super(CONFIG_ATTR_INTERNAL_IP4_DHCP, value); + } + } + + /** + * This class represents Configuration Attribute for IPv4 DNS. + * + * <p>There is no use case to create a DNS request for a specfic DNS server address. As an IKE + * client, we will only support building an empty DNS attribute for an outbound IKE packet. + */ + public static class ConfigAttributeIpv4Dns extends ConfigAttrIpv4AddressBase { + /** Construct an instance with specified DNS server address for an outbound packet. */ + public ConfigAttributeIpv4Dns(Inet4Address ipv4Address) { + super(CONFIG_ATTR_INTERNAL_IP4_DNS, ipv4Address); + } + + /** + * Construct an instance without a specified DNS server address for an outbound packet. + * + * <p>It must be only used in a configuration request. + */ + public ConfigAttributeIpv4Dns() { + super(CONFIG_ATTR_INTERNAL_IP4_DNS); + } + + /** Construct an instance with a decoded inbound packet. */ + @VisibleForTesting + ConfigAttributeIpv4Dns(byte[] value) throws InvalidSyntaxException { + super(CONFIG_ATTR_INTERNAL_IP4_DNS, value); + } + } + + /** This class represents Configuration Attribute for IPv4 subnets. */ + public static class ConfigAttributeIpv4Subnet extends ConfigAttribute { + private static final int VALUE_LEN = 2 * IPV4_ADDRESS_LEN; + + public final LinkAddress linkAddress; + + /** Construct an instance with specified subnet for an outbound packet. */ + public ConfigAttributeIpv4Subnet(LinkAddress ipv4LinkAddress) { + super(CONFIG_ATTR_INTERNAL_IP4_SUBNET); + + if (!ipv4LinkAddress.isIpv4()) { + throw new IllegalArgumentException("Input LinkAddress is not IPv4"); + } + + this.linkAddress = ipv4LinkAddress; + } + + /** + * Construct an instance without a specified subnet for an outbound packet. + * + * <p>It must be only used in a configuration request. + */ + public ConfigAttributeIpv4Subnet() { + super(CONFIG_ATTR_INTERNAL_IP4_SUBNET); + this.linkAddress = null; + } + + /** Construct an instance with a decoded inbound packet. */ + @VisibleForTesting + ConfigAttributeIpv4Subnet(byte[] value) throws InvalidSyntaxException { + super(CONFIG_ATTR_INTERNAL_IP4_SUBNET, value.length); + + if (value.length == VALUE_LEN_NOT_INCLUDED) { + linkAddress = null; + return; + } + + try { + ByteBuffer inputBuffer = ByteBuffer.wrap(value); + byte[] ipBytes = new byte[IPV4_ADDRESS_LEN]; + inputBuffer.get(ipBytes); + byte[] netmaskBytes = new byte[IPV4_ADDRESS_LEN]; + inputBuffer.get(netmaskBytes); + + InetAddress address = InetAddress.getByAddress(ipBytes); + InetAddress netmask = InetAddress.getByAddress(netmaskBytes); + validateInet4AddressTypeOrThrow(address); + validateInet4AddressTypeOrThrow(netmask); + + linkAddress = new LinkAddress(address, netmaskToPrefixLen((Inet4Address) netmask)); + } catch (UnknownHostException | IllegalArgumentException e) { + throw new InvalidSyntaxException("Invalid attribute value", e); + } + } + + private void validateInet4AddressTypeOrThrow(InetAddress address) { + if (!(address instanceof Inet4Address)) { + throw new IllegalArgumentException("Input InetAddress is not IPv4"); + } + } + + @Override + protected void encodeValueToByteBuffer(ByteBuffer buffer) { + if (linkAddress == null) { + buffer.put(new byte[VALUE_LEN_NOT_INCLUDED]); + return; + } + byte[] netmaskBytes = prefixToNetmaskBytes(linkAddress.getPrefixLength()); + buffer.put(linkAddress.getAddress().getAddress()).put(netmaskBytes); + } + + @Override + protected int getValueLength() { + return linkAddress == null ? 0 : VALUE_LEN; + } + + @Override + protected boolean isLengthValid(int length) { + return length == VALUE_LEN || length == VALUE_LEN_NOT_INCLUDED; + } + } + + /** + * This class represents common information of all Configuration Attributes whoses value is an + * IPv6 address range. + * + * <p>These attributes contains an IPv6 address and a prefix length. + */ + abstract static class ConfigAttrIpv6AddrRangeBase extends ConfigAttribute { + private static final int VALUE_LEN = IPV6_ADDRESS_LEN + PREFIX_LEN_LEN; + + public final LinkAddress linkAddress; + + protected ConfigAttrIpv6AddrRangeBase(int attributeType, LinkAddress ipv6LinkAddress) { + super(attributeType); + + validateIpv6LinkAddressTypeOrThrow(ipv6LinkAddress); + linkAddress = ipv6LinkAddress; + } + + protected ConfigAttrIpv6AddrRangeBase(int attributeType) { + super(attributeType); + linkAddress = null; + } + + protected ConfigAttrIpv6AddrRangeBase(int attributeType, byte[] value) + throws InvalidSyntaxException { + super(attributeType, value.length); + + if (value.length == VALUE_LEN_NOT_INCLUDED) { + linkAddress = null; + return; + } + + try { + ByteBuffer inputBuffer = ByteBuffer.wrap(value); + byte[] ip6AddrBytes = new byte[IPV6_ADDRESS_LEN]; + inputBuffer.get(ip6AddrBytes); + InetAddress address = InetAddress.getByAddress(ip6AddrBytes); + + int prefixLen = Byte.toUnsignedInt(inputBuffer.get()); + + linkAddress = new LinkAddress(address, prefixLen); + validateIpv6LinkAddressTypeOrThrow(linkAddress); + } catch (UnknownHostException | IllegalArgumentException e) { + throw new InvalidSyntaxException("Invalid attribute value", e); + } + } + + private void validateIpv6LinkAddressTypeOrThrow(LinkAddress address) { + if (!address.isIpv6()) { + throw new IllegalArgumentException("Input LinkAddress is not IPv6"); + } + } + + @Override + protected void encodeValueToByteBuffer(ByteBuffer buffer) { + if (linkAddress == null) { + buffer.put(new byte[VALUE_LEN_NOT_INCLUDED]); + return; + } + + buffer.put(linkAddress.getAddress().getAddress()) + .put((byte) linkAddress.getPrefixLength()); + } + + @Override + protected int getValueLength() { + return linkAddress == null ? VALUE_LEN_NOT_INCLUDED : VALUE_LEN; + } + + @Override + protected boolean isLengthValid(int length) { + return length == VALUE_LEN || length == VALUE_LEN_NOT_INCLUDED; + } + } + + /** This class represents Configuration Attribute for IPv6 internal addresses. */ + public static class ConfigAttributeIpv6Address extends ConfigAttrIpv6AddrRangeBase { + /** Construct an instance with specified address for an outbound packet. */ + public ConfigAttributeIpv6Address(LinkAddress ipv6LinkAddress) { + super(CONFIG_ATTR_INTERNAL_IP6_ADDRESS, ipv6LinkAddress); + } + + /** + * Construct an instance without a specified address for an outbound packet. + * + * <p>It must be only used in a configuration request. + */ + public ConfigAttributeIpv6Address() { + super(CONFIG_ATTR_INTERNAL_IP6_ADDRESS); + } + + /** Construct an instance with a decoded inbound packet. */ + @VisibleForTesting + ConfigAttributeIpv6Address(byte[] value) throws InvalidSyntaxException { + super(CONFIG_ATTR_INTERNAL_IP6_ADDRESS, value); + } + } + + /** This class represents Configuration Attribute for IPv6 subnets. */ + public static class ConfigAttributeIpv6Subnet extends ConfigAttrIpv6AddrRangeBase { + /** Construct an instance with specified subnet for an outbound packet. */ + public ConfigAttributeIpv6Subnet(LinkAddress ipv6LinkAddress) { + super(CONFIG_ATTR_INTERNAL_IP6_SUBNET, ipv6LinkAddress); + } + + /** + * Construct an instance without a specified subnet for an outbound packet. + * + * <p>It must be only used in a configuration request. + */ + public ConfigAttributeIpv6Subnet() { + super(CONFIG_ATTR_INTERNAL_IP6_SUBNET); + } + + /** Construct an instance with a decoded inbound packet. */ + @VisibleForTesting + ConfigAttributeIpv6Subnet(byte[] value) throws InvalidSyntaxException { + super(CONFIG_ATTR_INTERNAL_IP6_SUBNET, value); + } + } + + /** + * This class represents Configuration Attribute for IPv6 DNS. + * + * <p>There is no use case to create a DNS request for a specfic DNS server address. As an IKE + * client, we will only support building an empty DNS attribute for an outbound IKE packet. + */ + public static class ConfigAttributeIpv6Dns extends ConfigAttribute { + public final Inet6Address address; + + /** Construct an instance with specified DNS server address for an outbound packet. */ + public ConfigAttributeIpv6Dns(Inet6Address ipv6Address) { + super(CONFIG_ATTR_INTERNAL_IP6_DNS); + address = ipv6Address; + } + + /** + * Construct an instance without a specified DNS server address for an outbound packet. + * + * <p>It must be only used in a configuration request. + */ + public ConfigAttributeIpv6Dns() { + super(CONFIG_ATTR_INTERNAL_IP6_DNS); + this.address = null; + } + + protected ConfigAttributeIpv6Dns(byte[] value) throws InvalidSyntaxException { + super(CONFIG_ATTR_INTERNAL_IP6_DNS, value.length); + + if (value.length == VALUE_LEN_NOT_INCLUDED) { + address = null; + return; + } + + try { + InetAddress netAddress = InetAddress.getByAddress(value); + + if (!(netAddress instanceof Inet6Address)) { + throw new InvalidSyntaxException("Invalid IPv6 address."); + } + address = (Inet6Address) netAddress; + } catch (UnknownHostException e) { + throw new InvalidSyntaxException("Invalid attribute value", e); + } + } + + @Override + protected void encodeValueToByteBuffer(ByteBuffer buffer) { + if (address == null) { + buffer.put(new byte[0]); + return; + } + + buffer.put(address.getAddress()); + } + + @Override + protected int getValueLength() { + return address == null ? 0 : IPV6_ADDRESS_LEN; + } + + @Override + protected boolean isLengthValid(int length) { + return length == IPV6_ADDRESS_LEN || length == VALUE_LEN_NOT_INCLUDED; + } + } + + /** + * Encode Configuration payload to ByteBUffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + byteBuffer.put((byte) configType).put(new byte[CONFIG_HEADER_RESERVED_LEN]); + + for (ConfigAttribute attr : recognizedAttributeList) { + attr.encodeAttributeToByteBuffer(byteBuffer); + } + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + int len = GENERIC_HEADER_LENGTH + CONFIG_HEADER_LEN; + + for (ConfigAttribute attr : recognizedAttributeList) { + len += attr.getAttributeLen(); + } + + return len; + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + switch (configType) { + case CONFIG_TYPE_REQUEST: + return "CP(Req)"; + case CONFIG_TYPE_REPLY: + return "CP(Reply)"; + default: + return "CP(" + configType + ")"; + } + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeDeletePayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeDeletePayload.java new file mode 100644 index 00000000..f629f506 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeDeletePayload.java @@ -0,0 +1,177 @@ +/* + * 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.message; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException; + +import java.nio.ByteBuffer; + +/** + * IkeDeletePayload represents a Delete Payload. + * + * <p>As instructed in RFC 7296, deletion of the IKE SA is indicated by a protocol ID of 1 (IKE) but + * no SPIs. Deletion of a Child SA will contain the IPsec protocol ID and SPIs of inbound IPsec + * packets. Since IKE library only supports negotiating Child SA using ESP, only the protocol ID of + * 3 (ESP) is used for deleting Child SA. + * + * The possible request/response pairs for deletion are as follows: + * - IKE SA deletion: + * Incoming: INFORMATIONAL(DELETE(PROTO_IKE)) + * Outgoing: INFORMATIONAL() + * + * - ESP SA deletion: + * Incoming: INFORMATIONAL(DELETE(PROTO_ESP, SPI_A_OUT)) + * Outgoing: INFORMATIONAL(DELETE(PROTO_ESP, SPI_A_IN)) + * + * - ESP SA simultaneous deletion: + * Outgoing: INFORMATIONAL(DELETE(PROTO_ESP, SPI_A_IN)) + * Incoming: INFORMATIONAL(DELETE(PROTO_ESP, SPI_A_OUT)) + * Outgoing: INFORMATIONAL() // Notice DELETE payload omitted + * + * - ESP SA simultaneous multi-deletion: + * Outgoing: INFORMATIONAL(DELETE(PROTO_ESP, SPI_A_IN)) + * Incoming: INFORMATIONAL(DELETE(PROTO_ESP, SPI_A_OUT, SPI_B_OUT)) + * Outgoing: INFORMATIONAL(DELETE(PROTO_ESP, SPI_B_IN)) // Notice SPI_A_OUT omitted + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.11">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeDeletePayload extends IkeInformationalPayload { + private static final int DELETE_HEADER_LEN = 4; + + @ProtocolId public final int protocolId; + public final byte spiSize; + public final int numSpi; + public final int[] spisToDelete; + + /** + * Construct an instance of IkeDeletePayload from decoding inbound IKE packet. + * + * <p>NegativeArraySizeException and BufferUnderflowException will be caught in {@link + * IkeMessage} + * + * @param critical indicates if this payload is critical. Ignored in supported payload as + * instructed by the RFC 7296. + * @param payloadBody payload body in byte array + * @throws IkeProtocolException if there is any error + */ + IkeDeletePayload(boolean critical, byte[] payloadBody) throws IkeProtocolException { + super(PAYLOAD_TYPE_DELETE, critical); + + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + + protocolId = Byte.toUnsignedInt(inputBuffer.get()); + spiSize = inputBuffer.get(); + numSpi = Short.toUnsignedInt(inputBuffer.getShort()); + spisToDelete = new int[numSpi]; + + switch (protocolId) { + case PROTOCOL_ID_IKE: + // Delete payload for IKE SA must not include SPI. + if (spiSize != SPI_LEN_NOT_INCLUDED + || numSpi != 0 + || inputBuffer.remaining() != 0) { + throw new InvalidSyntaxException("Invalid Delete IKE Payload."); + } + break; + case PROTOCOL_ID_ESP: + // Delete payload for Child SA must include SPI + if (spiSize != SPI_LEN_IPSEC + || numSpi == 0 + || inputBuffer.remaining() != SPI_LEN_IPSEC * numSpi) { + throw new InvalidSyntaxException("Invalid Delete Child Payload."); + } + + for (int i = 0; i < numSpi; i++) { + spisToDelete[i] = inputBuffer.getInt(); + } + break; + default: + throw new InvalidSyntaxException("Unrecognized protocol in Delete Payload."); + } + } + + /** + * Constructor for an outbound IKE SA deletion payload. + * + * <p>This constructor takes no SPI, as IKE SAs are deleted by sending a delete payload within + * the negotiated session. As such, the SPIs are shared state that does not need to be sent. + */ + public IkeDeletePayload() { + super(PAYLOAD_TYPE_DELETE, false); + protocolId = PROTOCOL_ID_IKE; + spiSize = SPI_LEN_NOT_INCLUDED; + numSpi = 0; + spisToDelete = new int[0]; + } + + /** + * Constructor for an outbound Child SA deletion payload. + * + * @param spis array of SPIs of Child SAs to delete. Must contain at least one SPI. + */ + public IkeDeletePayload(int[] spis) { + super(PAYLOAD_TYPE_DELETE, false); + + if (spis == null || spis.length < 1) { + throw new IllegalArgumentException("No SPIs provided"); + } + + protocolId = PROTOCOL_ID_ESP; + spiSize = SPI_LEN_IPSEC; + numSpi = spis.length; + spisToDelete = spis; + } + + /** + * Encode Delete Payload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + byteBuffer.put((byte) protocolId).put(spiSize).putShort((short) numSpi); + + for (int toDelete : spisToDelete) { + byteBuffer.putInt(toDelete); + } + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + DELETE_HEADER_LEN + spisToDelete.length * spiSize; + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "Del"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeEapPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeEapPayload.java new file mode 100644 index 00000000..5dc14971 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeEapPayload.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2018 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.message; + +import com.android.internal.net.eap.message.EapMessage; + +import java.nio.ByteBuffer; + +/** + * IkeEapPayload represents an EAP payload. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.8">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + * @see <a href="https://tools.ietf.org/html/rfc3748#section-4">RFC 3748, Extensible Authentication + * Protocol (EAP)</a> + */ +public final class IkeEapPayload extends IkePayload { + public final byte[] eapMessage; + + /** + * Construct an instance of IkeEapPayload from a decoded inbound IKE packet. + * + * <p>Any syntax errors contained in the eapMessage will be handled in {@link EapMessage}. + * + * @param isCritical indicates if this payload is critical. Ignored in supported payload as + * instructed by the RFC 7296. + * @param eapMessage byte-array encoded EapMessage + */ + IkeEapPayload(boolean isCritical, byte[] eapMessage) { + super(PAYLOAD_TYPE_EAP, isCritical); + + this.eapMessage = eapMessage; + } + + /** + * Construct an instance of IkeEapPayload for an outbound IKE EAP message. + * + * <p>This eapMessage is constructed in the IKE session and is guaranteed to have valid syntax. + * + * @param eapMessage byte-array encoded EapMessage + */ + public IkeEapPayload(byte[] eapMessage) { + super(PAYLOAD_TYPE_EAP, false); + + this.eapMessage = eapMessage; + } + + /** + * Encode EAP Payload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + byteBuffer.put(eapMessage); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + eapMessage.length; + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "EAP"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeEncryptedPayloadBody.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeEncryptedPayloadBody.java new file mode 100644 index 00000000..77cc9f4e --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeEncryptedPayloadBody.java @@ -0,0 +1,390 @@ +/* + * 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.message; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.ipsec.ike.crypto.IkeCipher; +import com.android.internal.net.ipsec.ike.crypto.IkeCombinedModeCipher; +import com.android.internal.net.ipsec.ike.crypto.IkeMacIntegrity; +import com.android.internal.net.ipsec.ike.crypto.IkeNormalModeCipher; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.AEADBadTagException; +import javax.crypto.IllegalBlockSizeException; + +/** + * IkeEncryptedPayloadBody is a package private class that represents an IKE payload substructure + * that contains initialization vector, encrypted content, padding, pad length and integrity + * checksum. + * + * <p>Both an Encrypted Payload (IkeSkPayload) and an EncryptedFragmentPayload (IkeSkfPayload) + * consists of an IkeEncryptedPayloadBody instance. + * + * <p>When using normal cipher with separate integrity algorithm, data to authenticate includes + * bytes from beginning of IKE header to the pad length, which are concatenation of IKE header, + * current payload header, iv and encrypted and padded data. + * + * <p>When using AEAD, additional authentication data(also known as) associated data is required. It + * MUST include bytes from beginning of IKE header to the last octet of the Payload Header of the + * Encrypted Payload. Note fragment number and total fragments are also included if Encrypted + * Payload is SKF. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#page-105">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + * @see <a href="https://tools.ietf.org/html/rfc7383#page-6">RFC 7383, Internet Key Exchange + * Protocol Version 2 (IKEv2) Message Fragmentation</a> + */ +final class IkeEncryptedPayloadBody { + // Length of pad length field. + private static final int PAD_LEN_LEN = 1; + + private final byte[] mUnencryptedData; + private final byte[] mEncryptedAndPaddedData; + private final byte[] mIv; + private final byte[] mIntegrityChecksum; + + /** + * Package private constructor for constructing an instance of IkeEncryptedPayloadBody from + * decrypting an incoming packet. + */ + IkeEncryptedPayloadBody( + byte[] message, + int encryptedBodyOffset, + IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + byte[] integrityKey, + byte[] decryptionKey) + throws IkeProtocolException, GeneralSecurityException { + ByteBuffer inputBuffer = ByteBuffer.wrap(message); + + // Skip IKE header and generic payload header (and SKF header) + inputBuffer.get(new byte[encryptedBodyOffset]); + + // Extract bytes for authentication and decryption. + int expectedIvLen = decryptCipher.getIvLen(); + mIv = new byte[expectedIvLen]; + + int checksumLen = getChecksum(integrityMac, decryptCipher); + int encryptedDataLen = message.length - (encryptedBodyOffset + expectedIvLen + checksumLen); + // IkeMessage will catch exception if encryptedDataLen is negative. + mEncryptedAndPaddedData = new byte[encryptedDataLen]; + + mIntegrityChecksum = new byte[checksumLen]; + inputBuffer.get(mIv).get(mEncryptedAndPaddedData).get(mIntegrityChecksum); + + if (decryptCipher.isAead()) { + byte[] dataToAuthenticate = Arrays.copyOfRange(message, 0, encryptedBodyOffset); + mUnencryptedData = + combinedModeDecrypt( + (IkeCombinedModeCipher) decryptCipher, + mEncryptedAndPaddedData, + mIntegrityChecksum, + dataToAuthenticate, + decryptionKey, + mIv); + } else { + byte[] dataToAuthenticate = + Arrays.copyOfRange(message, 0, message.length - checksumLen); + + validateInboundChecksumOrThrow( + dataToAuthenticate, integrityMac, integrityKey, mIntegrityChecksum); + mUnencryptedData = + normalModeDecrypt( + mEncryptedAndPaddedData, + (IkeNormalModeCipher) decryptCipher, + decryptionKey, + mIv); + } + } + + /** + * Package private constructor for constructing an instance of IkeEncryptedPayloadBody for + * building an outbound packet. + */ + IkeEncryptedPayloadBody( + IkeHeader ikeHeader, + @IkePayload.PayloadType int firstPayloadType, + byte[] skfHeaderBytes, + byte[] unencryptedPayloads, + IkeMacIntegrity integrityMac, + IkeCipher encryptCipher, + byte[] integrityKey, + byte[] encryptionKey) { + this( + ikeHeader, + firstPayloadType, + skfHeaderBytes, + unencryptedPayloads, + integrityMac, + encryptCipher, + integrityKey, + encryptionKey, + encryptCipher.generateIv(), + calculatePadding(unencryptedPayloads.length, encryptCipher.getBlockSize())); + } + + /** Package private constructor only for testing. */ + @VisibleForTesting + IkeEncryptedPayloadBody( + IkeHeader ikeHeader, + @IkePayload.PayloadType int firstPayloadType, + byte[] skfHeaderBytes, + byte[] unencryptedPayloads, + IkeMacIntegrity integrityMac, + IkeCipher encryptCipher, + byte[] integrityKey, + byte[] encryptionKey, + byte[] iv, + byte[] padding) { + mUnencryptedData = unencryptedPayloads; + + mIv = iv; + if (encryptCipher.isAead()) { + byte[] paddedDataWithChecksum = + combinedModeEncrypt( + (IkeCombinedModeCipher) encryptCipher, + ikeHeader, + firstPayloadType, + skfHeaderBytes, + unencryptedPayloads, + encryptionKey, + iv, + padding); + + int checkSumLen = ((IkeCombinedModeCipher) encryptCipher).getChecksumLen(); + mIntegrityChecksum = new byte[checkSumLen]; + mEncryptedAndPaddedData = new byte[paddedDataWithChecksum.length - checkSumLen]; + + ByteBuffer buffer = ByteBuffer.wrap(paddedDataWithChecksum); + buffer.get(mEncryptedAndPaddedData); + buffer.get(mIntegrityChecksum); + } else { + // Encrypt data + mEncryptedAndPaddedData = + normalModeEncrypt( + unencryptedPayloads, + (IkeNormalModeCipher) encryptCipher, + encryptionKey, + iv, + padding); + // Calculate checksum + mIntegrityChecksum = + generateOutboundChecksum( + ikeHeader, + firstPayloadType, + skfHeaderBytes, + integrityMac, + iv, + mEncryptedAndPaddedData, + integrityKey); + } + } + + private int getChecksum(IkeMacIntegrity integrityMac, IkeCipher decryptCipher) { + if (decryptCipher.isAead()) { + return ((IkeCombinedModeCipher) decryptCipher).getChecksumLen(); + } else { + return integrityMac.getChecksumLen(); + } + } + + /** Package private for testing */ + @VisibleForTesting + static byte[] generateOutboundChecksum( + IkeHeader ikeHeader, + @IkePayload.PayloadType int firstPayloadType, + byte[] skfHeaderBytes, + IkeMacIntegrity integrityMac, + byte[] iv, + byte[] encryptedAndPaddedData, + byte[] integrityKey) { + // Length from encrypted payload header to the Pad Length field + int encryptedPayloadHeaderToPadLen = + IkePayload.GENERIC_HEADER_LENGTH + + skfHeaderBytes.length + + iv.length + + encryptedAndPaddedData.length; + + // Calculate length of authentication data and allocate ByteBuffer. + int dataToAuthenticateLength = IkeHeader.IKE_HEADER_LENGTH + encryptedPayloadHeaderToPadLen; + ByteBuffer authenticatedSectionBuffer = ByteBuffer.allocate(dataToAuthenticateLength); + + // Build data to authenticate. + int encryptedPayloadLength = encryptedPayloadHeaderToPadLen + integrityMac.getChecksumLen(); + ikeHeader.encodeToByteBuffer(authenticatedSectionBuffer, encryptedPayloadLength); + IkePayload.encodePayloadHeaderToByteBuffer( + firstPayloadType, encryptedPayloadLength, authenticatedSectionBuffer); + authenticatedSectionBuffer.put(skfHeaderBytes).put(iv).put(encryptedAndPaddedData); + + // Calculate checksum + return integrityMac.generateChecksum(integrityKey, authenticatedSectionBuffer.array()); + } + + /** Package private for testing */ + @VisibleForTesting + static void validateInboundChecksumOrThrow( + byte[] dataToAuthenticate, + IkeMacIntegrity integrityMac, + byte[] integrityKey, + byte[] integrityChecksum) + throws GeneralSecurityException { + // TODO: Make it package private and add test. + int checkSumLen = integrityChecksum.length; + byte[] calculatedChecksum = integrityMac.generateChecksum(integrityKey, dataToAuthenticate); + + if (!Arrays.equals(integrityChecksum, calculatedChecksum)) { + throw new GeneralSecurityException("Message authentication failed."); + } + } + + /** Package private for testing */ + @VisibleForTesting + static byte[] normalModeEncrypt( + byte[] dataToEncrypt, + IkeNormalModeCipher encryptCipher, + byte[] encryptionKey, + byte[] iv, + byte[] padding) { + byte[] paddedData = getPaddedData(dataToEncrypt, padding); + + // Encrypt data. + return encryptCipher.encrypt(paddedData, encryptionKey, iv); + } + + /** Package private for testing */ + @VisibleForTesting + static byte[] normalModeDecrypt( + byte[] encryptedData, + IkeNormalModeCipher decryptCipher, + byte[] decryptionKey, + byte[] iv) + throws IllegalBlockSizeException { + byte[] paddedPlaintext = decryptCipher.decrypt(encryptedData, decryptionKey, iv); + + return stripPadding(paddedPlaintext); + } + + /** Package private for testing */ + @VisibleForTesting + static byte[] combinedModeEncrypt( + IkeCombinedModeCipher encryptCipher, + IkeHeader ikeHeader, + @IkePayload.PayloadType int firstPayloadType, + byte[] skfHeaderBytes, + byte[] dataToEncrypt, + byte[] encryptionKey, + byte[] iv, + byte[] padding) { + int dataToAuthenticateLength = + IkeHeader.IKE_HEADER_LENGTH + + IkePayload.GENERIC_HEADER_LENGTH + + skfHeaderBytes.length; + ByteBuffer authenticatedSectionBuffer = ByteBuffer.allocate(dataToAuthenticateLength); + + byte[] paddedData = getPaddedData(dataToEncrypt, padding); + int encryptedPayloadLength = + IkePayload.GENERIC_HEADER_LENGTH + + skfHeaderBytes.length + + iv.length + + paddedData.length + + encryptCipher.getChecksumLen(); + ikeHeader.encodeToByteBuffer(authenticatedSectionBuffer, encryptedPayloadLength); + IkePayload.encodePayloadHeaderToByteBuffer( + firstPayloadType, encryptedPayloadLength, authenticatedSectionBuffer); + authenticatedSectionBuffer.put(skfHeaderBytes); + + return encryptCipher.encrypt( + paddedData, authenticatedSectionBuffer.array(), encryptionKey, iv); + } + + /** Package private for testing */ + @VisibleForTesting + static byte[] combinedModeDecrypt( + IkeCombinedModeCipher decryptCipher, + byte[] encryptedData, + byte[] checksum, + byte[] dataToAuthenticate, + byte[] decryptionKey, + byte[] iv) + throws AEADBadTagException { + ByteBuffer dataWithChecksumBuffer = + ByteBuffer.allocate(encryptedData.length + checksum.length); + dataWithChecksumBuffer.put(encryptedData); + dataWithChecksumBuffer.put(checksum); + dataWithChecksumBuffer.rewind(); + + byte[] paddedPlaintext = + decryptCipher.decrypt( + dataWithChecksumBuffer.array(), dataToAuthenticate, decryptionKey, iv); + + return stripPadding(paddedPlaintext); + } + + /** Package private for testing */ + @VisibleForTesting + static byte[] calculatePadding(int dataToEncryptLength, int blockSize) { + // Sum of dataToEncryptLength, PAD_LEN_LEN and padLength should be aligned with block size. + int unpaddedLen = dataToEncryptLength + PAD_LEN_LEN; + int padLength = (unpaddedLen + blockSize - 1) / blockSize * blockSize - unpaddedLen; + byte[] padding = new byte[padLength]; + + // According to RFC 7296, "Padding MAY contain any value". + new SecureRandom().nextBytes(padding); + + return padding; + } + + private static byte[] getPaddedData(byte[] data, byte[] padding) { + int padLength = padding.length; + int paddedDataLength = data.length + padLength + PAD_LEN_LEN; + ByteBuffer padBuffer = ByteBuffer.allocate(paddedDataLength); + padBuffer.put(data).put(padding).put((byte) padLength); + + return padBuffer.array(); + } + + private static byte[] stripPadding(byte[] paddedPlaintext) { + // Remove padding. Pad length value is the last byte of the padded unencrypted data. + int padLength = Byte.toUnsignedInt(paddedPlaintext[paddedPlaintext.length - 1]); + int decryptedDataLen = paddedPlaintext.length - padLength - PAD_LEN_LEN; + + return Arrays.copyOfRange(paddedPlaintext, 0, decryptedDataLen); + } + + /** Package private */ + byte[] getUnencryptedData() { + return mUnencryptedData; + } + + /** Package private */ + int getLength() { + return (mIv.length + mEncryptedAndPaddedData.length + mIntegrityChecksum.length); + } + + /** Package private */ + byte[] encode() { + ByteBuffer buffer = ByteBuffer.allocate(getLength()); + buffer.put(mIv).put(mEncryptedAndPaddedData).put(mIntegrityChecksum); + return buffer.array(); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeHeader.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeHeader.java new file mode 100644 index 00000000..7aa4fbc8 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeHeader.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2018 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.message; + +import static com.android.internal.net.ipsec.ike.message.IkePayload.PayloadType; + +import android.annotation.IntDef; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.ipsec.ike.exceptions.InvalidMajorVersionException; +import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; + +/** + * IkeHeader represents an IKE message header. It contains all header attributes and provide methods + * for encoding and decoding it. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.1">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeHeader { + // TODO: b/122838549 Change IkeHeader to static inner class of IkeMessage. + private static final byte IKE_HEADER_VERSION_INFO = (byte) 0x20; + + // Indicate whether this message is a response message + private static final byte IKE_HEADER_FLAG_IS_RESP_MSG = (byte) 0x20; + // Indicate whether this message is sent from the original IKE initiator + private static final byte IKE_HEADER_FLAG_FROM_IKE_INITIATOR = (byte) 0x08; + + private static final SparseArray<String> EXCHANGE_TYPE_TO_STRING; + + public static final int IKE_HEADER_LENGTH = 28; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + EXCHANGE_TYPE_IKE_SA_INIT, + EXCHANGE_TYPE_IKE_AUTH, + EXCHANGE_TYPE_CREATE_CHILD_SA, + EXCHANGE_TYPE_INFORMATIONAL + }) + public @interface ExchangeType {} + + public static final int EXCHANGE_TYPE_IKE_SA_INIT = 34; + public static final int EXCHANGE_TYPE_IKE_AUTH = 35; + public static final int EXCHANGE_TYPE_CREATE_CHILD_SA = 36; + public static final int EXCHANGE_TYPE_INFORMATIONAL = 37; + + static { + EXCHANGE_TYPE_TO_STRING = new SparseArray<>(); + EXCHANGE_TYPE_TO_STRING.put(EXCHANGE_TYPE_IKE_SA_INIT, "IKE INIT"); + EXCHANGE_TYPE_TO_STRING.put(EXCHANGE_TYPE_IKE_AUTH, "IKE AUTH"); + EXCHANGE_TYPE_TO_STRING.put(EXCHANGE_TYPE_CREATE_CHILD_SA, "Create Child"); + EXCHANGE_TYPE_TO_STRING.put(EXCHANGE_TYPE_INFORMATIONAL, "Informational"); + } + + public final long ikeInitiatorSpi; + public final long ikeResponderSpi; + @PayloadType public final int nextPayloadType; + public final byte majorVersion; + public final byte minorVersion; + @ExchangeType public final int exchangeType; + public final boolean isResponseMsg; + public final boolean fromIkeInitiator; + public final int messageId; + + // Cannot assign encoded message length value for an outbound IKE message before it's encoded. + private static final int ENCODED_MESSAGE_LEN_UNAVAILABLE = -1; + + // mEncodedMessageLength is only set for an inbound IkeMessage. When building an outbound + // IkeMessage, message length is not set because message body length is unknown until it gets + // encrypted and encoded. + private final int mEncodedMessageLength; + + /** + * Construct an instance of IkeHeader. It is only called in the process of building outbound + * message. + * + * @param iSpi the SPI of IKE initiator + * @param rSpi the SPI of IKE responder + * @param nextPType the first payload's type + * @param eType the type of IKE exchange being used + * @param isResp indicates if this message is a response or a request + * @param fromInit indictaes if this message is sent from the IKE initiator or the IKE responder + * @param msgId the message identifier + */ + public IkeHeader( + long iSpi, + long rSpi, + @PayloadType int nextPType, + @ExchangeType int eType, + boolean isResp, + boolean fromInit, + int msgId) { + ikeInitiatorSpi = iSpi; + ikeResponderSpi = rSpi; + nextPayloadType = nextPType; + exchangeType = eType; + isResponseMsg = isResp; + fromIkeInitiator = fromInit; + messageId = msgId; + + mEncodedMessageLength = ENCODED_MESSAGE_LEN_UNAVAILABLE; + + // Major version of IKE protocol in use; it must be set to 2 when building an IKEv2 message. + majorVersion = 2; + // Minor version of IKE protocol in use; it must be set to 0 when building an IKEv2 message. + minorVersion = 0; + } + + /** + * Decode IKE header from a byte array and construct an IkeHeader instance. + * + * @param packet the raw byte array of the whole IKE message + */ + public IkeHeader(byte[] packet) throws IkeProtocolException { + if (packet.length <= IKE_HEADER_LENGTH) { + throw new InvalidSyntaxException("IKE message is too short to contain a header"); + } + + ByteBuffer buffer = ByteBuffer.wrap(packet); + + ikeInitiatorSpi = buffer.getLong(); + ikeResponderSpi = buffer.getLong(); + nextPayloadType = Byte.toUnsignedInt(buffer.get()); + + byte versionByte = buffer.get(); + majorVersion = (byte) ((versionByte >> 4) & 0x0F); + minorVersion = (byte) (versionByte & 0x0F); + + exchangeType = Byte.toUnsignedInt(buffer.get()); + + byte flagsByte = buffer.get(); + isResponseMsg = ((flagsByte & 0x20) != 0); + fromIkeInitiator = ((flagsByte & 0x08) != 0); + + messageId = buffer.getInt(); + mEncodedMessageLength = buffer.getInt(); + } + + /** Packet private method to build header of an IKE fragemnt from current IKE header. */ + IkeHeader makeSkfHeaderFromSkHeader() { + if (nextPayloadType != IkePayload.PAYLOAD_TYPE_SK) { + throw new IllegalArgumentException("Next payload type is not SK."); + } + return new IkeHeader( + ikeInitiatorSpi, + ikeResponderSpi, + IkePayload.PAYLOAD_TYPE_SKF, + exchangeType, + isResponseMsg, + fromIkeInitiator, + messageId); + } + + /*Package private*/ + @VisibleForTesting + int getInboundMessageLength() { + if (mEncodedMessageLength == ENCODED_MESSAGE_LEN_UNAVAILABLE) { + throw new UnsupportedOperationException( + "It is not supported to get encoded message length from an outbound message."); + } + return mEncodedMessageLength; + } + + /** Validate major version of inbound IKE header. */ + public void validateMajorVersion() throws IkeProtocolException { + if (majorVersion > 2) { + // Receive higher version of protocol. Stop parsing. + throw new InvalidMajorVersionException(majorVersion); + } + if (majorVersion < 2) { + // There is no specific instruction for dealing with this error case. + // Since IKE library only supports IKEv2 and not allowed to check if message + // sender supports higher version, it is proper to treat this error as an invalid syntax + // error. + throw new InvalidSyntaxException("Major version is smaller than 2."); + } + } + + /** + * Validate syntax of inbound IKE header. + * + * <p>It MUST only be used for an inbound IKE header because we don't know the outbound message + * length before we encode it. + */ + public void validateInboundHeader(int packetLength) throws IkeProtocolException { + if (exchangeType < EXCHANGE_TYPE_IKE_SA_INIT + || exchangeType > EXCHANGE_TYPE_INFORMATIONAL) { + throw new InvalidSyntaxException("Invalid IKE Exchange Type."); + } + if (mEncodedMessageLength != packetLength) { + throw new InvalidSyntaxException("Invalid IKE Message Length."); + } + } + + /** Encode IKE header to ByteBuffer */ + public void encodeToByteBuffer(ByteBuffer byteBuffer, int encodedMessageBodyLen) { + byteBuffer + .putLong(ikeInitiatorSpi) + .putLong(ikeResponderSpi) + .put((byte) nextPayloadType) + .put(IKE_HEADER_VERSION_INFO) + .put((byte) exchangeType); + + byte flag = 0; + if (isResponseMsg) { + flag |= IKE_HEADER_FLAG_IS_RESP_MSG; + } + if (fromIkeInitiator) { + flag |= IKE_HEADER_FLAG_FROM_IKE_INITIATOR; + } + + byteBuffer.put(flag).putInt(messageId).putInt(IKE_HEADER_LENGTH + encodedMessageBodyLen); + } + + /** Returns basic information for logging. */ + public String getBasicInfoString() { + String exchangeStr = EXCHANGE_TYPE_TO_STRING.get(exchangeType); + if (exchangeStr == null) exchangeStr = "Unknown exchange (" + exchangeType + ")"; + + String reqOrResp = isResponseMsg ? "response" : "request"; + + return exchangeStr + " " + reqOrResp + " " + messageId; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeIdPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeIdPayload.java new file mode 100644 index 00000000..12cc14ab --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeIdPayload.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2018 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.message; + +import android.net.ipsec.ike.IkeFqdnIdentification; +import android.net.ipsec.ike.IkeIdentification; +import android.net.ipsec.ike.IkeIpv4AddrIdentification; +import android.net.ipsec.ike.IkeIpv6AddrIdentification; +import android.net.ipsec.ike.IkeKeyIdIdentification; +import android.net.ipsec.ike.IkeRfc822AddrIdentification; +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.net.ipsec.ike.exceptions.AuthenticationFailedException; +import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException; + +import java.nio.ByteBuffer; + +/** + * IkeIdPayload represents an Identification Initiator Payload or an Identification Responder + * Payload. + * + * <p>Identification Initiator Payload and Identification Responder Payload have same format but + * different payload type. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.5">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeIdPayload extends IkePayload { + // Length of ID Payload header in octets. + private static final int ID_HEADER_LEN = 4; + // Length of reserved field in octets. + private static final int ID_HEADER_RESERVED_LEN = 3; + + public final IkeIdentification ikeId; + + /** + * Construct IkeIdPayload for received IKE packet in the context of {@link IkePayloadFactory}. + * + * @param critical indicates if it is a critical payload. + * @param payloadBody payload body in byte array. + * @param isInitiator indicates whether this payload contains the ID of IKE initiator or IKE + * responder. + * @throws IkeProtocolException for decoding error. + */ + IkeIdPayload(boolean critical, byte[] payloadBody, boolean isInitiator) + throws IkeProtocolException { + super((isInitiator ? PAYLOAD_TYPE_ID_INITIATOR : PAYLOAD_TYPE_ID_RESPONDER), critical); + // TODO: b/119791832 Add helper method for checking payload body length in superclass. + if (payloadBody.length <= ID_HEADER_LEN) { + throw new InvalidSyntaxException(getTypeString() + " is too short."); + } + + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + int idType = Byte.toUnsignedInt(inputBuffer.get()); + + // Skip reserved field + inputBuffer.get(new byte[ID_HEADER_RESERVED_LEN]); + + byte[] idData = new byte[payloadBody.length - ID_HEADER_LEN]; + inputBuffer.get(idData); + + switch (idType) { + case IkeIdentification.ID_TYPE_IPV4_ADDR: + ikeId = new IkeIpv4AddrIdentification(idData); + return; + case IkeIdentification.ID_TYPE_FQDN: + ikeId = new IkeFqdnIdentification(idData); + return; + case IkeIdentification.ID_TYPE_RFC822_ADDR: + ikeId = new IkeRfc822AddrIdentification(idData); + return; + case IkeIdentification.ID_TYPE_IPV6_ADDR: + ikeId = new IkeIpv6AddrIdentification(idData); + return; + case IkeIdentification.ID_TYPE_DER_ASN1_DN: // Fall through + case IkeIdentification.ID_TYPE_DER_ASN1_GN: + throw new UnsupportedOperationException("ID type is not supported currently."); + case IkeIdentification.ID_TYPE_KEY_ID: + ikeId = new IkeKeyIdIdentification(idData); + return; + default: + throw new AuthenticationFailedException("Unsupported ID type: " + idType); + } + } + + /** + * Construct IkeIdPayload for an outbound IKE packet. + * + * @param isInitiator indicates whether this payload contains the ID of IKE initiator or IKE + * responder. + * @param ikeId the IkeIdentification. + */ + public IkeIdPayload(boolean isInitiator, IkeIdentification ikeId) { + super((isInitiator ? PAYLOAD_TYPE_ID_INITIATOR : PAYLOAD_TYPE_ID_RESPONDER), false); + this.ikeId = ikeId; + } + + /** + * Get encoded ID payload body for building or validating an Auth Payload. + * + * @return the byte array of encoded ID payload body. + */ + public byte[] getEncodedPayloadBody() { + ByteBuffer byteBuffer = ByteBuffer.allocate(getPayloadLength() - GENERIC_HEADER_LENGTH); + + byteBuffer + .put((byte) ikeId.idType) + .put(new byte[ID_HEADER_RESERVED_LEN]) + .put(ikeId.getEncodedIdData()); + return byteBuffer.array(); + } + + /** + * Encode Identification Payload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + byteBuffer.put(getEncodedPayloadBody()); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + ID_HEADER_LEN + ikeId.getEncodedIdData().length; + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + switch (payloadType) { + case PAYLOAD_TYPE_ID_INITIATOR: + return "IDi"; + case PAYLOAD_TYPE_ID_RESPONDER: + return "IDr"; + default: + // Won't reach here. + throw new IllegalArgumentException( + "Invalid Payload Type for Identification Payload."); + } + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeInformationalPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeInformationalPayload.java new file mode 100644 index 00000000..606c38f8 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeInformationalPayload.java @@ -0,0 +1,28 @@ +/* + * 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.message; + +/** + * IkeInformationalPayload abstracts all Payloads sent in INFORMATIONAL exchanges. + * + * <p>This class is a non-RFC payload for implementation simplicity. + */ +public abstract class IkeInformationalPayload extends IkePayload { + IkeInformationalPayload(int payloadType, boolean isCritical) { + super(payloadType, isCritical); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeKePayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeKePayload.java new file mode 100644 index 00000000..7389bf8d --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeKePayload.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2018 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.message; + +import android.annotation.Nullable; +import android.net.ipsec.ike.SaProposal; +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.net.ipsec.ike.IkeDhParams; +import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException; +import com.android.internal.net.utils.BigIntegerUtils; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.ProviderException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.KeyAgreement; +import javax.crypto.interfaces.DHPrivateKey; +import javax.crypto.interfaces.DHPublicKey; +import javax.crypto.spec.DHParameterSpec; +import javax.crypto.spec.DHPrivateKeySpec; +import javax.crypto.spec.DHPublicKeySpec; + +/** + * IkeKePayload represents a Key Exchange payload + * + * <p>This class provides methods for generating Diffie-Hellman value and doing Diffie-Hellman + * exhchange. Upper layer should ignore IkeKePayload with unsupported DH group type. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#page-89">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeKePayload extends IkePayload { + private static final int KE_HEADER_LEN = 4; + private static final int KE_HEADER_RESERVED = 0; + + // Key exchange data length in octets + private static final int DH_GROUP_1024_BIT_MODP_DATA_LEN = 128; + private static final int DH_GROUP_2048_BIT_MODP_DATA_LEN = 256; + + // Algorithm name of Diffie-Hellman + private static final String KEY_EXCHANGE_ALGORITHM = "DH"; + + // TODO: Create a library initializer that checks if Provider supports DH algorithm. + + /** Supported dhGroup falls into {@link DhGroup} */ + public final int dhGroup; + + /** Public DH key for the recipient to calculate shared key. */ + public final byte[] keyExchangeData; + + /** Flag indicates if this is an outbound payload. */ + public final boolean isOutbound; + + /** + * localPrivateKey caches the locally generated private key when building an outbound KE + * payload. It will not be sent out. It is only used to calculate DH shared key when IKE library + * receives a public key from the remote server. + * + * <p>localPrivateKey of a inbound payload will be set to null. Caller MUST ensure its an + * outbound payload before using localPrivateKey. + */ + @Nullable public final DHPrivateKeySpec localPrivateKey; + + /** + * Construct an instance of IkeKePayload in the context of IkePayloadFactory + * + * @param critical indicates if this payload is critical. Ignored in supported payload as + * instructed by the RFC 7296. + * @param payloadBody payload body in byte array + * @throws IkeProtocolException if there is any error + * @see <a href="https://tools.ietf.org/html/rfc7296#page-76">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2), Critical. + */ + IkeKePayload(boolean critical, byte[] payloadBody) throws IkeProtocolException { + super(PAYLOAD_TYPE_KE, critical); + + isOutbound = false; + localPrivateKey = null; + + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + + dhGroup = Short.toUnsignedInt(inputBuffer.getShort()); + // Skip reserved field + inputBuffer.getShort(); + + int dataSize = payloadBody.length - KE_HEADER_LEN; + // Check if dataSize matches the DH group type + boolean isValidSyntax = true; + switch (dhGroup) { + case SaProposal.DH_GROUP_1024_BIT_MODP: + isValidSyntax = DH_GROUP_1024_BIT_MODP_DATA_LEN == dataSize; + break; + case SaProposal.DH_GROUP_2048_BIT_MODP: + isValidSyntax = DH_GROUP_2048_BIT_MODP_DATA_LEN == dataSize; + break; + default: + // For unsupported DH group, we cannot check its syntax. Upper layer will ingore + // this payload. + } + if (!isValidSyntax) { + throw new InvalidSyntaxException("Invalid KE payload length for provided DH group."); + } + + keyExchangeData = new byte[dataSize]; + inputBuffer.get(keyExchangeData); + } + + /** + * Construct an instance of IkeKePayload for building an outbound packet. + * + * <p>Generate a DH key pair. Cache the private key and and send out the public key as + * keyExchangeData. + * + * <p>Critical bit in this payload must not be set as instructed in RFC 7296. + * + * @param dh DH group for this KE payload + * @see <a href="https://tools.ietf.org/html/rfc7296#page-76">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2), Critical. + */ + public IkeKePayload(@SaProposal.DhGroup int dh) { + super(PAYLOAD_TYPE_KE, false); + + dhGroup = dh; + isOutbound = true; + + BigInteger prime = BigInteger.ZERO; + int keySize = 0; + switch (dhGroup) { + case SaProposal.DH_GROUP_1024_BIT_MODP: + prime = + BigIntegerUtils.unsignedHexStringToBigInteger( + IkeDhParams.PRIME_1024_BIT_MODP); + keySize = DH_GROUP_1024_BIT_MODP_DATA_LEN; + break; + case SaProposal.DH_GROUP_2048_BIT_MODP: + prime = + BigIntegerUtils.unsignedHexStringToBigInteger( + IkeDhParams.PRIME_2048_BIT_MODP); + keySize = DH_GROUP_2048_BIT_MODP_DATA_LEN; + break; + default: + throw new IllegalArgumentException("DH group not supported: " + dh); + } + + try { + BigInteger baseGen = BigInteger.valueOf(IkeDhParams.BASE_GENERATOR_MODP); + DHParameterSpec dhParams = new DHParameterSpec(prime, baseGen); + + KeyPairGenerator dhKeyPairGen = + KeyPairGenerator.getInstance( + KEY_EXCHANGE_ALGORITHM, IkeMessage.getSecurityProvider()); + // By default SecureRandom uses AndroidOpenSSL provided SHA1PRNG Algorithm, which takes + // /dev/urandom as seed source. + dhKeyPairGen.initialize(dhParams, new SecureRandom()); + + KeyPair keyPair = dhKeyPairGen.generateKeyPair(); + + DHPrivateKey privateKey = (DHPrivateKey) keyPair.getPrivate(); + DHPrivateKeySpec dhPrivateKeyspec = + new DHPrivateKeySpec(privateKey.getX(), prime, baseGen); + DHPublicKey publicKey = (DHPublicKey) keyPair.getPublic(); + + // Zero-pad the public key without the sign bit + keyExchangeData = + BigIntegerUtils.bigIntegerToUnsignedByteArray(publicKey.getY(), keySize); + localPrivateKey = dhPrivateKeyspec; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException("Failed to obtain " + KEY_EXCHANGE_ALGORITHM, e); + } catch (InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException("Failed to initialize key generator", e); + } + } + + /** + * Encode KE payload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + byteBuffer + .putShort((short) dhGroup) + .putShort((short) KE_HEADER_RESERVED) + .put(keyExchangeData); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + KE_HEADER_LEN + keyExchangeData.length; + } + + /** + * Calculate the shared secret. + * + * @param privateKeySpec contains the local private key, DH prime and DH base generator. + * @param remotePublicKey the public key from remote server. + * @throws GeneralSecurityException if the remote public key is invalid. + */ + public static byte[] getSharedKey(DHPrivateKeySpec privateKeySpec, byte[] remotePublicKey) + throws GeneralSecurityException { + KeyAgreement dhKeyAgreement; + KeyFactory dhKeyFactory; + try { + // Apply local private key. + dhKeyAgreement = + KeyAgreement.getInstance( + KEY_EXCHANGE_ALGORITHM, IkeMessage.getSecurityProvider()); + dhKeyFactory = + KeyFactory.getInstance( + KEY_EXCHANGE_ALGORITHM, IkeMessage.getSecurityProvider()); + DHPrivateKey privateKey = (DHPrivateKey) dhKeyFactory.generatePrivate(privateKeySpec); + dhKeyAgreement.init(privateKey); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException e) { + throw new IllegalArgumentException("Failed to generate DH private key", e); + } + + // Build public key. + BigInteger publicKeyValue = BigIntegerUtils.unsignedByteArrayToBigInteger(remotePublicKey); + BigInteger primeValue = privateKeySpec.getP(); + BigInteger baseGenValue = privateKeySpec.getG(); + DHPublicKeySpec publicKeySpec = + new DHPublicKeySpec(publicKeyValue, primeValue, baseGenValue); + + // Validate and apply public key. Validation includes range check as instructed by RFC6989 + // section 2.1 + DHPublicKey publicKey = (DHPublicKey) dhKeyFactory.generatePublic(publicKeySpec); + + dhKeyAgreement.doPhase(publicKey, true /* Last phase */); + return dhKeyAgreement.generateSecret(); + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "KE"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeMessage.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeMessage.java new file mode 100644 index 00000000..27fb9651 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeMessage.java @@ -0,0 +1,981 @@ +/* + * Copyright (C) 2018 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.message; + +import static android.net.ipsec.ike.IkeManager.getIkeLog; + +import static com.android.internal.net.ipsec.ike.message.IkePayload.PayloadType; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.net.ipsec.ike.exceptions.IkeException; +import android.net.ipsec.ike.exceptions.IkeInternalException; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.util.Pair; + +import com.android.internal.annotations.VisibleForTesting; +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.exceptions.InvalidMessageIdException; +import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException; +import com.android.internal.net.ipsec.ike.exceptions.UnsupportedCriticalPayloadException; +import com.android.org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * IkeMessage represents an IKE message. + * + * <p>It contains all attributes and provides methods for encoding, decoding, encrypting and + * decrypting. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeMessage { + private static final String TAG = "IkeMessage"; + + private static IIkeMessageHelper sIkeMessageHelper = new IkeMessageHelper(); + + // Currently use Bouncy Castle as crypto security provider + static final Provider SECURITY_PROVIDER = new BouncyCastleProvider(); + + // TODO: b/142070035 Use Conscrypt as default security provider instead of BC + + // Currently use HarmonyJSSE as TrustManager provider + static final Provider TRUST_MANAGER_PROVIDER = Security.getProvider("HarmonyJSSE"); + + // Payload types in this set may be included multiple times within an IKE message. All other + // payload types can be included at most once. + private static final Set<Integer> REPEATABLE_PAYLOAD_TYPES = new HashSet<>(); + + static { + REPEATABLE_PAYLOAD_TYPES.add(IkePayload.PAYLOAD_TYPE_CERT); + REPEATABLE_PAYLOAD_TYPES.add(IkePayload.PAYLOAD_TYPE_CERT_REQUEST); + REPEATABLE_PAYLOAD_TYPES.add(IkePayload.PAYLOAD_TYPE_NOTIFY); + REPEATABLE_PAYLOAD_TYPES.add(IkePayload.PAYLOAD_TYPE_DELETE); + REPEATABLE_PAYLOAD_TYPES.add(IkePayload.PAYLOAD_TYPE_VENDOR); + } + + public final IkeHeader ikeHeader; + public final List<IkePayload> ikePayloadList; + /** + * Conctruct an instance of IkeMessage. It is called by decode or for building outbound message. + * + * @param header the header of this IKE message + * @param payloadList the list of decoded IKE payloads in this IKE message + */ + public IkeMessage(IkeHeader header, List<IkePayload> payloadList) { + ikeHeader = header; + ikePayloadList = payloadList; + } + + /** + * Get security provider for IKE library + * + * <p>Use BouncyCastleProvider as the default security provider. + * + * @return the security provider of IKE library. + */ + public static Provider getSecurityProvider() { + // TODO: Move this getter out of IKE message package since not only this package uses it. + return SECURITY_PROVIDER; + } + + /** + * Get security provider for X509TrustManager to do certificate validation. + * + * <p>Use JSSEProvdier as the default security provider. + * + * @return the provider for X509TrustManager + */ + public static Provider getTrustManagerProvider() { + return TRUST_MANAGER_PROVIDER; + } + + /** + * Decode unencrypted IKE message body and create an instance of IkeMessage. + * + * <p>This method catches all RuntimeException during decoding incoming IKE packet. + * + * @param expectedMsgId the expected message ID to validate against. + * @param header the IKE header that is decoded but not validated. + * @param inputPacket the byte array contains the whole IKE message. + * @return the decoding result. + */ + public static DecodeResult decode(int expectedMsgId, IkeHeader header, byte[] inputPacket) { + return sIkeMessageHelper.decode(expectedMsgId, header, inputPacket); + } + + /** + * Decrypt and decode encrypted IKE message body and create an instance of IkeMessage. + * + * @param expectedMsgId the expected message ID to validate against. + * @param integrityMac the negotiated integrity algorithm. + * @param decryptCipher the negotiated encryption algorithm. + * @param ikeSaRecord ikeSaRecord where this packet is sent on. + * @param ikeHeader header of IKE packet. + * @param packet IKE packet as a byte array. + * @param collectedFragments previously received IKE fragments. + * @return the decoding result. + */ + public static DecodeResult decode( + int expectedMsgId, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + IkeSaRecord ikeSaRecord, + IkeHeader ikeHeader, + byte[] packet, + DecodeResultPartial collectedFragments) { + return sIkeMessageHelper.decode( + expectedMsgId, + integrityMac, + decryptCipher, + ikeSaRecord, + ikeHeader, + packet, + collectedFragments); + } + + private static List<IkePayload> decodePayloadList( + @PayloadType int firstPayloadType, boolean isResp, byte[] unencryptedPayloads) + throws IkeProtocolException { + ByteBuffer inputBuffer = ByteBuffer.wrap(unencryptedPayloads); + int currentPayloadType = firstPayloadType; + // For supported payload + List<IkePayload> supportedPayloadList = new LinkedList<>(); + // For unsupported critical payload + List<Integer> unsupportedCriticalPayloadList = new LinkedList<>(); + + // For marking the existence of supported payloads in this message. + HashSet<Integer> supportedTypesFoundSet = new HashSet<>(); + + StringBuilder logPayloadsSb = new StringBuilder(); + logPayloadsSb.append("Decoded payloads [ "); + + while (currentPayloadType != IkePayload.PAYLOAD_TYPE_NO_NEXT) { + Pair<IkePayload, Integer> pair = + IkePayloadFactory.getIkePayload(currentPayloadType, isResp, inputBuffer); + IkePayload payload = pair.first; + logPayloadsSb.append(payload.getTypeString()).append(" "); + + if (!(payload instanceof IkeUnsupportedPayload)) { + int type = payload.payloadType; + if (!supportedTypesFoundSet.add(type) && !REPEATABLE_PAYLOAD_TYPES.contains(type)) { + throw new InvalidSyntaxException( + "It is not allowed to have multiple payloads with payload type: " + + type); + } + + supportedPayloadList.add(payload); + } else if (payload.isCritical) { + unsupportedCriticalPayloadList.add(payload.payloadType); + } + // Simply ignore unsupported uncritical payload. + + currentPayloadType = pair.second; + } + + logPayloadsSb.append("]"); + getIkeLog().d("IkeMessage", logPayloadsSb.toString()); + + if (inputBuffer.remaining() > 0) { + throw new InvalidSyntaxException( + "Malformed IKE Payload: Unexpected bytes at the end of packet."); + } + + if (unsupportedCriticalPayloadList.size() > 0) { + throw new UnsupportedCriticalPayloadException(unsupportedCriticalPayloadList); + } + + // TODO: Verify that for all status notification payloads, only + // NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP and NOTIFY_TYPE_IPCOMP_SUPPORTED can be included + // multiple times in a request message. There is not a clear number restriction for + // error notification payloads. + + return supportedPayloadList; + } + + /** + * Encode unencrypted IKE message. + * + * @return encoded IKE message in byte array. + */ + public byte[] encode() { + return sIkeMessageHelper.encode(this); + } + + /** + * Encrypt and encode packet. + * + * @param integrityMac the negotiated integrity algorithm. + * @param encryptCipher the negotiated encryption algortihm. + * @param ikeSaRecord the ikeSaRecord where this packet is sent on. + * @param supportFragment if IKE fragmentation is supported + * @param fragSize the maximum size of IKE fragment + * @return encoded IKE message in byte array. + */ + public byte[][] encryptAndEncode( + @Nullable IkeMacIntegrity integrityMac, + IkeCipher encryptCipher, + IkeSaRecord ikeSaRecord, + boolean supportFragment, + int fragSize) { + return sIkeMessageHelper.encryptAndEncode( + integrityMac, encryptCipher, ikeSaRecord, this, supportFragment, fragSize); + } + + /** + * Encode all payloads to a byte array. + * + * @return byte array contains all encoded payloads + */ + private byte[] encodePayloads() { + StringBuilder logPayloadsSb = new StringBuilder(); + logPayloadsSb.append("Generating payloads [ "); + + int payloadLengthSum = 0; + for (IkePayload payload : ikePayloadList) { + payloadLengthSum += payload.getPayloadLength(); + logPayloadsSb.append(payload.getTypeString()).append(" "); + } + logPayloadsSb.append("]"); + getIkeLog().d("IkeMessage", logPayloadsSb.toString()); + + if (ikePayloadList.isEmpty()) return new byte[0]; + + ByteBuffer byteBuffer = ByteBuffer.allocate(payloadLengthSum); + for (int i = 0; i < ikePayloadList.size() - 1; i++) { + ikePayloadList + .get(i) + .encodeToByteBuffer(ikePayloadList.get(i + 1).payloadType, byteBuffer); + } + ikePayloadList + .get(ikePayloadList.size() - 1) + .encodeToByteBuffer(IkePayload.PAYLOAD_TYPE_NO_NEXT, byteBuffer); + + return byteBuffer.array(); + } + + /** Package */ + @VisibleForTesting + byte[] attachEncodedHeader(byte[] encodedIkeBody) { + ByteBuffer outputBuffer = + ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH + encodedIkeBody.length); + ikeHeader.encodeToByteBuffer(outputBuffer, encodedIkeBody.length); + outputBuffer.put(encodedIkeBody); + return outputBuffer.array(); + } + + /** + * Obtain all payloads with input payload type. + * + * <p>This method can be only applied to the payload types that can be included multiple times + * within an IKE message. + * + * @param payloadType the payloadType to look for. + * @param payloadClass the class of the desired payloads. + * @return a list of IkePayloads with the payloadType. + */ + public <T extends IkePayload> List<T> getPayloadListForType( + @IkePayload.PayloadType int payloadType, Class<T> payloadClass) { + // STOPSHIP: b/130190639 Notify user the error and close IKE session. + if (!REPEATABLE_PAYLOAD_TYPES.contains(payloadType)) { + throw new IllegalArgumentException( + "Received unexpected payloadType: " + + payloadType + + " that can be included at most once within an IKE message."); + } + + return IkePayload.getPayloadListForTypeInProvidedList( + payloadType, payloadClass, ikePayloadList); + } + + /** + * Obtain the payload with the input payload type. + * + * <p>This method can be only applied to the payload type that can be included at most once + * within an IKE message. + * + * @param payloadType the payloadType to look for. + * @param payloadClass the class of the desired payload. + * @return the IkePayload with the payloadType. + */ + public <T extends IkePayload> T getPayloadForType( + @IkePayload.PayloadType int payloadType, Class<T> payloadClass) { + // STOPSHIP: b/130190639 Notify user the error and close IKE session. + if (REPEATABLE_PAYLOAD_TYPES.contains(payloadType)) { + throw new IllegalArgumentException( + "Received unexpected payloadType: " + + payloadType + + " that may be included multiple times within an IKE message."); + } + + return IkePayload.getPayloadForTypeInProvidedList( + payloadType, payloadClass, ikePayloadList); + } + + /** + * Checks if this Request IkeMessage was a DPD message + * + * <p>An IKE message is a DPD request iff the message was encrypted (has a SK payload) and there + * were no payloads within the SK payload (or outside the SK payload). + */ + public boolean isDpdRequest() { + return !ikeHeader.isResponseMsg + && ikeHeader.exchangeType == IkeHeader.EXCHANGE_TYPE_INFORMATIONAL + && ikePayloadList.isEmpty() + && ikeHeader.nextPayloadType == IkePayload.PAYLOAD_TYPE_SK; + } + + /** + * IIkeMessageHelper provides interface for decoding, encoding and processing IKE packet. + * + * <p>IkeMessageHelper exists so that the interface is injectable for testing. + */ + @VisibleForTesting + public interface IIkeMessageHelper { + /** + * Encode IKE message. + * + * @param ikeMessage message need to be encoded. + * @return encoded IKE message in byte array. + */ + byte[] encode(IkeMessage ikeMessage); + + /** + * Encrypt and encode IKE message. + * + * @param integrityMac the negotiated integrity algorithm. + * @param encryptCipher the negotiated encryption algortihm. + * @param ikeSaRecord the ikeSaRecord where this packet is sent on. + * @param ikeMessage message need to be encoded. * @param supportFragment if IKE + * fragmentation is supported. + * @param fragSize the maximum size of IKE fragment. + * @return encoded IKE message in byte array. + */ + byte[][] encryptAndEncode( + @Nullable IkeMacIntegrity integrityMac, + IkeCipher encryptCipher, + IkeSaRecord ikeSaRecord, + IkeMessage ikeMessage, + boolean supportFragment, + int fragSize); + + // TODO: Return DecodeResult when decoding unencrypted message + /** + * Decode unencrypted packet. + * + * @param expectedMsgId the expected message ID to validate against. + * @param ikeHeader header of IKE packet. + * @param packet IKE packet as a byte array. + * @return the decoding result. + */ + DecodeResult decode(int expectedMsgId, IkeHeader ikeHeader, byte[] packet); + + /** + * Decrypt and decode packet. + * + * @param expectedMsgId the expected message ID to validate against. + * @param integrityMac the negotiated integrity algorithm. + * @param decryptCipher the negotiated encryption algorithm. + * @param ikeSaRecord ikeSaRecord where this packet is sent on. + * @param ikeHeader header of IKE packet. + * @param packet IKE packet as a byte array. + * @param collectedFragments previously received IKE fragments. + * @return the decoding result. + */ + DecodeResult decode( + int expectedMsgId, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + IkeSaRecord ikeSaRecord, + IkeHeader ikeHeader, + byte[] packet, + DecodeResultPartial collectedFragments); + } + + /** IkeMessageHelper provides methods for decoding, encoding and processing IKE packet. */ + public static final class IkeMessageHelper implements IIkeMessageHelper { + @Override + public byte[] encode(IkeMessage ikeMessage) { + getIkeLog().d("IkeMessage", "Generating " + ikeMessage.ikeHeader.getBasicInfoString()); + + byte[] encodedIkeBody = ikeMessage.encodePayloads(); + byte[] packet = ikeMessage.attachEncodedHeader(encodedIkeBody); + getIkeLog().d("IkeMessage", "Build a complete IKE message: " + getIkeLog().pii(packet)); + return packet; + } + + @Override + public byte[][] encryptAndEncode( + @Nullable IkeMacIntegrity integrityMac, + IkeCipher encryptCipher, + IkeSaRecord ikeSaRecord, + IkeMessage ikeMessage, + boolean supportFragment, + int fragSize) { + getIkeLog().d("IkeMessage", "Generating " + ikeMessage.ikeHeader.getBasicInfoString()); + + return encryptAndEncode( + ikeMessage.ikeHeader, + ikeMessage.ikePayloadList.isEmpty() + ? IkePayload.PAYLOAD_TYPE_NO_NEXT + : ikeMessage.ikePayloadList.get(0).payloadType, + ikeMessage.encodePayloads(), + integrityMac, + encryptCipher, + ikeSaRecord.getOutboundIntegrityKey(), + ikeSaRecord.getOutboundEncryptionKey(), + supportFragment, + fragSize); + } + + @VisibleForTesting + byte[][] encryptAndEncode( + IkeHeader ikeHeader, + @PayloadType int firstInnerPayload, + byte[] unencryptedPayloads, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher encryptCipher, + byte[] integrityKey, + byte[] encryptionKey, + boolean supportFragment, + int fragSize) { + + IkeSkPayload skPayload = + new IkeSkPayload( + ikeHeader, + firstInnerPayload, + unencryptedPayloads, + integrityMac, + encryptCipher, + integrityKey, + encryptionKey); + int msgLen = IkeHeader.IKE_HEADER_LENGTH + skPayload.getPayloadLength(); + + // Build complete IKE message + if (!supportFragment || msgLen <= fragSize) { + byte[][] packetList = new byte[1][]; + packetList[0] = encodeHeaderAndBody(ikeHeader, skPayload, firstInnerPayload); + + getIkeLog() + .d( + "IkeMessage", + "Build a complete IKE message: " + getIkeLog().pii(packetList[0])); + return packetList; + } + + // Build IKE fragments + int dataLenPerPacket = + fragSize + - IkeHeader.IKE_HEADER_LENGTH + - IkePayload.GENERIC_HEADER_LENGTH + - IkeSkfPayload.SKF_HEADER_LEN + - encryptCipher.getIvLen() + - integrityMac.getChecksumLen() + - encryptCipher.getBlockSize(); + + // Caller of this method MUST validate fragSize is valid. + if (dataLenPerPacket <= 0) { + throw new IllegalArgumentException( + "Max fragment size is too small for an IKE fragment."); + } + + int totalFragments = + (unencryptedPayloads.length + dataLenPerPacket - 1) / dataLenPerPacket; + IkeHeader skfHeader = ikeHeader.makeSkfHeaderFromSkHeader(); + byte[][] packetList = new byte[totalFragments][]; + + ByteBuffer unencryptedDataBuffer = ByteBuffer.wrap(unencryptedPayloads); + for (int i = 0; i < totalFragments; i++) { + byte[] unencryptedData = + new byte[Math.min(dataLenPerPacket, unencryptedDataBuffer.remaining())]; + unencryptedDataBuffer.get(unencryptedData); + + int fragNum = i + 1; // 1-based + + IkeSkfPayload skfPayload = + new IkeSkfPayload( + ikeHeader, + firstInnerPayload, + unencryptedData, + integrityMac, + encryptCipher, + integrityKey, + encryptionKey, + fragNum, + totalFragments); + + packetList[i] = + encodeHeaderAndBody( + skfHeader, + skfPayload, + i == 0 ? firstInnerPayload : IkePayload.PAYLOAD_TYPE_NO_NEXT); + getIkeLog() + .d( + "IkeMessage", + "Build an IKE fragment (" + + (i + 1) + + "/" + + totalFragments + + "): " + + getIkeLog().pii(packetList[0])); + } + + return packetList; + } + + private byte[] encodeHeaderAndBody( + IkeHeader ikeHeader, IkeSkPayload skPayload, @PayloadType int firstInnerPayload) { + ByteBuffer outputBuffer = + ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH + skPayload.getPayloadLength()); + ikeHeader.encodeToByteBuffer(outputBuffer, skPayload.getPayloadLength()); + skPayload.encodeToByteBuffer(firstInnerPayload, outputBuffer); + return outputBuffer.array(); + } + + @Override + public DecodeResult decode(int expectedMsgId, IkeHeader header, byte[] inputPacket) { + try { + if (header.messageId != expectedMsgId) { + throw new InvalidMessageIdException(header.messageId); + } + + header.validateMajorVersion(); + header.validateInboundHeader(inputPacket.length); + + byte[] unencryptedPayloads = + Arrays.copyOfRange( + inputPacket, IkeHeader.IKE_HEADER_LENGTH, inputPacket.length); + List<IkePayload> supportedPayloadList = + decodePayloadList( + header.nextPayloadType, header.isResponseMsg, unencryptedPayloads); + return new DecodeResultOk( + new IkeMessage(header, supportedPayloadList), inputPacket); + } catch (NegativeArraySizeException | BufferUnderflowException e) { + // Invalid length error when parsing payload bodies. + return new DecodeResultUnprotectedError( + new InvalidSyntaxException("Malformed IKE Payload")); + } catch (IkeProtocolException e) { + return new DecodeResultUnprotectedError(e); + } + } + + @Override + public DecodeResult decode( + int expectedMsgId, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + IkeSaRecord ikeSaRecord, + IkeHeader ikeHeader, + byte[] packet, + DecodeResultPartial collectedFragments) { + return decode( + expectedMsgId, + ikeHeader, + packet, + integrityMac, + decryptCipher, + ikeSaRecord.getInboundIntegrityKey(), + ikeSaRecord.getInboundDecryptionKey(), + collectedFragments); + } + + private DecodeResult decode( + int expectedMsgId, + IkeHeader header, + byte[] inputPacket, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + byte[] integrityKey, + byte[] decryptionKey, + DecodeResultPartial collectedFragments) { + if (header.nextPayloadType != IkePayload.PAYLOAD_TYPE_SK + && header.nextPayloadType != IkePayload.PAYLOAD_TYPE_SKF) { + // TODO: b/123372339 Handle message containing unprotected payloads. + throw new UnsupportedOperationException("Message contains unprotected payloads"); + } + + // Decrypt message and do authentication + Pair<IkeSkPayload, Integer> pair; + try { + pair = + decryptAndAuthenticate( + expectedMsgId, + header, + inputPacket, + integrityMac, + decryptCipher, + integrityKey, + decryptionKey); + } catch (IkeException e) { + if (collectedFragments == null) { + return new DecodeResultUnprotectedError(e); + } else { + getIkeLog() + .i( + TAG, + "Message authentication or decryption failed on received" + + " message. Discard it ", + e); + return collectedFragments; + } + } + + // Handle IKE fragment + boolean isFragment = (header.nextPayloadType == IkePayload.PAYLOAD_TYPE_SKF); + boolean fragReassemblyStarted = (collectedFragments != null); + + if (isFragment) { + getIkeLog() + .d( + TAG, + "Received an IKE fragment (" + + ((IkeSkfPayload) pair.first).fragmentNum + + "/" + + ((IkeSkfPayload) pair.first).totalFragments + + ")"); + } + + // IKE fragment reassembly has started but a complete message was received. + if (!isFragment && fragReassemblyStarted) { + getIkeLog() + .w( + TAG, + "Received a complete IKE message while doing IKE fragment" + + " reassembly. Discard the newly received message."); + return collectedFragments; + } + + byte[] firstPacket = inputPacket; + byte[] decryptedBytes = pair.first.getUnencryptedData(); + int firstPayloadType = pair.second; + + // Received an IKE fragment + if (isFragment) { + validateFragmentHeader(header, inputPacket.length, collectedFragments); + + // Add the recently received fragment to the reassembly queue. + DecodeResultPartial DecodeResultPartial = + processIkeFragment( + header, + inputPacket, + (IkeSkfPayload) (pair.first), + pair.second, + collectedFragments); + + if (!DecodeResultPartial.isAllFragmentsReceived()) return DecodeResultPartial; + + firstPayloadType = DecodeResultPartial.firstPayloadType; + decryptedBytes = DecodeResultPartial.reassembleAllFrags(); + firstPacket = DecodeResultPartial.firstFragBytes; + } + + // Received or has reassembled a complete IKE message. Check if there is protocol error. + try { + // TODO: Log IKE header information and payload types + + List<IkePayload> supportedPayloadList = + decodePayloadList(firstPayloadType, header.isResponseMsg, decryptedBytes); + + header.validateInboundHeader(inputPacket.length); + return new DecodeResultOk( + new IkeMessage(header, supportedPayloadList), firstPacket); + } catch (NegativeArraySizeException | BufferUnderflowException e) { + // Invalid length error when parsing payload bodies. + return new DecodeResultProtectedError( + new InvalidSyntaxException("Malformed IKE Payload", e), firstPacket); + } catch (IkeProtocolException e) { + return new DecodeResultProtectedError(e, firstPacket); + } + } + + private Pair<IkeSkPayload, Integer> decryptAndAuthenticate( + int expectedMsgId, + IkeHeader header, + byte[] inputPacket, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + byte[] integrityKey, + byte[] decryptionKey) + throws IkeException { + + try { + if (header.messageId != expectedMsgId) { + throw new InvalidMessageIdException(header.messageId); + } + + header.validateMajorVersion(); + + boolean isSkf = header.nextPayloadType == IkePayload.PAYLOAD_TYPE_SKF; + return IkePayloadFactory.getIkeSkPayload( + isSkf, + inputPacket, + integrityMac, + decryptCipher, + integrityKey, + decryptionKey); + } catch (NegativeArraySizeException | BufferUnderflowException e) { + throw new InvalidSyntaxException("Malformed IKE Payload", e); + } catch (GeneralSecurityException e) { + throw new IkeInternalException(e); + } + } + + private void validateFragmentHeader( + IkeHeader fragIkeHeader, int packetLen, DecodeResultPartial collectedFragments) { + try { + fragIkeHeader.validateInboundHeader(packetLen); + } catch (IkeProtocolException e) { + getIkeLog() + .e( + TAG, + "Received an IKE fragment with invalid header. Will be handled when" + + " reassembly is done.", + e); + } + + if (collectedFragments == null) return; + if (fragIkeHeader.exchangeType != collectedFragments.ikeHeader.exchangeType) { + getIkeLog() + .e( + TAG, + "Received an IKE fragment with different exchange type from" + + " previously collected fragments. Ignore it."); + } + } + + private DecodeResultPartial processIkeFragment( + IkeHeader header, + byte[] inputPacket, + IkeSkfPayload skf, + int nextPayloadType, + @Nullable DecodeResultPartial collectedFragments) { + if (collectedFragments == null) { + return new DecodeResultPartial( + header, inputPacket, skf, nextPayloadType, collectedFragments); + } + + if (skf.totalFragments > collectedFragments.collectedFragsList.length) { + getIkeLog() + .i( + TAG, + "Received IKE fragment has larger total fragments number. Discard" + + " all previously collected fragments"); + return new DecodeResultPartial( + header, inputPacket, skf, nextPayloadType, null /*collectedFragments*/); + } + + if (skf.totalFragments < collectedFragments.collectedFragsList.length) { + getIkeLog() + .i( + TAG, + "Received IKE fragment has smaller total fragments number. Discard" + + " it."); + return collectedFragments; + } + + if (collectedFragments.collectedFragsList[skf.fragmentNum - 1] != null) { + getIkeLog().i(TAG, "Received IKE fragment is a replay."); + return collectedFragments; + } + + return new DecodeResultPartial( + header, inputPacket, skf, nextPayloadType, collectedFragments); + } + } + + /** Status to describe the result of decoding an inbound IKE message. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DECODE_STATUS_OK, + DECODE_STATUS_PARTIAL, + DECODE_STATUS_PROTECTED_ERROR, + DECODE_STATUS_UNPROTECTED_ERROR, + }) + public @interface DecodeStatus {} + + /** + * Represents a message that has been successfully (decrypted and) decoded or reassembled from + * IKE fragments + */ + public static final int DECODE_STATUS_OK = 0; + /** Represents that reassembly process of IKE fragments has started but has not finished */ + public static final int DECODE_STATUS_PARTIAL = 1; + /** Represents a crypto protected message with correct message ID but has parsing error. */ + public static final int DECODE_STATUS_PROTECTED_ERROR = 2; + /** + * Represents an unencrypted message with parsing error, an encrypted message with + * authentication or decryption error, or any message with wrong message ID. + */ + public static final int DECODE_STATUS_UNPROTECTED_ERROR = 3; + + /** This class represents common decoding result of an IKE message. */ + public abstract static class DecodeResult { + public final int status; + + /** Construct an instance of DecodeResult. */ + protected DecodeResult(int status) { + this.status = status; + } + } + + /** This class represents an IKE message has been successfully (decrypted and) decoded. */ + public static class DecodeResultOk extends DecodeResult { + public final IkeMessage ikeMessage; + public final byte[] firstPacket; + + public DecodeResultOk(IkeMessage ikeMessage, byte[] firstPacket) { + super(DECODE_STATUS_OK); + this.ikeMessage = ikeMessage; + this.firstPacket = firstPacket; + } + } + + /** + * This class represents IKE fragments are being reassembled to build a complete IKE message. + * + * <p>All IKE fragments should have the same IKE headers, except for the message length. This + * class only stores the IKE header of the first arrived IKE fragment to represent the IKE + * header of the complete IKE message. In this way we can verify all subsequent fragments' + * headers against it. + * + * <p>The first payload type is only stored in the first fragment, as indicated in RFC 7383. So + * this class only stores the next payload type field taken from the first fragment. + */ + public static class DecodeResultPartial extends DecodeResult { + public final int firstPayloadType; + public final byte[] firstFragBytes; + public final IkeHeader ikeHeader; + public final byte[][] collectedFragsList; + + /** + * Construct an instance of DecodeResultPartial with collected fragments and the newly + * received fragment. + * + * <p>The newly received fragment has been validated against collected fragments during + * decoding that all fragments have the same total fragments number and the newly received + * fragment is not a replay. + */ + public DecodeResultPartial( + IkeHeader ikeHeader, + byte[] inputPacket, + IkeSkfPayload skfPayload, + int nextPayloadType, + @Nullable DecodeResultPartial collectedFragments) { + super(DECODE_STATUS_PARTIAL); + + boolean isFirstFragment = 1 == skfPayload.fragmentNum; + if (collectedFragments == null) { + // First arrived IKE fragment + this.ikeHeader = ikeHeader; + this.firstPayloadType = + isFirstFragment ? nextPayloadType : IkePayload.PAYLOAD_TYPE_NO_NEXT; + this.firstFragBytes = isFirstFragment ? inputPacket : null; + this.collectedFragsList = new byte[skfPayload.totalFragments][]; + } else { + this.ikeHeader = collectedFragments.ikeHeader; + this.firstPayloadType = + isFirstFragment ? nextPayloadType : collectedFragments.firstPayloadType; + this.firstFragBytes = + isFirstFragment ? inputPacket : collectedFragments.firstFragBytes; + this.collectedFragsList = collectedFragments.collectedFragsList; + } + + this.collectedFragsList[skfPayload.fragmentNum - 1] = skfPayload.getUnencryptedData(); + } + + /** Return if all IKE fragments have been collected */ + public boolean isAllFragmentsReceived() { + for (byte[] frag : collectedFragsList) { + if (frag == null) return false; + } + return true; + } + + /** Reassemble all IKE fragments and return the unencrypted message body in byte array. */ + public byte[] reassembleAllFrags() { + if (!isAllFragmentsReceived()) { + throw new IllegalStateException("Not all fragments have been received"); + } + + int len = 0; + for (byte[] frag : collectedFragsList) { + len += frag.length; + } + + ByteBuffer buffer = ByteBuffer.allocate(len); + for (byte[] frag : collectedFragsList) { + buffer.put(frag); + } + + return buffer.array(); + } + } + + /** + * This class represents common information of error cases in decrypting and decoding message. + */ + public abstract static class DecodeResultError extends DecodeResult { + public final IkeException ikeException; + + protected DecodeResultError(int status, IkeException ikeException) { + super(status); + this.ikeException = ikeException; + } + } + /** + * This class represents that decoding errors have been found after the IKE message is + * authenticated and decrypted. + */ + public static class DecodeResultProtectedError extends DecodeResultError { + public final byte[] firstPacket; + + public DecodeResultProtectedError(IkeException ikeException, byte[] firstPacket) { + super(DECODE_STATUS_PROTECTED_ERROR, ikeException); + this.firstPacket = firstPacket; + } + } + /** This class represents errors have been found during message authentication or decryption. */ + public static class DecodeResultUnprotectedError extends DecodeResultError { + public DecodeResultUnprotectedError(IkeException ikeException) { + super(DECODE_STATUS_UNPROTECTED_ERROR, ikeException); + } + } + + /** + * For setting mocked IIkeMessageHelper for testing + * + * @param helper the mocked IIkeMessageHelper + */ + public static void setIkeMessageHelper(IIkeMessageHelper helper) { + sIkeMessageHelper = helper; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeNoncePayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeNoncePayload.java new file mode 100644 index 00000000..70fc824d --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeNoncePayload.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 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.message; + +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; + +/** + * IkeNoncePayload represents a Nonce payload. + * + * <p>Length of nonce data must be at least half the key size of negotiated PRF. It must be between + * 16 and 256 octets. IKE library always generates nonce of GENERATED_NONCE_LEN octets which is long + * enough for all currently known PRFs. + * + * <p>Critical bit must be ignored when doing decoding and must not be set when doing encoding for + * this payload. + * + * @see <a href="https://tools.ietf.org/html/rfc7296">RFC 7296, Internet Key Exchange Protocol + * Version 2 (IKEv2). + */ +public final class IkeNoncePayload extends IkePayload { + // The longest key size of all currently known PRFs is 512 bits (64 bytes). Since we are + // required to generate nonce that is long enough for all proposed PRFs, it is simple that we + // always generate 32 bytes nonce, which is enough for all known PRFs. + private static final int GENERATED_NONCE_LEN = 32; + + private static final int MIN_NONCE_LEN = 16; + private static final int MAX_NONCE_LEN = 256; + + public final byte[] nonceData; + + /** + * Construct an instance of IkeNoncePayload in the context of {@link IkePayloadFactory}. + * + * @param critical indicates if it is a critical payload. + * @param payloadBody the nonce data + */ + IkeNoncePayload(boolean critical, byte[] payloadBody) throws IkeProtocolException { + super(PAYLOAD_TYPE_NONCE, critical); + if (payloadBody.length < MIN_NONCE_LEN || payloadBody.length > MAX_NONCE_LEN) { + throw new InvalidSyntaxException( + "Invalid nonce data with length of: " + payloadBody.length); + } + // Check that the length of payloadBody satisfies the "half the key size of negotiated PRF" + // condition when processing IKE Message in upper layer. Cannot do this check here for + // lacking PRF information. + nonceData = payloadBody; + } + + /** Generate Nonce data and construct an instance of IkeNoncePayload. */ + public IkeNoncePayload() { + super(PAYLOAD_TYPE_NONCE, false); + nonceData = new byte[GENERATED_NONCE_LEN]; + new SecureRandom().nextBytes(nonceData); + } + + /** + * Encode Nonce payload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + int payloadLength = GENERIC_HEADER_LENGTH + nonceData.length; + + encodePayloadHeaderToByteBuffer(nextPayload, payloadLength, byteBuffer); + byteBuffer.put(nonceData); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + nonceData.length; + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "Nonce"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeNotifyPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeNotifyPayload.java new file mode 100644 index 00000000..ce9529ee --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeNotifyPayload.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2018 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.message; + +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_CHILD_SA_NOT_FOUND; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_FAILED_CP_REQUIRED; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INTERNAL_ADDRESS_FAILURE; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_IKE_SPI; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_KE_PAYLOAD; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_MAJOR_VERSION; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_MESSAGE_ID; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_SELECTORS; +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_NO_PROPOSAL_CHOSEN; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_SINGLE_PAIR_REQUIRED; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_TS_UNACCEPTABLE; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_UNSUPPORTED_CRITICAL_PAYLOAD; + +import android.annotation.IntDef; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.util.ArraySet; +import android.util.SparseArray; + +import com.android.internal.net.ipsec.ike.exceptions.AuthenticationFailedException; +import com.android.internal.net.ipsec.ike.exceptions.InvalidKeException; +import com.android.internal.net.ipsec.ike.exceptions.InvalidMajorVersionException; +import com.android.internal.net.ipsec.ike.exceptions.InvalidMessageIdException; +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.exceptions.TemporaryFailureException; +import com.android.internal.net.ipsec.ike.exceptions.TsUnacceptableException; +import com.android.internal.net.ipsec.ike.exceptions.UnrecognizedIkeProtocolException; +import com.android.internal.net.ipsec.ike.exceptions.UnsupportedCriticalPayloadException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.ProviderException; +import java.util.Set; + +/** + * IkeNotifyPayload represents a Notify Payload. + * + * <p>As instructed by RFC 7296, for IKE SA concerned Notify Payload, Protocol ID and SPI Size must + * be zero. Unrecognized notify message type must be ignored but should be logged. + * + * <p>Notification types that smaller or equal than ERROR_NOTIFY_TYPE_MAX are error types. The rest + * of them are status types. + * + * <p>Critical bit for this payload must be ignored in received packet and must not be set in + * outbound packet. + * + * @see <a href="https://tools.ietf.org/html/rfc7296">RFC 7296, Internet Key Exchange Protocol + * Version 2 (IKEv2)</a> + */ +public final class IkeNotifyPayload extends IkeInformationalPayload { + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + NOTIFY_TYPE_ADDITIONAL_TS_POSSIBLE, + NOTIFY_TYPE_IPCOMP_SUPPORTED, + NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP, + NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP, + NOTIFY_TYPE_USE_TRANSPORT_MODE, + NOTIFY_TYPE_REKEY_SA, + NOTIFY_TYPE_ESP_TFC_PADDING_NOT_SUPPORTED + }) + public @interface NotifyType {} + + /** + * Indicates that the responder has narrowed the proposed Traffic Selectors but other Traffic + * Selectors would also have been acceptable. Only allowed in the response for negotiating a + * Child SA. + */ + public static final int NOTIFY_TYPE_ADDITIONAL_TS_POSSIBLE = 16386; + /** + * Indicates a willingness by its sender to use IPComp on this Child SA. Only allowed in the + * request/response for negotiating a Child SA. + */ + public static final int NOTIFY_TYPE_IPCOMP_SUPPORTED = 16387; + /** + * Used for detecting if the IKE initiator is behind a NAT. Only allowed in the request/response + * of IKE_SA_INIT exchange. + */ + public static final int NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP = 16388; + /** + * Used for detecting if the IKE responder is behind a NAT. Only allowed in the request/response + * of IKE_SA_INIT exchange. + */ + public static final int NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP = 16389; + /** + * Indicates a willingness by its sender to use transport mode rather than tunnel mode on this + * Child SA. Only allowed in the request/response for negotiating a Child SA. + */ + public static final int NOTIFY_TYPE_USE_TRANSPORT_MODE = 16391; + /** + * Used for rekeying a Child SA or an IKE SA. Only allowed in the request/response of + * CREATE_CHILD_SA exchange. + */ + public static final int NOTIFY_TYPE_REKEY_SA = 16393; + /** + * Indicates that the sender will not accept packets that contain TFC padding over the Child SA + * being negotiated. Only allowed in the request/response for negotiating a Child SA. + */ + public static final int NOTIFY_TYPE_ESP_TFC_PADDING_NOT_SUPPORTED = 16394; + /** Indicates that the sender supports IKE fragmentation. */ + public static final int NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED = 16430; + + private static final int NOTIFY_HEADER_LEN = 4; + private static final int ERROR_NOTIFY_TYPE_MAX = 16383; + + private static final String NAT_DETECTION_DIGEST_ALGORITHM = "SHA-1"; + + private static final Set<Integer> VALID_NOTIFY_TYPES_FOR_EXISTING_CHILD_SA; + private static final Set<Integer> VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA; + + private static final SparseArray<String> NOTIFY_TYPE_TO_STRING; + + static { + VALID_NOTIFY_TYPES_FOR_EXISTING_CHILD_SA = new ArraySet<>(); + VALID_NOTIFY_TYPES_FOR_EXISTING_CHILD_SA.add(ERROR_TYPE_INVALID_SELECTORS); + VALID_NOTIFY_TYPES_FOR_EXISTING_CHILD_SA.add(ERROR_TYPE_CHILD_SA_NOT_FOUND); + VALID_NOTIFY_TYPES_FOR_EXISTING_CHILD_SA.add(NOTIFY_TYPE_REKEY_SA); + } + + static { + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA = new ArraySet<>(); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add(IkeProtocolException.ERROR_TYPE_NO_PROPOSAL_CHOSEN); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add(IkeProtocolException.ERROR_TYPE_INVALID_KE_PAYLOAD); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add( + IkeProtocolException.ERROR_TYPE_SINGLE_PAIR_REQUIRED); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add(IkeProtocolException.ERROR_TYPE_NO_ADDITIONAL_SAS); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add( + IkeProtocolException.ERROR_TYPE_INTERNAL_ADDRESS_FAILURE); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add(IkeProtocolException.ERROR_TYPE_FAILED_CP_REQUIRED); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add(IkeProtocolException.ERROR_TYPE_TS_UNACCEPTABLE); + + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add(NOTIFY_TYPE_ADDITIONAL_TS_POSSIBLE); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add(NOTIFY_TYPE_IPCOMP_SUPPORTED); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add(NOTIFY_TYPE_USE_TRANSPORT_MODE); + VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.add(NOTIFY_TYPE_ESP_TFC_PADDING_NOT_SUPPORTED); + } + + static { + NOTIFY_TYPE_TO_STRING = new SparseArray<>(); + NOTIFY_TYPE_TO_STRING.put( + ERROR_TYPE_UNSUPPORTED_CRITICAL_PAYLOAD, "Unsupported critical payload"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_INVALID_IKE_SPI, "Invalid IKE SPI"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_INVALID_MAJOR_VERSION, "Invalid major version"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_INVALID_SYNTAX, "Invalid syntax"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_INVALID_MESSAGE_ID, "Invalid message ID"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_NO_PROPOSAL_CHOSEN, "No proposal chosen"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_INVALID_KE_PAYLOAD, "Invalid KE payload"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_AUTHENTICATION_FAILED, "Authentication failed"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_SINGLE_PAIR_REQUIRED, "Single pair required"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_NO_ADDITIONAL_SAS, "No additional SAs"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_INTERNAL_ADDRESS_FAILURE, "Internal address failure"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_FAILED_CP_REQUIRED, "Failed CP required"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_TS_UNACCEPTABLE, "TS unacceptable"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_INVALID_SELECTORS, "Invalid selectors"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_TEMPORARY_FAILURE, "Temporary failure"); + NOTIFY_TYPE_TO_STRING.put(ERROR_TYPE_CHILD_SA_NOT_FOUND, "Child SA not found"); + + NOTIFY_TYPE_TO_STRING.put(NOTIFY_TYPE_ADDITIONAL_TS_POSSIBLE, "Additional TS possible"); + NOTIFY_TYPE_TO_STRING.put(NOTIFY_TYPE_IPCOMP_SUPPORTED, "IPCOMP supported"); + NOTIFY_TYPE_TO_STRING.put(NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP, "NAT detection source IP"); + NOTIFY_TYPE_TO_STRING.put( + NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP, "NAT detection destination IP"); + NOTIFY_TYPE_TO_STRING.put(NOTIFY_TYPE_USE_TRANSPORT_MODE, "Use transport mode"); + NOTIFY_TYPE_TO_STRING.put(NOTIFY_TYPE_REKEY_SA, "Rekey SA"); + NOTIFY_TYPE_TO_STRING.put( + NOTIFY_TYPE_ESP_TFC_PADDING_NOT_SUPPORTED, "ESP TCP Padding not supported"); + NOTIFY_TYPE_TO_STRING.put( + NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED, "Fragmentation supported"); + } + + public final int protocolId; + public final byte spiSize; + public final int notifyType; + public final int spi; + public final byte[] notifyData; + + /** + * Construct an instance of IkeNotifyPayload in the context of IkePayloadFactory + * + * @param critical indicates if this payload is critical. Ignored in supported payload as + * instructed by the RFC 7296. + * @param payloadBody payload body in byte array + * @throws IkeProtocolException if there is any error + */ + IkeNotifyPayload(boolean isCritical, byte[] payloadBody) throws IkeProtocolException { + super(PAYLOAD_TYPE_NOTIFY, isCritical); + + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + + protocolId = Byte.toUnsignedInt(inputBuffer.get()); + spiSize = inputBuffer.get(); + notifyType = Short.toUnsignedInt(inputBuffer.getShort()); + + // Validate syntax of spiSize, protocolId and notifyType. + // Reference: <https://tools.ietf.org/html/rfc7296#page-100> + if (spiSize == SPI_LEN_IPSEC) { + // For message concerning existing Child SA + validateNotifyPayloadForExistingChildSa(); + spi = inputBuffer.getInt(); + + } else if (spiSize == SPI_LEN_NOT_INCLUDED) { + // For message concerning IKE SA or for new Child SA that to be negotiated. + validateNotifyPayloadForIkeAndNewChild(); + spi = SPI_NOT_INCLUDED; + + } else { + throw new InvalidSyntaxException("Invalid SPI Size: " + spiSize); + } + + notifyData = new byte[payloadBody.length - NOTIFY_HEADER_LEN - spiSize]; + inputBuffer.get(notifyData); + } + + private void validateNotifyPayloadForExistingChildSa() throws InvalidSyntaxException { + if (protocolId != PROTOCOL_ID_AH && protocolId != PROTOCOL_ID_ESP) { + throw new InvalidSyntaxException( + "Expected Procotol ID AH(2) or ESP(3): Protocol ID is " + protocolId); + } + + if (!VALID_NOTIFY_TYPES_FOR_EXISTING_CHILD_SA.contains(notifyType)) { + throw new InvalidSyntaxException( + "Expected Notify Type for existing Child SA: Notify Type is " + notifyType); + } + } + + private void validateNotifyPayloadForIkeAndNewChild() throws InvalidSyntaxException { + if (protocolId != PROTOCOL_ID_UNSET) { + throw new InvalidSyntaxException( + "Expected Procotol ID unset: Protocol ID is " + protocolId); + } + + if (notifyType == ERROR_TYPE_INVALID_SELECTORS + || notifyType == ERROR_TYPE_CHILD_SA_NOT_FOUND) { + throw new InvalidSyntaxException( + "Expected Notify Type concerning IKE SA or new Child SA under negotiation" + + ": Notify Type is " + + notifyType); + } + } + + /** + * Generate NAT DETECTION notification data. + * + * <p>This method calculates NAT DETECTION notification data which is a SHA-1 digest of the IKE + * initiator's SPI, IKE responder's SPI, IP address and port. Source address and port should be + * used for generating NAT_DETECTION_SOURCE_IP data. Destination address and port should be used + * for generating NAT_DETECTION_DESTINATION_IP data. Here "source" and "destination" mean the + * direction of this IKE message. + * + * @param initiatorIkeSpi the SPI of IKE initiator + * @param responderIkeSpi the SPI of IKE responder + * @param ipAddress the IP address + * @param port the port + * @return the generated NAT DETECTION notification data as a byte array. + */ + public static byte[] generateNatDetectionData( + long initiatorIkeSpi, long responderIkeSpi, InetAddress ipAddress, int port) { + byte[] rawIpAddr = ipAddress.getAddress(); + + ByteBuffer byteBuffer = + ByteBuffer.allocate(2 * SPI_LEN_IKE + rawIpAddr.length + IP_PORT_LEN); + byteBuffer + .putLong(initiatorIkeSpi) + .putLong(responderIkeSpi) + .put(rawIpAddr) + .putShort((short) port); + + try { + MessageDigest natDetectionDataDigest = + MessageDigest.getInstance( + NAT_DETECTION_DIGEST_ALGORITHM, IkeMessage.getSecurityProvider()); + return natDetectionDataDigest.digest(byteBuffer.array()); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain algorithm :" + NAT_DETECTION_DIGEST_ALGORITHM, e); + } + } + + /** + * Encode Notify payload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + byteBuffer.put((byte) protocolId).put(spiSize).putShort((short) notifyType); + if (spiSize == SPI_LEN_IPSEC) { + byteBuffer.putInt(spi); + } + byteBuffer.put(notifyData); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + NOTIFY_HEADER_LEN + spiSize + notifyData.length; + } + + protected IkeNotifyPayload( + @ProtocolId int protocolId, byte spiSize, int spi, int notifyType, byte[] notifyData) { + super(PAYLOAD_TYPE_NOTIFY, false); + this.protocolId = protocolId; + this.spiSize = spiSize; + this.spi = spi; + this.notifyType = notifyType; + this.notifyData = notifyData; + } + + /** + * Construct IkeNotifyPayload concerning either an IKE SA, or Child SA that is going to be + * negotiated with associated notification data. + * + * @param notifyType the notify type concerning IKE SA + * @param notifytData status or error data transmitted. Values for this field are notify type + * specific. + */ + public IkeNotifyPayload(int notifyType, byte[] notifyData) { + this(PROTOCOL_ID_UNSET, SPI_LEN_NOT_INCLUDED, SPI_NOT_INCLUDED, notifyType, notifyData); + try { + validateNotifyPayloadForIkeAndNewChild(); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Construct IkeNotifyPayload concerning either an IKE SA, or Child SA that is going to be + * negotiated without additional notification data. + * + * @param notifyType the notify type concerning IKE SA + */ + public IkeNotifyPayload(int notifyType) { + this(notifyType, new byte[0]); + } + + /** + * Construct IkeNotifyPayload concerning existing Child SA + * + * @param notifyType the notify type concerning Child SA + * @param notifytData status or error data transmitted. Values for this field are notify type + * specific. + */ + public IkeNotifyPayload( + @ProtocolId int protocolId, int spi, int notifyType, byte[] notifyData) { + this(protocolId, SPI_LEN_IPSEC, spi, notifyType, notifyData); + try { + validateNotifyPayloadForExistingChildSa(); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Indicates if this is an error notification payload. + * + * @return if this is an error notification payload. + */ + public boolean isErrorNotify() { + return notifyType <= ERROR_NOTIFY_TYPE_MAX; + } + + /** + * Indicates if this is an notification for a new Child SA negotiation. + * + * <p>This notification may provide additional configuration information for negotiating a new + * Child SA or is an error notification of the Child SA negotiation failure. + * + * @return if this is an notification for a new Child SA negotiation. + */ + public boolean isNewChildSaNotify() { + return VALID_NOTIFY_TYPES_FOR_NEW_CHILD_SA.contains(notifyType); + } + + /** + * Validate error data and build IkeProtocolException for this error notification. + * + * @return the IkeProtocolException that represents this error. + * @throws InvalidSyntaxException if error data has invalid size. + */ + public IkeProtocolException validateAndBuildIkeException() throws InvalidSyntaxException { + if (!isErrorNotify()) { + throw new IllegalArgumentException( + "Do not support building IkeException for a non-error notificaton. Notify" + + " type: " + + notifyType); + } + + try { + switch (notifyType) { + case ERROR_TYPE_UNSUPPORTED_CRITICAL_PAYLOAD: + return new UnsupportedCriticalPayloadException(notifyData); + case ERROR_TYPE_INVALID_MAJOR_VERSION: + return new InvalidMajorVersionException(notifyData); + case ERROR_TYPE_INVALID_SYNTAX: + return new InvalidSyntaxException(notifyData); + case ERROR_TYPE_INVALID_MESSAGE_ID: + return new InvalidMessageIdException(notifyData); + case ERROR_TYPE_NO_PROPOSAL_CHOSEN: + return new NoValidProposalChosenException(notifyData); + case ERROR_TYPE_INVALID_KE_PAYLOAD: + return new InvalidKeException(notifyData); + case ERROR_TYPE_AUTHENTICATION_FAILED: + return new AuthenticationFailedException(notifyData); + case ERROR_TYPE_TS_UNACCEPTABLE: + return new TsUnacceptableException(notifyData); + case ERROR_TYPE_TEMPORARY_FAILURE: + return new TemporaryFailureException(notifyData); + default: + return new UnrecognizedIkeProtocolException(notifyType, notifyData); + } + } catch (IllegalArgumentException e) { + // Notification data length is invalid. + throw new InvalidSyntaxException(e); + } + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + String notifyTypeString = NOTIFY_TYPE_TO_STRING.get(notifyType); + + if (notifyTypeString == null) { + return "Notify(" + notifyType + ")"; + } + return "Notify(" + notifyTypeString + ")"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkePayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkePayload.java new file mode 100644 index 00000000..9ea54c14 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkePayload.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2018 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.message; + +import android.annotation.IntDef; +import android.util.SparseArray; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; + +/** + * IkePayload is an abstract class that represents the common information for all IKE payload types. + * + * <p>Each types of IKE payload should implement its own subclass with its own decoding and encoding + * logic. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.2">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public abstract class IkePayload { + // Critical bit and following reserved 7 bits in payload generic header must all be zero + private static final byte PAYLOAD_HEADER_CRITICAL_BIT_UNSET = 0; + /** Length of a generic IKE payload header */ + public static final int GENERIC_HEADER_LENGTH = 4; + + /** + * Payload types as defined by IANA: + * + * @see <a href="https://www.iana.org/assignments/ikev2-parameters/ikev2-parameters.xhtml"> + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PAYLOAD_TYPE_NO_NEXT, + PAYLOAD_TYPE_SA, + PAYLOAD_TYPE_KE, + PAYLOAD_TYPE_CERT, + PAYLOAD_TYPE_CERT_REQUEST, + PAYLOAD_TYPE_AUTH, + PAYLOAD_TYPE_ID_INITIATOR, + PAYLOAD_TYPE_ID_RESPONDER, + PAYLOAD_TYPE_NONCE, + PAYLOAD_TYPE_NOTIFY, + PAYLOAD_TYPE_DELETE, + PAYLOAD_TYPE_VENDOR, + PAYLOAD_TYPE_TS_INITIATOR, + PAYLOAD_TYPE_TS_RESPONDER, + PAYLOAD_TYPE_SK, + PAYLOAD_TYPE_CP, + PAYLOAD_TYPE_EAP, + PAYLOAD_TYPE_SKF + }) + public @interface PayloadType {} + + /** No Next Payload */ + public static final int PAYLOAD_TYPE_NO_NEXT = 0; + /** Security Association Payload */ + public static final int PAYLOAD_TYPE_SA = 33; + /** Key Exchange Payload */ + public static final int PAYLOAD_TYPE_KE = 34; + /** Identification Payload for IKE SA Initiator */ + public static final int PAYLOAD_TYPE_ID_INITIATOR = 35; + /** Identification Payload for IKE SA Responder */ + public static final int PAYLOAD_TYPE_ID_RESPONDER = 36; + /** Certificate Payload */ + public static final int PAYLOAD_TYPE_CERT = 37; + /** Certificate Request Payload */ + public static final int PAYLOAD_TYPE_CERT_REQUEST = 38; + /** Authentication Payload */ + public static final int PAYLOAD_TYPE_AUTH = 39; + /** Nonce Payload */ + public static final int PAYLOAD_TYPE_NONCE = 40; + /** Notify Payload */ + public static final int PAYLOAD_TYPE_NOTIFY = 41; + /** Delete Payload */ + public static final int PAYLOAD_TYPE_DELETE = 42; + /** Vendor Payload */ + public static final int PAYLOAD_TYPE_VENDOR = 43; + /** Traffic Selector Payload of Child SA Initiator */ + public static final int PAYLOAD_TYPE_TS_INITIATOR = 44; + /** Traffic Selector Payload of Child SA Responder */ + public static final int PAYLOAD_TYPE_TS_RESPONDER = 45; + /** Encrypted and Authenticated Payload */ + public static final int PAYLOAD_TYPE_SK = 46; + /** Configuration Payload */ + public static final int PAYLOAD_TYPE_CP = 47; + /** EAP Payload */ + public static final int PAYLOAD_TYPE_EAP = 48; + /** Encrypted and Authenticated Fragment */ + public static final int PAYLOAD_TYPE_SKF = 53; + + // TODO: List all payload types. + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PROTOCOL_ID_UNSET, + PROTOCOL_ID_IKE, + PROTOCOL_ID_AH, + PROTOCOL_ID_ESP, + }) + public @interface ProtocolId {} + + public static final int PROTOCOL_ID_UNSET = 0; + public static final int PROTOCOL_ID_IKE = 1; + public static final int PROTOCOL_ID_AH = 2; + public static final int PROTOCOL_ID_ESP = 3; + + private static final SparseArray<String> PROTOCOL_TO_STR; + + static { + PROTOCOL_TO_STR = new SparseArray<>(); + PROTOCOL_TO_STR.put(PROTOCOL_ID_UNSET, "Protocol Unset"); + PROTOCOL_TO_STR.put(PROTOCOL_ID_IKE, "IKE"); + PROTOCOL_TO_STR.put(PROTOCOL_ID_AH, "AH"); + PROTOCOL_TO_STR.put(PROTOCOL_ID_ESP, "ESP"); + } + + public static final byte SPI_LEN_NOT_INCLUDED = 0; + public static final byte SPI_LEN_IPSEC = 4; + public static final byte SPI_LEN_IKE = 8; + + public static final int SPI_NOT_INCLUDED = 0; + + /** Length of port number in bytes */ + public static final int IP_PORT_LEN = 2; + + public final int payloadType; + public final boolean isCritical; + + /** + * Construct a instance of IkePayload in the context of a IkePayloadFactory. + * + * <p>It should be overrided by subclass of IkePayload + * + * @param payload the payload type. All supported types will fall in {@link + * IkePayload.PayloadType} + * @param critical indicates if this payload is critical. Ignore it when payload type is + * supported. + */ + IkePayload(int payload, boolean critical) { + payloadType = payload; + isCritical = critical; + } + + /** + * A helper method to quickly obtain payloads with the input payload type in the provided + * payload list. + * + * <p>This method will not check if this payload type can be repeatable in an IKE message + * because it does not know the context of the provided payload list. Caller should call this + * method if they are expecting more than one payloads in the list. + * + * @param payloadType the payloadType to look for. + * @param payloadClass the class of the desired payload. + * @param searchList the payload list to do the search. + * @return a list of IkePayloads with the payloadType. + */ + public static <T extends IkePayload> List<T> getPayloadListForTypeInProvidedList( + @IkePayload.PayloadType int payloadType, + Class<T> payloadClass, + List<IkePayload> searchList) { + List<T> payloadList = new LinkedList<>(); + + for (IkePayload payload : searchList) { + if (payloadType == payload.payloadType) { + payloadList.add(payloadClass.cast(payload)); + } + } + + return payloadList; + } + + /** + * A helper method to quickly obtain the payload with the input payload type in the provided + * payload list. + * + * <p>This method will not check if this payload type can be repeatable in an IKE message + * because it does not know the context of the provided payload list. Caller should call this + * method if they are expecting no more than one payloads in the list. + * + * @param payloadType the payloadType to look for. + * @param payloadClass the class of the desired payload. + * @param searchList the payload list to do the search. + * @return the IkePayload with the payloadType. + */ + public static <T extends IkePayload> T getPayloadForTypeInProvidedList( + @IkePayload.PayloadType int payloadType, + Class<T> payloadClass, + List<IkePayload> searchList) { + List<T> payloadList = + getPayloadListForTypeInProvidedList(payloadType, payloadClass, searchList); + return payloadList.isEmpty() ? null : payloadList.get(0); + } + + /** + * Encode generic payload header to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param payloadLength length of the entire payload + * @param byteBuffer destination ByteBuffer that stores encoded payload header + */ + protected static void encodePayloadHeaderToByteBuffer( + @PayloadType int nextPayload, int payloadLength, ByteBuffer byteBuffer) { + byteBuffer + .put((byte) nextPayload) + .put(PAYLOAD_HEADER_CRITICAL_BIT_UNSET) + .putShort((short) payloadLength); + } + + /** Retuns protocol type as String. */ + public static String getProtocolTypeString(@ProtocolId int protocol) { + return PROTOCOL_TO_STR.get(protocol); + } + + /** + * Encode payload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + protected abstract void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer); + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + protected abstract int getPayloadLength(); + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + public abstract String getTypeString(); +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkePayloadFactory.java b/src/java/com/android/internal/net/ipsec/ike/message/IkePayloadFactory.java new file mode 100644 index 00000000..2f191d4e --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkePayloadFactory.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2018 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.message; + +import android.annotation.Nullable; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.util.Pair; + +import com.android.internal.annotations.VisibleForTesting; +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.exceptions.InvalidSyntaxException; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; + +/** + * IkePayloadFactory is used for creating IkePayload according to is type. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +final class IkePayloadFactory { + + // Critical bit is set and following reserved 7 bits are unset. + private static final byte PAYLOAD_HEADER_CRITICAL_BIT_SET = (byte) 0x80; + + private static boolean isCriticalPayload(byte flagByte) { + // Reserved 7 bits following critical bit must be ignore on receipt. + return (flagByte & PAYLOAD_HEADER_CRITICAL_BIT_SET) == PAYLOAD_HEADER_CRITICAL_BIT_SET; + } + + /** Default IIkePayloadDecoder instance used for constructing IkePayload */ + static IIkePayloadDecoder sDecoderInstance = new IkePayloadDecoder(); + + /** + * IkePayloadDecoder implements IIkePayloadDecoder for constructing IkePayload from decoding + * received message. + * + * <p>Package private + */ + @VisibleForTesting + static class IkePayloadDecoder implements IIkePayloadDecoder { + @Override + public IkePayload decodeIkePayload( + int payloadType, boolean isCritical, boolean isResp, byte[] payloadBody) + throws IkeProtocolException { + switch (payloadType) { + // TODO: Add cases for creating supported payloads. + case IkePayload.PAYLOAD_TYPE_SA: + return new IkeSaPayload(isCritical, isResp, payloadBody); + case IkePayload.PAYLOAD_TYPE_KE: + return new IkeKePayload(isCritical, payloadBody); + case IkePayload.PAYLOAD_TYPE_ID_INITIATOR: + return new IkeIdPayload(isCritical, payloadBody, true); + case IkePayload.PAYLOAD_TYPE_ID_RESPONDER: + return new IkeIdPayload(isCritical, payloadBody, false); + case IkePayload.PAYLOAD_TYPE_CERT: + return IkeCertPayload.getIkeCertPayload(isCritical, payloadBody); + case IkePayload.PAYLOAD_TYPE_AUTH: + return IkeAuthPayload.getIkeAuthPayload(isCritical, payloadBody); + case IkePayload.PAYLOAD_TYPE_NONCE: + return new IkeNoncePayload(isCritical, payloadBody); + case IkePayload.PAYLOAD_TYPE_NOTIFY: + return new IkeNotifyPayload(isCritical, payloadBody); + case IkePayload.PAYLOAD_TYPE_DELETE: + return new IkeDeletePayload(isCritical, payloadBody); + case IkePayload.PAYLOAD_TYPE_VENDOR: + return new IkeVendorPayload(isCritical, payloadBody); + case IkePayload.PAYLOAD_TYPE_TS_INITIATOR: + return new IkeTsPayload(isCritical, payloadBody, true); + case IkePayload.PAYLOAD_TYPE_TS_RESPONDER: + return new IkeTsPayload(isCritical, payloadBody, false); + case IkePayload.PAYLOAD_TYPE_CP: + return new IkeConfigPayload(isCritical, payloadBody); + case IkePayload.PAYLOAD_TYPE_EAP: + return new IkeEapPayload(isCritical, payloadBody); + default: + return new IkeUnsupportedPayload(payloadType, isCritical); + } + } + + @Override + public IkeSkPayload decodeIkeSkPayload( + boolean isSkf, + boolean critical, + byte[] message, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + byte[] integrityKey, + byte[] decryptionKey) + throws IkeProtocolException, GeneralSecurityException { + if (isSkf) { + return new IkeSkfPayload( + critical, + message, + integrityMac, + decryptCipher, + integrityKey, + decryptionKey); + } else { + return new IkeSkPayload( + critical, + message, + integrityMac, + decryptCipher, + integrityKey, + decryptionKey); + } + } + } + + /** + * Construct an instance of IkePayload according to its payload type. + * + * @param payloadType the current payload type. All supported types will fall in {@link + * IkePayload.PayloadType} + * @param input the encoded IKE message body containing all payloads. Position of it will + * increment. + * @return a Pair including IkePayload and next payload type. + */ + protected static Pair<IkePayload, Integer> getIkePayload( + int payloadType, boolean isResp, ByteBuffer input) throws IkeProtocolException { + int nextPayloadType = (int) input.get(); + // read critical bit + boolean isCritical = isCriticalPayload(input.get()); + + int payloadLength = Short.toUnsignedInt(input.getShort()); + if (payloadLength <= IkePayload.GENERIC_HEADER_LENGTH) { + throw new InvalidSyntaxException( + "Invalid Payload Length: Payload length is too short."); + } + int bodyLength = payloadLength - IkePayload.GENERIC_HEADER_LENGTH; + if (bodyLength > input.remaining()) { + // It is not clear whether previous payloads or current payload has invalid payload + // length. + throw new InvalidSyntaxException("Invalid Payload Length: Payload length is too long."); + } + byte[] payloadBody = new byte[bodyLength]; + input.get(payloadBody); + + IkePayload payload = + sDecoderInstance.decodeIkePayload(payloadType, isCritical, isResp, payloadBody); + return new Pair(payload, nextPayloadType); + } + + /** + * Construct an instance of IkeSkPayload by decrypting the received message. + * + * @param isSkf indicates if this is a SKF Payload. + * @param message the byte array contains the whole IKE message. + * @param integrityMac the negotiated integrity algorithm. + * @param decryptCipher the negotiated encryption algorithm. + * @param integrityKey the negotiated integrity algorithm key. + * @param decryptionKey the negotiated decryption key. + * @return a pair including IkePayload and next payload type. + * @throws IkeProtocolException for decoding errors. + * @throws GeneralSecurityException if there is any error during integrity check or decryption. + */ + protected static Pair<IkeSkPayload, Integer> getIkeSkPayload( + boolean isSkf, + byte[] message, + IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + byte[] integrityKey, + byte[] decryptionKey) + throws IkeProtocolException, GeneralSecurityException { + ByteBuffer input = + ByteBuffer.wrap( + message, + IkeHeader.IKE_HEADER_LENGTH, + message.length - IkeHeader.IKE_HEADER_LENGTH); + + int nextPayloadType = (int) input.get(); + // read critical bit + boolean isCritical = isCriticalPayload(input.get()); + + int payloadLength = Short.toUnsignedInt(input.getShort()); + + int bodyLength = message.length - IkeHeader.IKE_HEADER_LENGTH; + if (bodyLength < payloadLength) { + throw new InvalidSyntaxException( + "Invalid length of SK Payload: Payload length is too long."); + } else if (bodyLength > payloadLength) { + // According to RFC 7296, SK Payload must be the last payload and for CREATE_CHILD_SA, + // IKE_AUTH and INFORMATIONAL exchanges, message following the header is encrypted. Thus + // this implementaion only accepts that SK Payload to be the only payload. Any IKE + // packet violating this format will be treated as invalid. A request violating this + // format will be rejected and replied with an error notification. + throw new InvalidSyntaxException( + "Invalid length of SK Payload: Payload length is too short" + + " or SK Payload is not the only payload."); + } + + IkeSkPayload payload = + sDecoderInstance.decodeIkeSkPayload( + isSkf, + isCritical, + message, + integrityMac, + decryptCipher, + integrityKey, + decryptionKey); + + return new Pair(payload, nextPayloadType); + } + + /** + * IIkePayloadDecoder provides a package private interface for constructing IkePayload from + * decoding received message. + * + * <p>IIkePayloadDecoder exists so that the interface is injectable for testing. + */ + @VisibleForTesting + interface IIkePayloadDecoder { + IkePayload decodeIkePayload( + int payloadType, boolean isCritical, boolean isResp, byte[] payloadBody) + throws IkeProtocolException; + + IkeSkPayload decodeIkeSkPayload( + boolean isSkf, + boolean critical, + byte[] message, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + byte[] integrityKey, + byte[] decryptionKey) + throws IkeProtocolException, GeneralSecurityException; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java new file mode 100644 index 00000000..3cce6255 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java @@ -0,0 +1,1773 @@ +/* + * Copyright (C) 2018 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.message; + +import static android.net.ipsec.ike.IkeManager.getIkeLog; +import static android.net.ipsec.ike.SaProposal.DhGroup; +import static android.net.ipsec.ike.SaProposal.EncryptionAlgorithm; +import static android.net.ipsec.ike.SaProposal.IntegrityAlgorithm; +import static android.net.ipsec.ike.SaProposal.PseudorandomFunction; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.net.IpSecManager; +import android.net.IpSecManager.ResourceUnavailableException; +import android.net.IpSecManager.SecurityParameterIndex; +import android.net.IpSecManager.SpiUnavailableException; +import android.net.ipsec.ike.ChildSaProposal; +import android.net.ipsec.ike.IkeSaProposal; +import android.net.ipsec.ike.SaProposal; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.util.ArraySet; +import android.util.Pair; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.ipsec.ike.IkeSessionStateMachine.IkeSecurityParameterIndex; +import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException; +import com.android.internal.net.ipsec.ike.exceptions.NoValidProposalChosenException; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * IkeSaPayload represents a Security Association payload. It contains one or more {@link Proposal}. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeSaPayload extends IkePayload { + private static final String TAG = "IkeSaPayload"; + + public final boolean isSaResponse; + public final List<Proposal> proposalList; + /** + * Construct an instance of IkeSaPayload for decoding an inbound packet. + * + * @param critical indicates if this payload is critical. Ignored in supported payload as + * instructed by the RFC 7296. + * @param isResp indicates if this payload is in a response message. + * @param payloadBody the encoded payload body in byte array. + */ + IkeSaPayload(boolean critical, boolean isResp, byte[] payloadBody) throws IkeProtocolException { + super(IkePayload.PAYLOAD_TYPE_SA, critical); + + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + proposalList = new LinkedList<>(); + while (inputBuffer.hasRemaining()) { + Proposal proposal = Proposal.readFrom(inputBuffer); + proposalList.add(proposal); + } + + if (proposalList.isEmpty()) { + throw new InvalidSyntaxException("Found no SA Proposal in this SA Payload."); + } + + // An SA response must have exactly one SA proposal. + if (isResp && proposalList.size() != 1) { + throw new InvalidSyntaxException( + "Expected only one negotiated proposal from SA response: " + + "Multiple negotiated proposals found."); + } + isSaResponse = isResp; + + boolean firstIsIkeProposal = (proposalList.get(0).protocolId == PROTOCOL_ID_IKE); + for (int i = 1; i < proposalList.size(); i++) { + boolean isIkeProposal = (proposalList.get(i).protocolId == PROTOCOL_ID_IKE); + if (firstIsIkeProposal != isIkeProposal) { + getIkeLog() + .w(TAG, "Found both IKE proposals and Child proposals in this SA Payload."); + break; + } + } + + getIkeLog().d(TAG, "Receive " + toString()); + } + + /** Package private constructor for building a request for IKE SA initial creation or rekey */ + @VisibleForTesting + IkeSaPayload( + boolean isResp, byte spiSize, IkeSaProposal[] saProposals, InetAddress localAddress) + throws IOException { + this(isResp, spiSize, localAddress); + + if (saProposals.length < 1 || isResp && (saProposals.length > 1)) { + throw new IllegalArgumentException("Invalid SA payload."); + } + + for (int i = 0; i < saProposals.length; i++) { + // Proposal number must start from 1. + proposalList.add( + IkeProposal.createIkeProposal( + (byte) (i + 1) /*number*/, spiSize, saProposals[i], localAddress)); + } + + getIkeLog().d(TAG, "Generate " + toString()); + } + + /** Package private constructor for building an response SA Payload for IKE SA rekeys. */ + @VisibleForTesting + IkeSaPayload( + boolean isResp, + byte spiSize, + byte proposalNumber, + IkeSaProposal saProposal, + InetAddress localAddress) + throws IOException { + this(isResp, spiSize, localAddress); + + proposalList.add( + IkeProposal.createIkeProposal( + proposalNumber /*number*/, spiSize, saProposal, localAddress)); + + getIkeLog().d(TAG, "Generate " + toString()); + } + + private IkeSaPayload(boolean isResp, byte spiSize, InetAddress localAddress) + throws IOException { + super(IkePayload.PAYLOAD_TYPE_SA, false); + + // TODO: Check that proposals.length <= 255 in IkeSessionOptions and ChildSessionOptions + isSaResponse = isResp; + + // TODO: Allocate IKE SPI and pass to IkeProposal.createIkeProposal() + + // ProposalList populated in other constructors + proposalList = new ArrayList<Proposal>(); + } + + /** + * Package private constructor for building an outbound request SA Payload for Child SA + * negotiation. + */ + @VisibleForTesting + IkeSaPayload(ChildSaProposal[] saProposals, IpSecManager ipSecManager, InetAddress localAddress) + throws ResourceUnavailableException { + this(false /*isResp*/, ipSecManager, localAddress); + + if (saProposals.length < 1) { + throw new IllegalArgumentException("Invalid SA payload."); + } + + // TODO: Check that saProposals.length <= 255 in IkeSessionOptions and ChildSessionOptions + + for (int i = 0; i < saProposals.length; i++) { + // Proposal number must start from 1. + proposalList.add( + ChildProposal.createChildProposal( + (byte) (i + 1) /*number*/, saProposals[i], ipSecManager, localAddress)); + } + + getIkeLog().d(TAG, "Generate " + toString()); + } + + /** + * Package private constructor for building an outbound response SA Payload for Child SA + * negotiation. + */ + @VisibleForTesting + IkeSaPayload( + byte proposalNumber, + ChildSaProposal saProposal, + IpSecManager ipSecManager, + InetAddress localAddress) + throws ResourceUnavailableException { + this(true /*isResp*/, ipSecManager, localAddress); + + proposalList.add( + ChildProposal.createChildProposal( + proposalNumber /*number*/, saProposal, ipSecManager, localAddress)); + + getIkeLog().d(TAG, "Generate " + toString()); + } + + /** Constructor for building an outbound SA Payload for Child SA negotiation. */ + private IkeSaPayload(boolean isResp, IpSecManager ipSecManager, InetAddress localAddress) { + super(IkePayload.PAYLOAD_TYPE_SA, false); + + isSaResponse = isResp; + + // TODO: Allocate Child SPI and pass to ChildProposal.createChildProposal() + + // ProposalList populated in other constructors + proposalList = new ArrayList<Proposal>(); + } + + /** + * Construct an instance of IkeSaPayload for building an outbound IKE initial setup request. + * + * <p>According to RFC 7296, for an initial IKE SA negotiation, no SPI is included in SA + * Proposal. IKE library, as a client, only supports requesting this initial negotiation. + * + * @param saProposals the array of all SA Proposals. + */ + public static IkeSaPayload createInitialIkeSaPayload(IkeSaProposal[] saProposals) + throws IOException { + return new IkeSaPayload(false /*isResp*/, SPI_LEN_NOT_INCLUDED, saProposals, null); + } + + /** + * Construct an instance of IkeSaPayload for building an outbound request for Rekey IKE. + * + * @param saProposals the array of all IKE SA Proposals. + * @param localAddress the local address assigned on-device. + */ + public static IkeSaPayload createRekeyIkeSaRequestPayload( + IkeSaProposal[] saProposals, InetAddress localAddress) throws IOException { + return new IkeSaPayload(false /*isResp*/, SPI_LEN_IKE, saProposals, localAddress); + } + + /** + * Construct an instance of IkeSaPayload for building an outbound response for Rekey IKE. + * + * @param respProposalNumber the selected proposal's number. + * @param saProposal the expected selected IKE SA Proposal. + * @param localAddress the local address assigned on-device. + */ + public static IkeSaPayload createRekeyIkeSaResponsePayload( + byte respProposalNumber, IkeSaProposal saProposal, InetAddress localAddress) + throws IOException { + return new IkeSaPayload( + true /*isResp*/, SPI_LEN_IKE, respProposalNumber, saProposal, localAddress); + } + + /** + * Construct an instance of IkeSaPayload for building an outbound request for Child SA + * negotiation. + * + * @param saProposals the array of all Child SA Proposals. + * @param ipSecManager the IpSecManager for generating IPsec SPIs. + * @param localAddress the local address assigned on-device. + * @throws ResourceUnavailableException if too many SPIs are currently allocated for this user. + */ + public static IkeSaPayload createChildSaRequestPayload( + ChildSaProposal[] saProposals, IpSecManager ipSecManager, InetAddress localAddress) + throws ResourceUnavailableException { + + return new IkeSaPayload(saProposals, ipSecManager, localAddress); + } + + /** + * Construct an instance of IkeSaPayload for building an outbound response for Child SA + * negotiation. + * + * @param respProposalNumber the selected proposal's number. + * @param saProposal the expected selected Child SA Proposal. + * @param ipSecManager the IpSecManager for generating IPsec SPIs. + * @param localAddress the local address assigned on-device. + */ + public static IkeSaPayload createChildSaResponsePayload( + byte respProposalNumber, + ChildSaProposal saProposal, + IpSecManager ipSecManager, + InetAddress localAddress) + throws ResourceUnavailableException { + return new IkeSaPayload(respProposalNumber, saProposal, ipSecManager, localAddress); + } + + /** + * Finds the proposal in this (request) payload that matches the response proposal. + * + * @param respProposal the Proposal to match against. + * @return the byte-value proposal number of the selected proposal + * @throws NoValidProposalChosenException if no matching proposal was found. + */ + public byte getNegotiatedProposalNumber(SaProposal respProposal) + throws NoValidProposalChosenException { + for (int i = 0; i < proposalList.size(); i++) { + Proposal reqProposal = proposalList.get(i); + if (respProposal.isNegotiatedFrom(reqProposal.getSaProposal()) + && reqProposal.getSaProposal().getProtocolId() + == respProposal.getProtocolId()) { + return reqProposal.number; + } + } + throw new NoValidProposalChosenException("No remotely proposed protocol acceptable"); + } + + /** + * Validate the IKE SA Payload pair (request/response) and return the IKE SA negotiation result. + * + * <p>Caller is able to extract the negotiated IKE SA Proposal from the response Proposal and + * the IKE SPI pair generated by both sides. + * + * <p>In a locally-initiated case all IKE SA proposals (from users in initial creation or from + * previously negotiated proposal in rekey creation) in the locally generated reqSaPayload have + * been validated during building and are unmodified. All Transform combinations in these SA + * proposals are valid for IKE SA negotiation. It means each IKE SA request proposal MUST have + * Encryption algorithms, DH group configurations and PRFs. Integrity algorithms can only be + * omitted when AEAD is used. + * + * <p>In a remotely-initiated case the locally generated respSaPayload has exactly one SA + * proposal. It is validated during building and are unmodified. This proposal has a valid + * Transform combination for an IKE SA and has at most one value for each Transform type. + * + * <p>The response IKE SA proposal is validated against one of the request IKE SA proposals. It + * is guaranteed that for each Transform type that the request proposal has provided options, + * the response proposal has exact one Transform value. + * + * @param reqSaPayload the request payload. + * @param respSaPayload the response payload. + * @param remoteAddress the address of the remote IKE peer. + * @return the Pair of selected IkeProposal in request and the IkeProposal in response. + * @throws NoValidProposalChosenException if the response SA Payload cannot be negotiated from + * the request SA Payload. + */ + public static Pair<IkeProposal, IkeProposal> getVerifiedNegotiatedIkeProposalPair( + IkeSaPayload reqSaPayload, IkeSaPayload respSaPayload, InetAddress remoteAddress) + throws NoValidProposalChosenException, IOException { + Pair<Proposal, Proposal> proposalPair = + getVerifiedNegotiatedProposalPair(reqSaPayload, respSaPayload); + IkeProposal reqProposal = (IkeProposal) proposalPair.first; + IkeProposal respProposal = (IkeProposal) proposalPair.second; + + try { + // Allocate initiator's inbound SPI as needed for remotely initiated IKE SA creation + if (reqProposal.spiSize != SPI_NOT_INCLUDED + && reqProposal.getIkeSpiResource() == null) { + reqProposal.allocateResourceForRemoteIkeSpi(remoteAddress); + } + // Allocate responder's inbound SPI as needed for locally initiated IKE SA creation + if (respProposal.spiSize != SPI_NOT_INCLUDED + && respProposal.getIkeSpiResource() == null) { + respProposal.allocateResourceForRemoteIkeSpi(remoteAddress); + } + + return new Pair(reqProposal, respProposal); + } catch (Exception e) { + reqProposal.releaseSpiResourceIfExists(); + respProposal.releaseSpiResourceIfExists(); + throw e; + } + } + + /** + * Validate the SA Payload pair (request/response) and return the Child SA negotiation result. + * + * <p>Caller is able to extract the negotiated SA Proposal from the response Proposal and the + * IPsec SPI pair generated by both sides. + * + * <p>In a locally-initiated case all Child SA proposals (from users in initial creation or from + * previously negotiated proposal in rekey creation) in the locally generated reqSaPayload have + * been validated during building and are unmodified. All Transform combinations in these SA + * proposals are valid for Child SA negotiation. It means each request SA proposal MUST have + * Encryption algorithms and ESN configurations. + * + * <p>In a remotely-initiated case the locally generated respSapayload has exactly one SA + * proposal. It is validated during building and are unmodified. This proposal has a valid + * Transform combination for an Child SA and has at most one value for each Transform type. + * + * <p>The response Child SA proposal is validated against one of the request SA proposals. It is + * guaranteed that for each Transform type that the request proposal has provided options, the + * response proposal has exact one Transform value. + * + * @param reqSaPayload the request payload. + * @param respSaPayload the response payload. + * @param ipSecManager the IpSecManager to allocate SPI resource for the Proposal in this + * inbound SA Payload. + * @param remoteAddress the address of the remote IKE peer. + * @return the Pair of selected ChildProposal in the locally generated request and the + * ChildProposal in this response. + * @throws NoValidProposalChosenException if the response SA Payload cannot be negotiated from + * the request SA Payload. + * @throws ResourceUnavailableException if too many SPIs are currently allocated for this user. + * @throws SpiUnavailableException if the remotely generated SPI is in use. + */ + public static Pair<ChildProposal, ChildProposal> getVerifiedNegotiatedChildProposalPair( + IkeSaPayload reqSaPayload, + IkeSaPayload respSaPayload, + IpSecManager ipSecManager, + InetAddress remoteAddress) + throws NoValidProposalChosenException, ResourceUnavailableException, + SpiUnavailableException { + Pair<Proposal, Proposal> proposalPair = + getVerifiedNegotiatedProposalPair(reqSaPayload, respSaPayload); + ChildProposal reqProposal = (ChildProposal) proposalPair.first; + ChildProposal respProposal = (ChildProposal) proposalPair.second; + + try { + // Allocate initiator's inbound SPI as needed for remotely initiated Child SA creation + if (reqProposal.getChildSpiResource() == null) { + reqProposal.allocateResourceForRemoteChildSpi(ipSecManager, remoteAddress); + } + // Allocate responder's inbound SPI as needed for locally initiated Child SA creation + if (respProposal.getChildSpiResource() == null) { + respProposal.allocateResourceForRemoteChildSpi(ipSecManager, remoteAddress); + } + + return new Pair(reqProposal, respProposal); + } catch (Exception e) { + reqProposal.releaseSpiResourceIfExists(); + respProposal.releaseSpiResourceIfExists(); + throw e; + } + } + + private static Pair<Proposal, Proposal> getVerifiedNegotiatedProposalPair( + IkeSaPayload reqSaPayload, IkeSaPayload respSaPayload) + throws NoValidProposalChosenException { + try { + // If negotiated proposal has an unrecognized Transform, throw an exception. + Proposal respProposal = respSaPayload.proposalList.get(0); + if (respProposal.hasUnrecognizedTransform) { + throw new NoValidProposalChosenException( + "Negotiated proposal has unrecognized Transform."); + } + + // In SA request payload, the first proposal MUST be 1, and subsequent proposals MUST be + // one more than the previous proposal. In SA response payload, the negotiated proposal + // number MUST match the selected proposal number in SA request Payload. + int negotiatedProposalNum = respProposal.number; + List<Proposal> reqProposalList = reqSaPayload.proposalList; + if (negotiatedProposalNum < 1 || negotiatedProposalNum > reqProposalList.size()) { + throw new NoValidProposalChosenException( + "Negotiated proposal has invalid proposal number."); + } + + Proposal reqProposal = reqProposalList.get(negotiatedProposalNum - 1); + if (!respProposal.isNegotiatedFrom(reqProposal)) { + throw new NoValidProposalChosenException("Invalid negotiated proposal."); + } + + // In a locally-initiated creation, release locally generated SPIs in unselected request + // Proposals. In remotely-initiated SA creation, unused proposals do not have SPIs, and + // will silently succeed. + for (Proposal p : reqProposalList) { + if (reqProposal != p) p.releaseSpiResourceIfExists(); + } + + return new Pair<Proposal, Proposal>(reqProposal, respProposal); + } catch (Exception e) { + // In a locally-initiated case, release all locally generated SPIs in the SA request + // payload. + for (Proposal p : reqSaPayload.proposalList) p.releaseSpiResourceIfExists(); + throw e; + } + } + + @VisibleForTesting + interface TransformDecoder { + Transform[] decodeTransforms(int count, ByteBuffer inputBuffer) throws IkeProtocolException; + } + + // TODO: Add another constructor for building outbound message. + + /** + * This class represents the common information of an IKE Proposal and a Child Proposal. + * + * <p>Proposal represents a set contains cryptographic algorithms and key generating materials. + * It contains multiple {@link Transform}. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.1">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2)</a> + * <p>Proposals with an unrecognized Protocol ID, containing an unrecognized Transform Type + * or lacking a necessary Transform Type shall be ignored when processing a received SA + * Payload. + */ + public abstract static class Proposal { + private static final byte LAST_PROPOSAL = 0; + private static final byte NOT_LAST_PROPOSAL = 2; + + private static final int PROPOSAL_RESERVED_FIELD_LEN = 1; + private static final int PROPOSAL_HEADER_LEN = 8; + + @VisibleForTesting + static TransformDecoder sTransformDecoder = + new TransformDecoder() { + @Override + public Transform[] decodeTransforms(int count, ByteBuffer inputBuffer) + throws IkeProtocolException { + Transform[] transformArray = new Transform[count]; + for (int i = 0; i < count; i++) { + Transform transform = Transform.readFrom(inputBuffer); + if (transform.isSupported) { + transformArray[i] = transform; + } + } + return transformArray; + } + }; + + public final byte number; + /** All supported protocol will fall into {@link ProtocolId} */ + public final int protocolId; + + public final byte spiSize; + public final long spi; + + public final boolean hasUnrecognizedTransform; + + @VisibleForTesting + Proposal( + byte number, + int protocolId, + byte spiSize, + long spi, + boolean hasUnrecognizedTransform) { + this.number = number; + this.protocolId = protocolId; + this.spiSize = spiSize; + this.spi = spi; + this.hasUnrecognizedTransform = hasUnrecognizedTransform; + } + + @VisibleForTesting + static Proposal readFrom(ByteBuffer inputBuffer) throws IkeProtocolException { + byte isLast = inputBuffer.get(); + if (isLast != LAST_PROPOSAL && isLast != NOT_LAST_PROPOSAL) { + throw new InvalidSyntaxException( + "Invalid value of Last Proposal Substructure: " + isLast); + } + // Skip RESERVED byte + inputBuffer.get(new byte[PROPOSAL_RESERVED_FIELD_LEN]); + + int length = Short.toUnsignedInt(inputBuffer.getShort()); + byte number = inputBuffer.get(); + int protocolId = Byte.toUnsignedInt(inputBuffer.get()); + + byte spiSize = inputBuffer.get(); + int transformCount = Byte.toUnsignedInt(inputBuffer.get()); + + // TODO: Add check: spiSize must be 0 in initial IKE SA negotiation + // spiSize should be either 8 for IKE or 4 for IPsec. + long spi = SPI_NOT_INCLUDED; + switch (spiSize) { + case SPI_LEN_NOT_INCLUDED: + // No SPI attached for IKE initial exchange. + break; + case SPI_LEN_IPSEC: + spi = Integer.toUnsignedLong(inputBuffer.getInt()); + break; + case SPI_LEN_IKE: + spi = inputBuffer.getLong(); + break; + default: + throw new InvalidSyntaxException( + "Invalid value of spiSize in Proposal Substructure: " + spiSize); + } + + Transform[] transformArray = + sTransformDecoder.decodeTransforms(transformCount, inputBuffer); + // TODO: Validate that sum of all Transforms' lengths plus Proposal header length equals + // to Proposal's length. + + List<EncryptionTransform> encryptAlgoList = new LinkedList<>(); + List<PrfTransform> prfList = new LinkedList<>(); + List<IntegrityTransform> integAlgoList = new LinkedList<>(); + List<DhGroupTransform> dhGroupList = new LinkedList<>(); + List<EsnTransform> esnList = new LinkedList<>(); + + boolean hasUnrecognizedTransform = false; + + for (Transform transform : transformArray) { + switch (transform.type) { + case Transform.TRANSFORM_TYPE_ENCR: + encryptAlgoList.add((EncryptionTransform) transform); + break; + case Transform.TRANSFORM_TYPE_PRF: + prfList.add((PrfTransform) transform); + break; + case Transform.TRANSFORM_TYPE_INTEG: + integAlgoList.add((IntegrityTransform) transform); + break; + case Transform.TRANSFORM_TYPE_DH: + dhGroupList.add((DhGroupTransform) transform); + break; + case Transform.TRANSFORM_TYPE_ESN: + esnList.add((EsnTransform) transform); + break; + default: + hasUnrecognizedTransform = true; + } + } + + if (protocolId == PROTOCOL_ID_IKE) { + IkeSaProposal saProposal = + new IkeSaProposal( + encryptAlgoList.toArray( + new EncryptionTransform[encryptAlgoList.size()]), + prfList.toArray(new PrfTransform[prfList.size()]), + integAlgoList.toArray(new IntegrityTransform[integAlgoList.size()]), + dhGroupList.toArray(new DhGroupTransform[dhGroupList.size()])); + return new IkeProposal(number, spiSize, spi, saProposal, hasUnrecognizedTransform); + } else { + ChildSaProposal saProposal = + new ChildSaProposal( + encryptAlgoList.toArray( + new EncryptionTransform[encryptAlgoList.size()]), + integAlgoList.toArray(new IntegrityTransform[integAlgoList.size()]), + dhGroupList.toArray(new DhGroupTransform[dhGroupList.size()]), + esnList.toArray(new EsnTransform[esnList.size()])); + return new ChildProposal(number, spi, saProposal, hasUnrecognizedTransform); + } + } + + /** Package private */ + boolean isNegotiatedFrom(Proposal reqProposal) { + if (protocolId != reqProposal.protocolId || number != reqProposal.number) { + return false; + } + return getSaProposal().isNegotiatedFrom(reqProposal.getSaProposal()); + } + + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + Transform[] allTransforms = getSaProposal().getAllTransforms(); + byte isLastIndicator = isLast ? LAST_PROPOSAL : NOT_LAST_PROPOSAL; + + byteBuffer + .put(isLastIndicator) + .put(new byte[PROPOSAL_RESERVED_FIELD_LEN]) + .putShort((short) getProposalLength()) + .put(number) + .put((byte) protocolId) + .put(spiSize) + .put((byte) allTransforms.length); + + switch (spiSize) { + case SPI_LEN_NOT_INCLUDED: + // No SPI attached for IKE initial exchange. + break; + case SPI_LEN_IPSEC: + byteBuffer.putInt((int) spi); + break; + case SPI_LEN_IKE: + byteBuffer.putLong((long) spi); + break; + default: + throw new IllegalArgumentException( + "Invalid value of spiSize in Proposal Substructure: " + spiSize); + } + + // Encode all Transform. + for (int i = 0; i < allTransforms.length; i++) { + // The last transform has the isLast flag set to true. + allTransforms[i].encodeToByteBuffer(i == allTransforms.length - 1, byteBuffer); + } + } + + protected int getProposalLength() { + int len = PROPOSAL_HEADER_LEN + spiSize; + + Transform[] allTransforms = getSaProposal().getAllTransforms(); + for (Transform t : allTransforms) len += t.getTransformLength(); + return len; + } + + @Override + @NonNull + public String toString() { + return "Proposal(" + number + ") " + getSaProposal().toString(); + } + + /** Package private method for releasing SPI resource in this unselected Proposal. */ + abstract void releaseSpiResourceIfExists(); + + /** Package private method for getting SaProposal */ + abstract SaProposal getSaProposal(); + } + + /** This class represents a Proposal for IKE SA negotiation. */ + public static final class IkeProposal extends Proposal { + private IkeSecurityParameterIndex mIkeSpiResource; + + public final IkeSaProposal saProposal; + + /** + * Construct IkeProposal from a decoded inbound message for IKE negotiation. + * + * <p>Package private + */ + IkeProposal( + byte number, + byte spiSize, + long spi, + IkeSaProposal saProposal, + boolean hasUnrecognizedTransform) { + super(number, PROTOCOL_ID_IKE, spiSize, spi, hasUnrecognizedTransform); + this.saProposal = saProposal; + } + + /** Construct IkeProposal for an outbound message for IKE negotiation. */ + private IkeProposal( + byte number, + byte spiSize, + IkeSecurityParameterIndex ikeSpiResource, + IkeSaProposal saProposal) { + super( + number, + PROTOCOL_ID_IKE, + spiSize, + ikeSpiResource == null ? SPI_NOT_INCLUDED : ikeSpiResource.getSpi(), + false /*hasUnrecognizedTransform*/); + mIkeSpiResource = ikeSpiResource; + this.saProposal = saProposal; + } + + /** + * Construct IkeProposal for an outbound message for IKE negotiation. + * + * <p>Package private + */ + @VisibleForTesting + static IkeProposal createIkeProposal( + byte number, byte spiSize, IkeSaProposal saProposal, InetAddress localAddress) + throws IOException { + // IKE_INIT uses SPI_LEN_NOT_INCLUDED, while rekeys use SPI_LEN_IKE + IkeSecurityParameterIndex spiResource = + (spiSize == SPI_LEN_NOT_INCLUDED + ? null + : IkeSecurityParameterIndex.allocateSecurityParameterIndex( + localAddress)); + return new IkeProposal(number, spiSize, spiResource, saProposal); + } + + /** Package private method for releasing SPI resource in this unselected Proposal. */ + void releaseSpiResourceIfExists() { + // mIkeSpiResource is null when doing IKE initial exchanges. + if (mIkeSpiResource == null) return; + mIkeSpiResource.close(); + mIkeSpiResource = null; + } + + /** + * Package private method for allocating SPI resource for a validated remotely generated IKE + * SA proposal. + */ + void allocateResourceForRemoteIkeSpi(InetAddress remoteAddress) throws IOException { + mIkeSpiResource = + IkeSecurityParameterIndex.allocateSecurityParameterIndex(remoteAddress, spi); + } + + @Override + public SaProposal getSaProposal() { + return saProposal; + } + + /** + * Get the IKE SPI resource. + * + * @return the IKE SPI resource or null for IKE initial exchanges. + */ + public IkeSecurityParameterIndex getIkeSpiResource() { + return mIkeSpiResource; + } + } + + /** This class represents a Proposal for Child SA negotiation. */ + public static final class ChildProposal extends Proposal { + private SecurityParameterIndex mChildSpiResource; + + public final ChildSaProposal saProposal; + + /** + * Construct ChildProposal from a decoded inbound message for Child SA negotiation. + * + * <p>Package private + */ + ChildProposal( + byte number, + long spi, + ChildSaProposal saProposal, + boolean hasUnrecognizedTransform) { + super( + number, + PROTOCOL_ID_ESP, + SPI_LEN_IPSEC, + spi, + hasUnrecognizedTransform); + this.saProposal = saProposal; + } + + /** Construct ChildProposal for an outbound message for Child SA negotiation. */ + private ChildProposal( + byte number, SecurityParameterIndex childSpiResource, ChildSaProposal saProposal) { + super( + number, + PROTOCOL_ID_ESP, + SPI_LEN_IPSEC, + (long) childSpiResource.getSpi(), + false /*hasUnrecognizedTransform*/); + mChildSpiResource = childSpiResource; + this.saProposal = saProposal; + } + + /** + * Construct ChildProposal for an outbound message for Child SA negotiation. + * + * <p>Package private + */ + @VisibleForTesting + static ChildProposal createChildProposal( + byte number, + ChildSaProposal saProposal, + IpSecManager ipSecManager, + InetAddress localAddress) + throws ResourceUnavailableException { + return new ChildProposal( + number, ipSecManager.allocateSecurityParameterIndex(localAddress), saProposal); + } + + /** Package private method for releasing SPI resource in this unselected Proposal. */ + void releaseSpiResourceIfExists() { + if (mChildSpiResource == null) return; + + mChildSpiResource.close(); + mChildSpiResource = null; + } + + /** + * Package private method for allocating SPI resource for a validated remotely generated + * Child SA proposal. + */ + void allocateResourceForRemoteChildSpi(IpSecManager ipSecManager, InetAddress remoteAddress) + throws ResourceUnavailableException, SpiUnavailableException { + mChildSpiResource = + ipSecManager.allocateSecurityParameterIndex(remoteAddress, (int) spi); + } + + @Override + public SaProposal getSaProposal() { + return saProposal; + } + + /** + * Get the IPsec SPI resource. + * + * @return the IPsec SPI resource. + */ + public SecurityParameterIndex getChildSpiResource() { + return mChildSpiResource; + } + } + + @VisibleForTesting + interface AttributeDecoder { + List<Attribute> decodeAttributes(int length, ByteBuffer inputBuffer) + throws IkeProtocolException; + } + + /** + * Transform is an abstract base class that represents the common information for all Transform + * types. It may contain one or more {@link Attribute}. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2)</a> + * <p>Transforms with unrecognized Transform ID or containing unrecognized Attribute Type + * shall be ignored when processing received SA payload. + */ + public abstract static class Transform { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TRANSFORM_TYPE_ENCR, + TRANSFORM_TYPE_PRF, + TRANSFORM_TYPE_INTEG, + TRANSFORM_TYPE_DH, + TRANSFORM_TYPE_ESN + }) + public @interface TransformType {} + + public static final int TRANSFORM_TYPE_ENCR = 1; + public static final int TRANSFORM_TYPE_PRF = 2; + public static final int TRANSFORM_TYPE_INTEG = 3; + public static final int TRANSFORM_TYPE_DH = 4; + public static final int TRANSFORM_TYPE_ESN = 5; + + private static final byte LAST_TRANSFORM = 0; + private static final byte NOT_LAST_TRANSFORM = 3; + + // Length of reserved field of a Transform. + private static final int TRANSFORM_RESERVED_FIELD_LEN = 1; + + // Length of the Transform that with no Attribute. + protected static final int BASIC_TRANSFORM_LEN = 8; + + // TODO: Add constants for supported algorithms + + @VisibleForTesting + static AttributeDecoder sAttributeDecoder = + new AttributeDecoder() { + public List<Attribute> decodeAttributes(int length, ByteBuffer inputBuffer) + throws IkeProtocolException { + List<Attribute> list = new LinkedList<>(); + int parsedLength = BASIC_TRANSFORM_LEN; + while (parsedLength < length) { + Pair<Attribute, Integer> pair = Attribute.readFrom(inputBuffer); + parsedLength += pair.second; + list.add(pair.first); + } + // TODO: Validate that parsedLength equals to length. + return list; + } + }; + + // Only supported type falls into {@link TransformType} + public final int type; + public final int id; + public final boolean isSupported; + + /** Construct an instance of Transform for building an outbound packet. */ + protected Transform(int type, int id) { + this.type = type; + this.id = id; + if (!isSupportedTransformId(id)) { + throw new IllegalArgumentException( + "Unsupported " + getTransformTypeString() + " Algorithm ID: " + id); + } + this.isSupported = true; + } + + /** Construct an instance of Transform for decoding an inbound packet. */ + protected Transform(int type, int id, List<Attribute> attributeList) { + this.type = type; + this.id = id; + this.isSupported = + isSupportedTransformId(id) && !hasUnrecognizedAttribute(attributeList); + } + + @VisibleForTesting + static Transform readFrom(ByteBuffer inputBuffer) throws IkeProtocolException { + byte isLast = inputBuffer.get(); + if (isLast != LAST_TRANSFORM && isLast != NOT_LAST_TRANSFORM) { + throw new InvalidSyntaxException( + "Invalid value of Last Transform Substructure: " + isLast); + } + + // Skip RESERVED byte + inputBuffer.get(new byte[TRANSFORM_RESERVED_FIELD_LEN]); + + int length = Short.toUnsignedInt(inputBuffer.getShort()); + int type = Byte.toUnsignedInt(inputBuffer.get()); + + // Skip RESERVED byte + inputBuffer.get(new byte[TRANSFORM_RESERVED_FIELD_LEN]); + + int id = Short.toUnsignedInt(inputBuffer.getShort()); + + // Decode attributes + List<Attribute> attributeList = sAttributeDecoder.decodeAttributes(length, inputBuffer); + + validateAttributeUniqueness(attributeList); + + switch (type) { + case TRANSFORM_TYPE_ENCR: + return new EncryptionTransform(id, attributeList); + case TRANSFORM_TYPE_PRF: + return new PrfTransform(id, attributeList); + case TRANSFORM_TYPE_INTEG: + return new IntegrityTransform(id, attributeList); + case TRANSFORM_TYPE_DH: + return new DhGroupTransform(id, attributeList); + case TRANSFORM_TYPE_ESN: + return new EsnTransform(id, attributeList); + default: + return new UnrecognizedTransform(type, id, attributeList); + } + } + + // Throw InvalidSyntaxException if there are multiple Attributes of the same type + private static void validateAttributeUniqueness(List<Attribute> attributeList) + throws IkeProtocolException { + Set<Integer> foundTypes = new ArraySet<>(); + for (Attribute attr : attributeList) { + if (!foundTypes.add(attr.type)) { + throw new InvalidSyntaxException( + "There are multiple Attributes of the same type. "); + } + } + } + + // Check if there is Attribute with unrecognized type. + protected abstract boolean hasUnrecognizedAttribute(List<Attribute> attributeList); + + // Check if this Transform ID is supported. + protected abstract boolean isSupportedTransformId(int id); + + // Encode Transform to a ByteBuffer. + protected abstract void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer); + + // Get entire Transform length. + protected abstract int getTransformLength(); + + protected void encodeBasicTransformToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + byte isLastIndicator = isLast ? LAST_TRANSFORM : NOT_LAST_TRANSFORM; + byteBuffer + .put(isLastIndicator) + .put(new byte[TRANSFORM_RESERVED_FIELD_LEN]) + .putShort((short) getTransformLength()) + .put((byte) type) + .put(new byte[TRANSFORM_RESERVED_FIELD_LEN]) + .putShort((short) id); + } + + /** + * Get Tranform Type as a String. + * + * @return Tranform Type as a String. + */ + public abstract String getTransformTypeString(); + + // TODO: Add abstract getTransformIdString() to return specific algorithm/dhGroup name + } + + /** + * EncryptionTransform represents an encryption algorithm. It may contain an Atrribute + * specifying the key length. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2)</a> + */ + public static final class EncryptionTransform extends Transform { + public static final int KEY_LEN_UNSPECIFIED = 0; + + // When using encryption algorithm with variable-length keys, mSpecifiedKeyLength MUST be + // set and a KeyLengthAttribute MUST be attached. Otherwise, mSpecifiedKeyLength MUST NOT be + // set and KeyLengthAttribute MUST NOT be attached. + private final int mSpecifiedKeyLength; + + /** + * Contruct an instance of EncryptionTransform with fixed key length for building an + * outbound packet. + * + * @param id the IKE standard Transform ID. + */ + public EncryptionTransform(@EncryptionAlgorithm int id) { + this(id, KEY_LEN_UNSPECIFIED); + } + + /** + * Contruct an instance of EncryptionTransform with variable key length for building an + * outbound packet. + * + * @param id the IKE standard Transform ID. + * @param specifiedKeyLength the specified key length of this encryption algorithm. + */ + public EncryptionTransform(@EncryptionAlgorithm int id, int specifiedKeyLength) { + super(Transform.TRANSFORM_TYPE_ENCR, id); + + mSpecifiedKeyLength = specifiedKeyLength; + try { + validateKeyLength(); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Contruct an instance of EncryptionTransform for decoding an inbound packet. + * + * @param id the IKE standard Transform ID. + * @param attributeList the decoded list of Attribute. + * @throws InvalidSyntaxException for syntax error. + */ + protected EncryptionTransform(int id, List<Attribute> attributeList) + throws InvalidSyntaxException { + super(Transform.TRANSFORM_TYPE_ENCR, id, attributeList); + if (!isSupported) { + mSpecifiedKeyLength = KEY_LEN_UNSPECIFIED; + } else { + if (attributeList.size() == 0) { + mSpecifiedKeyLength = KEY_LEN_UNSPECIFIED; + } else { + KeyLengthAttribute attr = getKeyLengthAttribute(attributeList); + mSpecifiedKeyLength = attr.keyLength; + } + validateKeyLength(); + } + } + + /** + * Get the specified key length. + * + * @return the specified key length. + */ + public int getSpecifiedKeyLength() { + return mSpecifiedKeyLength; + } + + @Override + public int hashCode() { + return Objects.hash(type, id, mSpecifiedKeyLength); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof EncryptionTransform)) return false; + + EncryptionTransform other = (EncryptionTransform) o; + return (type == other.type + && id == other.id + && mSpecifiedKeyLength == other.mSpecifiedKeyLength); + } + + @Override + protected boolean isSupportedTransformId(int id) { + return SaProposal.isSupportedEncryptionAlgorithm(id); + } + + @Override + protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) { + for (Attribute attr : attributeList) { + if (attr instanceof UnrecognizedAttribute) { + return true; + } + } + return false; + } + + private KeyLengthAttribute getKeyLengthAttribute(List<Attribute> attributeList) { + for (Attribute attr : attributeList) { + if (attr.type == Attribute.ATTRIBUTE_TYPE_KEY_LENGTH) { + return (KeyLengthAttribute) attr; + } + } + throw new IllegalArgumentException("Cannot find Attribute with Key Length type"); + } + + private void validateKeyLength() throws InvalidSyntaxException { + switch (id) { + case SaProposal.ENCRYPTION_ALGORITHM_3DES: + if (mSpecifiedKeyLength != KEY_LEN_UNSPECIFIED) { + throw new InvalidSyntaxException( + "Must not set Key Length value for this " + + getTransformTypeString() + + " Algorithm ID: " + + id); + } + return; + case SaProposal.ENCRYPTION_ALGORITHM_AES_CBC: + /* fall through */ + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8: + /* fall through */ + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12: + /* fall through */ + case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16: + if (mSpecifiedKeyLength == KEY_LEN_UNSPECIFIED) { + throw new InvalidSyntaxException( + "Must set Key Length value for this " + + getTransformTypeString() + + " Algorithm ID: " + + id); + } + if (mSpecifiedKeyLength != SaProposal.KEY_LEN_AES_128 + && mSpecifiedKeyLength != SaProposal.KEY_LEN_AES_192 + && mSpecifiedKeyLength != SaProposal.KEY_LEN_AES_256) { + throw new InvalidSyntaxException( + "Invalid key length for this " + + getTransformTypeString() + + " Algorithm ID: " + + id); + } + return; + default: + // Won't hit here. + throw new IllegalArgumentException( + "Unrecognized Encryption Algorithm ID: " + id); + } + } + + @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + + if (mSpecifiedKeyLength != KEY_LEN_UNSPECIFIED) { + new KeyLengthAttribute(mSpecifiedKeyLength).encodeToByteBuffer(byteBuffer); + } + } + + @Override + protected int getTransformLength() { + int len = BASIC_TRANSFORM_LEN; + + if (mSpecifiedKeyLength != KEY_LEN_UNSPECIFIED) { + len += new KeyLengthAttribute(mSpecifiedKeyLength).getAttributeLength(); + } + + return len; + } + + @Override + public String getTransformTypeString() { + return "Encryption Algorithm"; + } + + @Override + @NonNull + public String toString() { + return SaProposal.getEncryptionAlgorithmString(id) + + "(" + + getSpecifiedKeyLength() + + ")"; + } + } + + /** + * PrfTransform represents an pseudorandom function. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2)</a> + */ + public static final class PrfTransform extends Transform { + /** + * Contruct an instance of PrfTransform for building an outbound packet. + * + * @param id the IKE standard Transform ID. + */ + public PrfTransform(@PseudorandomFunction int id) { + super(Transform.TRANSFORM_TYPE_PRF, id); + } + + /** + * Contruct an instance of PrfTransform for decoding an inbound packet. + * + * @param id the IKE standard Transform ID. + * @param attributeList the decoded list of Attribute. + * @throws InvalidSyntaxException for syntax error. + */ + protected PrfTransform(int id, List<Attribute> attributeList) + throws InvalidSyntaxException { + super(Transform.TRANSFORM_TYPE_PRF, id, attributeList); + } + + @Override + public int hashCode() { + return Objects.hash(type, id); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PrfTransform)) return false; + + PrfTransform other = (PrfTransform) o; + return (type == other.type && id == other.id); + } + + @Override + protected boolean isSupportedTransformId(int id) { + return SaProposal.isSupportedPseudorandomFunction(id); + } + + @Override + protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) { + return !attributeList.isEmpty(); + } + + @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + } + + @Override + protected int getTransformLength() { + return BASIC_TRANSFORM_LEN; + } + + @Override + public String getTransformTypeString() { + return "Pseudorandom Function"; + } + + @Override + @NonNull + public String toString() { + return SaProposal.getPseudorandomFunctionString(id); + } + } + + /** + * IntegrityTransform represents an integrity algorithm. + * + * <p>Proposing integrity algorithm for ESP SA is optional. Omitting the IntegrityTransform is + * equivalent to including it with a value of NONE. When multiple integrity algorithms are + * provided, choosing any of them are acceptable. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2)</a> + */ + public static final class IntegrityTransform extends Transform { + /** + * Contruct an instance of IntegrityTransform for building an outbound packet. + * + * @param id the IKE standard Transform ID. + */ + public IntegrityTransform(@IntegrityAlgorithm int id) { + super(Transform.TRANSFORM_TYPE_INTEG, id); + } + + /** + * Contruct an instance of IntegrityTransform for decoding an inbound packet. + * + * @param id the IKE standard Transform ID. + * @param attributeList the decoded list of Attribute. + * @throws InvalidSyntaxException for syntax error. + */ + protected IntegrityTransform(int id, List<Attribute> attributeList) + throws InvalidSyntaxException { + super(Transform.TRANSFORM_TYPE_INTEG, id, attributeList); + } + + @Override + public int hashCode() { + return Objects.hash(type, id); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof IntegrityTransform)) return false; + + IntegrityTransform other = (IntegrityTransform) o; + return (type == other.type && id == other.id); + } + + @Override + protected boolean isSupportedTransformId(int id) { + return SaProposal.isSupportedIntegrityAlgorithm(id); + } + + @Override + protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) { + return !attributeList.isEmpty(); + } + + @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + } + + @Override + protected int getTransformLength() { + return BASIC_TRANSFORM_LEN; + } + + @Override + public String getTransformTypeString() { + return "Integrity Algorithm"; + } + + @Override + @NonNull + public String toString() { + return SaProposal.getIntegrityAlgorithmString(id); + } + } + + /** + * DhGroupTransform represents a Diffie-Hellman Group + * + * <p>Proposing DH group for non-first Child SA is optional. Omitting the DhGroupTransform is + * equivalent to including it with a value of NONE. When multiple DH groups are provided, + * choosing any of them are acceptable. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2)</a> + */ + public static final class DhGroupTransform extends Transform { + /** + * Contruct an instance of DhGroupTransform for building an outbound packet. + * + * @param id the IKE standard Transform ID. + */ + public DhGroupTransform(@DhGroup int id) { + super(Transform.TRANSFORM_TYPE_DH, id); + } + + /** + * Contruct an instance of DhGroupTransform for decoding an inbound packet. + * + * @param id the IKE standard Transform ID. + * @param attributeList the decoded list of Attribute. + * @throws InvalidSyntaxException for syntax error. + */ + protected DhGroupTransform(int id, List<Attribute> attributeList) + throws InvalidSyntaxException { + super(Transform.TRANSFORM_TYPE_DH, id, attributeList); + } + + @Override + public int hashCode() { + return Objects.hash(type, id); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DhGroupTransform)) return false; + + DhGroupTransform other = (DhGroupTransform) o; + return (type == other.type && id == other.id); + } + + @Override + protected boolean isSupportedTransformId(int id) { + return SaProposal.isSupportedDhGroup(id); + } + + @Override + protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) { + return !attributeList.isEmpty(); + } + + @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + } + + @Override + protected int getTransformLength() { + return BASIC_TRANSFORM_LEN; + } + + @Override + public String getTransformTypeString() { + return "Diffie-Hellman Group"; + } + + @Override + @NonNull + public String toString() { + return SaProposal.getDhGroupString(id); + } + } + + /** + * EsnTransform represents ESN policy that indicates if IPsec SA uses tranditional 32-bit + * sequence numbers or extended(64-bit) sequence numbers. + * + * <p>Currently IKE library only supports negotiating IPsec SA that do not use extended sequence + * numbers. The Transform ID of EsnTransform in outbound packets is not user configurable. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2)</a> + */ + public static final class EsnTransform extends Transform { + @Retention(RetentionPolicy.SOURCE) + @IntDef({ESN_POLICY_NO_EXTENDED, ESN_POLICY_EXTENDED}) + public @interface EsnPolicy {} + + public static final int ESN_POLICY_NO_EXTENDED = 0; + public static final int ESN_POLICY_EXTENDED = 1; + + /** + * Construct an instance of EsnTransform indicates using no-extended sequence numbers for + * building an outbound packet. + */ + public EsnTransform() { + super(Transform.TRANSFORM_TYPE_ESN, ESN_POLICY_NO_EXTENDED); + } + + /** + * Contruct an instance of EsnTransform for decoding an inbound packet. + * + * @param id the IKE standard Transform ID. + * @param attributeList the decoded list of Attribute. + * @throws InvalidSyntaxException for syntax error. + */ + protected EsnTransform(int id, List<Attribute> attributeList) + throws InvalidSyntaxException { + super(Transform.TRANSFORM_TYPE_ESN, id, attributeList); + } + + @Override + public int hashCode() { + return Objects.hash(type, id); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof EsnTransform)) return false; + + EsnTransform other = (EsnTransform) o; + return (type == other.type && id == other.id); + } + + @Override + protected boolean isSupportedTransformId(int id) { + return (id == ESN_POLICY_NO_EXTENDED || id == ESN_POLICY_EXTENDED); + } + + @Override + protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) { + return !attributeList.isEmpty(); + } + + @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + } + + @Override + protected int getTransformLength() { + return BASIC_TRANSFORM_LEN; + } + + @Override + public String getTransformTypeString() { + return "Extended Sequence Numbers"; + } + + @Override + @NonNull + public String toString() { + if (id == ESN_POLICY_NO_EXTENDED) { + return "ESN_No_Extended"; + } + return "ESN_Extended"; + } + } + + /** + * UnrecognizedTransform represents a Transform with unrecognized Transform Type. + * + * <p>Proposals containing an UnrecognizedTransform should be ignored. + */ + protected static final class UnrecognizedTransform extends Transform { + protected UnrecognizedTransform(int type, int id, List<Attribute> attributeList) { + super(type, id, attributeList); + } + + @Override + protected boolean isSupportedTransformId(int id) { + return false; + } + + @Override + protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) { + return !attributeList.isEmpty(); + } + + @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + throw new UnsupportedOperationException( + "It is not supported to encode a Transform with" + getTransformTypeString()); + } + + @Override + protected int getTransformLength() { + throw new UnsupportedOperationException( + "It is not supported to get length of a Transform with " + + getTransformTypeString()); + } + + /** + * Return Tranform Type of Unrecognized Transform as a String. + * + * @return Tranform Type of Unrecognized Transform as a String. + */ + @Override + public String getTransformTypeString() { + return "Unrecognized Transform Type."; + } + } + + /** + * Attribute is an abtract base class for completing the specification of some {@link + * Transform}. + * + * <p>Attribute is either in Type/Value format or Type/Length/Value format. For TV format, + * Attribute length is always 4 bytes containing value for 2 bytes. While for TLV format, + * Attribute length is determined by length field. + * + * <p>Currently only Key Length type is supported + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.5">RFC 7296, Internet Key + * Exchange Protocol Version 2 (IKEv2)</a> + */ + public abstract static class Attribute { + @Retention(RetentionPolicy.SOURCE) + @IntDef({ATTRIBUTE_TYPE_KEY_LENGTH}) + public @interface AttributeType {} + + // Support only one Attribute type: Key Length. Should use Type/Value format. + public static final int ATTRIBUTE_TYPE_KEY_LENGTH = 14; + + // Mask to extract the left most AF bit to indicate Attribute Format. + private static final int ATTRIBUTE_FORMAT_MASK = 0x8000; + // Mask to extract 15 bits after the AF bit to indicate Attribute Type. + private static final int ATTRIBUTE_TYPE_MASK = 0x7fff; + + // Package private mask to indicate that Type-Value (TV) Attribute Format is used. + static final int ATTRIBUTE_FORMAT_TV = ATTRIBUTE_FORMAT_MASK; + + // Package private + static final int TV_ATTRIBUTE_VALUE_LEN = 2; + static final int TV_ATTRIBUTE_TOTAL_LEN = 4; + static final int TVL_ATTRIBUTE_HEADER_LEN = TV_ATTRIBUTE_TOTAL_LEN; + + // Only Key Length type belongs to AttributeType + public final int type; + + /** Construct an instance of an Attribute when decoding message. */ + protected Attribute(int type) { + this.type = type; + } + + @VisibleForTesting + static Pair<Attribute, Integer> readFrom(ByteBuffer inputBuffer) + throws IkeProtocolException { + short formatAndType = inputBuffer.getShort(); + int format = formatAndType & ATTRIBUTE_FORMAT_MASK; + int type = formatAndType & ATTRIBUTE_TYPE_MASK; + + int length = 0; + byte[] value = new byte[0]; + if (format == ATTRIBUTE_FORMAT_TV) { + // Type/Value format + length = TV_ATTRIBUTE_TOTAL_LEN; + value = new byte[TV_ATTRIBUTE_VALUE_LEN]; + } else { + // Type/Length/Value format + if (type == ATTRIBUTE_TYPE_KEY_LENGTH) { + throw new InvalidSyntaxException("Wrong format in Transform Attribute"); + } + + length = Short.toUnsignedInt(inputBuffer.getShort()); + int valueLen = length - TVL_ATTRIBUTE_HEADER_LEN; + // IkeMessage will catch exception if valueLen is negative. + value = new byte[valueLen]; + } + + inputBuffer.get(value); + + switch (type) { + case ATTRIBUTE_TYPE_KEY_LENGTH: + return new Pair(new KeyLengthAttribute(value), length); + default: + return new Pair(new UnrecognizedAttribute(type, value), length); + } + } + + // Encode Attribute to a ByteBuffer. + protected abstract void encodeToByteBuffer(ByteBuffer byteBuffer); + + // Get entire Attribute length. + protected abstract int getAttributeLength(); + } + + /** KeyLengthAttribute represents a Key Length type Attribute */ + public static final class KeyLengthAttribute extends Attribute { + public final int keyLength; + + protected KeyLengthAttribute(byte[] value) { + this(Short.toUnsignedInt(ByteBuffer.wrap(value).getShort())); + } + + protected KeyLengthAttribute(int keyLength) { + super(ATTRIBUTE_TYPE_KEY_LENGTH); + this.keyLength = keyLength; + } + + @Override + protected void encodeToByteBuffer(ByteBuffer byteBuffer) { + byteBuffer + .putShort((short) (ATTRIBUTE_FORMAT_TV | ATTRIBUTE_TYPE_KEY_LENGTH)) + .putShort((short) keyLength); + } + + @Override + protected int getAttributeLength() { + return TV_ATTRIBUTE_TOTAL_LEN; + } + } + + /** + * UnrecognizedAttribute represents a Attribute with unrecoginzed Attribute Type. + * + * <p>Transforms containing UnrecognizedAttribute should be ignored. + */ + protected static final class UnrecognizedAttribute extends Attribute { + protected UnrecognizedAttribute(int type, byte[] value) { + super(type); + } + + @Override + protected void encodeToByteBuffer(ByteBuffer byteBuffer) { + throw new UnsupportedOperationException( + "It is not supported to encode an unrecognized Attribute."); + } + + @Override + protected int getAttributeLength() { + throw new UnsupportedOperationException( + "It is not supported to get length of an unrecognized Attribute."); + } + } + + /** + * Encode SA payload to ByteBUffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + + for (int i = 0; i < proposalList.size(); i++) { + // The last proposal has the isLast flag set to true. + proposalList.get(i).encodeToByteBuffer(i == proposalList.size() - 1, byteBuffer); + } + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + int len = GENERIC_HEADER_LENGTH; + + for (Proposal p : proposalList) len += p.getProposalLength(); + + return len; + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "SA"; + } + + @Override + @NonNull + public String toString() { + StringBuilder sb = new StringBuilder(); + if (isSaResponse) { + sb.append("SA Response: "); + } else { + sb.append("SA Request: "); + } + + int len = proposalList.size(); + for (int i = 0; i < len; i++) { + sb.append(proposalList.get(i).toString()); + if (i < len - 1) sb.append(", "); + } + + return sb.toString(); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeSkPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeSkPayload.java new file mode 100644 index 00000000..0c2d1fff --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeSkPayload.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2018 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.message; + +import android.annotation.Nullable; +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.ipsec.ike.crypto.IkeCipher; +import com.android.internal.net.ipsec.ike.crypto.IkeMacIntegrity; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; + +/** + * IkeSkPayload represents the common information of an Encrypted and Authenticated Payload and an + * Encrypted and Authenticated Fragment Payload. + * + * <p>It contains other payloads in encrypted form. It is must be the last payload in the message. + * It should be the only payload in this implementation. + * + * <p>Critical bit must be ignored when doing decoding. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#page-105">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public class IkeSkPayload extends IkePayload { + + protected final IkeEncryptedPayloadBody mIkeEncryptedPayloadBody; + + /** + * Construct an instance of IkeSkPayload from decrypting an incoming packet. + * + * @param critical indicates if it is a critical payload. + * @param message the byte array contains the whole IKE message. + * @param integrityMac the negotiated integrity algorithm. + * @param decryptCipher the negotiated encryption algorithm. + * @param integrityKey the negotiated integrity algorithm key. + * @param decryptionKey the negotiated decryption key. + */ + @VisibleForTesting + IkeSkPayload( + boolean critical, + byte[] message, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + byte[] integrityKey, + byte[] decryptionKey) + throws IkeProtocolException, GeneralSecurityException { + + this( + false /*isSkf*/, + critical, + IkeHeader.IKE_HEADER_LENGTH + GENERIC_HEADER_LENGTH, + message, + integrityMac, + decryptCipher, + integrityKey, + decryptionKey); + } + + /** Construct an instance of IkeSkPayload for testing.*/ + @VisibleForTesting + IkeSkPayload(boolean isSkf, IkeEncryptedPayloadBody encryptedPayloadBody) { + super(isSkf ? PAYLOAD_TYPE_SKF : PAYLOAD_TYPE_SK, false/*critical*/); + mIkeEncryptedPayloadBody = encryptedPayloadBody; + } + + /** Construct an instance of IkeSkPayload from decrypting an incoming packet. */ + protected IkeSkPayload( + boolean isSkf, + boolean critical, + int encryptedBodyOffset, + byte[] message, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + byte[] integrityKey, + byte[] decryptionKey) + throws IkeProtocolException, GeneralSecurityException { + super(isSkf ? PAYLOAD_TYPE_SKF : PAYLOAD_TYPE_SK, critical); + + // TODO: Support constructing IkeEncryptedPayloadBody using AEAD. + + mIkeEncryptedPayloadBody = + new IkeEncryptedPayloadBody( + message, + encryptedBodyOffset, + integrityMac, + decryptCipher, + integrityKey, + decryptionKey); + } + + /** + * Construct an instance of IkeSkPayload for building outbound packet. + * + * @param ikeHeader the IKE header. + * @param firstPayloadType the type of first payload nested in SkPayload. + * @param unencryptedPayloads the encoded payload list to protect. + * @param integrityMac the negotiated integrity algorithm. + * @param encryptCipher the negotiated encryption algorithm. + * @param integrityKey the negotiated integrity algorithm key. + * @param encryptionKey the negotiated encryption key. + */ + @VisibleForTesting + IkeSkPayload( + IkeHeader ikeHeader, + @PayloadType int firstPayloadType, + byte[] unencryptedPayloads, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher encryptCipher, + byte[] integrityKey, + byte[] encryptionKey) { + + this( + ikeHeader, + firstPayloadType, + new byte[0] /*skfHeaderBytes*/, + unencryptedPayloads, + integrityMac, + encryptCipher, + integrityKey, + encryptionKey); + } + + /** Construct an instance of IkeSkPayload for building outbound packet. */ + @VisibleForTesting + protected IkeSkPayload( + IkeHeader ikeHeader, + @PayloadType int firstPayloadType, + byte[] skfHeaderBytes, + byte[] unencryptedPayloads, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher encryptCipher, + byte[] integrityKey, + byte[] encryptionKey) { + super(skfHeaderBytes.length == 0 ? PAYLOAD_TYPE_SK : PAYLOAD_TYPE_SKF, false); + + // TODO: Support constructing IkeEncryptedPayloadBody using AEAD. + + mIkeEncryptedPayloadBody = + new IkeEncryptedPayloadBody( + ikeHeader, + firstPayloadType, + skfHeaderBytes, + unencryptedPayloads, + integrityMac, + encryptCipher, + integrityKey, + encryptionKey); + } + + /** + * Return unencrypted data. + * + * @return unencrypted data in a byte array. + */ + public byte[] getUnencryptedData() { + return mIkeEncryptedPayloadBody.getUnencryptedData(); + } + + /** + * Encode this payload to a ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + byteBuffer.put(mIkeEncryptedPayloadBody.encode()); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + mIkeEncryptedPayloadBody.getLength(); + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "SK"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeSkfPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeSkfPayload.java new file mode 100644 index 00000000..6faea123 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeSkfPayload.java @@ -0,0 +1,175 @@ +/* + * 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.message; + +import android.annotation.Nullable; +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.annotations.VisibleForTesting; +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.exceptions.InvalidSyntaxException; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; + +/** + * IkeSkfPayload represents an Encrypted and Authenticated Fragment Payload. + * + * @see <a href="https://tools.ietf.org/html/rfc7383">RFC 7383, Internet Key Exchange Protocol + * Version 2 (IKEv2) Message Fragmentation</a> + */ +public final class IkeSkfPayload extends IkeSkPayload { + public static final int SKF_HEADER_LEN = 4; + + /** Current Fragment message number, starting from 1 */ + public final int fragmentNum; + /** Number of Fragment messages into which the original message was divided */ + public final int totalFragments; + + /** + * Construct an instance of IkeSkfPayload by authenticating and decrypting an incoming packet. + * + * <p>SKF Payload with invalid fragmentNum or invalid totalFragments, or cannot be authenticated + * or decrypted MUST be discarded + * + * @param critical indicates if it is a critical payload. + * @param message the byte array contains the whole IKE message. + * @param integrityMac the negotiated integrity algorithm. + * @param decryptCipher the negotiated encryption algorithm. + * @param integrityKey the negotiated integrity algorithm key. + * @param decryptionKey the negotiated decryption key. + */ + IkeSkfPayload( + boolean critical, + byte[] message, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher decryptCipher, + byte[] integrityKey, + byte[] decryptionKey) + throws IkeProtocolException, GeneralSecurityException { + super( + true /*isSkf*/, + critical, + IkeHeader.IKE_HEADER_LENGTH + GENERIC_HEADER_LENGTH + SKF_HEADER_LEN, + message, + integrityMac, + decryptCipher, + integrityKey, + decryptionKey); + + // TODO: Support constructing IkeEncryptedPayloadBody using AEAD. + + ByteBuffer inputBuffer = ByteBuffer.wrap(message); + inputBuffer.get(new byte[IkeHeader.IKE_HEADER_LENGTH + GENERIC_HEADER_LENGTH]); + + fragmentNum = Short.toUnsignedInt(inputBuffer.getShort()); + totalFragments = Short.toUnsignedInt(inputBuffer.getShort()); + + if (fragmentNum < 1 || totalFragments < 1 || fragmentNum > totalFragments) { + throw new InvalidSyntaxException( + "Received invalid Fragment Number or Total Fragments Number. Fragment Number: " + + fragmentNum + + " Total Fragments: " + + totalFragments); + } + } + + /** + * Construct an instance of IkeSkfPayload for building outbound packet. + * + * @param ikeHeader the IKE header. + * @param firstPayloadType the type of first payload nested in SkPayload. + * @param unencryptedPayloads the encoded payload list to protect. + * @param integrityMac the negotiated integrity algorithm. + * @param encryptCipher the negotiated encryption algorithm. + * @param integrityKey the negotiated integrity algorithm key. + * @param encryptionKey the negotiated encryption key. + */ + IkeSkfPayload( + IkeHeader ikeHeader, + @PayloadType int firstPayloadType, + byte[] unencryptedPayloads, + @Nullable IkeMacIntegrity integrityMac, + IkeCipher encryptCipher, + byte[] integrityKey, + byte[] encryptionKey, + int fragNum, + int totalFrags) { + super( + ikeHeader, + firstPayloadType, + encodeSkfHeader(fragNum, totalFrags), + unencryptedPayloads, + integrityMac, + encryptCipher, + integrityKey, + encryptionKey); + fragmentNum = fragNum; + totalFragments = totalFrags; + } + + /** Construct an instance of IkeSkfPayload for testing. */ + @VisibleForTesting + IkeSkfPayload(IkeEncryptedPayloadBody encryptedPayloadBody, int fragNum, int totalFrags) { + super(true /*isSkf*/, encryptedPayloadBody); + fragmentNum = fragNum; + totalFragments = totalFrags; + } + + @VisibleForTesting + static byte[] encodeSkfHeader(int fragNum, int totalFrags) { + ByteBuffer buffer = ByteBuffer.allocate(SKF_HEADER_LEN); + buffer.putShort((short) fragNum).putShort((short) totalFrags); + return buffer.array(); + } + + /** + * Encode this payload to a ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + byteBuffer + .putShort((short) fragmentNum) + .putShort((short) totalFragments) + .put(mIkeEncryptedPayloadBody.encode()); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + SKF_HEADER_LEN + mIkeEncryptedPayloadBody.getLength(); + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "SKF"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeTsPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeTsPayload.java new file mode 100644 index 00000000..207bdc36 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeTsPayload.java @@ -0,0 +1,160 @@ +/* + * 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.message; + +import android.net.ipsec.ike.IkeTrafficSelector; +import android.net.ipsec.ike.exceptions.IkeProtocolException; + +import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException; + +import java.nio.ByteBuffer; + +/** + * IkeTsPayload represents an Traffic Selector Initiator Payload or an Traffic Selector Responder + * Payload. + * + * <p>Traffic Selector Initiator Payload and Traffic Selector Responder Payload have same format but + * different payload types. They describe the address ranges and port ranges of Child SA initiator + * and Child SA responder. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.13">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeTsPayload extends IkePayload { + // Length of Traffic Selector Payload header. + private static final int TS_HEADER_LEN = 4; + // Length of reserved field in octets. + private static final int TS_HEADER_RESERVED_LEN = 3; + + /** Number of Traffic Selectors */ + public final int numTs; + /** Array of Traffic Selectors */ + public final IkeTrafficSelector[] trafficSelectors; + + IkeTsPayload(boolean critical, byte[] payloadBody, boolean isInitiator) + throws IkeProtocolException { + super((isInitiator ? PAYLOAD_TYPE_TS_INITIATOR : PAYLOAD_TYPE_TS_RESPONDER), critical); + + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + numTs = Byte.toUnsignedInt(inputBuffer.get()); + if (numTs == 0) { + throw new InvalidSyntaxException("Cannot find Traffic Selector in TS payload."); + } + + // Skip RESERVED byte + inputBuffer.get(new byte[TS_HEADER_RESERVED_LEN]); + + // Decode Traffic Selectors + byte[] tsBytes = new byte[inputBuffer.remaining()]; + inputBuffer.get(tsBytes); + trafficSelectors = IkeTrafficSelector.decodeIkeTrafficSelectors(numTs, tsBytes); + } + + /** + * Construct an instance of IkeTsPayload for building an outbound IKE message. + * + * @param isInitiator indicates if this payload is for a Child SA initiator or responder. + * @param ikeTrafficSelectors the array of included traffic selectors. + */ + public IkeTsPayload(boolean isInitiator, IkeTrafficSelector[] ikeTrafficSelectors) { + super((isInitiator ? PAYLOAD_TYPE_TS_INITIATOR : PAYLOAD_TYPE_TS_RESPONDER), false); + + if (ikeTrafficSelectors == null || ikeTrafficSelectors.length == 0) { + throw new IllegalArgumentException( + "TS Payload requires at least one Traffic Selector."); + } + + numTs = ikeTrafficSelectors.length; + trafficSelectors = ikeTrafficSelectors; + } + + /** + * Check if this TS payload contains the all TS in the provided TS payload. + * + * <p>A TS response cannot be narrower than a TS request. When doing rekey, the newly negotiated + * TS cannot be narrower than old negotiated TS. + * + * <p>This method will be used to (1) validate that an inbound response is subset of a locally + * generated request; and (2) validate that an inbound rekey request/response is superset of + * current negotiated TS. + * + * @param tsPayload the other TS payload to validate + * @return true if current TS Payload contains all TS in the input tsPayload + */ + public boolean contains(IkeTsPayload tsPayload) { + subTsLoop: + for (IkeTrafficSelector subTs : tsPayload.trafficSelectors) { + for (IkeTrafficSelector superTs : this.trafficSelectors) { + if (superTs.contains(subTs)) { + continue subTsLoop; + } + } + return false; + } + return true; + } + + /** + * Encode Traffic Selector Payload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + + byteBuffer.put((byte) numTs).put(new byte[TS_HEADER_RESERVED_LEN]); + for (IkeTrafficSelector ts : trafficSelectors) { + ts.encodeToByteBuffer(byteBuffer); + } + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + int len = GENERIC_HEADER_LENGTH + TS_HEADER_LEN; + for (IkeTrafficSelector ts : trafficSelectors) { + len += ts.selectorLength; + } + + return len; + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + switch (payloadType) { + case PAYLOAD_TYPE_TS_INITIATOR: + return "TSi"; + case PAYLOAD_TYPE_TS_RESPONDER: + return "TSr"; + default: + // Won't reach here. + throw new IllegalArgumentException( + "Invalid Payload Type for Traffic Selector Payload."); + } + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeUnsupportedPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeUnsupportedPayload.java new file mode 100644 index 00000000..ecfe1e9c --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeUnsupportedPayload.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 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.message; + +import java.nio.ByteBuffer; + +/** + * IkeUnsupportedPayload represents anunsupported payload. + * + * <p>This special payload is only created in decoding process. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.5">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +final class IkeUnsupportedPayload extends IkePayload { + /** + * Construct an instance of IkeSaPayload in the context of IkePayloadFactory. + * + * @param payload indicates the current payload type + * @param critical indicates if it is a critical payload. + */ + IkeUnsupportedPayload(int payload, boolean critical) { + super(payload, critical); + } + + /** + * Throw an Exception when trying to encode this payload. + * + * @throws UnsupportedOperationException for this payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + throw new UnsupportedOperationException( + "It is not supported to encode a " + getTypeString()); + } + + /** + * Throw an Exception when trying to get payload length + * + * @throws UnsupportedOperationException for this payload. + */ + @Override + protected int getPayloadLength() { + throw new UnsupportedOperationException( + "It is not supported to get payload length of " + getTypeString()); + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return String.valueOf(payloadType); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeVendorPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeVendorPayload.java new file mode 100644 index 00000000..d3464071 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeVendorPayload.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2018 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.message; + +import java.nio.ByteBuffer; + +/** + * IkeVendorPayload represents a Vendor ID payload + * + * <p>Vendor ID allows a vendor to experiment with new features. This implementation doesn't have + * support for any specific Vendor IDs. + * + * <p>Critical bit must be ignored when doing decoding. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#page-105">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeVendorPayload extends IkePayload { + public final byte[] vendorId; + + /** + * Construct an instance of IkeVendorPayload in the context of {@link IkePayloadFactory}. + * + * @param critical indicates if it is a critical payload. + * @param payloadBody the vendor ID. + */ + IkeVendorPayload(boolean critical, byte[] payloadBody) { + super(PAYLOAD_TYPE_VENDOR, critical); + vendorId = payloadBody; + } + + /** + * Throw an Exception when trying to encode this payload. + * + * @throws UnsupportedOperationException for this payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + throw new UnsupportedOperationException( + "It is not supported to encode a " + getTypeString()); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + return GENERIC_HEADER_LENGTH + vendorId.length; + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "Vendor"; + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/utils/FdEventsReader.java b/src/java/com/android/internal/net/ipsec/ike/utils/FdEventsReader.java new file mode 100644 index 00000000..65f9cedc --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/utils/FdEventsReader.java @@ -0,0 +1,270 @@ +/* + * 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.utils; + +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.util.SocketUtils; +import android.os.Handler; +import android.os.Looper; +import android.os.MessageQueue; +import android.system.ErrnoException; +import android.system.OsConstants; + +import java.io.FileDescriptor; +import java.io.IOException; + +/** + * This class encapsulates the mechanics of registering a file descriptor + * with a thread's Looper and handling read events (and errors). + * + * Subclasses MUST implement createFd() and SHOULD override handlePacket(). They MAY override + * onStop() and onStart(). + * + * Subclasses can expect a call life-cycle like the following: + * + * [1] when a client calls start(), createFd() is called, followed by the onStart() hook if all + * goes well. Implementations may override onStart() for additional initialization. + * + * [2] yield, waiting for read event or error notification: + * + * [a] readPacket() && handlePacket() + * + * [b] if (no error): + * goto 2 + * else: + * goto 3 + * + * [3] when a client calls stop(), the onStop() hook is called (unless already stopped or never + * started). Implementations may override onStop() for additional cleanup. + * + * The packet receive buffer is recycled on every read call, so subclasses + * should make any copies they would like inside their handlePacket() + * implementation. + * + * All public methods MUST only be called from the same thread with which + * the Handler constructor argument is associated. + * + * <p> This code is an exact copy of {@link FdEventsReader} in + * frameworks/base/packages/NetworkStack/src/android/net/util/FdEventsReader.java, except the class + * name is changed to avoid confusion. + * + * FIXME: b/130058477 Find a way to share the code between network stack and code outside. + * + * @param <BufferType> the type of the buffer used to read data. + * @hide + */ +public abstract class FdEventsReader<BufferType> { + private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; + private static final int UNREGISTER_THIS_FD = 0; + + @NonNull + private final Handler mHandler; + @NonNull + private final MessageQueue mQueue; + @NonNull + private final BufferType mBuffer; + @Nullable + private FileDescriptor mFd; + private long mPacketsReceived; + + protected static void closeFd(FileDescriptor fd) { + try { + SocketUtils.closeSocket(fd); + } catch (IOException ignored) { + } + } + + protected FdEventsReader(@NonNull Handler h, @NonNull BufferType buffer) { + mHandler = h; + mQueue = mHandler.getLooper().getQueue(); + mBuffer = buffer; + } + + /** Start this FdEventsReader. */ + public void start() { + if (onCorrectThread()) { + createAndRegisterFd(); + } else { + mHandler.post(() -> { + logError("start() called from off-thread", null); + createAndRegisterFd(); + }); + } + } + + /** Stop this FdEventsReader and destroy the file descriptor. */ + public void stop() { + if (onCorrectThread()) { + unregisterAndDestroyFd(); + } else { + mHandler.post(() -> { + logError("stop() called from off-thread", null); + unregisterAndDestroyFd(); + }); + } + } + + @NonNull + public Handler getHandler() { + return mHandler; + } + + protected abstract int recvBufSize(@NonNull BufferType buffer); + + /** Returns the size of the receive buffer. */ + public int recvBufSize() { + return recvBufSize(mBuffer); + } + + /** + * Get the number of successful calls to {@link #readPacket(FileDescriptor, Object)}. + * + * <p>A call was successful if {@link #readPacket(FileDescriptor, Object)} returned a value > 0. + */ + public final long numPacketsReceived() { + return mPacketsReceived; + } + + /** + * Subclasses MUST create the listening socket here, including setting all desired socket + * options, interface or address/port binding, etc. The socket MUST be created nonblocking. + */ + @Nullable + protected abstract FileDescriptor createFd(); + + /** + * Implementations MUST return the bytes read or throw an Exception. + * + * <p>The caller may throw a {@link ErrnoException} with {@link OsConstants#EAGAIN} or + * {@link OsConstants#EINTR}, in which case {@link FdEventsReader} will ignore the buffer + * contents and respectively wait for further input or retry the read immediately. For all other + * exceptions, the {@link FdEventsReader} will be stopped with no more interactions with this + * method. + */ + protected abstract int readPacket(@NonNull FileDescriptor fd, @NonNull BufferType buffer) + throws Exception; + + /** + * Called by the main loop for every packet. Any desired copies of + * |recvbuf| should be made in here, as the underlying byte array is + * reused across all reads. + */ + protected void handlePacket(@NonNull BufferType recvbuf, int length) {} + + /** + * Called by the main loop to log errors. In some cases |e| may be null. + */ + protected void logError(@NonNull String msg, @Nullable Exception e) {} + + /** + * Called by start(), if successful, just prior to returning. + */ + protected void onStart() {} + + /** + * Called by stop() just prior to returning. + */ + protected void onStop() {} + + private void createAndRegisterFd() { + if (mFd != null) return; + + try { + mFd = createFd(); + } catch (Exception e) { + logError("Failed to create socket: ", e); + closeFd(mFd); + mFd = null; + } + + if (mFd == null) return; + + mQueue.addOnFileDescriptorEventListener( + mFd, + FD_EVENTS, + (fd, events) -> { + // Always call handleInput() so read/recvfrom are given + // a proper chance to encounter a meaningful errno and + // perhaps log a useful error message. + if (!isRunning() || !handleInput()) { + unregisterAndDestroyFd(); + return UNREGISTER_THIS_FD; + } + return FD_EVENTS; + }); + onStart(); + } + + private boolean isRunning() { + return (mFd != null) && mFd.valid(); + } + + // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error. + private boolean handleInput() { + while (isRunning()) { + final int bytesRead; + + try { + bytesRead = readPacket(mFd, mBuffer); + if (bytesRead < 1) { + if (isRunning()) logError("Socket closed, exiting", null); + break; + } + mPacketsReceived++; + } catch (ErrnoException e) { + if (e.errno == OsConstants.EAGAIN) { + // We've read everything there is to read this time around. + return true; + } else if (e.errno == OsConstants.EINTR) { + continue; + } else { + if (isRunning()) logError("readPacket error: ", e); + break; + } + } catch (Exception e) { + if (isRunning()) logError("readPacket error: ", e); + break; + } + + try { + handlePacket(mBuffer, bytesRead); + } catch (Exception e) { + logError("handlePacket error: ", e); + break; + } + } + + return false; + } + + private void unregisterAndDestroyFd() { + if (mFd == null) return; + + mQueue.removeOnFileDescriptorEventListener(mFd); + closeFd(mFd); + mFd = null; + onStop(); + } + + private boolean onCorrectThread() { + return (mHandler.getLooper() == Looper.myLooper()); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/utils/PacketReader.java b/src/java/com/android/internal/net/ipsec/ike/utils/PacketReader.java new file mode 100644 index 00000000..cd6b98d2 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/utils/PacketReader.java @@ -0,0 +1,66 @@ +/* + * 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.utils; + +import static java.lang.Math.max; + +import android.os.Handler; +import android.system.Os; + +import java.io.FileDescriptor; + +/** + * Specialization of {@link FdEventsReader} that reads packets into a byte array. + * + * TODO: rename this class to something more correctly descriptive (something + * like [or less horrible than] IkeFdReadEventsHandler?). + * + * <p> This code is a exact copy of {@link PacketReader} in + * frameworks/base/packages/NetworkStack/src/android/net/util/PacketReader.java, except the class + * name is changed to avoid confusion. + * + * FIXME: b/130058477 Find a way to share the code between network stack and code outside. + * + * @hide + */ +public abstract class PacketReader extends FdEventsReader<byte[]> { + + public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024; + + protected PacketReader(Handler h) { + this(h, DEFAULT_RECV_BUF_SIZE); + } + + protected PacketReader(Handler h, int recvBufSize) { + super(h, new byte[max(recvBufSize, DEFAULT_RECV_BUF_SIZE)]); + } + + @Override + protected final int recvBufSize(byte[] buffer) { + return buffer.length; + } + + /** + * Subclasses MAY override this to change the default read() implementation + * in favour of, say, recvfrom(). + * + * Implementations MUST return the bytes read or throw an Exception. + */ + @Override + protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception { + return Os.read(fd, packetBuffer, 0, packetBuffer.length); + } +} diff --git a/src/java/com/android/internal/net/ipsec/ike/utils/Retransmitter.java b/src/java/com/android/internal/net/ipsec/ike/utils/Retransmitter.java new file mode 100644 index 00000000..778d6859 --- /dev/null +++ b/src/java/com/android/internal/net/ipsec/ike/utils/Retransmitter.java @@ -0,0 +1,139 @@ +/* + * 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.utils; + +import static com.android.internal.net.ipsec.ike.IkeSessionStateMachine.CMD_RETRANSMIT; + +import android.os.Handler; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.ipsec.ike.message.IkeMessage; + +/** + * Retransmitter represents a class that will send a message and trigger delayed retransmissions + * + * <p>The Retransmitter class will queue retransmission signals on the provided handler. The owner + * of this retransmitter instance is expected to wait for the signal, and call retransmit() on the + * instance of this class. + */ +public abstract class Retransmitter { + private static IBackoffTimeoutCalculator sBackoffTimeoutCalculator = + new BackoffTimeoutCalculator(); + + /* + * Retransmit parameters + * + * (Re)transmission count | Relative timeout | Absolute timeout + * -------------------------+-------------------+------------------ + * 0 | 500ms | 500ms + * 1 | 1s | 1.5s + * 2 | 2s | 3.5s + * 3 | 4s | 7.5s + * 4 | 8s | 15.5s + * 5 | 16s | 31.5s + * + * TODO: Add retransmitter configurability + */ + static final double RETRANSMIT_BACKOFF_FACTOR = 2.0; + static final long RETRANSMIT_TIMEOUT_MS = 500L; + static final int RETRANSMIT_MAX_ATTEMPTS = 5; + + private final Handler mHandler; + private final IkeMessage mRetransmitMsg; + private int mRetransmitCount = 0; + + public Retransmitter(Handler handler, IkeMessage msg) { + mHandler = handler; + mRetransmitMsg = msg; + } + + /** + * Triggers a (re)transmission. Will enqueue a future retransmission signal on the given handler + */ + public void retransmit() { + if (mRetransmitMsg == null) { + return; + } + + // If the failed iteration is beyond the max attempts, clean up and shut down. + if (mRetransmitCount > RETRANSMIT_MAX_ATTEMPTS) { + handleRetransmissionFailure(); + return; + } + + send(mRetransmitMsg); + + long timeout = sBackoffTimeoutCalculator.getExponentialBackoffTimeout(mRetransmitCount++); + mHandler.sendMessageDelayed(mHandler.obtainMessage(CMD_RETRANSMIT, this), timeout); + } + + /** Cancels any future retransmissions */ + public void stopRetransmitting() { + mHandler.removeMessages(CMD_RETRANSMIT, this); + } + + /** Retrieves the message this retransmitter is tracking */ + public IkeMessage getMessage() { + return mRetransmitMsg; + } + + /** + * Implementation-provided sender + * + * <p>For Retransmitter-internal use only. + * + * @param msg the message to be sent + */ + protected abstract void send(IkeMessage msg); + + /** + * Callback for implementations to be informed that we have reached the max retransmissions. + * + * <p>For Retransmitter-internal use only. + */ + protected abstract void handleRetransmissionFailure(); + + /** + * IBackoffTimeoutCalculator provides interface for calculating retransmission backoff timeout. + * + * <p>IBackoffTimeoutCalculator exists so that the interface is injectable for testing. + */ + @VisibleForTesting + public interface IBackoffTimeoutCalculator { + /** Calculate retransmission backoff timeout */ + long getExponentialBackoffTimeout(int retransmitCount); + } + + private static final class BackoffTimeoutCalculator implements IBackoffTimeoutCalculator { + @Override + public long getExponentialBackoffTimeout(int retransmitCount) { + double expBackoffFactor = Math.pow(RETRANSMIT_BACKOFF_FACTOR, retransmitCount); + return (long) (RETRANSMIT_TIMEOUT_MS * expBackoffFactor); + } + } + + /** Sets IBackoffTimeoutCalculator */ + @VisibleForTesting + public static void setBackoffTimeoutCalculator(IBackoffTimeoutCalculator calculator) { + sBackoffTimeoutCalculator = calculator; + } + + /** Resets BackoffTimeoutCalculator of retransmitter */ + @VisibleForTesting + public static void resetBackoffTimeoutCalculator() { + sBackoffTimeoutCalculator = new BackoffTimeoutCalculator(); + } +} |