Merge "Create Traffic Selector Payload"
diff --git a/src/java/com/android/ike/ikev2/SaProposal.java b/src/java/com/android/ike/ikev2/SaProposal.java
index e478efd..9297179 100644
--- a/src/java/com/android/ike/ikev2/SaProposal.java
+++ b/src/java/com/android/ike/ikev2/SaProposal.java
@@ -19,6 +19,12 @@
 import android.annotation.IntDef;
 import android.util.ArraySet;
 
+import com.android.ike.ikev2.message.IkePayload;
+import com.android.ike.ikev2.message.IkeSaPayload.DhGroupTransform;
+import com.android.ike.ikev2.message.IkeSaPayload.EncryptionTransform;
+import com.android.ike.ikev2.message.IkeSaPayload.IntegrityTransform;
+import com.android.ike.ikev2.message.IkeSaPayload.PrfTransform;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Set;
@@ -127,6 +133,230 @@
         SUPPORTED_DH_GROUP.add(DH_GROUP_2048_BIT_MODP);
     }
 
+    /** Package private */
+    @IkePayload.ProtocolId final int mProtocolId;
+    /** Package private */
+    final EncryptionTransform[] mEncryptionAlgorithms;
+    /** Package private */
+    final PrfTransform[] mPseudorandomFunctions;
+    /** Package private */
+    final IntegrityTransform[] mIntegrityAlgorithms;
+    /** Package private */
+    final DhGroupTransform[] mDhGroups;
+
+    private SaProposal(
+            @IkePayload.ProtocolId int protocol,
+            EncryptionTransform[] encryptionAlgos,
+            PrfTransform[] prfs,
+            IntegrityTransform[] integrityAlgos,
+            DhGroupTransform[] dhGroups) {
+        mProtocolId = protocol;
+        mEncryptionAlgorithms = encryptionAlgos;
+        mPseudorandomFunctions = prfs;
+        mIntegrityAlgorithms = integrityAlgos;
+        mDhGroups = dhGroups;
+    }
+
+    /**
+     * This class can be used to incrementally construct a SaProposal. SaProposal instances are
+     * immutable once built.
+     */
+    public static final class Builder {
+        private static final String ERROR_TAG = "Invalid SA Proposal: ";
+
+        /** Indicate if Builder is for building IKE SA proposal or Child SA proposal. */
+        private final boolean mIsIkeProposal;
+        /**
+         * Indicate if Builder is for building first Child SA proposal or addtional Child SA
+         * proposal. Only valid if mIsIkeProposal is false.
+         */
+        private final boolean mIsFirstChild;
+
+        // Use set to avoid adding repeated algorithms.
+        private final Set<EncryptionTransform> mProposedEncryptAlgos = new ArraySet<>();
+        private final Set<PrfTransform> mProposedPrfs = new ArraySet<>();
+        private final Set<IntegrityTransform> mProposedIntegrityAlgos = new ArraySet<>();
+        private final Set<DhGroupTransform> mProposedDhGroups = new ArraySet<>();
+
+        private boolean mHasAead = false;
+
+        private Builder(boolean isIke, boolean isFirstChild) {
+            mIsIkeProposal = isIke;
+            mIsFirstChild = isFirstChild;
+        }
+
+        private static boolean isAead(@EncryptionAlgorithm int algorithm) {
+            switch (algorithm) {
+                case ENCRYPTION_ALGORITHM_3DES:
+                    // Fall through
+                case ENCRYPTION_ALGORITHM_AES_CBC:
+                    return false;
+                case ENCRYPTION_ALGORITHM_AES_GCM_8:
+                    // Fall through
+                case ENCRYPTION_ALGORITHM_AES_GCM_12:
+                    // Fall through
+                case ENCRYPTION_ALGORITHM_AES_GCM_16:
+                    return true;
+                default:
+                    // Won't hit here.
+                    throw new IllegalArgumentException("Unsupported Encryption Algorithm.");
+            }
+        }
+
+        private EncryptionTransform[] buildEncryptAlgosOrThrow() {
+            if (mProposedEncryptAlgos.isEmpty()) {
+                throw new IllegalArgumentException(
+                        ERROR_TAG + "Encryption algorithm must be proposed.");
+            }
+
+            return (EncryptionTransform[]) mProposedEncryptAlgos.toArray();
+        }
+
+        private PrfTransform[] buildPrfsOrThrow() {
+            // TODO: Validate that PRF must be proposed for IKE SA and PRF must not be
+            // proposed for Child SA.
+            throw new UnsupportedOperationException("Cannot validate user proposed algorithm.");
+        }
+
+        private IntegrityTransform[] buildIntegAlgosOrThrow() {
+            // TODO: Validate proposed integrity algorithms according to existence of AEAD.
+            throw new UnsupportedOperationException("Cannot validate user proposed algorithm.");
+        }
+
+        private DhGroupTransform[] buildDhGroupsOrThrow() {
+            // TODO: Validate proposed DH groups according to the usage of SaProposal (for
+            // IKE SA, for first Child SA or for addtional Child SA)
+            throw new UnsupportedOperationException("Cannot validate user proposed algorithm.");
+        }
+
+        /** Returns a new Builder for a IKE SA Proposal. */
+        public static Builder newIkeSaProposalBuilder() {
+            return new Builder(true, false);
+        }
+
+        /**
+         * Returns a new Builder for a Child SA Proposal.
+         *
+         * @param isFirstChildSaProposal indicates if this SA proposal for first Child SA.
+         * @return Builder for a Child SA Proposal.
+         */
+        public static Builder newChildSaProposalBuilder(boolean isFirstChildSaProposal) {
+            return new Builder(false, isFirstChildSaProposal);
+        }
+
+        /**
+         * Adds an encryption algorithm to SA proposal being built.
+         *
+         * @param algorithm encryption algorithm to add to SaProposal.
+         * @return Builder of SaProposal.
+         */
+        public Builder addEncryptionAlgorithm(@EncryptionAlgorithm int algorithm) {
+            // Construct EncryptionTransform and validate proposed algorithm during
+            // construction.
+            EncryptionTransform encryptionTransform = new EncryptionTransform(algorithm);
+
+            validateOnlyOneModeEncryptAlgoProposedOrThrow(algorithm);
+
+            mProposedEncryptAlgos.add(encryptionTransform);
+            return this;
+        }
+
+        /**
+         * Adds an encryption algorithm with specific key length to SA proposal being built.
+         *
+         * @param algorithm encryption algorithm to add to SaProposal.
+         * @param keyLength key length of algorithm.
+         * @return Builder of SaProposal.
+         * @throws IllegalArgumentException if AEAD and non-combined mode algorithms are mixed.
+         */
+        public Builder addEncryptionAlgorithm(@EncryptionAlgorithm int algorithm, int keyLength) {
+            // Construct EncryptionTransform and validate proposed algorithm during
+            // construction.
+            EncryptionTransform encryptionTransform = new EncryptionTransform(algorithm, keyLength);
+
+            validateOnlyOneModeEncryptAlgoProposedOrThrow(algorithm);
+
+            mProposedEncryptAlgos.add(encryptionTransform);
+            return this;
+        }
+
+        private void validateOnlyOneModeEncryptAlgoProposedOrThrow(
+                @EncryptionAlgorithm int algorithm) {
+            boolean isCurrentAead = isAead(algorithm);
+
+            if (!mProposedEncryptAlgos.isEmpty() && (mHasAead ^ isCurrentAead)) {
+                throw new IllegalArgumentException(
+                        ERROR_TAG
+                                + "Proposal cannot has both normal ciphers "
+                                + "and combined-mode ciphers.");
+            }
+
+            if (isCurrentAead) mHasAead = true;
+        }
+
+        /**
+         * Adds a pseudorandom function to SA proposal being built.
+         *
+         * @param algorithm pseudorandom function to add to SaProposal.
+         * @return Builder of SaProposal.
+         */
+        public Builder addPseudorandomFunction(@PseudorandomFunction int algorithm) {
+            // Construct PrfTransform and validate proposed algorithm during
+            // construction.
+            mProposedPrfs.add(new PrfTransform(algorithm));
+            return this;
+        }
+
+        /**
+         * Adds an integrity algorithm to SA proposal being built.
+         *
+         * @param algorithm integrity algorithm to add to SaProposal.
+         * @return Builder of SaProposal.
+         */
+        public Builder addIntegrityAlgorithm(@IntegrityAlgorithm int algorithm) {
+            // Construct IntegrityTransform and validate proposed algorithm during
+            // construction.
+            mProposedIntegrityAlgos.add(new IntegrityTransform(algorithm));
+            return this;
+        }
+
+        /**
+         * Adds a Diffie-Hellman Group to SA proposal being built.
+         *
+         * @param dhGroup to add to SaProposal.
+         * @return Builder of SaProposal.
+         */
+        public Builder addDhGroup(@DhGroup int dhGroup) {
+            // Construct DhGroupTransform and validate proposed dhGroup during
+            // construction.
+            mProposedDhGroups.add(new DhGroupTransform(dhGroup));
+            return this;
+        }
+
+        /**
+         * Validates, builds and returns the SaProposal
+         *
+         * @return SaProposal the validated SaProposal.
+         * @throws IllegalArgumentException if SaProposal is invalid.
+         * */
+        public SaProposal buildOrThrow() {
+            EncryptionTransform[] encryptionTransforms = buildEncryptAlgosOrThrow();
+            PrfTransform[] prfTransforms = buildPrfsOrThrow();
+            IntegrityTransform[] integrityTransforms = buildIntegAlgosOrThrow();
+            DhGroupTransform[] dhGroupTransforms = buildDhGroupsOrThrow();
+
+            // IKE library only supports negotiating ESP Child SA.
+            int protocol = mIsIkeProposal ? IkePayload.PROTOCOL_ID_IKE : IkePayload.PROTOCOL_ID_ESP;
+
+            return new SaProposal(
+                    protocol,
+                    encryptionTransforms,
+                    prfTransforms,
+                    integrityTransforms,
+                    dhGroupTransforms);
+        }
+    }
+
     /**
      * Check if the provided algorithm is a supported encryption algorithm.
      *
diff --git a/src/java/com/android/ike/ikev2/message/IkeAuthDigitalSignPayload.java b/src/java/com/android/ike/ikev2/message/IkeAuthDigitalSignPayload.java
index 12fb5f6..5b5a3f0 100644
--- a/src/java/com/android/ike/ikev2/message/IkeAuthDigitalSignPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeAuthDigitalSignPayload.java
@@ -138,14 +138,14 @@
     // TODO: Add methods for generating and validating signature.
 
     @Override
-    protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) {
+    protected void encodeAuthDataToByteBuffer(ByteBuffer byteBuffer) {
         // TODO: Implement it.
         throw new UnsupportedOperationException(
                 "It is not supported to encode a " + getTypeString());
     }
 
     @Override
-    protected int getPayloadLength() {
+    protected int getAuthDataLength() {
         // TODO: Implement it.
         throw new UnsupportedOperationException(
                 "It is not supported to get payload length of " + getTypeString());
diff --git a/src/java/com/android/ike/ikev2/message/IkeAuthPayload.java b/src/java/com/android/ike/ikev2/message/IkeAuthPayload.java
index beb2bdc..502af36 100644
--- a/src/java/com/android/ike/ikev2/message/IkeAuthPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeAuthPayload.java
@@ -61,7 +61,7 @@
 
     @AuthMethod public final int authMethod;
 
-    protected IkeAuthPayload(boolean critical, int authMethod) throws IkeException {
+    protected IkeAuthPayload(boolean critical, int authMethod) {
         super(PAYLOAD_TYPE_AUTH, critical);
         this.authMethod = authMethod;
     }
@@ -131,7 +131,23 @@
     }
 
     @Override
+    protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) {
+        encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer);
+        byteBuffer.put((byte) authMethod).put(new byte[AUTH_RESERVED_FIELD_LEN]);
+        encodeAuthDataToByteBuffer(byteBuffer);
+    }
+
+    @Override
+    protected int getPayloadLength() {
+        return GENERIC_HEADER_LENGTH + AUTH_HEADER_LEN + getAuthDataLength();
+    }
+
+    @Override
     public String getTypeString() {
         return "Authentication Payload";
     }
+
+    protected abstract void encodeAuthDataToByteBuffer(ByteBuffer byteBuffer);
+
+    protected abstract int getAuthDataLength();
 }
diff --git a/src/java/com/android/ike/ikev2/message/IkeAuthPskPayload.java b/src/java/com/android/ike/ikev2/message/IkeAuthPskPayload.java
index 63f45dd..213e24a 100644
--- a/src/java/com/android/ike/ikev2/message/IkeAuthPskPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeAuthPskPayload.java
@@ -16,9 +16,10 @@
 
 package com.android.ike.ikev2.message;
 
-import com.android.ike.ikev2.exceptions.IkeException;
-
 import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+
+import javax.crypto.Mac;
 
 /**
  * IkeAuthPskPayload represents an Authentication Payload using Pre-Shared Key to do authentication.
@@ -27,25 +28,79 @@
  *     Protocol Version 2 (IKEv2).
  */
 public final class IkeAuthPskPayload extends IkeAuthPayload {
+    // Hex of ASCII characters "Key Pad for IKEv2" for calculating PSK signature.
+    private static final byte[] IKE_KEY_PAD_STRING_ASCII_HEX_BYTES = {
+        (byte) 0x4b, (byte) 0x65, (byte) 0x79, (byte) 0x20,
+        (byte) 0x50, (byte) 0x61, (byte) 0x64, (byte) 0x20,
+        (byte) 0x66, (byte) 0x6f, (byte) 0x72, (byte) 0x20,
+        (byte) 0x49, (byte) 0x4b, (byte) 0x45, (byte) 0x76,
+        (byte) 0x32
+    };
+
     public final byte[] signature;
 
-    protected IkeAuthPskPayload(boolean critical, byte[] authData) throws IkeException {
+    /**
+     * Construct IkeAuthPskPayload for received IKE packet in the context of {@link
+     * IkePayloadFactory}.
+     */
+    protected IkeAuthPskPayload(boolean critical, byte[] authData) {
         super(critical, IkeAuthPayload.AUTH_METHOD_PRE_SHARED_KEY);
         signature = authData;
     }
 
-    @Override
-    protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) {
-        // TODO: Implement it.
-        throw new UnsupportedOperationException(
-                "It is not supported to encode a " + getTypeString());
+    /**
+     * Construct IkeAuthPskPayload for an outbound IKE packet.
+     *
+     * <p>Since IKE library is always a client, outbound IkeAuthPskPayload always signs IKE
+     * initiator's SignedOctets, which is concatenation of the IKE_INIT request message, the Nonce
+     * of IKE responder and the signed ID-Initiator payload body.
+     *
+     * @param psk locally stored pre-shared key
+     * @param ikeInitBytes IKE_INIT request for calculating IKE initiator's SignedOctets.
+     * @param nonce nonce of IKE responder for calculating IKE initiator's SignedOctets.
+     * @param idPayloadBodyBytes ID-Initiator payload body for calculating IKE initiator's
+     *     SignedOctets.
+     * @param prfMac locally store PRF
+     * @param prfKeyBytes locally store PRF keys
+     */
+    public IkeAuthPskPayload(
+            byte[] psk,
+            byte[] ikeInitBytes,
+            byte[] nonce,
+            byte[] idPayloadBodyBytes,
+            Mac prfMac,
+            byte[] prfKeyBytes) {
+        super(false, IkeAuthPayload.AUTH_METHOD_PRE_SHARED_KEY);
+        signature =
+                calculatePskSignature(
+                        psk, ikeInitBytes, nonce, idPayloadBodyBytes, prfMac, prfKeyBytes);
+    }
+
+    private static byte[] calculatePskSignature(
+            byte[] psk,
+            byte[] ikeInitBytes,
+            byte[] nonce,
+            byte[] idPayloadBodyBytes,
+            Mac prfMac,
+            byte[] prfKeyBytes) {
+        try {
+            byte[] signingKeyBytes = signWithPrf(prfMac, psk, IKE_KEY_PAD_STRING_ASCII_HEX_BYTES);
+            byte[] dataToSignBytes =
+                    getSignedOctets(ikeInitBytes, nonce, idPayloadBodyBytes, prfMac, prfKeyBytes);
+            return signWithPrf(prfMac, signingKeyBytes, dataToSignBytes);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("Locally stored PRF key is invalid: ", e);
+        }
     }
 
     @Override
-    protected int getPayloadLength() {
-        // TODO: Implement it.
-        throw new UnsupportedOperationException(
-                "It is not supported to get payload length of " + getTypeString());
+    protected void encodeAuthDataToByteBuffer(ByteBuffer byteBuffer) {
+        byteBuffer.put(signature);
+    }
+
+    @Override
+    protected int getAuthDataLength() {
+        return signature.length;
     }
 
     @Override
diff --git a/src/java/com/android/ike/ikev2/message/IkeSaPayload.java b/src/java/com/android/ike/ikev2/message/IkeSaPayload.java
index c31575d..e809c9c 100644
--- a/src/java/com/android/ike/ikev2/message/IkeSaPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeSaPayload.java
@@ -35,6 +35,7 @@
 import java.nio.ByteBuffer;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -376,6 +377,19 @@
         }
 
         @Override
+        public int hashCode() {
+            return Objects.hash(type, id, keyLength);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof EncryptionTransform)) return false;
+
+            EncryptionTransform other = (EncryptionTransform) o;
+            return (type == other.type && id == other.id && keyLength == other.keyLength);
+        }
+
+        @Override
         protected boolean isSupportedTransformId(int id) {
             return SaProposal.isSupportedEncryptionAlgorithm(id);
         }
@@ -476,6 +490,19 @@
         }
 
         @Override
+        public int hashCode() {
+            return Objects.hash(type, id);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof PrfTransform)) return false;
+
+            PrfTransform other = (PrfTransform) o;
+            return (type == other.type && id == other.id);
+        }
+
+        @Override
         protected boolean isSupportedTransformId(int id) {
             return SaProposal.isSupportedPseudorandomFunction(id);
         }
@@ -524,6 +551,19 @@
         }
 
         @Override
+        public int hashCode() {
+            return Objects.hash(type, id);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof IntegrityTransform)) return false;
+
+            IntegrityTransform other = (IntegrityTransform) o;
+            return (type == other.type && id == other.id);
+        }
+
+        @Override
         protected boolean isSupportedTransformId(int id) {
             return SaProposal.isSupportedIntegrityAlgorithm(id);
         }
@@ -572,6 +612,19 @@
         }
 
         @Override
+        public int hashCode() {
+            return Objects.hash(type, id);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof DhGroupTransform)) return false;
+
+            DhGroupTransform other = (DhGroupTransform) o;
+            return (type == other.type && id == other.id);
+        }
+
+        @Override
         protected boolean isSupportedTransformId(int id) {
             return SaProposal.isSupportedDhGroup(id);
         }
@@ -626,6 +679,19 @@
         }
 
         @Override
+        public int hashCode() {
+            return Objects.hash(type, id);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof EsnTransform)) return false;
+
+            EsnTransform other = (EsnTransform) o;
+            return (type == other.type && id == other.id);
+        }
+
+        @Override
         protected boolean isSupportedTransformId(int id) {
             return (id == ESN_POLICY_NO_EXTENDED || id == ESN_POLICY_EXTENDED);
         }
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java b/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java
new file mode 100644
index 0000000..522d44b
--- /dev/null
+++ b/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.fail;
+
+import com.android.ike.ikev2.SaProposal.Builder;
+
+import org.junit.Test;
+
+public final class SaProposalTest {
+    @Test
+    public void testBuildEncryptAlgosWithNoAlgorithm() throws Exception {
+        Builder builder = Builder.newIkeSaProposalBuilder();
+        try {
+            builder.buildOrThrow();
+            fail("Encryption algorithm is not provided.");
+        } catch (IllegalArgumentException expected) {
+
+        }
+    }
+
+    @Test
+    public void testBuildEncryptAlgosWithUnrecognizedAlgorithm() throws Exception {
+        Builder builder = Builder.newIkeSaProposalBuilder();
+        try {
+            builder.addEncryptionAlgorithm(-1);
+            fail("Encryption algorithm is not recognized.");
+        } catch (IllegalArgumentException expected) {
+
+        }
+    }
+
+    @Test
+    public void testBuildEncryptAlgosWithTwoModes() throws Exception {
+        Builder builder = Builder.newIkeSaProposalBuilder();
+        try {
+            builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES)
+                    .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12);
+            fail("Expect failure when normal and combined-mode ciphers are proposed together.");
+        } catch (IllegalArgumentException expected) {
+
+        }
+    }
+}
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeAuthPayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeAuthPayloadTest.java
index a67f04c..7c630b7 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeAuthPayloadTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeAuthPayloadTest.java
@@ -28,7 +28,7 @@
 public final class IkeAuthPayloadTest {
     private static final String PSK_AUTH_PAYLOAD_HEX_STRING =
             "02000000df7c038aefaaa32d3f44b228b52a332744dfb2c1";
-    private static final String PSK_AUTH_PAYLOAD_SIGNATRUE_HEX_STRING =
+    private static final String PSK_AUTH_PAYLOAD_SIGNATURE_HEX_STRING =
             "df7c038aefaaa32d3f44b228b52a332744dfb2c1";
     private static final String PSK_ID_PAYLOAD_HEX_STRING = "010000000a50500d";
     private static final String PSK_SKP_HEX_STRING = "094787780EE466E2CB049FA327B43908BC57E485";
@@ -89,7 +89,7 @@
         assertTrue(payload instanceof IkeAuthPskPayload);
 
         byte[] expectedSignature =
-                TestUtils.hexStringToByteArray(PSK_AUTH_PAYLOAD_SIGNATRUE_HEX_STRING);
+                TestUtils.hexStringToByteArray(PSK_AUTH_PAYLOAD_SIGNATURE_HEX_STRING);
         assertArrayEquals(expectedSignature, ((IkeAuthPskPayload) payload).signature);
     }
 
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeAuthPskPayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeAuthPskPayloadTest.java
new file mode 100644
index 0000000..398e962
--- /dev/null
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeAuthPskPayloadTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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 org.junit.Test;
+
+import java.nio.ByteBuffer;
+
+import javax.crypto.Mac;
+
+
+public final class IkeAuthPskPayloadTest {
+    private static final String PSK_AUTH_PAYLOAD_HEX_STRING =
+            "2100001c02000000df7c038aefaaa32d3f44b228b52a332744dfb2c1";
+    private static final String PSK_AUTH_PAYLOAD_SIGNATURE_HEX_STRING =
+            "df7c038aefaaa32d3f44b228b52a332744dfb2c1";
+
+    private static final String PSK_IKE_INIT_REQUEST_HEX_STRING =
+            "5f54bf6d8b48e6e1000000000000000021202208"
+                    + "0000000000000150220000300000002c01010004"
+                    + "0300000c0100000c800e00800300000803000002"
+                    + "0300000804000002000000080200000228000088"
+                    + "00020000b4a2faf4bb54878ae21d638512ece55d"
+                    + "9236fc5046ab6cef82220f421f3ce6361faf3656"
+                    + "4ecb6d28798a94aad7b2b4b603ddeaaa5630adb9"
+                    + "ece8ac37534036040610ebdd92f46bef84f0be7d"
+                    + "b860351843858f8acf87056e272377f70c9f2d81"
+                    + "e29c7b0ce4f291a3a72476bb0b278fd4b7b0a4c2"
+                    + "6bbeb08214c707137607958729000024c39b7f36"
+                    + "8f4681b89fa9b7be6465abd7c5f68b6ed5d3b4c7"
+                    + "2cb4240eb5c464122900001c00004004e54f73b7"
+                    + "d83f6beb881eab2051d8663f421d10b02b00001c"
+                    + "00004005d915368ca036004cb578ae3e3fb26850"
+                    + "9aeab19000000020699369228741c6d4ca094c93"
+                    + "e242c9de19e7b7c60000000500000500";
+    private static final String PSK_NONCE_RESP_HEX_STRING =
+            "9756112ca539f5c25abacc7ee92b73091942a9c06950f98848f1af1694c4ddff";
+    private static final String PSK_ID_INITIATOR_PAYLOAD_HEX_STRING = "010000000a50500d";
+
+    private static final String PSK_HEX_STRING = "6A756E69706572313233";
+    private static final String PSK_SKP_HEX_STRING = "094787780EE466E2CB049FA327B43908BC57E485";
+
+    private static final String PRF_HMAC_SHA1_ALGO_NAME = "HmacSHA1";
+
+    @Test
+    public void testBuildOutboundIkeAuthPskPayload() throws Exception {
+        byte[] psk = TestUtils.hexStringToByteArray(PSK_HEX_STRING);
+        byte[] ikeInitBytes = TestUtils.hexStringToByteArray(PSK_IKE_INIT_REQUEST_HEX_STRING);
+        byte[] nonce = TestUtils.hexStringToByteArray(PSK_NONCE_RESP_HEX_STRING);
+        byte[] idPayloadBodyBytes =
+                TestUtils.hexStringToByteArray(PSK_ID_INITIATOR_PAYLOAD_HEX_STRING);
+        byte[] prfKeyBytes = TestUtils.hexStringToByteArray(PSK_SKP_HEX_STRING);
+        Mac prfMac = Mac.getInstance(PRF_HMAC_SHA1_ALGO_NAME, IkeMessage.getSecurityProvider());
+
+        IkeAuthPskPayload payload =
+                new IkeAuthPskPayload(
+                        psk, ikeInitBytes, nonce, idPayloadBodyBytes, prfMac, prfKeyBytes);
+
+        assertEquals(IkeAuthPayload.AUTH_METHOD_PRE_SHARED_KEY, payload.authMethod);
+        byte[] expectedSignature =
+                TestUtils.hexStringToByteArray(PSK_AUTH_PAYLOAD_SIGNATURE_HEX_STRING);
+        assertArrayEquals(expectedSignature, payload.signature);
+
+        // Verify payload length
+        int payloadLength = payload.getPayloadLength();
+        byte[] expectedPayload = TestUtils.hexStringToByteArray(PSK_AUTH_PAYLOAD_HEX_STRING);
+        assertEquals(expectedPayload.length, payloadLength);
+
+        // Verify encoding
+        ByteBuffer byteBuffer = ByteBuffer.allocate(payloadLength);
+        payload.encodeToByteBuffer(IkePayload.PAYLOAD_TYPE_SA, byteBuffer);
+        assertArrayEquals(expectedPayload, byteBuffer.array());
+    }
+}