// --- BEGIN COPYRIGHT BLOCK ---
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// (C) 2007 Red Hat, Inc.
// All rights reserved.
// --- END COPYRIGHT BLOCK ---
package com.netscape.cms.profile.constraint;

import java.math.BigInteger;
import java.security.interfaces.DSAParams;
import java.util.Locale;
import java.util.Vector;

import org.dogtagpki.server.ca.CAEngine;
import org.dogtagpki.server.ca.CAEngineConfig;
import org.mozilla.jss.netscape.security.provider.DSAPublicKey;
import org.mozilla.jss.netscape.security.provider.RSAPublicKey;
import org.mozilla.jss.netscape.security.x509.AlgorithmId;
import org.mozilla.jss.netscape.security.x509.CertificateX509Key;
import org.mozilla.jss.netscape.security.x509.X509CertInfo;
import org.mozilla.jss.netscape.security.x509.X509Key;

import com.netscape.certsrv.base.IConfigStore;
import com.netscape.certsrv.profile.EProfileException;
import com.netscape.certsrv.profile.ERejectException;
import com.netscape.certsrv.property.Descriptor;
import com.netscape.certsrv.property.EPropertyException;
import com.netscape.certsrv.property.IDescriptor;
import com.netscape.certsrv.request.IRequest;
import com.netscape.cms.profile.def.NoDefault;
import com.netscape.cms.profile.def.PolicyDefault;
import com.netscape.cms.profile.def.UserKeyDefault;
import com.netscape.cmscore.apps.CMS;
import com.netscape.cmsutil.crypto.CryptoUtil;

/**
 * This constraint is to check the key type and
 * key length.
 *
 * @version $Revision$, $Date$
 */

public class KeyConstraint extends EnrollConstraint {

    public static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(KeyConstraint.class);

    public static final String CONFIG_KEY_TYPE = "keyType"; // (EC, RSA)
    public static final String CONFIG_KEY_PARAMETERS = "keyParameters";

    private static String[] cfgECCurves = null;
    private static String keyType = "";
    private static String keyParams = "";

    public KeyConstraint() {
        super();
        addConfigName(CONFIG_KEY_TYPE);
        addConfigName(CONFIG_KEY_PARAMETERS);
    }

    @Override
    public void init(IConfigStore config) throws EProfileException {
        super.init(config);

        CAEngine engine = CAEngine.getInstance();
        CAEngineConfig cs = engine.getConfig();

        String ecNames = "";
        try {
            ecNames = cs.getString("keys.ecc.curve.list");
        } catch (Exception e) {
        }

        logger.debug("KeyConstraint.init ecNames: " + ecNames);
        if (ecNames != null && ecNames.length() != 0) {
            cfgECCurves = ecNames.split(",");
        }
    }

    @Override
    public IDescriptor getConfigDescriptor(Locale locale, String name) {
        if (name.equals(CONFIG_KEY_TYPE)) {
            return new Descriptor(IDescriptor.CHOICE, "-,RSA,EC",
                    "RSA",
                    CMS.getUserMessage(locale, "CMS_PROFILE_KEY_TYPE"));
        } else if (name.equals(CONFIG_KEY_PARAMETERS)) {
            return new Descriptor(IDescriptor.STRING, null, "",
                    CMS.getUserMessage(locale, "CMS_PROFILE_KEY_PARAMETERS"));
        }

        return null;
    }

    /**
     * Validates the request. The request is not modified
     * during the validation.
     */
    @Override
    public void validate(IRequest request, X509CertInfo info)
            throws ERejectException {
        try {
            CertificateX509Key infokey = (CertificateX509Key) info.get(X509CertInfo.KEY);
            X509Key key = (X509Key) infokey.get(CertificateX509Key.KEY);
            String alg = key.getAlgorithmId().getName().toUpperCase();
            logger.info("KeyConstraint: Key algorithnm: " + alg);

            String value = getConfig(CONFIG_KEY_TYPE);
            logger.info("KeyConstraint: Key type: " + value);

            String keyType = value;

            if (!isOptional(value)) {
                if (!alg.equals(value)) {
                    logger.error("Invalid key type: " + value);
                    throw new ERejectException(
                            CMS.getUserMessage(
                                    getLocale(request),
                                    "CMS_PROFILE_KEY_TYPE_NOT_MATCHED",
                                    value));
                }
            }

            int keySize = 0;

            if (alg.equals("RSA")) {
                keySize = getRSAKeyLen(key);
            } else if (alg.equals("DSA")) {
                keySize = getDSAKeyLen(key);
            } else if (alg.equals("EC")) {
                //EC key case.
            } else {
                throw new ERejectException(
                        CMS.getUserMessage(
                                getLocale(request),
                                "CMS_PROFILE_INVALID_KEY_TYPE",
                                alg));
            }

            value = getConfig(CONFIG_KEY_PARAMETERS);

            String[] keyParams = value.split(",");

            if (alg.equals("EC")) {
                if (!alg.equals(keyType) && !isOptional(keyType)) {
                    logger.error("Invalid key type: " + keyType);
                    throw new ERejectException(
                            CMS.getUserMessage(
                                    getLocale(request),
                                    "CMS_PROFILE_KEY_PARAMS_NOT_MATCHED",
                                    value));
                }

                Vector<?> vect = CryptoUtil.getECKeyCurve(key);

                boolean curveFound = false;

                if (vect != null) {
                    logger.debug("vect: " + vect);

                    if (!isOptional(keyType)) {
                        //Check the curve parameters only if explicit ECC or not optional
                        for (int i = 0; i < keyParams.length; i++) {
                            String ecParam = keyParams[i];
                            logger.debug("keyParams[i]: " + i + " param: " + ecParam);
                            if (vect.contains(ecParam)) {
                                curveFound = true;
                                logger.debug("KeyConstraint.validate: EC key constrainst passed.");
                                break;
                            }
                        }
                    } else {
                        curveFound = true;
                    }
                }

                if (!curveFound) {
                    String message = CMS.getUserMessage(
                            getLocale(request),
                            "CMS_PROFILE_KEY_PARAMS_NOT_MATCHED",
                            value);
                    logger.error("KeyConstraint.validate: EC key constrainst failed: " + message);
                    throw new ERejectException(message);
                }

            } else {
                if (!arrayContainsString(keyParams, Integer.toString(keySize))) {
                    throw new ERejectException(
                            CMS.getUserMessage(
                                    getLocale(request),
                                    "CMS_PROFILE_KEY_PARAMS_NOT_MATCHED",
                                    value));
                }
                logger.debug("KeyConstraint.validate: RSA key contraints passed.");
            }
        } catch (Exception e) {
            if (e instanceof ERejectException) {
                throw (ERejectException) e;
            }
            logger.error("KeyConstraint: " + e.getMessage(), e);
            throw new ERejectException(CMS.getUserMessage(
                        getLocale(request), "CMS_PROFILE_KEY_NOT_FOUND"));
        }
    }

    public int getRSAKeyLen(X509Key key) throws Exception {
        X509Key newkey = null;

        try {
            newkey = new X509Key(AlgorithmId.get("RSA"),
                        key.getKey());
        } catch (Exception e) {
            logger.error("KeyConstraint: getRSAKey Len " + e.getMessage(), e);
            return -1;
        }
        RSAPublicKey rsaKey = new RSAPublicKey(newkey.getEncoded());

        return rsaKey.getKeySize();
    }

    public int getDSAKeyLen(X509Key key) throws Exception {
        // Check DSAKey parameters.
        // size refers to the p parameter.
        DSAPublicKey dsaKey = new DSAPublicKey(key.getEncoded());
        DSAParams keyParams = dsaKey.getParams();
        BigInteger p = keyParams.getP();
        int len = p.bitLength();

        return len;
    }

    @Override
    public String getText(Locale locale) {
        String params[] = {
                getConfig(CONFIG_KEY_TYPE),
                getConfig(CONFIG_KEY_PARAMETERS)
            };

        return CMS.getUserMessage(locale,
                "CMS_PROFILE_CONSTRAINT_KEY_TEXT", params);
    }

    @Override
    public boolean isApplicable(PolicyDefault def) {
        if (def instanceof NoDefault)
            return true;
        if (def instanceof UserKeyDefault)
            return true;
        return false;
    }

    @Override
    public void setConfig(String name, String value)
            throws EPropertyException {

        logger.info("KeyConstraint: Setting " + name + ": " + value);

        //establish keyType, we don't know which order these params will arrive
        if (name.equals(CONFIG_KEY_TYPE)) {
            keyType = value;
            if (keyParams.equals(""))
                return;
        }

        //establish keyParams
        if (name.equals(CONFIG_KEY_PARAMETERS)) {
            keyParams = value;

            if (keyType.equals(""))
                return;
        }

        // All the params we need for validation have been collected,
        // we don't know which order they will show up
        if (keyType.length() > 0 && keyParams.length() > 0) {
            String[] params = keyParams.split(",");
            boolean isECCurve = false;
            int keySize = 0;

            if (keyType.equals("EC")) {

                for (String param : params) {
                    logger.info("KeyConstraint: EC curve: " + param);

                    if (cfgECCurves == null) {
                        //Use the static array as a backup if the config values are not present.
                        isECCurve = arrayContainsString(CryptoUtil.getECcurves(), param);
                    } else {
                        isECCurve = arrayContainsString(cfgECCurves, param);
                    }

                    if (!isECCurve) {
                        logger.error("Invalid EC curve: " + param);
                        keyType = "";
                        keyParams = "";
                        throw new EPropertyException(CMS.getUserMessage("CMS_INVALID_PROPERTY", name));
                    }
                }

            } else {

                for (String param : params) {
                    try {
                        logger.info("KeyConstraint: RSA key size: " + param);
                        keySize = Integer.parseInt(param);

                    } catch (NumberFormatException e) {
                        if (isOptional(keyType)) {
                            logger.info("KeyConstraint: EC curve: " + param);
                            isECCurve = arrayContainsString(CryptoUtil.getECcurves(), param);
                        }
                        keySize = 0;
                    }

                    if (keySize <= 0 && !isECCurve) {
                        logger.error("Invalid EC curve: " + param);
                        keyType = "";
                        keyParams = "";
                        throw new EPropertyException(CMS.getUserMessage("CMS_INVALID_PROPERTY", name));
                    }
                }
            }
        }

        //Actually set the configuration in the profile
        super.setConfig(CONFIG_KEY_TYPE, keyType);
        super.setConfig(CONFIG_KEY_PARAMETERS, keyParams);

        //Reset the vars for next round.
        keyType = "";
        keyParams = "";
    }

    private boolean arrayContainsString(String[] array, String value) {

        if (array == null || value == null) {
            return false;
        }

        for (int i = 0; i < array.length; i++) {
            if (array[i].equals(value)) {
                return true;
            }
        }

        return false;
    }
}
