aboutsummaryrefslogtreecommitdiff
path: root/src/java/com/android/internal/net/ipsec
diff options
context:
space:
mode:
Diffstat (limited to 'src/java/com/android/internal/net/ipsec')
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/AbstractSessionStateMachine.java199
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/ChildSessionStateMachine.java2268
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/ChildSessionStateMachineFactory.java95
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/IkeDhParams.java43
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/IkeEapAuthenticatorFactory.java40
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/IkeLocalRequestScheduler.java119
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachine.java4235
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/IkeSocket.java281
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/SaRecord.java1098
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/crypto/IkeCipher.java178
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/crypto/IkeCombinedModeCipher.java209
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/crypto/IkeCrypto.java57
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/crypto/IkeMac.java93
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/crypto/IkeMacIntegrity.java189
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/crypto/IkeMacPrf.java152
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/crypto/IkeNormalModeCipher.java124
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/AuthenticationFailedException.java64
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidKeException.java66
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidMajorVersionException.java65
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidMessageIdException.java68
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/InvalidSyntaxException.java73
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/NoValidProposalChosenException.java67
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/TemporaryFailureException.java54
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/TsUnacceptableException.java50
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/UnrecognizedIkeProtocolException.java41
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/exceptions/UnsupportedCriticalPayloadException.java78
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeAuthDigitalSignPayload.java265
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeAuthPayload.java128
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeAuthPskPayload.java141
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeCertPayload.java175
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeCertX509CertPayload.java105
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeConfigPayload.java767
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeDeletePayload.java177
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeEapPayload.java93
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeEncryptedPayloadBody.java390
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeHeader.java244
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeIdPayload.java167
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeInformationalPayload.java28
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeKePayload.java272
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeMessage.java981
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeNoncePayload.java108
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeNotifyPayload.java462
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkePayload.java252
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkePayloadFactory.java244
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java1773
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeSkPayload.java207
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeSkfPayload.java175
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeTsPayload.java160
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeUnsupportedPayload.java71
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeVendorPayload.java76
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/utils/FdEventsReader.java270
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/utils/PacketReader.java66
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/utils/Retransmitter.java139
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();
+ }
+}