/*
 * 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 android.net.ipsec.ike;

import static com.android.internal.net.ipsec.ike.message.IkeSaPayload.EsnTransform.ESN_POLICY_NO_EXTENDED;

import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.net.IpSecAlgorithm;
import android.net.vcn.util.PersistableBundleUtils;
import android.os.PersistableBundle;
import android.util.ArraySet;

import com.android.internal.net.ipsec.ike.crypto.IkeCipher;
import com.android.internal.net.ipsec.ike.crypto.IkeMacIntegrity;
import com.android.internal.net.ipsec.ike.message.IkePayload;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.DhGroupTransform;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.EncryptionTransform;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.EsnTransform;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.IntegrityTransform;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.Transform;
import com.android.modules.utils.build.SdkLevel;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * ChildSaProposal represents a proposed configuration to negotiate a Child SA.
 *
 * <p>ChildSaProposal will contain cryptograhic algorithms and key generation materials for the
 * negotiation of a Child SA.
 *
 * <p>User must provide at least one valid ChildSaProposal when they are creating a new Child SA.
 *
 * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3">RFC 7296, Internet Key Exchange
 *     Protocol Version 2 (IKEv2)</a>
 */
public final class ChildSaProposal extends SaProposal {
    // Before SDK S, there is no API in IpSecAlgorithm to retrieve supported algorithms. Thus hard
    // coded these algorithms here.
    private static final Set<Integer> SUPPORTED_IPSEC_ENCRYPTION_BEFORE_SDK_S;
    private static final Set<Integer> SUPPORTED_IPSEC_INTEGRITY_BEFORE_SDK_S;

    static {
        SUPPORTED_IPSEC_ENCRYPTION_BEFORE_SDK_S = new ArraySet<>();
        SUPPORTED_IPSEC_ENCRYPTION_BEFORE_SDK_S.add(ENCRYPTION_ALGORITHM_AES_CBC);
        SUPPORTED_IPSEC_ENCRYPTION_BEFORE_SDK_S.add(ENCRYPTION_ALGORITHM_AES_GCM_8);
        SUPPORTED_IPSEC_ENCRYPTION_BEFORE_SDK_S.add(ENCRYPTION_ALGORITHM_AES_GCM_12);
        SUPPORTED_IPSEC_ENCRYPTION_BEFORE_SDK_S.add(ENCRYPTION_ALGORITHM_AES_GCM_16);

        SUPPORTED_IPSEC_INTEGRITY_BEFORE_SDK_S = new ArraySet<>();
        SUPPORTED_IPSEC_INTEGRITY_BEFORE_SDK_S.add(INTEGRITY_ALGORITHM_HMAC_SHA1_96);
        SUPPORTED_IPSEC_INTEGRITY_BEFORE_SDK_S.add(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128);
        SUPPORTED_IPSEC_INTEGRITY_BEFORE_SDK_S.add(INTEGRITY_ALGORITHM_HMAC_SHA2_384_192);
        SUPPORTED_IPSEC_INTEGRITY_BEFORE_SDK_S.add(INTEGRITY_ALGORITHM_HMAC_SHA2_512_256);
    }

    private static final String ESN_KEY = "mEsns";
    private final EsnTransform[] mEsns;

    /**
     * Construct an instance of ChildSaProposal.
     *
     * <p>This constructor is either called by ChildSaPayload for building an inbound proposal from
     * a decoded packet, or called by the inner Builder to build an outbound proposal from user
     * provided parameters
     *
     * @param encryptionAlgos encryption algorithms
     * @param integrityAlgos integrity algorithms
     * @param dhGroups Diffie-Hellman Groups
     * @param esns ESN policies
     * @hide
     */
    public ChildSaProposal(
            EncryptionTransform[] encryptionAlgos,
            IntegrityTransform[] integrityAlgos,
            DhGroupTransform[] dhGroups,
            EsnTransform[] esns) {
        super(IkePayload.PROTOCOL_ID_ESP, encryptionAlgos, integrityAlgos, dhGroups);
        mEsns = esns;
    }

    /**
     * Constructs this object by deserializing a PersistableBundle
     *
     * <p>Constructed proposals are guaranteed to be valid, as checked by the
     * ChildSaProposal.Builder.
     *
     * @hide
     */
    @NonNull
    public static ChildSaProposal fromPersistableBundle(@NonNull PersistableBundle in) {
        Objects.requireNonNull(in, "PersistableBundle is null");

        ChildSaProposal.Builder builder = new ChildSaProposal.Builder();

        PersistableBundle encryptionBundle = in.getPersistableBundle(ENCRYPT_ALGO_KEY);
        Objects.requireNonNull(encryptionBundle, "Encryption algo bundle is null");
        List<EncryptionTransform> encryptList =
                PersistableBundleUtils.toList(
                        encryptionBundle, EncryptionTransform::fromPersistableBundle);
        for (EncryptionTransform t : encryptList) {
            builder.addEncryptionAlgorithm(t.id, t.getSpecifiedKeyLength());
        }

        int[] integrityAlgoIdArray = in.getIntArray(INTEGRITY_ALGO_KEY);
        Objects.requireNonNull(integrityAlgoIdArray, "Integrity algo array is null");
        for (int algo : integrityAlgoIdArray) {
            builder.addIntegrityAlgorithm(algo);
        }

        int[] dhGroupArray = in.getIntArray(DH_GROUP_KEY);
        Objects.requireNonNull(dhGroupArray, "DH Group array is null");
        for (int dh : dhGroupArray) {
            builder.addDhGroup(dh);
        }

        int[] esnPolicies = in.getIntArray(ESN_KEY);
        Objects.requireNonNull(esnPolicies, "ESN policy array is null");

        for (int p : esnPolicies) {
            switch (p) {
                case ESN_POLICY_NO_EXTENDED:
                    // Ignored. All ChildSaProposal(s) are proposed with this automatically
                    break;
                default:
                    throw new IllegalArgumentException(
                            "Proposing ESN policy: " + p + " is unsupported");
            }
        }

        return builder.build();
    }

    /**
     * Serializes this object to a PersistableBundle
     *
     * @hide
     */
    @Override
    @NonNull
    public PersistableBundle toPersistableBundle() {
        final PersistableBundle result = super.toPersistableBundle();
        int[] esnPolicies = Arrays.asList(mEsns).stream().mapToInt(esn -> esn.id).toArray();
        result.putIntArray(ESN_KEY, esnPolicies);

        return result;
    }

    /**
     * Returns supported encryption algorithms for Child SA proposal negotiation.
     *
     * <p>Some algorithms may not be supported on old devices.
     */
    @NonNull
    public static Set<Integer> getSupportedEncryptionAlgorithms() {
        if (SdkLevel.isAtLeastS()) {
            Set<Integer> algoIds = new ArraySet<>();
            for (int i = 0; i < SUPPORTED_ENCRYPTION_ALGO_TO_STR.size(); i++) {
                int ikeAlgoId = SUPPORTED_ENCRYPTION_ALGO_TO_STR.keyAt(i);
                String ipSecAlgoName = IkeCipher.getIpSecAlgorithmName(ikeAlgoId);
                if (IpSecAlgorithm.getSupportedAlgorithms().contains(ipSecAlgoName)) {
                    algoIds.add(ikeAlgoId);
                }
            }
            return algoIds;
        } else {
            return SUPPORTED_IPSEC_ENCRYPTION_BEFORE_SDK_S;
        }
    }

    /**
     * Returns supported integrity algorithms for Child SA proposal negotiation.
     *
     * <p>Some algorithms may not be supported on old devices.
     */
    @NonNull
    public static Set<Integer> getSupportedIntegrityAlgorithms() {
        Set<Integer> algoIds = new ArraySet<>();

        // Although IpSecAlgorithm does not support INTEGRITY_ALGORITHM_NONE, IKE supports
        // negotiating it and won't build IpSecAlgorithm with it.
        algoIds.add(INTEGRITY_ALGORITHM_NONE);

        if (SdkLevel.isAtLeastS()) {
            for (int i = 0; i < SUPPORTED_INTEGRITY_ALGO_TO_STR.size(); i++) {
                int ikeAlgoId = SUPPORTED_INTEGRITY_ALGO_TO_STR.keyAt(i);
                String ipSecAlgoName = IkeMacIntegrity.getIpSecAlgorithmName(ikeAlgoId);
                if (IpSecAlgorithm.getSupportedAlgorithms().contains(ipSecAlgoName)) {
                    algoIds.add(ikeAlgoId);
                }
            }
        } else {
            algoIds.addAll(SUPPORTED_IPSEC_INTEGRITY_BEFORE_SDK_S);
        }
        return algoIds;
    }

    /**
     * Gets all ESN policies.
     *
     * @hide
     */
    public EsnTransform[] getEsnTransforms() {
        return mEsns;
    }

    /**
     * Gets a copy of proposal without all proposed DH groups.
     *
     * <p>This is used to avoid negotiating DH Group for negotiating first Child SA.
     *
     * @hide
     */
    public ChildSaProposal getCopyWithoutDhTransform() {
        return new ChildSaProposal(
                getEncryptionTransforms(),
                getIntegrityTransforms(),
                new DhGroupTransform[0],
                getEsnTransforms());
    }

    /** @hide */
    @Override
    public Transform[] getAllTransforms() {
        List<Transform> transformList = getAllTransformsAsList();
        transformList.addAll(Arrays.asList(mEsns));

        return transformList.toArray(new Transform[transformList.size()]);
    }

    /** @hide */
    @Override
    public boolean isNegotiatedFrom(SaProposal reqProposal) {
        return super.isNegotiatedFrom(reqProposal)
                && isTransformSelectedFrom(mEsns, ((ChildSaProposal) reqProposal).mEsns);
    }

    /** @hide */
    public boolean isNegotiatedFromExceptDhGroup(SaProposal saProposal) {
        return getProtocolId() == saProposal.getProtocolId()
                && isTransformSelectedFrom(
                        getEncryptionTransforms(), saProposal.getEncryptionTransforms())
                && isTransformSelectedFrom(
                        getIntegrityTransforms(), saProposal.getIntegrityTransforms())
                && isTransformSelectedFrom(mEsns, ((ChildSaProposal) saProposal).mEsns);
    }

    /** @hide */
    public ChildSaProposal getCopyWithAdditionalDhTransform(int dhGroup) {
        return new ChildSaProposal(
                getEncryptionTransforms(),
                getIntegrityTransforms(),
                new DhGroupTransform[] {new DhGroupTransform(dhGroup)},
                getEsnTransforms());
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), Arrays.hashCode(mEsns));
    }

    @Override
    public boolean equals(Object o) {
        if (!super.equals(o) || !(o instanceof ChildSaProposal)) {
            return false;
        }

        return Arrays.equals(mEsns, ((ChildSaProposal) o).mEsns);
    }

    /**
     * This class is used to incrementally construct a ChildSaProposal. ChildSaProposal instances
     * are immutable once built.
     */
    public static final class Builder extends SaProposal.Builder {
        /**
         * Adds an encryption algorithm with a specific key length to the SA proposal being built.
         *
         * @param algorithm encryption algorithm to add to ChildSaProposal.
         * @param keyLength key length of algorithm. For algorithms that have fixed key length (e.g.
         *     3DES) only {@link SaProposal#KEY_LEN_UNUSED} is allowed.
         * @return Builder of ChildSaProposal.
         */
        // The matching getter is defined in the super class. Please see {@link
        // SaProposal#getEncryptionAlgorithms}
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder addEncryptionAlgorithm(@EncryptionAlgorithm int algorithm, int keyLength) {
            validateAndAddEncryptAlgo(algorithm, keyLength, true /* isChild */);
            return this;
        }

        /**
         * Adds an integrity algorithm to the SA proposal being built.
         *
         * @param algorithm integrity algorithm to add to ChildSaProposal.
         * @return Builder of ChildSaProposal.
         */
        // The matching getter is defined in the super class. Please see
        // {@link SaProposal#getIntegrityAlgorithms}
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder addIntegrityAlgorithm(@IntegrityAlgorithm int algorithm) {
            validateAndAddIntegrityAlgo(algorithm, true /* isChild */);
            return this;
        }

        /**
         * Adds a Diffie-Hellman Group to the SA proposal being built.
         *
         * <p>If this ChildSaProposal will be used for the first Child SA created as part of IKE
         * AUTH exchange, DH groups configured here will only apply when the Child SA is later
         * rekeyed. In this case, configuring different DH groups for IKE and Child SA may cause
         * Rekey Child to fail.
         *
         * <p>If no DH groups are supplied here, but the server requests a DH exchange during rekey,
         * the IKE SA's negotiated DH group will still be accepted.
         *
         * @param dhGroup to add to ChildSaProposal.
         * @return Builder of ChildSaProposal.
         */
        // The matching getter is defined in the super class. Please see
        // {@link SaProposal#getDhGroups}
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder addDhGroup(@DhGroup int dhGroup) {
            addDh(dhGroup);
            return this;
        }

        private IntegrityTransform[] buildIntegAlgosOrThrow() {
            // When building Child SA Proposal with normal-mode ciphers, there is no contraint on
            // integrity algorithm. When building Child SA Proposal with combined-mode ciphers,
            // mProposedIntegrityAlgos must be either empty or only have INTEGRITY_ALGORITHM_NONE.
            for (IntegrityTransform transform : mProposedIntegrityAlgos) {
                if (transform.id != INTEGRITY_ALGORITHM_NONE && mHasAead) {
                    throw new IllegalArgumentException(
                            ERROR_TAG
                                    + "Only INTEGRITY_ALGORITHM_NONE can be"
                                    + " proposed with combined-mode ciphers in any proposal.");
                }
            }

            return mProposedIntegrityAlgos.toArray(
                    new IntegrityTransform[mProposedIntegrityAlgos.size()]);
        }

        /**
         * Validates and builds the ChildSaProposal.
         *
         * @return the validated ChildSaProposal.
         */
        @NonNull
        public ChildSaProposal build() {
            EncryptionTransform[] encryptionTransforms = buildEncryptAlgosOrThrow();
            IntegrityTransform[] integrityTransforms = buildIntegAlgosOrThrow();

            return new ChildSaProposal(
                    encryptionTransforms,
                    integrityTransforms,
                    mProposedDhGroups.toArray(new DhGroupTransform[mProposedDhGroups.size()]),
                    new EsnTransform[] {new EsnTransform()});
        }
    }
}
