Merge master@5406228 into git_qt-dev-plus-aosp.
am: 50360dc367

Change-Id: Icb589c27b56a9c4f61d362f9ddb4d5479dce8b9f
diff --git a/Android.mk b/Android.mk
index 16642a2..14e9e9e 100644
--- a/Android.mk
+++ b/Android.mk
@@ -20,7 +20,7 @@
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src/java)
 
-LOCAL_JAVA_LIBRARIES := bouncycastle
+LOCAL_JAVA_LIBRARIES := bouncycastle NetworkStackBase
 
 LOCAL_MODULE_TAGS := optional
 LOCAL_MODULE := ike
diff --git a/src/java/com/android/ike/ikev2/ChildSessionOptions.java b/src/java/com/android/ike/ikev2/ChildSessionOptions.java
index cbca9f4..b311c66 100644
--- a/src/java/com/android/ike/ikev2/ChildSessionOptions.java
+++ b/src/java/com/android/ike/ikev2/ChildSessionOptions.java
@@ -20,6 +20,6 @@
  * ChildSessionOptions contains user-provided Child SA proposals and negotiated Child SA
  * information.
  */
-public class ChildSessionOptions {
+public final class ChildSessionOptions {
     // TODO: Implement it.
 }
diff --git a/src/java/com/android/ike/ikev2/IkeSessionOptions.java b/src/java/com/android/ike/ikev2/IkeSessionOptions.java
index 66ef94d..9f2094f 100644
--- a/src/java/com/android/ike/ikev2/IkeSessionOptions.java
+++ b/src/java/com/android/ike/ikev2/IkeSessionOptions.java
@@ -16,9 +16,107 @@
 
 package com.android.ike.ikev2;
 
+import android.net.IpSecManager.UdpEncapsulationSocket;
+
+import com.android.ike.ikev2.message.IkePayload;
+
+import java.net.InetAddress;
+import java.util.LinkedList;
+import java.util.List;
+
 /**
- * IkeSessionOptions contains all configurations including cryptographic algorithm set of an IKE SA.
+ * IkeSessionOptions contains all user provided configurations for negotiating an IKE SA.
+ *
+ * <p>TODO: Make this doc more user-friendly.
  */
-public class IkeSessionOptions {
-    // TODO: Implement it.
+public final class IkeSessionOptions {
+    private final InetAddress mServerAddress;
+    private final UdpEncapsulationSocket mUdpEncapSocket;
+    private final SaProposal[] mSaProposals;
+    private final boolean mIsIkeFragmentationSupported;
+
+    private IkeSessionOptions(
+            InetAddress serverAddress,
+            UdpEncapsulationSocket udpEncapsulationSocket,
+            SaProposal[] proposals,
+            boolean isIkeFragmentationSupported) {
+        mServerAddress = serverAddress;
+        mUdpEncapSocket = udpEncapsulationSocket;
+        mSaProposals = proposals;
+        mIsIkeFragmentationSupported = isIkeFragmentationSupported;
+    }
+
+    /** Package private */
+    InetAddress getServerAddress() {
+        return mServerAddress;
+    }
+    /** Package private */
+    UdpEncapsulationSocket getUdpEncapsulationSocket() {
+        return mUdpEncapSocket;
+    }
+    /** Package private */
+    SaProposal[] getSaProposals() {
+        return mSaProposals;
+    }
+    /** Package private */
+    boolean isIkeFragmentationSupported() {
+        return mIsIkeFragmentationSupported;
+    }
+
+    /** This class can be used to incrementally construct a IkeSessionOptions. */
+    public static final class Builder {
+        private final InetAddress mServerAddress;
+        private final UdpEncapsulationSocket mUdpEncapSocket;
+        private final List<SaProposal> mSaProposalList = new LinkedList<>();
+
+        private boolean mIsIkeFragmentationSupported = false;
+
+        /**
+         * Returns a new Builder for an IkeSessionOptions.
+         *
+         * @param serverAddress IP address of remote IKE server.
+         * @param udpEncapsulationSocket {@link IpSecManager.UdpEncapsulationSocket} for sending and
+         *     receiving IKE message.
+         * @return Builder for an IkeSessionOptions.
+         */
+        public Builder(InetAddress serverAddress, UdpEncapsulationSocket udpEncapsulationSocket) {
+            mServerAddress = serverAddress;
+            mUdpEncapSocket = udpEncapsulationSocket;
+        }
+
+        /**
+         * Adds an IKE SA proposal to IkeSessionOptions being built.
+         *
+         * @param proposal IKE SA proposal.
+         * @return Builder for an IkeSessionOptions.
+         * @throws IllegalArgumentException if input proposal is not IKE SA proposal.
+         */
+        public Builder addSaProposal(SaProposal proposal) {
+            if (proposal.getProtocolId() != IkePayload.PROTOCOL_ID_IKE) {
+                throw new IllegalArgumentException(
+                        "Expected IKE SA Proposal but received Child SA proposal");
+            }
+            mSaProposalList.add(proposal);
+            return this;
+        }
+
+        /**
+         * Validates, builds and returns the IkeSessionOptions
+         *
+         * @return IkeSessionOptions the validated IkeSessionOptions
+         * @throws IllegalStateException if no IKE SA proposal is provided
+         */
+        public IkeSessionOptions build() {
+            if (mSaProposalList.isEmpty()) {
+                throw new IllegalArgumentException("IKE SA proposal not found");
+            }
+            return new IkeSessionOptions(
+                    mServerAddress,
+                    mUdpEncapSocket,
+                    mSaProposalList.toArray(new SaProposal[mSaProposalList.size()]),
+                    mIsIkeFragmentationSupported);
+        }
+
+        // TODO: add methods for supporting IKE fragmentation.
+    }
 }
diff --git a/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java b/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java
index c887ecb..b4a7b9c 100644
--- a/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java
+++ b/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java
@@ -17,20 +17,30 @@
 
 import android.os.Looper;
 import android.os.Message;
+import android.system.ErrnoException;
 import android.util.LongSparseArray;
-import android.util.Pair;
 import android.util.SparseArray;
 
 import com.android.ike.ikev2.SaRecord.IkeSaRecord;
 import com.android.ike.ikev2.exceptions.IkeException;
 import com.android.ike.ikev2.message.IkeHeader;
+import com.android.ike.ikev2.message.IkeKePayload;
 import com.android.ike.ikev2.message.IkeMessage;
+import com.android.ike.ikev2.message.IkeNoncePayload;
 import com.android.ike.ikev2.message.IkeNotifyPayload;
+import com.android.ike.ikev2.message.IkePayload;
+import com.android.ike.ikev2.message.IkeSaPayload;
+import com.android.ike.ikev2.message.IkeSaPayload.DhGroupTransform;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 
 import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
 
 /**
  * IkeSessionStateMachine tracks states and manages exchanges of this IKE session.
@@ -74,6 +84,11 @@
     static final int CMD_LOCAL_REQUEST_REKEY_CHILD = CMD_LOCAL_REQUEST_BASE + 7;
     // TODO: Add signals for other procedure types and notificaitons.
 
+    // Remember locally assigned IKE SPIs to avoid SPI collision.
+    private static final Set<Long> ASSIGNED_LOCAL_IKE_SPI_SET = new HashSet<>();
+    private static final int MAX_ASSIGN_IKE_SPI_ATTEMPTS = 100;
+    private static final SecureRandom IKE_SPI_RANDOM = new SecureRandom();
+
     private final IkeSessionOptions mIkeSessionOptions;
     private final ChildSessionOptions mFirstChildSessionOptions;
     /** Map that stores all IkeSaRecords, keyed by remotely generated IKE SPI. */
@@ -85,6 +100,12 @@
      */
     private final SparseArray<ChildSessionStateMachine> mSpiToChildSessionMap;
 
+    /**
+     * Package private socket that sends and receives encoded IKE message. Initialized in Initial
+     * State.
+     */
+    @VisibleForTesting IkeSocket mIkeSocket;
+
     /** Package */
     @VisibleForTesting IkeSaRecord mCurrentIkeSaRecord;
     /** Package */
@@ -146,9 +167,55 @@
         setInitialState(mInitial);
     }
 
+    // Generate IKE SPI. Throw an exception if it failed and handle this exception in current State.
+    private static Long getIkeSpiOrThrow() {
+        for (int i = 0; i < MAX_ASSIGN_IKE_SPI_ATTEMPTS; i++) {
+            long spi = IKE_SPI_RANDOM.nextLong();
+            if (ASSIGNED_LOCAL_IKE_SPI_SET.add(spi)) return spi;
+        }
+        throw new IllegalStateException("Failed to generate IKE SPI.");
+    }
+
     private IkeMessage buildIkeInitReq() {
-        // TODO:Build packet according to mIkeSessionOptions.
-        return null;
+        // TODO: Handle IKE SPI assigning error in CreateIkeLocalIkeInit State.
+
+        List<IkePayload> payloadList = new LinkedList<>();
+
+        // Generate IKE SPI
+        long initSpi = getIkeSpiOrThrow();
+        long respSpi = 0;
+
+        // It is validated in IkeSessionOptions.Builder to ensure IkeSessionOptions has at least one
+        // SaProposal and all SaProposals are valid for IKE SA negotiation.
+        SaProposal[] saProposals = mIkeSessionOptions.getSaProposals();
+
+        // Build SA Payload
+        IkeSaPayload saPayload = new IkeSaPayload(saProposals);
+        payloadList.add(saPayload);
+
+        // Build KE Payload using the first DH group number in the first SaProposal.
+        DhGroupTransform dhGroupTransform = saProposals[0].getDhGroupTransforms()[0];
+        IkeKePayload kePayload = new IkeKePayload(dhGroupTransform.id);
+        payloadList.add(kePayload);
+
+        // Build Nonce Payload
+        IkeNoncePayload noncePayload = new IkeNoncePayload();
+        payloadList.add(noncePayload);
+
+        // 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 IkeMessage buildIkeAuthReq() {
@@ -214,6 +281,19 @@
     }
 
     /**
+     * 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.
      */
@@ -223,9 +303,9 @@
         /** Entire encoded IKE message including IKE header */
         public final byte[] ikePacketBytes;
 
-        ReceivedIkePacket(Pair<IkeHeader, byte[]> ikePacketPair) {
-            ikeHeader = ikePacketPair.first;
-            ikePacketBytes = ikePacketPair.second;
+        ReceivedIkePacket(IkeHeader ikeHeader, byte[] ikePacketBytes) {
+            this.ikeHeader = ikeHeader;
+            this.ikePacketBytes = ikePacketBytes;
         }
     }
 
@@ -260,6 +340,15 @@
     /** Initial state of IkeSessionStateMachine. */
     class Initial extends State {
         @Override
+        public void enter() {
+            try {
+                mIkeSocket = IkeSocket.getIkeSocket(mIkeSessionOptions.getUdpEncapsulationSocket());
+            } catch (ErrnoException e) {
+                // TODO: handle exception and close IkeSession.
+            }
+        }
+
+        @Override
         public boolean processMessage(Message message) {
             switch (message.what) {
                 case CMD_LOCAL_REQUEST_CREATE_IKE:
@@ -417,6 +506,7 @@
         public void enter() {
             mRequestMsg = buildRequest();
             mRequestPacket = encodeRequest();
+            mIkeSocket.sendIkePacket(mRequestPacket, mIkeSessionOptions.getServerAddress());
             // TODO: Send out packet and start retransmission timer.
         }
 
@@ -432,12 +522,20 @@
         // CreateIkeLocalInit should override encodeRequest() to encode unencrypted packet
         protected byte[] encodeRequest() {
             // TODO: encrypt and encode mRequestMsg
-            return null;
-        };
+            return new byte[0];
+        }
     }
 
     /** CreateIkeLocalIkeInit represents state when IKE library initiates IKE_INIT exchange. */
     class CreateIkeLocalIkeInit extends LocalNewExchangeBase {
+
+        @Override
+        public void enter() {
+            super.enter();
+            mIkeSocket.registerIke(
+                    mRequestMsg.ikeHeader.ikeInitiatorSpi, IkeSessionStateMachine.this);
+        }
+
         @Override
         protected IkeMessage buildRequest() {
             return buildIkeInitReq();
@@ -445,8 +543,7 @@
 
         @Override
         protected byte[] encodeRequest() {
-            // TODO: Encode an unencrypted IKE packet.
-            return null;
+            return mRequestMsg.encode();
         }
 
         @Override
@@ -522,8 +619,7 @@
                                         mFirstChildSessionOptions);
                         // TODO: Replace null input params to payload lists in IKE_AUTH request and
                         // IKE_AUTH response for negotiating Child SA.
-                        firstChild.handleFirstChildExchange(
-                                null, null, new ChildSessionCallback());
+                        firstChild.handleFirstChildExchange(null, null, new ChildSessionCallback());
 
                         transitionTo(mIdle);
                     } catch (IkeException e) {
diff --git a/src/java/com/android/ike/ikev2/IkeSocket.java b/src/java/com/android/ike/ikev2/IkeSocket.java
new file mode 100644
index 0000000..e235ab8
--- /dev/null
+++ b/src/java/com/android/ike/ikev2/IkeSocket.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ike.ikev2;
+
+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.util.PacketReader;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+import android.util.LongSparseArray;
+
+import com.android.ike.ikev2.exceptions.IkeException;
+import com.android.ike.ikev2.message.IkeHeader;
+import com.android.internal.annotations.VisibleForTesting;
+
+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.Map;
+
+/**
+ * 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<>();
+    // UdpEncapsulationSocket for sending and receving IKE packet.
+    private final UdpEncapsulationSocket mUdpEncapSocket;
+
+    /** Package private */
+    @VisibleForTesting
+    int mRefCount;
+
+    private IkeSocket(UdpEncapsulationSocket udpEncapSocket, Handler handler) {
+        super(handler);
+        mRefCount = 1;
+        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
+     * @return an IkSocket instance
+     */
+    public static IkeSocket getIkeSocket(UdpEncapsulationSocket udpEncapSocket)
+            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);
+
+        if (sFdToIkeSocketMap.containsKey(udpEncapSocket)) {
+            IkeSocket ikeSocket = sFdToIkeSocketMap.get(udpEncapSocket);
+            ikeSocket.mRefCount++;
+            return ikeSocket;
+        } else {
+            IkeSocket ikeSocket = new IkeSocket(udpEncapSocket, new Handler());
+            // Create and register FileDescriptor for receiving IKE packet on current thread.
+            ikeSocket.start();
+
+            sFdToIkeSocketMap.put(udpEncapSocket, ikeSocket);
+            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) {
+            // TODO: b/129708574 Consider only logging the error some % of the time it happens, or
+            // only logging the error the first time it happens and then keep a count to prevent
+            // logspam.
+
+            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.
+                Log.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);
+                IkeHeader ikeHeader = new IkeHeader(ikePacketBytes);
+
+                long localGeneratedSpi =
+                        ikeHeader.fromIkeInitiator
+                                ? ikeHeader.ikeResponderSpi
+                                : ikeHeader.ikeInitiatorSpi;
+
+                IkeSessionStateMachine ikeStateMachine = spiToIkeSession.get(localGeneratedSpi);
+                if (ikeStateMachine == null) {
+                    Log.e(TAG, "Unrecognized IKE SPI.");
+                    // TODO: Handle invalid IKE SPI error
+                } else {
+                    ikeStateMachine.receiveIkePacket(ikeHeader, ikePacketBytes);
+                }
+            } catch (IkeException e) {
+                // Handle invalid IKE header
+                Log.e(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) {
+        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() {
+        mRefCount--;
+        if (mRefCount == 0) 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/ike/ikev2/SaProposal.java b/src/java/com/android/ike/ikev2/SaProposal.java
index f932be2..53206cc 100644
--- a/src/java/com/android/ike/ikev2/SaProposal.java
+++ b/src/java/com/android/ike/ikev2/SaProposal.java
@@ -29,7 +29,9 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -137,17 +139,17 @@
     }
 
     /** Package private */
-    @IkePayload.ProtocolId final int mProtocolId;
+    @IkePayload.ProtocolId private final int mProtocolId;
     /** Package private */
-    final EncryptionTransform[] mEncryptionAlgorithms;
+    private final EncryptionTransform[] mEncryptionAlgorithms;
     /** Package private */
-    final PrfTransform[] mPseudorandomFunctions;
+    private final PrfTransform[] mPseudorandomFunctions;
     /** Package private */
-    final IntegrityTransform[] mIntegrityAlgorithms;
+    private final IntegrityTransform[] mIntegrityAlgorithms;
     /** Package private */
-    final DhGroupTransform[] mDhGroups;
+    private final DhGroupTransform[] mDhGroups;
     /** Package private */
-    final EsnTransform[] mEsns;
+    private final EsnTransform[] mEsns;
 
     private SaProposal(
             @IkePayload.ProtocolId int protocol,
@@ -225,6 +227,63 @@
         return Arrays.asList(selectFrom).contains(selected[0]);
     }
 
+    /*Package private*/
+    @IkePayload.ProtocolId
+    int getProtocolId() {
+        return mProtocolId;
+    }
+
+    /*Package private*/
+    EncryptionTransform[] getEncryptionTransforms() {
+        return mEncryptionAlgorithms;
+    }
+
+    /*Package private*/
+    PrfTransform[] getPrfTransforms() {
+        return mPseudorandomFunctions;
+    }
+
+    /*Package private*/
+    IntegrityTransform[] getIntegrityTransforms() {
+        return mIntegrityAlgorithms;
+    }
+
+    /*Package private*/
+    DhGroupTransform[] getDhGroupTransforms() {
+        return mDhGroups;
+    }
+
+    /*Package private*/
+    EsnTransform[] getEsnTransforms() {
+        return mEsns;
+    }
+
+    /**
+     * Return all SA Transforms in this SaProposal to be encoded for building an outbound IKE
+     * message.
+     *
+     * <p>This method can be called by only IKE library.
+     *
+     * @return Array of Transforms to be encoded.
+     */
+    public Transform[] getAllTransforms() {
+        int encodedNumTransforms =
+                mEncryptionAlgorithms.length
+                        + mPseudorandomFunctions.length
+                        + mIntegrityAlgorithms.length
+                        + mDhGroups.length
+                        + mEsns.length;
+
+        List<Transform> transformList = new ArrayList<Transform>(encodedNumTransforms);
+        transformList.addAll(Arrays.asList(mEncryptionAlgorithms));
+        transformList.addAll(Arrays.asList(mPseudorandomFunctions));
+        transformList.addAll(Arrays.asList(mIntegrityAlgorithms));
+        transformList.addAll(Arrays.asList(mDhGroups));
+        transformList.addAll(Arrays.asList(mEsns));
+
+        return transformList.toArray(new Transform[encodedNumTransforms]);
+    }
+
     /**
      * This class can be used to incrementally construct a SaProposal. SaProposal instances are
      * immutable once built.
@@ -474,7 +533,7 @@
          * @return SaProposal the validated SaProposal.
          * @throws IllegalArgumentException if SaProposal is invalid.
          */
-        public SaProposal buildOrThrow() {
+        public SaProposal build() {
             EncryptionTransform[] encryptionTransforms = buildEncryptAlgosOrThrow();
             PrfTransform[] prfTransforms = buildPrfsOrThrow();
             IntegrityTransform[] integrityTransforms =
diff --git a/src/java/com/android/ike/ikev2/SaRecord.java b/src/java/com/android/ike/ikev2/SaRecord.java
index 355d41d..4dcddec 100644
--- a/src/java/com/android/ike/ikev2/SaRecord.java
+++ b/src/java/com/android/ike/ikev2/SaRecord.java
@@ -17,9 +17,16 @@
 
 import com.android.ike.ikev2.message.IkeMessage;
 import com.android.ike.ikev2.message.IkePayload;
+import com.android.internal.annotations.VisibleForTesting;
 
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
 import java.util.List;
 
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
 /**
  * SaRecord represents common information of an IKE SA and a Child SA.
  *
@@ -199,4 +206,71 @@
         ChildSaRecord makeChildSaRecord(
                 List<IkePayload> reqPayloads, List<IkePayload> respPayloads);
     }
+
+    /** Generate SKEYSEED using negotiated PRF. */
+    @VisibleForTesting
+    static byte[] generateSKeySeed(
+            String prfAlgorithm, byte[] nonceInit, byte[] nonceResp, byte[] sharedDhKey) {
+        try {
+            ByteBuffer keyBuffer = ByteBuffer.allocate(nonceInit.length + nonceResp.length);
+            keyBuffer.put(nonceInit).put(nonceResp);
+            SecretKeySpec prfKeySpec = new SecretKeySpec(keyBuffer.array(), prfAlgorithm);
+
+            Mac prfMac = Mac.getInstance(prfAlgorithm, IkeMessage.getSecurityProvider());
+            prfMac.init(prfKeySpec);
+
+            ByteBuffer sharedKeyBuffer = ByteBuffer.wrap(sharedDhKey);
+            prfMac.update(sharedKeyBuffer);
+
+            return prfMac.doFinal();
+        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+            throw new IllegalArgumentException("Failed to generate SKEYSEED", e);
+        }
+    }
+
+    /**
+     * Derives key materials using negotiated PRF.
+     *
+     * <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 nternet Key Exchange
+     *     Protocol Version 2 (IKEv2) 2.13. Generating Keying Material </a>
+     */
+    @VisibleForTesting
+    static byte[] generateKeyMat(
+            String prfAlgorithm, byte[] prfKey, byte[] dataToSign, int keyMaterialLen)
+            throws InvalidKeyException {
+        try {
+            SecretKeySpec prfKeySpec = new SecretKeySpec(prfKey, prfAlgorithm);
+            Mac prfMac = Mac.getInstance(prfAlgorithm, IkeMessage.getSecurityProvider());
+
+            ByteBuffer keyMatBuffer = ByteBuffer.allocate(keyMaterialLen);
+
+            byte[] previousMac = new byte[0];
+            final int padLen = 1;
+            byte padValue = 1;
+
+            while (keyMatBuffer.remaining() > 0) {
+                prfMac.init(prfKeySpec);
+
+                ByteBuffer dataToSignBuffer =
+                        ByteBuffer.allocate(previousMac.length + dataToSign.length + padLen);
+                dataToSignBuffer.put(previousMac).put(dataToSign).put(padValue);
+                dataToSignBuffer.rewind();
+
+                prfMac.update(dataToSignBuffer);
+
+                previousMac = prfMac.doFinal();
+                keyMatBuffer.put(
+                        previousMac, 0, Math.min(previousMac.length, keyMatBuffer.remaining()));
+
+                padValue++;
+            }
+
+            return keyMatBuffer.array();
+        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+            throw new IllegalArgumentException("Failed to generate keying material", e);
+        }
+    }
 }
diff --git a/src/java/com/android/ike/ikev2/message/IkeDeletePayload.java b/src/java/com/android/ike/ikev2/message/IkeDeletePayload.java
new file mode 100644
index 0000000..fd4f364
--- /dev/null
+++ b/src/java/com/android/ike/ikev2/message/IkeDeletePayload.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ike.ikev2.message;
+
+import com.android.ike.ikev2.exceptions.IkeException;
+import com.android.ike.ikev2.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.
+ *
+ * @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 IkePayload {
+
+    @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 IkeException if there is any error
+     */
+    IkeDeletePayload(boolean critical, byte[] payloadBody) throws IkeException {
+        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.");
+        }
+    }
+
+    // TODO: Add a constructor for building outbound IKE message.
+
+    /**
+     * 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) {
+        throw new UnsupportedOperationException("Operation not supported.");
+        // TODO: Implement it.
+    }
+
+    /**
+     * Get entire payload length.
+     *
+     * @return entire payload length.
+     */
+    @Override
+    protected int getPayloadLength() {
+        throw new UnsupportedOperationException("Operation not supported.");
+        // TODO: Implement it.
+    }
+
+    /**
+     * Return the payload type as a String.
+     *
+     * @return the payload type as a String.
+     */
+    @Override
+    public String getTypeString() {
+        return "Delete Payload";
+    }
+}
diff --git a/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java b/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java
index 52ca2a7..a41d855 100644
--- a/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java
+++ b/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java
@@ -56,11 +56,7 @@
      * decrypting an incoming packet.
      */
     IkeEncryptedPayloadBody(
-            byte[] message,
-            Mac integrityMac,
-            int expectedChecksumLen,
-            Cipher decryptCipher,
-            SecretKey dKey)
+            byte[] message, Mac integrityMac, int checksumLen, Cipher decryptCipher, SecretKey dKey)
             throws IkeException, GeneralSecurityException {
         ByteBuffer inputBuffer = ByteBuffer.wrap(message);
 
@@ -77,16 +73,15 @@
                         - (IkeHeader.IKE_HEADER_LENGTH
                                 + IkePayload.GENERIC_HEADER_LENGTH
                                 + expectedIvLen
-                                + expectedChecksumLen);
+                                + checksumLen);
         // IkeMessage will catch exception if encryptedDataLen is negative.
         mEncryptedAndPaddedData = new byte[encryptedDataLen];
 
-        mIntegrityChecksum = new byte[expectedChecksumLen];
+        mIntegrityChecksum = new byte[checksumLen];
         inputBuffer.get(mIv).get(mEncryptedAndPaddedData).get(mIntegrityChecksum);
 
         // Authenticate and decrypt.
-        byte[] dataToAuthenticate =
-                Arrays.copyOfRange(message, 0, message.length - expectedChecksumLen);
+        byte[] dataToAuthenticate = Arrays.copyOfRange(message, 0, message.length - checksumLen);
         validateChecksumOrThrow(dataToAuthenticate, integrityMac, mIntegrityChecksum);
         mUnencryptedData = decrypt(mEncryptedAndPaddedData, decryptCipher, dKey, mIv);
     }
@@ -96,17 +91,19 @@
      * building an outbound packet.
      */
     IkeEncryptedPayloadBody(
-            byte[] ikeAndPayloadHeader,
+            IkeHeader ikeHeader,
+            @IkePayload.PayloadType int firstPayloadType,
             byte[] unencryptedPayloads,
             Mac integrityMac,
-            int expectedChecksumLen,
+            int checksumLen,
             Cipher encryptCipher,
             SecretKey eKey) {
         this(
-                ikeAndPayloadHeader,
+                ikeHeader,
+                firstPayloadType,
                 unencryptedPayloads,
                 integrityMac,
-                expectedChecksumLen,
+                checksumLen,
                 encryptCipher,
                 eKey,
                 encryptCipher.getIV(),
@@ -116,10 +113,11 @@
     /** Package private constructor only for testing. */
     @VisibleForTesting
     IkeEncryptedPayloadBody(
-            byte[] ikeAndPayloadHeader,
+            IkeHeader ikeHeader,
+            @IkePayload.PayloadType int firstPayloadType,
             byte[] unencryptedPayloads,
             Mac integrityMac,
-            int expectedChecksumLen,
+            int checksumLen,
             Cipher encryptCipher,
             SecretKey eKey,
             byte[] iv,
@@ -130,13 +128,40 @@
         mIv = iv;
         mEncryptedAndPaddedData = encrypt(unencryptedPayloads, encryptCipher, eKey, iv, padding);
 
+        // Build authenticated section using ByteBuffer. Authenticated section 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.
+        int dataToAuthenticateLength =
+                IkeHeader.IKE_HEADER_LENGTH
+                        + IkePayload.GENERIC_HEADER_LENGTH
+                        + iv.length
+                        + mEncryptedAndPaddedData.length;
+        ByteBuffer authenticatedSectionBuffer = ByteBuffer.allocate(dataToAuthenticateLength);
+
+        // Encode IKE header
+        int encryptedPayloadLength =
+                IkePayload.GENERIC_HEADER_LENGTH
+                        + iv.length
+                        + mEncryptedAndPaddedData.length
+                        + checksumLen;
+        ikeHeader.encodeToByteBuffer(authenticatedSectionBuffer, encryptedPayloadLength);
+
+        // Encode payload header. The next payload type field indicates the first payload nested in
+        // this SkPayload/SkfPayload.
+        int payloadLength =
+                IkePayload.GENERIC_HEADER_LENGTH
+                        + iv.length
+                        + mEncryptedAndPaddedData.length
+                        + checksumLen;
+        IkePayload.encodePayloadHeaderToByteBuffer(
+                firstPayloadType, payloadLength, authenticatedSectionBuffer);
+
+        // Encode iv and padded encrypted data.
+        authenticatedSectionBuffer.put(iv).put(mEncryptedAndPaddedData);
+
         // Calculate checksum
-        ByteBuffer inputBuffer =
-                ByteBuffer.allocate(
-                        ikeAndPayloadHeader.length + iv.length + mEncryptedAndPaddedData.length);
-        inputBuffer.put(ikeAndPayloadHeader).put(iv).put(mEncryptedAndPaddedData);
         mIntegrityChecksum =
-                calculateChecksum(inputBuffer.array(), integrityMac, expectedChecksumLen);
+                calculateChecksum(authenticatedSectionBuffer.array(), integrityMac, checksumLen);
     }
 
     // TODO: Add another constructor for AEAD protected payload.
@@ -145,16 +170,16 @@
 
     /** Package private for testing */
     @VisibleForTesting
-    static byte[] calculateChecksum(
-            byte[] dataToAuthenticate, Mac integrityMac, int expectedChecksumLen) {
+    static byte[] calculateChecksum(byte[] dataToAuthenticate, Mac integrityMac, int checksumLen) {
         ByteBuffer inputBuffer = ByteBuffer.wrap(dataToAuthenticate);
         integrityMac.update(inputBuffer);
-        byte[] calculatedChecksum =
-                Arrays.copyOfRange(integrityMac.doFinal(), 0, expectedChecksumLen);
+        byte[] calculatedChecksum = Arrays.copyOfRange(integrityMac.doFinal(), 0, checksumLen);
         return calculatedChecksum;
     }
 
-    private static void validateChecksumOrThrow(
+    /** Package private for testing */
+    @VisibleForTesting
+    static void validateChecksumOrThrow(
             byte[] dataToAuthenticate, Mac integrityMac, byte[] integrityChecksum)
             throws GeneralSecurityException {
         // TODO: Make it package private and add test.
@@ -188,8 +213,9 @@
         }
     }
 
-    private static byte[] decrypt(
-            byte[] encryptedData, Cipher decryptCipher, SecretKey dKey, byte[] iv)
+    /** Package private for testing */
+    @VisibleForTesting
+    static byte[] decrypt(byte[] encryptedData, Cipher decryptCipher, SecretKey dKey, byte[] iv)
             throws GeneralSecurityException {
         // TODO: Make it package private and add test.
         decryptCipher.init(Cipher.DECRYPT_MODE, dKey, new IvParameterSpec(iv));
diff --git a/src/java/com/android/ike/ikev2/message/IkeHeader.java b/src/java/com/android/ike/ikev2/message/IkeHeader.java
index ebd3553..c4f215c 100644
--- a/src/java/com/android/ike/ikev2/message/IkeHeader.java
+++ b/src/java/com/android/ike/ikev2/message/IkeHeader.java
@@ -23,6 +23,7 @@
 import com.android.ike.ikev2.exceptions.IkeException;
 import com.android.ike.ikev2.exceptions.InvalidMajorVersionException;
 import com.android.ike.ikev2.exceptions.InvalidSyntaxException;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -36,7 +37,7 @@
  *     Protocol Version 2 (IKEv2)</a>
  */
 public final class IkeHeader {
-    //TODO: b/122838549 Change IkeHeader to static inner class of IkeMessage.
+    // 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
@@ -69,7 +70,14 @@
     public final boolean isResponseMsg;
     public final boolean fromIkeInitiator;
     public final int messageId;
-    public final int messageLength;
+
+    // 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
@@ -82,7 +90,6 @@
      * @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
-     * @param length the length of the total message in octets
      */
     public IkeHeader(
             long iSpi,
@@ -91,8 +98,7 @@
             @ExchangeType int eType,
             boolean isResp,
             boolean fromInit,
-            int msgId,
-            int length) {
+            int msgId) {
         ikeInitiatorSpi = iSpi;
         ikeResponderSpi = rSpi;
         nextPayloadType = nextPType;
@@ -100,7 +106,8 @@
         isResponseMsg = isResp;
         fromIkeInitiator = fromInit;
         messageId = msgId;
-        messageLength = length;
+
+        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;
@@ -135,11 +142,21 @@
         fromIkeInitiator = ((flagsByte & 0x08) != 0);
 
         messageId = buffer.getInt();
-        messageLength = buffer.getInt();
+        mEncodedMessageLength = buffer.getInt();
     }
 
-    /** Validate syntax and major version. */
-    public void checkValidOrThrow(int packetLength) throws IkeException {
+    /*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 syntax and major version of inbound IKE header. */
+    public void checkInboundValidOrThrow(int packetLength) throws IkeException {
         if (majorVersion > 2) {
             // Receive higher version of protocol. Stop parsing.
             throw new InvalidMajorVersionException(majorVersion);
@@ -155,13 +172,13 @@
                 || exchangeType > EXCHANGE_TYPE_INFORMATIONAL) {
             throw new InvalidSyntaxException("Invalid IKE Exchange Type.");
         }
-        if (messageLength != packetLength) {
+        if (mEncodedMessageLength != packetLength) {
             throw new InvalidSyntaxException("Invalid IKE Message Length.");
         }
     }
 
     /** Encode IKE header to ByteBuffer */
-    public void encodeToByteBuffer(ByteBuffer byteBuffer) {
+    public void encodeToByteBuffer(ByteBuffer byteBuffer, int encodedMessageBodyLen) {
         byteBuffer
                 .putLong(ikeInitiatorSpi)
                 .putLong(ikeResponderSpi)
@@ -177,6 +194,6 @@
             flag |= IKE_HEADER_FLAG_FROM_IKE_INITIATOR;
         }
 
-        byteBuffer.put(flag).putInt(messageId).putInt(messageLength);
+        byteBuffer.put(flag).putInt(messageId).putInt(IKE_HEADER_LENGTH + encodedMessageBodyLen);
     }
 }
diff --git a/src/java/com/android/ike/ikev2/message/IkeKePayload.java b/src/java/com/android/ike/ikev2/message/IkeKePayload.java
index a742d94..92e2238 100644
--- a/src/java/com/android/ike/ikev2/message/IkeKePayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeKePayload.java
@@ -16,7 +16,7 @@
 
 package com.android.ike.ikev2.message;
 
-import android.util.Pair;
+import android.annotation.Nullable;
 
 import com.android.ike.ikev2.IkeDhParams;
 import com.android.ike.ikev2.SaProposal;
@@ -27,9 +27,12 @@
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
 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 javax.crypto.KeyAgreement;
@@ -64,8 +67,22 @@
     /** 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
      *
@@ -79,6 +96,9 @@
     IkeKePayload(boolean critical, byte[] payloadBody) throws IkeException {
         super(PAYLOAD_TYPE_KE, critical);
 
+        isOutbound = false;
+        localPrivateKey = null;
+
         ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody);
 
         dhGroup = Short.toUnsignedInt(inputBuffer.getShort());
@@ -110,17 +130,67 @@
     /**
      * 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
-     * @param keData the Key Exchange data
      * @see <a href="https://tools.ietf.org/html/rfc7296#page-76">RFC 7296, Internet Key Exchange
      *     Protocol Version 2 (IKEv2), Critical.
      */
-    private IkeKePayload(@SaProposal.DhGroup int dh, byte[] keData) {
+    public IkeKePayload(@SaProposal.DhGroup int dh) {
         super(PAYLOAD_TYPE_KE, false);
+
         dhGroup = dh;
-        keyExchangeData = keData;
+        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);
+        }
     }
 
     /**
@@ -149,56 +219,6 @@
     }
 
     /**
-     * Construct an instance of IkeKePayload according to its {@link DhGroup}.
-     *
-     * @param dh the Dh-Group. It should be in {@link DhGroup}
-     * @return Pair of generated private key and an instance of IkeKePayload with key exchange data.
-     * @throws GeneralSecurityException for security-related exception.
-     */
-    public static Pair<DHPrivateKeySpec, IkeKePayload> getKePayload(@SaProposal.DhGroup int dh)
-            throws GeneralSecurityException {
-        BigInteger baseGen = BigInteger.valueOf(IkeDhParams.BASE_GENERATOR_MODP);
-        BigInteger prime = BigInteger.ZERO;
-        int keySize = 0;
-        switch (dh) {
-            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);
-        }
-
-        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
-        byte[] keData = BigIntegerUtils.bigIntegerToUnsignedByteArray(publicKey.getY(), keySize);
-
-        return new Pair(dhPrivateKeyspec, new IkeKePayload(dh, keData));
-    }
-
-    /**
      * Calculate the shared secret.
      *
      * @param privateKeySpec contains the local private key, DH prime and DH base generator.
diff --git a/src/java/com/android/ike/ikev2/message/IkeMessage.java b/src/java/com/android/ike/ikev2/message/IkeMessage.java
index 1594967..5b63ee7 100644
--- a/src/java/com/android/ike/ikev2/message/IkeMessage.java
+++ b/src/java/com/android/ike/ikev2/message/IkeMessage.java
@@ -103,7 +103,15 @@
         ikePayloadList = payloadList;
     }
 
-    static Provider getSecurityProvider() {
+    /**
+     * 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;
     }
 
@@ -231,7 +239,7 @@
     byte[] attachEncodedHeader(byte[] encodedIkeBody) {
         ByteBuffer outputBuffer =
                 ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH + encodedIkeBody.length);
-        ikeHeader.encodeToByteBuffer(outputBuffer);
+        ikeHeader.encodeToByteBuffer(outputBuffer, encodedIkeBody.length);
         outputBuffer.put(encodedIkeBody);
         return outputBuffer.array();
     }
@@ -319,13 +327,40 @@
                 IkeSessionOptions ikeSessionOptions,
                 IkeSaRecord ikeSaRecord,
                 IkeMessage ikeMessage) {
-            // TODO: Implement it.
+            // TODO: Extract crypto attributes and call encrypt()
             return null;
         }
 
+        //TODO: Create and use a container class for crypto algorithms and keys.
+        private byte[] encryptAndEncode(
+                IkeHeader ikeHeader,
+                @PayloadType int firstPayload,
+                byte[] unencryptedPayloads,
+                Mac integrityMac,
+                int checksumLen,
+                Cipher encryptCipher,
+                SecretKey eKey) {
+            IkeSkPayload skPayload =
+                    new IkeSkPayload(
+                            ikeHeader,
+                            firstPayload,
+                            unencryptedPayloads,
+                            integrityMac,
+                            checksumLen,
+                            encryptCipher,
+                            eKey);
+
+            ByteBuffer outputBuffer =
+                    ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH + skPayload.getPayloadLength());
+            ikeHeader.encodeToByteBuffer(outputBuffer, skPayload.getPayloadLength());
+            skPayload.encodeToByteBuffer(firstPayload, outputBuffer);
+
+            return outputBuffer.array();
+        }
+
         @Override
         public IkeMessage decode(IkeHeader header, byte[] inputPacket) throws IkeException {
-            header.checkValidOrThrow(inputPacket.length);
+            header.checkInboundValidOrThrow(inputPacket.length);
 
             byte[] unencryptedPayloads =
                     Arrays.copyOfRange(
@@ -362,7 +397,7 @@
                 SecretKey dKey)
                 throws IkeException, GeneralSecurityException {
 
-            header.checkValidOrThrow(inputPacket.length);
+            header.checkInboundValidOrThrow(inputPacket.length);
 
             if (header.nextPayloadType != IkePayload.PAYLOAD_TYPE_SK) {
                 // TODO: b/123372339 Handle message containing unprotected payloads.
diff --git a/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java b/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java
index 4b581e0..ee6fe47 100644
--- a/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java
@@ -40,7 +40,7 @@
  * outbound packet.
  *
  * @see <a href="https://tools.ietf.org/html/rfc7296">RFC 7296, Internet Key Exchange Protocol
- *     Version 2 (IKEv2).
+ *     Version 2 (IKEv2)</a>
  */
 public final class IkeNotifyPayload extends IkePayload {
 
diff --git a/src/java/com/android/ike/ikev2/message/IkePayload.java b/src/java/com/android/ike/ikev2/message/IkePayload.java
index a9d9f06..2474bb6 100644
--- a/src/java/com/android/ike/ikev2/message/IkePayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkePayload.java
@@ -53,6 +53,7 @@
         PAYLOAD_TYPE_ID_RESPONDER,
         PAYLOAD_TYPE_NONCE,
         PAYLOAD_TYPE_NOTIFY,
+        PAYLOAD_TYPE_DELETE,
         PAYLOAD_TYPE_VENDOR,
         PAYLOAD_TYPE_TS_INITIATOR,
         PAYLOAD_TYPE_TS_RESPONDER,
@@ -78,6 +79,8 @@
     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 */
@@ -137,7 +140,7 @@
      * @param payloadLength length of the entire payload
      * @param byteBuffer destination ByteBuffer that stores encoded payload header
      */
-    protected void encodePayloadHeaderToByteBuffer(
+    protected static void encodePayloadHeaderToByteBuffer(
             @PayloadType int nextPayload, int payloadLength, ByteBuffer byteBuffer) {
         byteBuffer
                 .put((byte) nextPayload)
diff --git a/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java b/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java
index 8f53018..bea87ca 100644
--- a/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java
+++ b/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java
@@ -78,6 +78,8 @@
                     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:
@@ -129,7 +131,7 @@
      *
      * @param message the byte array contains the whole IKE message.
      * @param integrityMac the initialized Mac for integrity check.
-     * @param expectedChecksumLen the expected length of integrity checksum.
+     * @param checksumLen the checksum length of negotiated integrity algorithm.
      * @param decryptCipher the uninitialized Cipher for doing decryption.
      * @param dKey the decryption key.
      * @return a pair including IkePayload and next payload type.
@@ -139,7 +141,7 @@
     protected static Pair<IkeSkPayload, Integer> getIkeSkPayload(
             byte[] message,
             Mac integrityMac,
-            int expectedChecksumLen,
+            int checksumLen,
             Cipher decryptCipher,
             SecretKey dKey)
             throws IkeException, GeneralSecurityException {
@@ -175,7 +177,7 @@
                         isCritical,
                         message,
                         integrityMac,
-                        expectedChecksumLen,
+                        checksumLen,
                         decryptCipher,
                         dKey);
         return new Pair(payload, nextPayloadType);
diff --git a/src/java/com/android/ike/ikev2/message/IkeSaPayload.java b/src/java/com/android/ike/ikev2/message/IkeSaPayload.java
index 7028084..c0651ae 100644
--- a/src/java/com/android/ike/ikev2/message/IkeSaPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeSaPayload.java
@@ -192,6 +192,9 @@
         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() {
@@ -246,7 +249,7 @@
                         "Invalid value of Last Proposal Substructure: " + isLast);
             }
             // Skip RESERVED byte
-            inputBuffer.get();
+            inputBuffer.get(new byte[PROPOSAL_RESERVED_FIELD_LEN]);
 
             int length = Short.toUnsignedInt(inputBuffer.getShort());
             byte number = inputBuffer.get();
@@ -259,8 +262,8 @@
             // spiSize should be either 8 for IKE or 4 for IPsec.
             long spi = SPI_NOT_INCLUDED;
             switch (spiSize) {
-                case 0:
-                    // No SPI field here.
+                case SPI_LEN_NOT_INCLUDED:
+                    // No SPI attached for IKE initial exchange.
                     break;
                 case SPI_LEN_IPSEC:
                     spi = Integer.toUnsignedLong(inputBuffer.getInt());
@@ -330,6 +333,49 @@
             }
             return saProposal.isNegotiatedFrom(reqProposal.saProposal);
         }
+
+        protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) {
+            Transform[] allTransforms = saProposal.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 = saProposal.getAllTransforms();
+            for (Transform t : allTransforms) len += t.getTransformLength();
+            return len;
+        }
     }
 
     @VisibleForTesting
@@ -366,7 +412,12 @@
 
         private static final byte LAST_TRANSFORM = 0;
         private static final byte NOT_LAST_TRANSFORM = 3;
-        private static final int TRANSFORM_HEADER_LEN = 8;
+
+        // 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
 
@@ -376,7 +427,7 @@
                     public List<Attribute> decodeAttributes(int length, ByteBuffer inputBuffer)
                             throws IkeException {
                         List<Attribute> list = new LinkedList<>();
-                        int parsedLength = TRANSFORM_HEADER_LEN;
+                        int parsedLength = BASIC_TRANSFORM_LEN;
                         while (parsedLength < length) {
                             Pair<Attribute, Integer> pair = Attribute.readFrom(inputBuffer);
                             parsedLength += pair.second;
@@ -420,13 +471,13 @@
             }
 
             // Skip RESERVED byte
-            inputBuffer.get();
+            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();
+            inputBuffer.get(new byte[TRANSFORM_RESERVED_FIELD_LEN]);
 
             int id = Short.toUnsignedInt(inputBuffer.getShort());
 
@@ -469,6 +520,23 @@
         // 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.
          *
@@ -487,9 +555,12 @@
      *     Exchange Protocol Version 2 (IKEv2)</a>
      */
     public static final class EncryptionTransform extends Transform {
-        private static final int KEY_LEN_UNASSIGNED = 0;
+        private static final int KEY_LEN_UNSPECIFIED = 0;
 
-        public final int keyLength;
+        // 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
@@ -498,7 +569,7 @@
          * @param id the IKE standard Transform ID.
          */
         public EncryptionTransform(@EncryptionAlgorithm int id) {
-            this(id, KEY_LEN_UNASSIGNED);
+            this(id, KEY_LEN_UNSPECIFIED);
         }
 
         /**
@@ -506,12 +577,12 @@
          * outbound packet.
          *
          * @param id the IKE standard Transform ID.
-         * @param keyLength the specified key length of this encryption algorithm.
+         * @param specifiedKeyLength the specified key length of this encryption algorithm.
          */
-        public EncryptionTransform(@EncryptionAlgorithm int id, int keyLength) {
+        public EncryptionTransform(@EncryptionAlgorithm int id, int specifiedKeyLength) {
             super(Transform.TRANSFORM_TYPE_ENCR, id);
 
-            this.keyLength = keyLength;
+            mSpecifiedKeyLength = specifiedKeyLength;
             try {
                 validateKeyLength();
             } catch (InvalidSyntaxException e) {
@@ -530,13 +601,13 @@
                 throws InvalidSyntaxException {
             super(Transform.TRANSFORM_TYPE_ENCR, id, attributeList);
             if (!isSupported) {
-                keyLength = KEY_LEN_UNASSIGNED;
+                mSpecifiedKeyLength = KEY_LEN_UNSPECIFIED;
             } else {
                 if (attributeList.size() == 0) {
-                    keyLength = KEY_LEN_UNASSIGNED;
+                    mSpecifiedKeyLength = KEY_LEN_UNSPECIFIED;
                 } else {
                     KeyLengthAttribute attr = getKeyLengthAttribute(attributeList);
-                    keyLength = attr.keyLength;
+                    mSpecifiedKeyLength = attr.keyLength;
                 }
                 validateKeyLength();
             }
@@ -544,7 +615,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(type, id, keyLength);
+            return Objects.hash(type, id, mSpecifiedKeyLength);
         }
 
         @Override
@@ -552,7 +623,9 @@
             if (!(o instanceof EncryptionTransform)) return false;
 
             EncryptionTransform other = (EncryptionTransform) o;
-            return (type == other.type && id == other.id && keyLength == other.keyLength);
+            return (type == other.type
+                    && id == other.id
+                    && mSpecifiedKeyLength == other.mSpecifiedKeyLength);
         }
 
         @Override
@@ -582,7 +655,7 @@
         private void validateKeyLength() throws InvalidSyntaxException {
             switch (id) {
                 case SaProposal.ENCRYPTION_ALGORITHM_3DES:
-                    if (keyLength != KEY_LEN_UNASSIGNED) {
+                    if (mSpecifiedKeyLength != KEY_LEN_UNSPECIFIED) {
                         throw new InvalidSyntaxException(
                                 "Must not set Key Length value for this "
                                         + getTransformTypeString()
@@ -597,16 +670,16 @@
                 case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12:
                     /* fall through */
                 case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16:
-                    if (keyLength == KEY_LEN_UNASSIGNED) {
+                    if (mSpecifiedKeyLength == KEY_LEN_UNSPECIFIED) {
                         throw new InvalidSyntaxException(
                                 "Must set Key Length value for this "
                                         + getTransformTypeString()
                                         + " Algorithm ID: "
                                         + id);
                     }
-                    if (keyLength != SaProposal.KEY_LEN_AES_128
-                            && keyLength != SaProposal.KEY_LEN_AES_192
-                            && keyLength != SaProposal.KEY_LEN_AES_256) {
+                    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()
@@ -622,6 +695,26 @@
         }
 
         @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";
         }
@@ -679,6 +772,16 @@
         }
 
         @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";
         }
@@ -740,6 +843,16 @@
         }
 
         @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";
         }
@@ -801,6 +914,16 @@
         }
 
         @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";
         }
@@ -868,6 +991,16 @@
         }
 
         @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";
         }
@@ -893,6 +1026,19 @@
             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.
          *
@@ -900,7 +1046,7 @@
          */
         @Override
         public String getTransformTypeString() {
-            return "Unrecognized Transform Type";
+            return "Unrecognized Transform Type.";
         }
     }
 
@@ -925,9 +1071,18 @@
         // Support only one Attribute type: Key Length. Should use Type/Value format.
         public static final int ATTRIBUTE_TYPE_KEY_LENGTH = 14;
 
-        private static final int TV_ATTRIBUTE_VALUE_LEN = 2;
-        private static final int TV_ATTRIBUTE_TOTAL_LEN = 4;
-        private static final int TVL_ATTRIBUTE_HEADER_LEN = TV_ATTRIBUTE_TOTAL_LEN;
+        // 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;
@@ -940,11 +1095,12 @@
         @VisibleForTesting
         static Pair<Attribute, Integer> readFrom(ByteBuffer inputBuffer) throws IkeException {
             short formatAndType = inputBuffer.getShort();
-            int type = formatAndType & 0x7fff;
+            int format = formatAndType & ATTRIBUTE_FORMAT_MASK;
+            int type = formatAndType & ATTRIBUTE_TYPE_MASK;
 
             int length = 0;
             byte[] value = new byte[0];
-            if ((formatAndType & 0x8000) == 0x8000) {
+            if (format == ATTRIBUTE_FORMAT_TV) {
                 // Type/Value format
                 length = TV_ATTRIBUTE_TOTAL_LEN;
                 value = new byte[TV_ATTRIBUTE_VALUE_LEN];
@@ -969,6 +1125,12 @@
                     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 */
@@ -983,6 +1145,18 @@
             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;
+        }
     }
 
     /**
@@ -994,6 +1168,18 @@
         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.");
+        }
     }
 
     /**
@@ -1004,9 +1190,12 @@
      */
     @Override
     protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) {
-        throw new UnsupportedOperationException(
-                "It is not supported to encode a " + getTypeString());
-        // TODO: Implement encoding SA payload.
+        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);
+        }
     }
 
     /**
@@ -1016,9 +1205,11 @@
      */
     @Override
     protected int getPayloadLength() {
-        throw new UnsupportedOperationException(
-                "It is not supported to get payload length of " + getTypeString());
-        // TODO: Implement this method for SA payload.
+        int len = GENERIC_HEADER_LENGTH;
+
+        for (Proposal p : proposalList) len += p.getProposalLength();
+
+        return len;
     }
 
     /**
diff --git a/src/java/com/android/ike/ikev2/message/IkeSkPayload.java b/src/java/com/android/ike/ikev2/message/IkeSkPayload.java
index 6a89a6a..d753ee8 100644
--- a/src/java/com/android/ike/ikev2/message/IkeSkPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeSkPayload.java
@@ -17,7 +17,6 @@
 package com.android.ike.ikev2.message;
 
 import com.android.ike.ikev2.exceptions.IkeException;
-import com.android.ike.ikev2.message.IkePayload.PayloadType;
 
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
@@ -47,7 +46,7 @@
      * @param critical indicates if it is a critical payload.
      * @param message the byte array contains the whole IKE message.
      * @param integrityMac the initialized Mac for integrity check.
-     * @param expectedChecksumLen the expected length of integrity checksum.
+     * @param checksumLen the checksum length of negotiated integrity algorithm.
      * @param decryptCipher the uninitialized Cipher for doing decryption.
      * @param dKey the decryption key.
      */
@@ -55,7 +54,7 @@
             boolean critical,
             byte[] message,
             Mac integrityMac,
-            int expectedChecksumLen,
+            int checksumLen,
             Cipher decryptCipher,
             SecretKey dKey)
             throws IkeException, GeneralSecurityException {
@@ -63,7 +62,39 @@
 
         mIkeEncryptedPayloadBody =
                 new IkeEncryptedPayloadBody(
-                        message, integrityMac, expectedChecksumLen, decryptCipher, dKey);
+                        message, integrityMac, checksumLen, decryptCipher, dKey);
+    }
+
+    /**
+     * 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 initialized Mac for calculating integrity checksum
+     * @param checksumLen the checksum length of negotiated integrity algorithm.
+     * @param encryptCipher the uninitialized Cipher for doing encryption.
+     * @param eKey the encryption key.
+     */
+    IkeSkPayload(
+            IkeHeader ikeHeader,
+            @PayloadType int firstPayloadType,
+            byte[] unencryptedPayloads,
+            Mac integrityMac,
+            int checksumLen,
+            Cipher encryptCipher,
+            SecretKey eKey) {
+        super(PAYLOAD_TYPE_SK, false);
+
+        mIkeEncryptedPayloadBody =
+                new IkeEncryptedPayloadBody(
+                        ikeHeader,
+                        firstPayloadType,
+                        unencryptedPayloads,
+                        integrityMac,
+                        checksumLen,
+                        encryptCipher,
+                        eKey);
     }
 
     /**
diff --git a/tests/iketests/Android.mk b/tests/iketests/Android.mk
index a45703c..0da49b4 100644
--- a/tests/iketests/Android.mk
+++ b/tests/iketests/Android.mk
@@ -29,6 +29,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES := ike \
         androidx.test.rules \
         frameworks-base-testutils \
-        mockito-target-minus-junit4
+        mockito-target-minus-junit4 \
+        NetworkStackBase
 
 include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/tests/iketests/AndroidManifest.xml b/tests/iketests/AndroidManifest.xml
index 80f54e8..d69cbc8 100644
--- a/tests/iketests/AndroidManifest.xml
+++ b/tests/iketests/AndroidManifest.xml
@@ -18,6 +18,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.ike.tests">
 
+    <uses-permission android:name="android.permission.INTERNET"/>
+
     <application android:label="FrameworksIkeTests">
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/ChildSessionStateMachineTest.java b/tests/iketests/src/java/com/android/ike/ikev2/ChildSessionStateMachineTest.java
index e81b17a..1d760b7 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/ChildSessionStateMachineTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/ChildSessionStateMachineTest.java
@@ -63,12 +63,13 @@
 
     private ISaRecordHelper mMockSaRecordHelper;
     private IChildSessionCallback mMockChildSessionCallback;
-    private ChildSessionOptions mMockChildSessionOptions;
+    private ChildSessionOptions mChildSessionOptions;
 
     public ChildSessionStateMachineTest() {
         mMockSaRecordHelper = mock(SaRecord.ISaRecordHelper.class);
         mMockChildSessionCallback = mock(IChildSessionCallback.class);
-        mMockChildSessionOptions = mock(ChildSessionOptions.class);
+
+        mChildSessionOptions = new ChildSessionOptions();
     }
 
     @Before
@@ -77,7 +78,7 @@
         mLooper = new TestLooper();
         mChildSessionStateMachine =
                 new ChildSessionStateMachine(
-                        "ChildSessionStateMachine", mLooper.getLooper(), mMockChildSessionOptions);
+                        "ChildSessionStateMachine", mLooper.getLooper(), mChildSessionOptions);
         mChildSessionStateMachine.setDbg(true);
         SaRecord.setSaRecordHelper(mMockSaRecordHelper);
 
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionOptionsTest.java b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionOptionsTest.java
new file mode 100644
index 0000000..bdaf135
--- /dev/null
+++ b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionOptionsTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ike.ikev2;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.net.IpSecManager;
+import android.net.IpSecManager.UdpEncapsulationSocket;
+
+import androidx.test.InstrumentationRegistry;
+
+import libcore.net.InetAddressUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.Inet4Address;
+
+public final class IkeSessionOptionsTest {
+    private static final Inet4Address IPV4_ADDRESS =
+            (Inet4Address) (InetAddressUtils.parseNumericAddress("192.0.2.100"));
+
+    private UdpEncapsulationSocket mUdpEncapSocket;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        IpSecManager ipSecManager = (IpSecManager) context.getSystemService(Context.IPSEC_SERVICE);
+        mUdpEncapSocket = ipSecManager.openUdpEncapsulationSocket();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mUdpEncapSocket.close();
+    }
+
+    @Test
+    public void testBuild() throws Exception {
+        SaProposal saProposal =
+                SaProposal.Builder.newIkeSaProposalBuilder()
+                        .addEncryptionAlgorithm(
+                                SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8,
+                                SaProposal.KEY_LEN_AES_128)
+                        .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
+                        .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
+                        .build();
+
+        IkeSessionOptions sessionOptions =
+                new IkeSessionOptions.Builder(IPV4_ADDRESS, mUdpEncapSocket)
+                        .addSaProposal(saProposal)
+                        .build();
+
+        assertEquals(IPV4_ADDRESS, sessionOptions.getServerAddress());
+        assertEquals(mUdpEncapSocket, sessionOptions.getUdpEncapsulationSocket());
+        assertArrayEquals(new SaProposal[] {saProposal}, sessionOptions.getSaProposals());
+        assertFalse(sessionOptions.isIkeFragmentationSupported());
+    }
+
+    @Test
+    public void testBuildWithoutSaProposal() throws Exception {
+        try {
+            new IkeSessionOptions.Builder(IPV4_ADDRESS, mUdpEncapSocket).build();
+            fail("Expected to fail due to absence of SA proposal.");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildWithChildSaProposal() throws Exception {
+        SaProposal saProposal =
+                SaProposal.Builder.newChildSaProposalBuilder(true)
+                        .addEncryptionAlgorithm(
+                                SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8,
+                                SaProposal.KEY_LEN_AES_128)
+                        .build();
+        try {
+            new IkeSessionOptions.Builder(IPV4_ADDRESS, mUdpEncapSocket)
+                    .addSaProposal(saProposal)
+                    .build();
+            fail("Expected to fail due to wrong type of SA proposal.");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+}
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java
index 02aaa4c..1dcf5b8 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java
@@ -17,6 +17,9 @@
 package com.android.ike.ikev2;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
@@ -26,8 +29,13 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
+import android.net.IpSecManager;
+import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.os.Looper;
 import android.os.test.TestLooper;
-import android.util.Pair;
+
+import androidx.test.InstrumentationRegistry;
 
 import com.android.ike.ikev2.ChildSessionStateMachineFactory.ChildSessionFactoryHelper;
 import com.android.ike.ikev2.ChildSessionStateMachineFactory.IChildSessionFactoryHelper;
@@ -44,35 +52,46 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 
+import java.net.InetAddress;
 import java.util.LinkedList;
+import java.util.List;
 
 public final class IkeSessionStateMachineTest {
 
+    private static final String SERVER_ADDRESS = "192.0.2.100";
+
+    private UdpEncapsulationSocket mUdpEncapSocket;
+
     private TestLooper mLooper;
     private IkeSessionStateMachine mIkeSessionStateMachine;
 
+    private IkeSessionOptions mIkeSessionOptions;
+    private ChildSessionOptions mChildSessionOptions;
+
     private IIkeMessageHelper mMockIkeMessageHelper;
     private ISaRecordHelper mMockSaRecordHelper;
-    private IkeSessionOptions mMockIkeSessionOptions;
 
     private ChildSessionStateMachine mMockChildSessionStateMachine;
-    private ChildSessionOptions mMockChildSessionOptions;
     private IChildSessionFactoryHelper mMockChildSessionFactoryHelper;
 
     private IkeSaRecord mSpyCurrentIkeSaRecord;
     private IkeSaRecord mSpyLocalInitIkeSaRecord;
     private IkeSaRecord mSpyRemoteInitIkeSaRecord;
 
+    private ArgumentCaptor<IkeMessage> mIkeMessageCaptor =
+            ArgumentCaptor.forClass(IkeMessage.class);
+
     private ReceivedIkePacket makeDummyUnencryptedReceivedIkePacket(int packetType)
             throws Exception {
         IkeMessage dummyIkeMessage = makeDummyIkeMessageForTest(0, 0, false, false);
-        Pair<IkeHeader, byte[]> dummyIkePacketPair =
-                new Pair<>(dummyIkeMessage.ikeHeader, new byte[0]);
-        when(mMockIkeMessageHelper.decode(dummyIkePacketPair.first, dummyIkePacketPair.second))
+        byte[] dummyIkePacketBytes = new byte[0];
+
+        when(mMockIkeMessageHelper.decode(dummyIkeMessage.ikeHeader, dummyIkePacketBytes))
                 .thenReturn(dummyIkeMessage);
         when(mMockIkeMessageHelper.getMessageType(dummyIkeMessage)).thenReturn(packetType);
-        return new ReceivedIkePacket(dummyIkePacketPair);
+        return new ReceivedIkePacket(dummyIkeMessage.ikeHeader, dummyIkePacketBytes);
     }
 
     private ReceivedIkePacket makeDummyEncryptedReceivedIkePacket(
@@ -81,16 +100,16 @@
         IkeMessage dummyIkeMessage =
                 makeDummyIkeMessageForTest(
                         ikeSaRecord.initiatorSpi, ikeSaRecord.responderSpi, fromIkeInit, true);
-        Pair<IkeHeader, byte[]> dummyIkePacketPair =
-                new Pair<>(dummyIkeMessage.ikeHeader, new byte[0]);
+        byte[] dummyIkePacketBytes = new byte[0];
+
         when(mMockIkeMessageHelper.decode(
-                        mMockIkeSessionOptions,
+                        mIkeSessionOptions,
                         ikeSaRecord,
-                        dummyIkePacketPair.first,
-                        dummyIkePacketPair.second))
+                        dummyIkeMessage.ikeHeader,
+                        dummyIkePacketBytes))
                 .thenReturn(dummyIkeMessage);
         when(mMockIkeMessageHelper.getMessageType(dummyIkeMessage)).thenReturn(packetType);
-        return new ReceivedIkePacket(dummyIkePacketPair);
+        return new ReceivedIkePacket(dummyIkeMessage.ikeHeader, dummyIkePacketBytes);
     }
 
     private IkeMessage makeDummyIkeMessageForTest(
@@ -98,27 +117,21 @@
         int firstPayloadType =
                 isEncrypted ? IkePayload.PAYLOAD_TYPE_SK : IkePayload.PAYLOAD_TYPE_NO_NEXT;
         IkeHeader header =
-                new IkeHeader(initSpi, respSpi, firstPayloadType, 0, true, fromikeInit, 0, 0);
+                new IkeHeader(initSpi, respSpi, firstPayloadType, 0, true, fromikeInit, 0);
         return new IkeMessage(header, new LinkedList<IkePayload>());
     }
 
     private void verifyDecodeEncryptedMessage(IkeSaRecord record, ReceivedIkePacket rcvPacket)
             throws Exception {
         verify(mMockIkeMessageHelper)
-                .decode(
-                        mMockIkeSessionOptions,
-                        record,
-                        rcvPacket.ikeHeader,
-                        rcvPacket.ikePacketBytes);
+                .decode(mIkeSessionOptions, record, rcvPacket.ikeHeader, rcvPacket.ikePacketBytes);
     }
 
     public IkeSessionStateMachineTest() {
         mMockIkeMessageHelper = mock(IkeMessage.IIkeMessageHelper.class);
         mMockSaRecordHelper = mock(SaRecord.ISaRecordHelper.class);
-        mMockIkeSessionOptions = mock(IkeSessionOptions.class);
 
         mMockChildSessionStateMachine = mock(ChildSessionStateMachine.class);
-        mMockChildSessionOptions = mock(ChildSessionOptions.class);
         mMockChildSessionFactoryHelper = mock(IChildSessionFactoryHelper.class);
 
         mSpyCurrentIkeSaRecord = spy(new IkeSaRecord(11, 12, true, null, null));
@@ -132,17 +145,25 @@
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        IpSecManager ipSecManager = (IpSecManager) context.getSystemService(Context.IPSEC_SERVICE);
+        mUdpEncapSocket = ipSecManager.openUdpEncapsulationSocket();
+
+        mIkeSessionOptions = buildIkeSessionOptions();
+        mChildSessionOptions = new ChildSessionOptions();
+
         // Setup thread and looper
         mLooper = new TestLooper();
         mIkeSessionStateMachine =
                 new IkeSessionStateMachine(
                         "IkeSessionStateMachine",
                         mLooper.getLooper(),
-                        mMockIkeSessionOptions,
-                        mMockChildSessionOptions);
+                        mIkeSessionOptions,
+                        mChildSessionOptions);
         mIkeSessionStateMachine.setDbg(true);
         mIkeSessionStateMachine.start();
+
         IkeMessage.setIkeMessageHelper(mMockIkeMessageHelper);
         SaRecord.setSaRecordHelper(mMockSaRecordHelper);
         ChildSessionStateMachineFactory.setChildSessionFactoryHelper(
@@ -150,17 +171,46 @@
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws Exception {
         mIkeSessionStateMachine.quit();
         mIkeSessionStateMachine.setDbg(false);
+        mUdpEncapSocket.close();
+
         IkeMessage.setIkeMessageHelper(new IkeMessageHelper());
         SaRecord.setSaRecordHelper(new SaRecordHelper());
         ChildSessionStateMachineFactory.setChildSessionFactoryHelper(
                 new ChildSessionFactoryHelper());
     }
 
+    private IkeSessionOptions buildIkeSessionOptions() throws Exception {
+        SaProposal saProposal =
+                SaProposal.Builder.newIkeSaProposalBuilder()
+                        .addEncryptionAlgorithm(
+                                SaProposal.ENCRYPTION_ALGORITHM_AES_CBC, SaProposal.KEY_LEN_AES_128)
+                        .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
+                        .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1)
+                        .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
+                        .build();
+
+        InetAddress serveAddress = InetAddress.getByName(SERVER_ADDRESS);
+        IkeSessionOptions sessionOptions =
+                new IkeSessionOptions.Builder(serveAddress, mUdpEncapSocket)
+                        .addSaProposal(saProposal)
+                        .build();
+        return sessionOptions;
+    }
+
+    private static boolean isIkePayloadExist(
+            List<IkePayload> payloadList, @IkePayload.PayloadType int payloadType) {
+        for (IkePayload payload : payloadList) {
+            if (payload.payloadType == payloadType) return true;
+        }
+        return false;
+    }
+
     @Test
     public void testCreateIkeLocalIkeInit() throws Exception {
+        if (Looper.myLooper() == null) Looper.myLooper().prepare();
         // Mock IKE_INIT response.
         ReceivedIkePacket dummyReceivedIkePacket =
                 makeDummyUnencryptedReceivedIkePacket(IkeMessage.MESSAGE_TYPE_IKE_INIT_RESP);
@@ -172,15 +222,37 @@
                 IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, dummyReceivedIkePacket);
 
         mLooper.dispatchAll();
+
+        // Validate outbound IKE INIT request
+        verify(mMockIkeMessageHelper).encode(mIkeMessageCaptor.capture());
+        IkeMessage ikeInitReqMessage = mIkeMessageCaptor.getValue();
+
+        IkeHeader ikeHeader = ikeInitReqMessage.ikeHeader;
+        assertEquals(IkeHeader.EXCHANGE_TYPE_IKE_SA_INIT, ikeHeader.exchangeType);
+        assertFalse(ikeHeader.isResponseMsg);
+        assertTrue(ikeHeader.fromIkeInitiator);
+
+        List<IkePayload> payloadList = ikeInitReqMessage.ikePayloadList;
+        assertTrue(isIkePayloadExist(payloadList, IkePayload.PAYLOAD_TYPE_SA));
+        assertTrue(isIkePayloadExist(payloadList, IkePayload.PAYLOAD_TYPE_KE));
+        assertTrue(isIkePayloadExist(payloadList, IkePayload.PAYLOAD_TYPE_NONCE));
+
+        IkeSocket ikeSocket = mIkeSessionStateMachine.mIkeSocket;
+        assertNotNull(ikeSocket);
+        assertNotEquals(
+                -1 /*not found*/, ikeSocket.mSpiToIkeSession.indexOfValue(mIkeSessionStateMachine));
+
         verify(mMockIkeMessageHelper)
                 .decode(dummyReceivedIkePacket.ikeHeader, dummyReceivedIkePacket.ikePacketBytes);
         verify(mMockIkeMessageHelper).getMessageType(any());
+
         assertTrue(
                 mIkeSessionStateMachine.getCurrentState()
                         instanceof IkeSessionStateMachine.CreateIkeLocalIkeAuth);
     }
 
     private void mockIkeSetup() throws Exception {
+        if (Looper.myLooper() == null) Looper.myLooper().prepare();
         // Mock IKE_INIT response
         ReceivedIkePacket dummyIkeInitRespReceivedPacket =
                 makeDummyUnencryptedReceivedIkePacket(IkeMessage.MESSAGE_TYPE_IKE_INIT_RESP);
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java b/tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java
new file mode 100644
index 0000000..5f15f55
--- /dev/null
+++ b/tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ike.ikev2;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.IpSecManager;
+import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.LongSparseArray;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.ike.ikev2.IkeSocket.PacketReceiver;
+import com.android.ike.ikev2.message.IkeHeader;
+import com.android.ike.ikev2.message.TestUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class IkeSocketTest {
+    private static final int REMOTE_RECV_BUFF_SIZE = 2048;
+    private static final int TIMEOUT = 1000;
+
+    private static final String NON_ESP_MARKER_HEX_STRING = "00000000";
+    private static final String IKE_REQ_MESSAGE_HEX_STRING =
+            "5f54bf6d8b48e6e100000000000000002120220800000000"
+                    + "00000150220000300000002c010100040300000c0100000c"
+                    + "800e00800300000803000002030000080400000200000008"
+                    + "020000022800008800020000b4a2faf4bb54878ae21d6385"
+                    + "12ece55d9236fc5046ab6cef82220f421f3ce6361faf3656"
+                    + "4ecb6d28798a94aad7b2b4b603ddeaaa5630adb9ece8ac37"
+                    + "534036040610ebdd92f46bef84f0be7db860351843858f8a"
+                    + "cf87056e272377f70c9f2d81e29c7b0ce4f291a3a72476bb"
+                    + "0b278fd4b7b0a4c26bbeb08214c707137607958729000024"
+                    + "c39b7f368f4681b89fa9b7be6465abd7c5f68b6ed5d3b4c7"
+                    + "2cb4240eb5c464122900001c00004004e54f73b7d83f6beb"
+                    + "881eab2051d8663f421d10b02b00001c00004005d915368c"
+                    + "a036004cb578ae3e3fb268509aeab1900000002069936922"
+                    + "8741c6d4ca094c93e242c9de19e7b7c60000000500000500";
+
+    private static final String LOCAL_SPI = "0000000000000000";
+    private static final String REMOTE_SPI = "5f54bf6d8b48e6e1";
+
+    private static final String DATA_ONE = "one 1";
+    private static final String DATA_TWO = "two 2";
+
+    private static final String IPV4_LOOPBACK = "127.0.0.1";
+
+    private byte[] mDataOne;
+    private byte[] mDataTwo;
+
+    private long mLocalSpi;
+    private long mRemoteSpi;
+
+    private LongSparseArray mSpiToIkeStateMachineMap;
+    private PacketReceiver mPacketReceiver;
+
+    private UdpEncapsulationSocket mClientUdpEncapSocket;
+    private InetAddress mLocalAddress;
+    private FileDescriptor mDummyRemoteServerFd;
+
+    private IkeSessionStateMachine mMockIkeSessionStateMachine;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        IpSecManager ipSecManager = (IpSecManager) context.getSystemService(Context.IPSEC_SERVICE);
+        mClientUdpEncapSocket = ipSecManager.openUdpEncapsulationSocket();
+
+        mLocalAddress = InetAddress.getByName(IPV4_LOOPBACK);
+        mDummyRemoteServerFd = getBoundUdpSocket(mLocalAddress);
+
+        mDataOne = DATA_ONE.getBytes("UTF-8");
+        mDataTwo = DATA_TWO.getBytes("UTF-8");
+
+        ByteBuffer localSpiBuffer = ByteBuffer.wrap(TestUtils.hexStringToByteArray(LOCAL_SPI));
+        mLocalSpi = localSpiBuffer.getLong();
+        ByteBuffer remoteSpiBuffer = ByteBuffer.wrap(TestUtils.hexStringToByteArray(REMOTE_SPI));
+        mRemoteSpi = remoteSpiBuffer.getLong();
+
+        mMockIkeSessionStateMachine = mock(IkeSessionStateMachine.class);
+
+        mSpiToIkeStateMachineMap = new LongSparseArray<IkeSessionStateMachine>();
+        mSpiToIkeStateMachineMap.put(mLocalSpi, mMockIkeSessionStateMachine);
+
+        mPacketReceiver = new IkeSocket.PacketReceiver();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mClientUdpEncapSocket.close();
+        IkeSocket.setPacketReceiver(mPacketReceiver);
+        Os.close(mDummyRemoteServerFd);
+    }
+
+    private static FileDescriptor getBoundUdpSocket(InetAddress address) throws Exception {
+        FileDescriptor sock =
+                Os.socket(OsConstants.AF_INET, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP);
+        Os.bind(sock, address, IkeSocket.IKE_SERVER_PORT);
+        return sock;
+    }
+
+    @Test
+    public void testGetAndCloseIkeSocket() throws Exception {
+        if (Looper.myLooper() == null) Looper.myLooper().prepare();
+
+        IkeSocket ikeSocketOne = IkeSocket.getIkeSocket(mClientUdpEncapSocket);
+        assertEquals(1, ikeSocketOne.mRefCount);
+
+        IkeSocket ikeSocketTwo = IkeSocket.getIkeSocket(mClientUdpEncapSocket);
+        assertEquals(ikeSocketOne, ikeSocketTwo);
+        assertEquals(2, ikeSocketTwo.mRefCount);
+
+        ikeSocketOne.releaseReference();
+        assertEquals(1, ikeSocketOne.mRefCount);
+
+        ikeSocketTwo.releaseReference();
+        assertEquals(0, ikeSocketTwo.mRefCount);
+    }
+
+    @Test
+    public void testSendIkePacket() throws Exception {
+        if (Looper.myLooper() == null) Looper.myLooper().prepare();
+
+        // Send IKE packet
+        IkeSocket ikeSocket = IkeSocket.getIkeSocket(mClientUdpEncapSocket);
+        ikeSocket.sendIkePacket(mDataOne, mLocalAddress);
+
+        byte[] receivedData = receive(mDummyRemoteServerFd);
+
+        // Verify received data
+        ByteBuffer expectedBuffer =
+                ByteBuffer.allocate(IkeSocket.NON_ESP_MARKER_LEN + mDataOne.length);
+        expectedBuffer.put(IkeSocket.NON_ESP_MARKER).put(mDataOne);
+
+        assertArrayEquals(expectedBuffer.array(), receivedData);
+
+        ikeSocket.releaseReference();
+    }
+
+    @Test
+    public void testReceiveIkePacket() throws Exception {
+        // Create working thread.
+        HandlerThread mIkeThread = new HandlerThread("IkeSocketTest");
+        mIkeThread.start();
+
+        // Create IkeSocket on working thread.
+        IkeSocketReceiver socketReceiver = new IkeSocketReceiver();
+        TestCountDownLatch createLatch = new TestCountDownLatch();
+        mIkeThread
+                .getThreadHandler()
+                .post(
+                        () -> {
+                            try {
+                                socketReceiver.setIkeSocket(
+                                        IkeSocket.getIkeSocket(mClientUdpEncapSocket));
+                                createLatch.countDown();
+                                Log.d("IkeSocketTest", "IkeSocket created.");
+                            } catch (ErrnoException e) {
+                                Log.e("IkeSocketTest", "error encountered creating IkeSocket ", e);
+                            }
+                        });
+        createLatch.await();
+
+        IkeSocket ikeSocket = socketReceiver.getIkeSocket();
+        assertNotNull(ikeSocket);
+
+        // Configure IkeSocket
+        TestCountDownLatch receiveLatch = new TestCountDownLatch();
+        DummyPacketReceiver packetReceiver = new DummyPacketReceiver(receiveLatch);
+        IkeSocket.setPacketReceiver(packetReceiver);
+
+        // Send first packet.
+        sendToIkeSocket(mDummyRemoteServerFd, mDataOne, mLocalAddress);
+        receiveLatch.await();
+
+        assertEquals(1, ikeSocket.numPacketsReceived());
+        assertArrayEquals(mDataOne, packetReceiver.mReceivedData);
+
+        // Send second packet.
+        sendToIkeSocket(mDummyRemoteServerFd, mDataTwo, mLocalAddress);
+        receiveLatch.await();
+
+        assertEquals(2, ikeSocket.numPacketsReceived());
+        assertArrayEquals(mDataTwo, packetReceiver.mReceivedData);
+
+        // Close IkeSocket.
+        TestCountDownLatch closeLatch = new TestCountDownLatch();
+        ikeSocket
+                .getHandler()
+                .post(
+                        () -> {
+                            ikeSocket.releaseReference();
+                            closeLatch.countDown();
+                        });
+        closeLatch.await();
+
+        mIkeThread.quitSafely();
+    }
+
+    @Test
+    public void testHandlePacket() throws Exception {
+        byte[] recvBuf =
+                TestUtils.hexStringToByteArray(
+                        NON_ESP_MARKER_HEX_STRING + IKE_REQ_MESSAGE_HEX_STRING);
+
+        mPacketReceiver.handlePacket(recvBuf, mSpiToIkeStateMachineMap);
+
+        byte[] expectedIkePacketBytes = TestUtils.hexStringToByteArray(IKE_REQ_MESSAGE_HEX_STRING);
+        ArgumentCaptor<IkeHeader> ikeHeaderCaptor = ArgumentCaptor.forClass(IkeHeader.class);
+        verify(mMockIkeSessionStateMachine)
+                .receiveIkePacket(ikeHeaderCaptor.capture(), eq(expectedIkePacketBytes));
+
+        IkeHeader capturedIkeHeader = ikeHeaderCaptor.getValue();
+        assertEquals(mRemoteSpi, capturedIkeHeader.ikeInitiatorSpi);
+        assertEquals(mLocalSpi, capturedIkeHeader.ikeResponderSpi);
+    }
+
+    @Test
+    public void testHandleEspPacket() throws Exception {
+        byte[] recvBuf =
+                TestUtils.hexStringToByteArray(
+                        NON_ESP_MARKER_HEX_STRING + IKE_REQ_MESSAGE_HEX_STRING);
+        // Modify Non-ESP Marker
+        recvBuf[0] = 1;
+
+        mPacketReceiver.handlePacket(recvBuf, mSpiToIkeStateMachineMap);
+
+        verify(mMockIkeSessionStateMachine, never()).receiveIkePacket(any(), any());
+    }
+
+    @Test
+    public void testHandlePacketWithMalformedHeader() throws Exception {
+        String malformedIkePacketHexString = "5f54bf6d8b48e6e100000000000000002120220800000000";
+        byte[] recvBuf =
+                TestUtils.hexStringToByteArray(
+                        NON_ESP_MARKER_HEX_STRING + malformedIkePacketHexString);
+
+        mPacketReceiver.handlePacket(recvBuf, mSpiToIkeStateMachineMap);
+
+        verify(mMockIkeSessionStateMachine, never()).receiveIkePacket(any(), any());
+    }
+
+    private byte[] receive(FileDescriptor mfd) throws Exception {
+        byte[] receiveBuffer = new byte[REMOTE_RECV_BUFF_SIZE];
+        AtomicInteger bytesRead = new AtomicInteger(-1);
+        Thread receiveThread =
+                new Thread(
+                        () -> {
+                            while (bytesRead.get() < 0) {
+                                try {
+                                    bytesRead.set(
+                                            Os.recvfrom(
+                                                    mDummyRemoteServerFd,
+                                                    receiveBuffer,
+                                                    0,
+                                                    REMOTE_RECV_BUFF_SIZE,
+                                                    0,
+                                                    null));
+                                } catch (Exception e) {
+                                    Log.e(
+                                            "IkeSocketTest",
+                                            "Error encountered reading from socket",
+                                            e);
+                                }
+                            }
+                            Log.d(
+                                    "IkeSocketTest",
+                                    "Packet received with size of " + bytesRead.get());
+                        });
+
+        receiveThread.start();
+        receiveThread.join(TIMEOUT);
+
+        return Arrays.copyOfRange(receiveBuffer, 0, bytesRead.get());
+    }
+
+    private void sendToIkeSocket(FileDescriptor fd, byte[] data, InetAddress destAddress)
+            throws Exception {
+        Os.sendto(fd, data, 0, data.length, 0, destAddress, mClientUdpEncapSocket.getPort());
+    }
+
+    private static class IkeSocketReceiver {
+        private IkeSocket mIkeSocket;
+
+        void setIkeSocket(IkeSocket ikeSocket) {
+            mIkeSocket = ikeSocket;
+        }
+
+        IkeSocket getIkeSocket() {
+            return mIkeSocket;
+        }
+    }
+
+    private static class DummyPacketReceiver implements IkeSocket.IPacketReceiver {
+        byte[] mReceivedData = null;
+        final TestCountDownLatch mLatch;
+
+        DummyPacketReceiver(TestCountDownLatch latch) {
+            mLatch = latch;
+        }
+
+        public void handlePacket(
+                byte[] revbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession) {
+            mReceivedData = Arrays.copyOfRange(revbuf, 0, revbuf.length);
+            mLatch.countDown();
+            Log.d("IkeSocketTest", "Packet received");
+        }
+    }
+
+    private static class TestCountDownLatch {
+        private CountDownLatch mLatch;
+
+        TestCountDownLatch() {
+            reset();
+        }
+
+        private void reset() {
+            mLatch = new CountDownLatch(1);
+        }
+
+        void countDown() {
+            mLatch.countDown();
+        }
+
+        void await() {
+            try {
+                if (!mLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) {
+                    fail("Time out");
+                }
+            } catch (InterruptedException e) {
+                fail(e.toString());
+            }
+            reset();
+        }
+    }
+}
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java b/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java
index 2ce2a9d..7f40d72 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java
@@ -64,18 +64,19 @@
                         .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
                         .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
                         .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
-                        .buildOrThrow();
+                        .build();
 
-        assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.mProtocolId);
+        assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.getProtocolId());
         assertArrayEquals(
                 new EncryptionTransform[] {mEncryption3DesTransform},
-                proposal.mEncryptionAlgorithms);
+                proposal.getEncryptionTransforms());
         assertArrayEquals(
                 new IntegrityTransform[] {mIntegrityHmacSha1Transform},
-                proposal.mIntegrityAlgorithms);
+                proposal.getIntegrityTransforms());
         assertArrayEquals(
-                new PrfTransform[] {mPrfAes128XCbcTransform}, proposal.mPseudorandomFunctions);
-        assertArrayEquals(new DhGroupTransform[] {mDhGroup1024Transform}, proposal.mDhGroups);
+                new PrfTransform[] {mPrfAes128XCbcTransform}, proposal.getPrfTransforms());
+        assertArrayEquals(
+                new DhGroupTransform[] {mDhGroup1024Transform}, proposal.getDhGroupTransforms());
     }
 
     @Test
@@ -87,16 +88,17 @@
                                 SaProposal.KEY_LEN_AES_128)
                         .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
                         .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
-                        .buildOrThrow();
+                        .build();
 
-        assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.mProtocolId);
+        assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.getProtocolId());
         assertArrayEquals(
                 new EncryptionTransform[] {mEncryptionAesGcm8Transform},
-                proposal.mEncryptionAlgorithms);
+                proposal.getEncryptionTransforms());
         assertArrayEquals(
-                new PrfTransform[] {mPrfAes128XCbcTransform}, proposal.mPseudorandomFunctions);
-        assertArrayEquals(new DhGroupTransform[] {mDhGroup1024Transform}, proposal.mDhGroups);
-        assertTrue(proposal.mIntegrityAlgorithms.length == 0);
+                new PrfTransform[] {mPrfAes128XCbcTransform}, proposal.getPrfTransforms());
+        assertArrayEquals(
+                new DhGroupTransform[] {mDhGroup1024Transform}, proposal.getDhGroupTransforms());
+        assertTrue(proposal.getIntegrityTransforms().length == 0);
     }
 
     @Test
@@ -107,16 +109,17 @@
                                 SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8,
                                 SaProposal.KEY_LEN_AES_128)
                         .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE)
-                        .buildOrThrow();
+                        .build();
 
-        assertEquals(IkePayload.PROTOCOL_ID_ESP, proposal.mProtocolId);
+        assertEquals(IkePayload.PROTOCOL_ID_ESP, proposal.getProtocolId());
         assertArrayEquals(
                 new EncryptionTransform[] {mEncryptionAesGcm8Transform},
-                proposal.mEncryptionAlgorithms);
+                proposal.getEncryptionTransforms());
         assertArrayEquals(
-                new IntegrityTransform[] {mIntegrityNoneTransform}, proposal.mIntegrityAlgorithms);
-        assertTrue(proposal.mPseudorandomFunctions.length == 0);
-        assertTrue(proposal.mDhGroups.length == 0);
+                new IntegrityTransform[] {mIntegrityNoneTransform},
+                proposal.getIntegrityTransforms());
+        assertTrue(proposal.getPrfTransforms().length == 0);
+        assertTrue(proposal.getDhGroupTransforms().length == 0);
     }
 
     @Test
@@ -127,23 +130,25 @@
                 builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES)
                         .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE)
                         .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
-                        .buildOrThrow();
+                        .build();
 
-        assertEquals(IkePayload.PROTOCOL_ID_ESP, proposal.mProtocolId);
+        assertEquals(IkePayload.PROTOCOL_ID_ESP, proposal.getProtocolId());
         assertArrayEquals(
                 new EncryptionTransform[] {mEncryption3DesTransform},
-                proposal.mEncryptionAlgorithms);
+                proposal.getEncryptionTransforms());
         assertArrayEquals(
-                new IntegrityTransform[] {mIntegrityNoneTransform}, proposal.mIntegrityAlgorithms);
-        assertArrayEquals(new DhGroupTransform[] {mDhGroup1024Transform}, proposal.mDhGroups);
-        assertTrue(proposal.mPseudorandomFunctions.length == 0);
+                new IntegrityTransform[] {mIntegrityNoneTransform},
+                proposal.getIntegrityTransforms());
+        assertArrayEquals(
+                new DhGroupTransform[] {mDhGroup1024Transform}, proposal.getDhGroupTransforms());
+        assertTrue(proposal.getPrfTransforms().length == 0);
     }
 
     @Test
     public void testBuildEncryptAlgosWithNoAlgorithm() throws Exception {
         Builder builder = Builder.newIkeSaProposalBuilder();
         try {
-            builder.buildOrThrow();
+            builder.build();
             fail("Expected to fail when no encryption algorithm is proposed.");
         } catch (IllegalArgumentException expected) {
 
@@ -179,7 +184,7 @@
     public void testBuildIkeProposalWithoutPrf() throws Exception {
         Builder builder = Builder.newIkeSaProposalBuilder();
         try {
-            builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES).buildOrThrow();
+            builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES).build();
             fail("Expected to fail when PRF is not provided in IKE SA proposal.");
         } catch (IllegalArgumentException expected) {
 
@@ -192,7 +197,7 @@
         try {
             builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES)
                     .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1)
-                    .buildOrThrow();
+                    .build();
 
             fail("Expected to fail when PRF is provided in Child SA proposal.");
         } catch (IllegalArgumentException expected) {
@@ -209,7 +214,7 @@
             builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12)
                     .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE)
                     .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
-                    .buildOrThrow();
+                    .build();
 
             fail("Expected to fail when not-none integrity algorithm is proposed with AEAD");
         } catch (IllegalArgumentException expected) {
@@ -225,7 +230,7 @@
         try {
             builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES)
                     .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1)
-                    .buildOrThrow();
+                    .build();
 
             fail(
                     "Expected to fail when"
@@ -245,7 +250,7 @@
                     .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1)
                     .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE)
                     .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
-                    .buildOrThrow();
+                    .build();
 
             fail(
                     "Expected to fail when none-value integrity algorithm is proposed"
@@ -262,7 +267,7 @@
             builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES)
                     .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
                     .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
-                    .buildOrThrow();
+                    .build();
 
             fail("Expected to fail when no DH Group is proposed in IKE SA proposal.");
         } catch (IllegalArgumentException expected) {
@@ -279,7 +284,7 @@
                     .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
                     .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
                     .addDhGroup(SaProposal.DH_GROUP_NONE)
-                    .buildOrThrow();
+                    .build();
 
             fail("Expected to fail when none-value DH Group is proposed in IKE SA proposal.");
         } catch (IllegalArgumentException expected) {
@@ -295,7 +300,7 @@
             builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES)
                     .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
                     .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
-                    .buildOrThrow();
+                    .build();
 
             fail(
                     "Expected to fail when"
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java b/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java
new file mode 100644
index 0000000..5c61105
--- /dev/null
+++ b/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ike.ikev2;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import com.android.ike.ikev2.message.TestUtils;
+
+import org.junit.Test;
+
+public final class SaRecordTest {
+    private static final String IKE_INIT_SPI = "5F54BF6D8B48E6E1";
+    private static final String IKE_RESP_SPI = "909232B3D1EDCB5C";
+
+    private static final String IKE_NONCE_INIT_HEX_STRING =
+            "C39B7F368F4681B89FA9B7BE6465ABD7C5F68B6ED5D3B4C72CB4240EB5C46412";
+    private static final String IKE_NONCE_RESP_HEX_STRING =
+            "9756112CA539F5C25ABACC7EE92B73091942A9C06950F98848F1AF1694C4DDFF";
+
+    private static final String IKE_SHARED_DH_KEY_HEX_STRING =
+            "C14155DEA40056BD9C76FB4819687B7A397582F4CD5AFF4B"
+                    + "8F441C56E0C08C84234147A0BA249A555835A048E3CA2980"
+                    + "7D057A61DD26EEFAD9AF9C01497005E52858E29FB42EB849"
+                    + "6731DF96A11CCE1F51137A9A1B900FA81AEE7898E373D4E4"
+                    + "8B899BBECA091314ECD4B6E412EF4B0FEF798F54735F3180"
+                    + "7424A318287F20E8";
+
+    private static final String IKE_SKEYSEED_HEX_STRING =
+            "8C42F3B1F5F81C7BAAC5F33E9A4F01987B2F9657";
+    private static final String IKE_SK_D_HEX_STRING = "C86B56EFCF684DCC2877578AEF3137167FE0EBF6";
+    private static final String IKE_SK_AUTH_INIT_HEX_STRING =
+            "554FBF5A05B7F511E05A30CE23D874DB9EF55E51";
+    private static final String IKE_SK_AUTH_RESP_HEX_STRING =
+            "36D83420788337CA32ECAA46892C48808DCD58B1";
+    private static final String IKE_SK_ENCR_INIT_HEX_STRING = "5CBFD33F75796C0188C4A3A546AEC4A1";
+    private static final String IKE_SK_ENCR_RESP_HEX_STRING = "C33B35FCF29514CD9D8B4A695E1A816E";
+    private static final String IKE_SK_PRF_INIT_HEX_STRING =
+            "094787780EE466E2CB049FA327B43908BC57E485";
+    private static final String IKE_SK_PRF_RESP_HEX_STRING =
+            "A30E6B08BE56C0E6BFF4744143C75219299E1BEB";
+    private static final String IKE_KEY_MAT =
+            IKE_SK_D_HEX_STRING
+                    + IKE_SK_AUTH_INIT_HEX_STRING
+                    + IKE_SK_AUTH_RESP_HEX_STRING
+                    + IKE_SK_ENCR_INIT_HEX_STRING
+                    + IKE_SK_ENCR_RESP_HEX_STRING
+                    + IKE_SK_PRF_INIT_HEX_STRING
+                    + IKE_SK_PRF_RESP_HEX_STRING;
+
+    private static final int IKE_AUTH_ALGO_KEY_LEN = 20;
+    private static final int IKE_ENCR_ALGO_KEY_LEN = 16;
+    private static final int IKE_PRF_KEY_LEN = 20;
+    private static final int IKE_SK_D_KEY_LEN = IKE_PRF_KEY_LEN;
+
+    private static final String FIRST_CHILD_ENCR_INIT_HEX_STRING =
+            "1B865CEA6E2C23973E8C5452ADC5CD7D";
+    private static final String FIRST_CHILD_ENCR_RESP_HEX_STRING =
+            "5E82FEDACC6DCB0756DDD7553907EBD1";
+    private static final String FIRST_CHILD_AUTH_INIT_HEX_STRING =
+            "A7A5A44F7EF4409657206C7DC52B7E692593B51E";
+    private static final String FIRST_CHILD_AUTH_RESP_HEX_STRING =
+            "CDE612189FD46DE870FAEC04F92B40B0BFDBD9E1";
+    private static final String FIRST_CHILD_KEY_MAT =
+            FIRST_CHILD_ENCR_INIT_HEX_STRING
+                    + FIRST_CHILD_AUTH_INIT_HEX_STRING
+                    + FIRST_CHILD_ENCR_RESP_HEX_STRING
+                    + FIRST_CHILD_AUTH_RESP_HEX_STRING;
+
+    private static final int FIRST_CHILD_AUTH_ALGO_KEY_LEN = 20;
+    private static final int FIRST_CHILD_ENCR_ALGO_KEY_LEN = 16;
+
+    private static final String PRF_HMAC_SHA1_ALGO_NAME = "HmacSHA1";
+
+    @Test
+    public void testCalculateSKeySeed() throws Exception {
+        byte[] nonceInit = TestUtils.hexStringToByteArray(IKE_NONCE_INIT_HEX_STRING);
+        byte[] nonceResp = TestUtils.hexStringToByteArray(IKE_NONCE_RESP_HEX_STRING);
+        byte[] sharedDhKey = TestUtils.hexStringToByteArray(IKE_SHARED_DH_KEY_HEX_STRING);
+
+        byte[] calculatedSKeySeed =
+                SaRecord.generateSKeySeed(
+                        PRF_HMAC_SHA1_ALGO_NAME, nonceInit, nonceResp, sharedDhKey);
+
+        byte[] expectedSKeySeed = TestUtils.hexStringToByteArray(IKE_SKEYSEED_HEX_STRING);
+        assertArrayEquals(expectedSKeySeed, calculatedSKeySeed);
+    }
+
+    @Test
+    public void testSignWithPrfPlusForIke() throws Exception {
+        byte[] prfKey = TestUtils.hexStringToByteArray(IKE_SKEYSEED_HEX_STRING);
+        byte[] prfData =
+                TestUtils.hexStringToByteArray(
+                        IKE_NONCE_INIT_HEX_STRING
+                                + IKE_NONCE_RESP_HEX_STRING
+                                + IKE_INIT_SPI
+                                + IKE_RESP_SPI);
+        int keyMaterialLen =
+                IKE_SK_D_KEY_LEN
+                        + IKE_AUTH_ALGO_KEY_LEN * 2
+                        + IKE_ENCR_ALGO_KEY_LEN * 2
+                        + IKE_PRF_KEY_LEN * 2;
+
+        byte[] calculatedKeyMat =
+                SaRecord.generateKeyMat(PRF_HMAC_SHA1_ALGO_NAME, prfKey, prfData, keyMaterialLen);
+
+        byte[] expectedKeyMat = TestUtils.hexStringToByteArray(IKE_KEY_MAT);
+        assertArrayEquals(expectedKeyMat, calculatedKeyMat);
+    }
+
+    @Test
+    public void testSignWithPrfPlusForFirstChild() throws Exception {
+        byte[] prfKey = TestUtils.hexStringToByteArray(IKE_SK_D_HEX_STRING);
+        byte[] prfData =
+                TestUtils.hexStringToByteArray(
+                        IKE_NONCE_INIT_HEX_STRING + IKE_NONCE_RESP_HEX_STRING);
+        int keyMaterialLen = FIRST_CHILD_AUTH_ALGO_KEY_LEN * 2 + FIRST_CHILD_ENCR_ALGO_KEY_LEN * 2;
+
+        byte[] calculatedKeyMat =
+                SaRecord.generateKeyMat(PRF_HMAC_SHA1_ALGO_NAME, prfKey, prfData, keyMaterialLen);
+
+        byte[] expectedKeyMat = TestUtils.hexStringToByteArray(FIRST_CHILD_KEY_MAT);
+        assertArrayEquals(expectedKeyMat, calculatedKeyMat);
+    }
+}
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeDeletePayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeDeletePayloadTest.java
new file mode 100644
index 0000000..1a7b9a2
--- /dev/null
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeDeletePayloadTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ike.ikev2.message;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.ike.ikev2.exceptions.InvalidSyntaxException;
+
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+
+public final class IkeDeletePayloadTest {
+    private static final String DELETE_IKE_PAYLOAD_HEX_STRING = "0000000801000000";
+    private static final String DELETE_CHILD_PAYLOAD_HEX_STRING = "0000000c030400012ad4c0a2";
+    private static final String CHILD_SPI = "2ad4c0a2";
+
+    private static final int NUM_CHILD_SPI = 1;
+
+    private static final int PROTOCOL_ID_OFFSET = 4;
+    private static final int SPI_SIZE_OFFSET = 5;
+    private static final int NUM_OF_SPI_OFFSET = 6;
+
+    @Test
+    public void testDecodeDeleteIkePayload() throws Exception {
+        ByteBuffer inputBuffer =
+                ByteBuffer.wrap(TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING));
+
+        IkePayload payload =
+                IkePayloadFactory.getIkePayload(
+                                IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer)
+                        .first;
+
+        assertTrue(payload instanceof IkeDeletePayload);
+
+        IkeDeletePayload deletePayload = (IkeDeletePayload) payload;
+        assertEquals(IkePayload.PROTOCOL_ID_IKE, deletePayload.protocolId);
+        assertEquals(IkePayload.SPI_LEN_NOT_INCLUDED, deletePayload.spiSize);
+        assertEquals(0, deletePayload.numSpi);
+        assertArrayEquals(new int[0], deletePayload.spisToDelete);
+    }
+
+    @Test
+    public void testDecodeDeleteChildPayload() throws Exception {
+        ByteBuffer inputBuffer =
+                ByteBuffer.wrap(TestUtils.hexStringToByteArray(DELETE_CHILD_PAYLOAD_HEX_STRING));
+
+        IkePayload payload =
+                IkePayloadFactory.getIkePayload(
+                                IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer)
+                        .first;
+
+        assertTrue(payload instanceof IkeDeletePayload);
+
+        IkeDeletePayload deletePayload = (IkeDeletePayload) payload;
+        assertEquals(IkePayload.PROTOCOL_ID_ESP, deletePayload.protocolId);
+        assertEquals(IkePayload.SPI_LEN_IPSEC, deletePayload.spiSize);
+        assertEquals(NUM_CHILD_SPI, deletePayload.numSpi);
+
+        byte[] childSpiBytes = TestUtils.hexStringToByteArray(CHILD_SPI);
+        ByteBuffer buffer = ByteBuffer.wrap(childSpiBytes);
+        int expectedChildSpi = buffer.getInt();
+
+        assertArrayEquals(new int[] {expectedChildSpi}, deletePayload.spisToDelete);
+    }
+
+    @Test
+    public void testDecodeWithInvalidProtocol() throws Exception {
+        byte[] deletePayloadBytes = TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING);
+        deletePayloadBytes[PROTOCOL_ID_OFFSET] = -1;
+        ByteBuffer inputBuffer = ByteBuffer.wrap(deletePayloadBytes);
+
+        try {
+            IkePayloadFactory.getIkePayload(
+                    IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer);
+            fail("Expected to fail due to unrecognized protocol ID.");
+        } catch (InvalidSyntaxException expected) {
+
+        }
+    }
+
+    @Test
+    public void testDecodeWithInvalidSpiSize() throws Exception {
+        byte[] deletePayloadBytes = TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING);
+        deletePayloadBytes[SPI_SIZE_OFFSET] = IkePayload.SPI_LEN_IPSEC;
+        ByteBuffer inputBuffer = ByteBuffer.wrap(deletePayloadBytes);
+
+        try {
+            IkePayloadFactory.getIkePayload(
+                    IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer);
+            fail("Expected to fail due to invalid SPI size in Delete IKE Payload.");
+        } catch (InvalidSyntaxException expected) {
+
+        }
+    }
+
+    @Test
+    public void testDecodeWithInvalidNumSpi() throws Exception {
+        byte[] deletePayloadBytes = TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING);
+        deletePayloadBytes[NUM_OF_SPI_OFFSET] = 1;
+        ByteBuffer inputBuffer = ByteBuffer.wrap(deletePayloadBytes);
+
+        try {
+            IkePayloadFactory.getIkePayload(
+                    IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer);
+            fail("Expected to fail because number of SPI is not zero in Delete IKE Payload.");
+        } catch (InvalidSyntaxException expected) {
+
+        }
+    }
+
+    @Test
+    public void testDecodeWithInvalidNumSpiAndSpiSize() throws Exception {
+        byte[] deletePayloadBytes = TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING);
+        deletePayloadBytes[SPI_SIZE_OFFSET] = 1;
+        deletePayloadBytes[NUM_CHILD_SPI] = 4;
+        ByteBuffer inputBuffer = ByteBuffer.wrap(deletePayloadBytes);
+
+        try {
+            IkePayloadFactory.getIkePayload(
+                    IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer);
+            fail("Expected to fail due to invalid SPI size in Delete IKE Payload.");
+        } catch (InvalidSyntaxException expected) {
+
+        }
+    }
+}
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBodyTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBodyTest.java
index 48669e8..fcb3eff 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBodyTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBodyTest.java
@@ -17,10 +17,14 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 import org.junit.Before;
 import org.junit.Test;
 
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
 import javax.crypto.Cipher;
 import javax.crypto.Mac;
 import javax.crypto.SecretKey;
@@ -65,9 +69,40 @@
     private SecretKey mAesCbcKey;
     private Mac mHmacSha1IntegrityMac;
 
+    private byte[] mDataToPadAndEncrypt;
+    private byte[] mDataToAuthenticate;
+    private byte[] mEncryptedPaddedData;
+    private byte[] mIkeMessage;
+
+    private byte[] mChecksum;
+    private byte[] mIv;
+    private byte[] mPadding;
+
     // TODO: Add tests for authenticating and decrypting received message.
     @Before
     public void setUp() throws Exception {
+        mDataToPadAndEncrypt =
+                TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_UNENCRYPTED_DATA);
+        String hexStringToAuthenticate =
+                IKE_AUTH_INIT_REQUEST_HEADER
+                        + IKE_AUTH_INIT_REQUEST_SK_HEADER
+                        + IKE_AUTH_INIT_REQUEST_IV
+                        + IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA;
+        mDataToAuthenticate = TestUtils.hexStringToByteArray(hexStringToAuthenticate);
+        mEncryptedPaddedData =
+                TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA);
+        mIkeMessage =
+                TestUtils.hexStringToByteArray(
+                        IKE_AUTH_INIT_REQUEST_HEADER
+                                + IKE_AUTH_INIT_REQUEST_SK_HEADER
+                                + IKE_AUTH_INIT_REQUEST_IV
+                                + IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA
+                                + IKE_AUTH_INIT_REQUEST_CHECKSUM);
+
+        mChecksum = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_CHECKSUM);
+        mIv = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_IV);
+        mPadding = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_PADDING);
+
         mAesCbcCipher = Cipher.getInstance(ENCR_ALGO_AES_CBC, IkeMessage.getSecurityProvider());
         byte[] encryptKeyBytes = TestUtils.hexStringToByteArray(ENCR_KEY_FROM_INIT_TO_RESP);
         mAesCbcKey = new SecretKeySpec(encryptKeyBytes, ENCR_ALGO_AES_CBC);
@@ -81,20 +116,30 @@
 
     @Test
     public void testCalculateChecksum() throws Exception {
-        String hexStringToAuthenticate =
-                IKE_AUTH_INIT_REQUEST_HEADER
-                        + IKE_AUTH_INIT_REQUEST_SK_HEADER
-                        + IKE_AUTH_INIT_REQUEST_IV
-                        + IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA;
-        byte[] byteToAuthenticate = TestUtils.hexStringToByteArray(hexStringToAuthenticate);
-
-        byte[] expectedCheckSum = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_CHECKSUM);
-
         byte[] calculatedChecksum =
                 IkeEncryptedPayloadBody.calculateChecksum(
-                        byteToAuthenticate, mHmacSha1IntegrityMac, HMAC_SHA1_CHECKSUM_LEN);
+                        mDataToAuthenticate, mHmacSha1IntegrityMac, HMAC_SHA1_CHECKSUM_LEN);
 
-        assertArrayEquals(expectedCheckSum, calculatedChecksum);
+        assertArrayEquals(mChecksum, calculatedChecksum);
+    }
+
+    @Test
+    public void testValidateChecksum() throws Exception {
+        IkeEncryptedPayloadBody.validateChecksumOrThrow(
+                mDataToAuthenticate, mHmacSha1IntegrityMac, mChecksum);
+    }
+
+    @Test
+    public void testThrowForInvalidChecksum() throws Exception {
+        byte[] dataToAuthenticate = Arrays.copyOf(mDataToAuthenticate, mDataToAuthenticate.length);
+        dataToAuthenticate[0]++;
+
+        try {
+            IkeEncryptedPayloadBody.validateChecksumOrThrow(
+                    dataToAuthenticate, mHmacSha1IntegrityMac, mChecksum);
+            fail("Expected GeneralSecurityException due to mismatched checksum.");
+        } catch (GeneralSecurityException expected) {
+        }
     }
 
     @Test
@@ -132,40 +177,37 @@
 
     @Test
     public void testEncrypt() throws Exception {
-        byte[] dataToEncrypt =
-                TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_UNENCRYPTED_DATA);
-        byte[] iv = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_IV);
-        byte[] padding = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_PADDING);
-
         byte[] calculatedData =
                 IkeEncryptedPayloadBody.encrypt(
-                        dataToEncrypt, mAesCbcCipher, mAesCbcKey, iv, padding);
-        byte[] expectedData =
-                TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA);
+                        mDataToPadAndEncrypt, mAesCbcCipher, mAesCbcKey, mIv, mPadding);
 
-        assertArrayEquals(expectedData, calculatedData);
+        assertArrayEquals(mEncryptedPaddedData, calculatedData);
+    }
+
+    @Test
+    public void testDecrypt() throws Exception {
+        byte[] calculatedPlainText =
+                IkeEncryptedPayloadBody.decrypt(
+                        mEncryptedPaddedData, mAesCbcCipher, mAesCbcKey, mIv);
+
+        assertArrayEquals(mDataToPadAndEncrypt, calculatedPlainText);
     }
 
     @Test
     public void testBuildAndEncodeOutboundIkeEncryptedPayloadBody() throws Exception {
-        byte[] ikeAndPayloadHeader =
-                TestUtils.hexStringToByteArray(
-                        IKE_AUTH_INIT_REQUEST_HEADER + IKE_AUTH_INIT_REQUEST_SK_HEADER);
-        byte[] unencryptedPayloads =
-                TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_UNENCRYPTED_DATA);
-        byte[] iv = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_IV);
-        byte[] padding = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_PADDING);
+        IkeHeader ikeHeader = new IkeHeader(mIkeMessage);
 
         IkeEncryptedPayloadBody paylaodBody =
                 new IkeEncryptedPayloadBody(
-                        ikeAndPayloadHeader,
-                        unencryptedPayloads,
+                        ikeHeader,
+                        IkePayload.PAYLOAD_TYPE_ID_INITIATOR,
+                        mDataToPadAndEncrypt,
                         mHmacSha1IntegrityMac,
                         HMAC_SHA1_CHECKSUM_LEN,
                         mAesCbcCipher,
                         mAesCbcKey,
-                        iv,
-                        padding);
+                        mIv,
+                        mPadding);
 
         byte[] expectedEncodedData =
                 TestUtils.hexStringToByteArray(
@@ -174,4 +216,17 @@
                                 + IKE_AUTH_INIT_REQUEST_CHECKSUM);
         assertArrayEquals(expectedEncodedData, paylaodBody.encode());
     }
+
+    @Test
+    public void testAuthenticateAndDecryptInboundIkeEncryptedPayloadBody() throws Exception {
+        IkeEncryptedPayloadBody paylaodBody =
+                new IkeEncryptedPayloadBody(
+                        mIkeMessage,
+                        mHmacSha1IntegrityMac,
+                        HMAC_SHA1_CHECKSUM_LEN,
+                        mAesCbcCipher,
+                        mAesCbcKey);
+
+        assertArrayEquals(mDataToPadAndEncrypt, paylaodBody.getUnencryptedData());
+    }
 }
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeHeaderTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeHeaderTest.java
index 1bc52c3..08b1612 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeHeaderTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeHeaderTest.java
@@ -62,6 +62,7 @@
 
     private static final int IKE_MSG_ID = 0;
     private static final int IKE_MSG_LENGTH = 336;
+    private static final int IKE_MSG_BODY_LENGTH = IKE_MSG_LENGTH - IkeHeader.IKE_HEADER_LENGTH;
 
     // Byte offsets of version field in IKE message header.
     private static final int VERSION_OFFSET = 17;
@@ -89,7 +90,7 @@
         assertFalse(header.isResponseMsg);
         assertTrue(header.fromIkeInitiator);
         assertEquals(IKE_MSG_ID, header.messageId);
-        assertEquals(IKE_MSG_LENGTH, header.messageLength);
+        assertEquals(IKE_MSG_LENGTH, header.getInboundMessageLength());
     }
 
     @Test
@@ -142,7 +143,7 @@
         IkeHeader header = new IkeHeader(inputPacket);
 
         ByteBuffer byteBuffer = ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH);
-        header.encodeToByteBuffer(byteBuffer);
+        header.encodeToByteBuffer(byteBuffer, IKE_MSG_BODY_LENGTH);
 
         byte[] expectedPacket = TestUtils.hexStringToByteArray(IKE_HEADER_HEX_STRING);
         assertArrayEquals(expectedPacket, byteBuffer.array());
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeKePayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeKePayloadTest.java
index 4f45f26..1bb0b70 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeKePayloadTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeKePayloadTest.java
@@ -18,11 +18,10 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import android.util.Pair;
-
 import com.android.ike.ikev2.IkeDhParams;
 import com.android.ike.ikev2.SaProposal;
 import com.android.ike.ikev2.exceptions.InvalidSyntaxException;
@@ -100,6 +99,7 @@
 
         IkeKePayload payload = new IkeKePayload(CRITICAL_BIT, inputPacket);
 
+        assertFalse(payload.isOutbound);
         assertEquals(EXPECTED_DH_GROUP, payload.dhGroup);
 
         byte[] keyExchangeData = TestUtils.hexStringToByteArray(KEY_EXCHANGE_DATA_RAW_PACKET);
@@ -138,11 +138,11 @@
 
     @Test
     public void testGetIkeKePayload() throws Exception {
-        Pair<DHPrivateKeySpec, IkeKePayload> pair =
-                IkeKePayload.getKePayload(SaProposal.DH_GROUP_1024_BIT_MODP);
+        IkeKePayload payload = new IkeKePayload(SaProposal.DH_GROUP_1024_BIT_MODP);
 
         // Test DHPrivateKeySpec
-        DHPrivateKeySpec privateKeySpec = pair.first;
+        assertTrue(payload.isOutbound);
+        DHPrivateKeySpec privateKeySpec = payload.localPrivateKey;
 
         BigInteger primeValue = privateKeySpec.getP();
         BigInteger expectedPrimeValue = new BigInteger(IkeDhParams.PRIME_1024_BIT_MODP, 16);
@@ -153,8 +153,6 @@
         assertEquals(0, expectedGenValue.compareTo(genValue));
 
         // Test IkeKePayload
-        IkeKePayload payload = pair.second;
-
         assertEquals(EXPECTED_DH_GROUP, payload.dhGroup);
         assertEquals(EXPECTED_KE_DATA_LEN, payload.keyExchangeData.length);
     }
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSaPayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSaPayloadTest.java
index b0d927d..afb3a81 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSaPayloadTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSaPayloadTest.java
@@ -16,6 +16,7 @@
 
 package com.android.ike.ikev2.message;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -57,11 +58,14 @@
 import java.util.Random;
 
 public final class IkeSaPayloadTest {
-    private static final String PROPOSAL_RAW_PACKET =
+    private static final String OUTBOUND_SA_PAYLOAD_HEADER = "22000030";
+    private static final String OUTBOUND_PROPOSAL_RAW_PACKET =
+            "0000002C010100040300000C0100000C800E0080030000080200000203000008030"
+                    + "000020000000804000002";
+    private static final String INBOUND_PROPOSAL_RAW_PACKET =
             "0000002c010100040300000c0100000c800e0080030000080300000203000008040"
                     + "000020000000802000002";
-
-    private static final String TWO_PROPOSAL_RAW_PACKET =
+    private static final String INBOUND_TWO_PROPOSAL_RAW_PACKET =
             "020000dc010100190300000c0100000c800e00800300000c0100000c800e00c0030"
                     + "0000c0100000c800e01000300000801000003030000080300000c0300"
                     + "00080300000d030000080300000e03000008030000020300000803000"
@@ -91,16 +95,10 @@
     private static final String ATTRIBUTE_RAW_PACKET = "800e0080";
 
     private static final int PROPOSAL_NUMBER = 1;
+
     private static final int PROPOSAL_NUMBER_OFFSET = 4;
-
-    @IkePayload.ProtocolId
-    private static final int PROPOSAL_PROTOCOL_ID = IkePayload.PROTOCOL_ID_IKE;
-
     private static final int PROTOCOL_ID_OFFSET = 5;
 
-    private static final byte PROPOSAL_SPI_SIZE = 0;
-    private static final byte PROPOSAL_SPI = 0;
-
     // Constants for multiple proposals test
     private static final byte[] PROPOSAL_NUMBER_LIST = {1, 2};
 
@@ -153,7 +151,7 @@
                         .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
                         .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
                         .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1)
-                        .buildOrThrow();
+                        .build();
 
         mSaProposalTwo =
                 SaProposal.Builder.newIkeSaProposalBuilder()
@@ -166,7 +164,7 @@
                         .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
                         .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
                         .addDhGroup(SaProposal.DH_GROUP_2048_BIT_MODP)
-                        .buildOrThrow();
+                        .build();
         mTwoSaProposalsArray = new SaProposal[] {mSaProposalOne, mSaProposalTwo};
     }
 
@@ -186,6 +184,16 @@
     }
 
     @Test
+    public void testEncodeAttribute() throws Exception {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(mAttributeKeyLength128.getAttributeLength());
+        mAttributeKeyLength128.encodeToByteBuffer(byteBuffer);
+
+        byte[] expectedBytes = TestUtils.hexStringToByteArray(ATTRIBUTE_RAW_PACKET);
+
+        assertArrayEquals(expectedBytes, byteBuffer.array());
+    }
+
+    @Test
     public void testDecodeEncryptionTransform() throws Exception {
         byte[] inputPacket = TestUtils.hexStringToByteArray(ENCR_TRANSFORM_RAW_PACKET);
         ByteBuffer inputBuffer = ByteBuffer.wrap(inputPacket);
@@ -221,6 +229,16 @@
     }
 
     @Test
+    public void testEncodeEncryptionTransform() throws Exception {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(mEncrAesCbc128Transform.getTransformLength());
+        mEncrAesCbc128Transform.encodeToByteBuffer(false, byteBuffer);
+
+        byte[] expectedBytes = TestUtils.hexStringToByteArray(ENCR_TRANSFORM_RAW_PACKET);
+
+        assertArrayEquals(expectedBytes, byteBuffer.array());
+    }
+
+    @Test
     public void testConstructEncryptionTransformWithUnsupportedId() throws Exception {
         try {
             new EncryptionTransform(-1);
@@ -255,6 +273,16 @@
     }
 
     @Test
+    public void testEncodePrfTransform() throws Exception {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(mPrfHmacSha1Transform.getTransformLength());
+        mPrfHmacSha1Transform.encodeToByteBuffer(true, byteBuffer);
+
+        byte[] expectedBytes = TestUtils.hexStringToByteArray(PRF_TRANSFORM_RAW_PACKET);
+
+        assertArrayEquals(expectedBytes, byteBuffer.array());
+    }
+
+    @Test
     public void testConstructPrfTransformWithUnsupportedId() throws Exception {
         try {
             new PrfTransform(-1);
@@ -296,6 +324,16 @@
     }
 
     @Test
+    public void testEncodeIntegrityTransform() throws Exception {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(mIntegHmacSha1Transform.getTransformLength());
+        mIntegHmacSha1Transform.encodeToByteBuffer(false, byteBuffer);
+
+        byte[] expectedBytes = TestUtils.hexStringToByteArray(INTEG_TRANSFORM_RAW_PACKET);
+
+        assertArrayEquals(expectedBytes, byteBuffer.array());
+    }
+
+    @Test
     public void testConstructIntegrityTransformWithUnsupportedId() throws Exception {
         try {
             new IntegrityTransform(-1);
@@ -321,6 +359,16 @@
     }
 
     @Test
+    public void testEncodeDhGroupTransform() throws Exception {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(mDhGroup1024Transform.getTransformLength());
+        mDhGroup1024Transform.encodeToByteBuffer(false, byteBuffer);
+
+        byte[] expectedBytes = TestUtils.hexStringToByteArray(DH_GROUP_TRANSFORM_RAW_PACKET);
+
+        assertArrayEquals(expectedBytes, byteBuffer.array());
+    }
+
+    @Test
     public void testConstructDhGroupTransformWithUnsupportedId() throws Exception {
         try {
             new DhGroupTransform(-1);
@@ -378,6 +426,17 @@
     }
 
     @Test
+    public void testEncodeEsnTransform() throws Exception {
+        EsnTransform mEsnTransform = new EsnTransform();
+        ByteBuffer byteBuffer = ByteBuffer.allocate(mEsnTransform.getTransformLength());
+        mEsnTransform.encodeToByteBuffer(true, byteBuffer);
+
+        byte[] expectedBytes = TestUtils.hexStringToByteArray(ESN_TRANSFORM_RAW_PACKET);
+
+        assertArrayEquals(expectedBytes, byteBuffer.array());
+    }
+
+    @Test
     public void testDecodeUnrecognizedTransform() throws Exception {
         byte[] inputPacket = TestUtils.hexStringToByteArray(ENCR_TRANSFORM_RAW_PACKET);
         inputPacket[TRANSFORM_TYPE_OFFSET] = 6;
@@ -484,23 +543,23 @@
 
     @Test
     public void testDecodeSingleProposal() throws Exception {
-        byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET);
+        byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET);
         ByteBuffer inputBuffer = ByteBuffer.wrap(inputPacket);
         Proposal.sTransformDecoder = getDummyTransformDecoder(new Transform[0]);
 
         Proposal proposal = Proposal.readFrom(inputBuffer);
 
         assertEquals(PROPOSAL_NUMBER, proposal.number);
-        assertEquals(PROPOSAL_PROTOCOL_ID, proposal.protocolId);
-        assertEquals(PROPOSAL_SPI_SIZE, proposal.spiSize);
-        assertEquals(PROPOSAL_SPI, proposal.spi);
+        assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.protocolId);
+        assertEquals(IkePayload.SPI_LEN_NOT_INCLUDED, proposal.spiSize);
+        assertEquals(IkePayload.SPI_NOT_INCLUDED, proposal.spi);
         assertFalse(proposal.hasUnrecognizedTransform);
         assertNotNull(proposal.saProposal);
     }
 
     @Test
     public void testDecodeSaRequestWithMultipleProposal() throws Exception {
-        byte[] inputPacket = TestUtils.hexStringToByteArray(TWO_PROPOSAL_RAW_PACKET);
+        byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_TWO_PROPOSAL_RAW_PACKET);
         Proposal.sTransformDecoder = getDummyTransformDecoder(new Transform[0]);
 
         IkeSaPayload payload = new IkeSaPayload(false, false, inputPacket);
@@ -515,8 +574,26 @@
     }
 
     @Test
+    public void testEncodeProposal() throws Exception {
+        Proposal proposal =
+                new Proposal(
+                        (byte) PROPOSAL_NUMBER,
+                        IkePayload.PROTOCOL_ID_IKE,
+                        IkePayload.SPI_LEN_NOT_INCLUDED,
+                        IkePayload.SPI_NOT_INCLUDED,
+                        mSaProposalOne,
+                        false /*has no unrecognized Tramsform*/);
+
+        ByteBuffer byteBuffer = ByteBuffer.allocate(proposal.getProposalLength());
+        proposal.encodeToByteBuffer(true /*is the last*/, byteBuffer);
+
+        byte[] expectedBytes = TestUtils.hexStringToByteArray(OUTBOUND_PROPOSAL_RAW_PACKET);
+        assertArrayEquals(expectedBytes, byteBuffer.array());
+    }
+
+    @Test
     public void testDecodeSaResponseWithMultipleProposal() throws Exception {
-        byte[] inputPacket = TestUtils.hexStringToByteArray(TWO_PROPOSAL_RAW_PACKET);
+        byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_TWO_PROPOSAL_RAW_PACKET);
         Proposal.sTransformDecoder = getDummyTransformDecoder(new Transform[0]);
 
         try {
@@ -561,6 +638,19 @@
         }
     }
 
+    @Test
+    public void testEncodeIkeSaPayload() throws Exception {
+        IkeSaPayload saPayload = new IkeSaPayload(new SaProposal[] {mSaProposalOne});
+
+        ByteBuffer byteBuffer = ByteBuffer.allocate(saPayload.getPayloadLength());
+        saPayload.encodeToByteBuffer(IkePayload.PAYLOAD_TYPE_KE, byteBuffer);
+
+        byte[] expectedBytes =
+                TestUtils.hexStringToByteArray(
+                        OUTBOUND_SA_PAYLOAD_HEADER + OUTBOUND_PROPOSAL_RAW_PACKET);
+        assertArrayEquals(expectedBytes, byteBuffer.array());
+    }
+
     private void buildAndVerifySaRespProposal(byte[] saResponseBytes, Transform[] decodedTransforms)
             throws Exception {
         // Build response SA payload from decoding bytes.
@@ -577,7 +667,7 @@
 
     @Test
     public void testGetVerifiedNegotiatedProposal() throws Exception {
-        byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET);
+        byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET);
 
         buildAndVerifySaRespProposal(inputPacket, mValidNegotiatedTransformSet);
     }
@@ -585,7 +675,7 @@
     // Test throwing when negotiated proposal in SA response payload has unrecognized Transform.
     @Test
     public void testGetVerifiedNegotiatedProposalWithUnrecogTransform() throws Exception {
-        byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET);
+        byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET);
 
         Transform[] negotiatedTransformSet =
                 Arrays.copyOfRange(
@@ -602,7 +692,7 @@
     // Test throwing when negotiated proposal has invalid proposal number.
     @Test
     public void testGetVerifiedNegotiatedProposalWithInvalidNumber() throws Exception {
-        byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET);
+        byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET);
         inputPacket[PROPOSAL_NUMBER_OFFSET] = (byte) 10;
 
         try {
@@ -615,7 +705,7 @@
     // Test throwing when negotiated proposal has mismatched protocol ID.
     @Test
     public void testGetVerifiedNegotiatedProposalWithMisMatchedProtocol() throws Exception {
-        byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET);
+        byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET);
         inputPacket[PROTOCOL_ID_OFFSET] = IkePayload.PROTOCOL_ID_ESP;
 
         try {
@@ -628,7 +718,7 @@
     // Test throwing when negotiated proposal has Transform that was not proposed in request.
     @Test
     public void testGetVerifiedNegotiatedProposalWithMismatchedTransform() throws Exception {
-        byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET);
+        byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET);
 
         Transform[] negotiatedTransformSet =
                 Arrays.copyOfRange(
@@ -645,7 +735,7 @@
     // Test throwing when negotiated proposal is lack of a certain type Transform.
     @Test
     public void testGetVerifiedNegotiatedProposalWithoutTransform() throws Exception {
-        byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET);
+        byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET);
 
         try {
             buildAndVerifySaRespProposal(inputPacket, new Transform[0]);
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java
index aebd119..13866fb 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java
@@ -17,13 +17,11 @@
 package com.android.ike.ikev2.message;
 
 import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.fail;
 
 import org.junit.Before;
 import org.junit.Test;
 
 import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
 import java.util.Arrays;
 
 import javax.crypto.Cipher;
@@ -82,44 +80,11 @@
     }
 
     @Test
-    public void testAuthenticateAndDecryptMessage() throws Exception {
-        byte[] message = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_HEX_STRING);
-
-        IkeSkPayload payload =
-                IkePayloadFactory.getIkeSkPayload(
-                                message,
-                                mHmacSha1IntegrityMac,
-                                CHECKSUM_LEN,
-                                mAesCbcDecryptCipher,
-                                mAesCbcDecryptKey)
-                        .first;
-        byte[] expectedPlaintext =
-                TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_DECRYPTED_BODY_HEX_STRING);
-        assertArrayEquals(expectedPlaintext, payload.getUnencryptedPayloads());
-    }
-
-    @Test
-    public void testThrowExceptionForInvalidChecksum() throws Exception {
-        byte[] message = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_HEX_STRING);
-        // Change last bit of checksum.
-        message[message.length - 1]++;
-        try {
-            IkePayloadFactory.getIkeSkPayload(
-                    message,
-                    mHmacSha1IntegrityMac,
-                    CHECKSUM_LEN,
-                    mAesCbcDecryptCipher,
-                    mAesCbcDecryptKey);
-            fail("Expected GeneralSecurityException: Invalid checksum.");
-        } catch (GeneralSecurityException expected) {
-        }
-    }
-
-    @Test
     public void testEncode() throws Exception {
         byte[] message = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_HEX_STRING);
         byte[] payloadBytes =
                 Arrays.copyOfRange(message, IkeHeader.IKE_HEADER_LENGTH, message.length);
+
         IkeSkPayload payload =
                 IkePayloadFactory.getIkeSkPayload(
                                 message,