/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 org.apache.cxf.configuration.jsse;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.CertPathTrustManagerParameters;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.common.classloader.ClassLoaderUtils;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.configuration.security.CertStoreType;
import org.apache.cxf.configuration.security.KeyManagersType;
import org.apache.cxf.configuration.security.KeyStoreType;
import org.apache.cxf.configuration.security.SecureRandomParameters;
import org.apache.cxf.configuration.security.TrustManagersType;
import org.apache.cxf.resource.ResourceManager;

/**
 * This class provides some functionality to convert the JAXB
 * generated types in the security.xsd to the items needed
 * to programatically configure the HTTPConduit and HTTPDestination
 * with TLSClientParameters and TLSServerParameters respectively.
 */
public final class TLSParameterJaxBUtils {

    private static final Logger LOG =
        LogUtils.getL7dLogger(TLSParameterJaxBUtils.class);

    private TLSParameterJaxBUtils() {
        // empty
    }
    /**
     * This method converts the JAXB generated type into a SecureRandom.
     */
    public static SecureRandom getSecureRandom(
            SecureRandomParameters secureRandomParams
    ) throws GeneralSecurityException {

        SecureRandom secureRandom = null;
        if (secureRandomParams != null) {
            String secureRandomAlg =
                secureRandomParams.getAlgorithm();
            String randomProvider =
                secureRandomParams.getProvider();
            if (randomProvider != null) {
                secureRandom = secureRandomAlg != null
                               ? SecureRandom.getInstance(
                                       secureRandomAlg,
                                       randomProvider)
                               : null;
            } else {
                secureRandom = secureRandomAlg != null
                               ? SecureRandom.getInstance(
                                       secureRandomAlg)
                               : null;
            }
        }
        return secureRandom;
    }

    public static KeyStore getKeyStore(KeyStoreType kst) throws GeneralSecurityException, IOException {
        return getKeyStore(kst, false);
    }

    /**
     * This method converts a JAXB generated KeyStoreType into a KeyStore.
     */
    public static KeyStore getKeyStore(KeyStoreType kst, boolean trustStore)
        throws GeneralSecurityException,
               IOException {

        if (kst == null) {
            return null;
        }
        final String type;
        if (trustStore) {
            type = SSLUtils.getTrustStoreType(kst.isSetType()
                                     ? kst.getType() : null, LOG, KeyStore.getDefaultType());
        } else {
            type = SSLUtils.getKeystoreType(kst.isSetType()
                                 ? kst.getType() : null, LOG, KeyStore.getDefaultType());
        }

        char[] password = kst.isSetPassword()
                    ? deobfuscate(kst.getPassword())
                    : null;
        if (password == null) {
            final String tmp;
            if (trustStore) {
                tmp = SSLUtils.getTruststorePassword(null, LOG);
            } else {
                tmp = SSLUtils.getKeystorePassword(null, LOG);
            }
            if (tmp != null) {
                password = tmp.toCharArray();
            }
        }
        final String provider;
        if (trustStore) {
            provider = SSLUtils.getTruststoreProvider(kst.isSetProvider() ? kst.getProvider() : null, LOG);
        } else {
            provider = SSLUtils.getKeystoreProvider(kst.isSetProvider() ? kst.getProvider() : null, LOG);
        }
        KeyStore keyStore = provider == null
                    ? KeyStore.getInstance(type)
                    : KeyStore.getInstance(type, provider);

        if (kst.isSetFile()) {
            try (InputStream kstInputStream = Files.newInputStream(Paths.get(kst.getFile()))) {
                keyStore.load(kstInputStream, password);
            }
        } else if (kst.isSetResource()) {
            final InputStream is = getResourceAsStream(kst.getResource());
            if (is == null) {
                final String msg =
                    "Could not load keystore resource " + kst.getResource();
                LOG.severe(msg);
                throw new IOException(msg);
            }
            keyStore.load(is, password);
        } else if (kst.isSetUrl()) {
            keyStore.load(new URL(kst.getUrl()).openStream(), password);
        } else {
            final String loc;
            if (trustStore) {
                loc = SSLUtils.getTruststore(null, LOG);
            } else {
                loc = SSLUtils.getKeystore(null, LOG);
            }
            if (loc != null) {
                try (InputStream ins = Files.newInputStream(Paths.get(loc))) {
                    keyStore.load(ins, password);
                } catch (NoSuchFileException ex) {
                    // Fall back to load the location as a stream
                    try (InputStream ins = getResourceAsStream(loc)) {
                        keyStore.load(ins, password);
                    }
                }
            }
        }
        return keyStore;
    }

    /**
     * This method converts a JAXB generated CertStoreType into a KeyStore.
     */
    public static KeyStore getKeyStore(final CertStoreType pst)
        throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException {

        if (pst == null) {
            return null;
        }
        String type;
        if (pst.isSetType()) {
            type = pst.getType();
        } else {
            type = KeyStore.getDefaultType();
        }
        if (pst.isSetFile()) {
            InputStream is = Files.newInputStream(Paths.get(pst.getFile()));
            return createTrustStore(is, type);
        }
        if (pst.isSetResource()) {
            final InputStream is = getResourceAsStream(pst.getResource());
            if (is == null) {
                final String msg =
                    "Could not load truststore resource " + pst.getResource();
                LOG.severe(msg);
                throw new IOException(msg);
            }
            return createTrustStore(is, type);
        }
        if (pst.isSetUrl()) {
            return createTrustStore(new URL(pst.getUrl()).openStream(), type);
        }
        throw new IllegalArgumentException("Could not create KeyStore based on information in CertStoreType");
    }

    private static InputStream getResourceAsStream(String resource) {
        InputStream is = ClassLoaderUtils.getResourceAsStream(resource, TLSParameterJaxBUtils.class);
        if (is == null) {
            Bus bus = BusFactory.getThreadDefaultBus(true);
            ResourceManager rm = bus.getExtension(ResourceManager.class);
            if (rm != null) {
                is = rm.getResourceAsStream(resource);
            }
        }
        return is;
    }

    /**
     * Create a KeyStore containing the trusted CA certificates contained
     * in the supplied input stream.
     */
    private static KeyStore createTrustStore(final InputStream is, String type)
        throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException {

        final Collection<? extends Certificate> certs = loadCertificates(is);
        final KeyStore keyStore =
            KeyStore.getInstance(type);
        keyStore.load(null, null);
        for (Certificate cert : certs) {
            final X509Certificate xcert = (X509Certificate) cert;
            keyStore.setCertificateEntry(
                xcert.getSubjectX500Principal().getName(),
                cert
            );
        }
        return keyStore;
    }

    /**
     * load the certificates as X.509 certificates
     */
    private static Collection<? extends Certificate> loadCertificates(final InputStream is)
        throws IOException, CertificateException {

        final CertificateFactory factory = CertificateFactory.getInstance("X.509");
        return factory.generateCertificates(is);
    }

    private static char[] deobfuscate(String s) {
        // From the Jetty org.eclipse.jetty.http.security.Password class
        if (!s.startsWith("OBF:")) {
            return s.toCharArray();
        }
        s = s.substring(4);

        char[] b = new char[s.length() / 2];
        int l = 0;
        for (int i = 0; i < s.length(); i += 4) {
            String x = s.substring(i, i + 4);
            int i0 = Integer.parseInt(x, 36);
            int i1 = i0 / 256;
            int i2 = i0 % 256;
            b[l++] = (char) ((i1 + i2 - 254) / 2);
        }

        return new String(b, 0, l).toCharArray();
    }

    /**
     * This method converts the JAXB KeyManagersType into a list of
     * JSSE KeyManagers.
     */
    public static KeyManager[] getKeyManagers(KeyManagersType kmc)
        throws GeneralSecurityException,
               IOException {

        KeyStore keyStore = getKeyStore(kmc.getKeyStore(), false);

        String alg = kmc.isSetFactoryAlgorithm()
                     ? kmc.getFactoryAlgorithm()
                     : KeyManagerFactory.getDefaultAlgorithm();

        char[] keyPass = getKeyPassword(kmc);

        KeyManagerFactory fac =
                     kmc.isSetProvider()
                     ? KeyManagerFactory.getInstance(alg, kmc.getProvider())
                     : KeyManagerFactory.getInstance(alg);

        fac.init(keyStore, keyPass);

        return fac.getKeyManagers();
    }

    /**
     * This method converts the JAXB KeyManagersType into a list of
     * JSSE KeyManagers.
     */
    public static KeyManager[] getKeyManagers(KeyManagersType kmc, String alias)
        throws GeneralSecurityException,
               IOException {

        KeyStore keyStore = getKeyStore(kmc.getKeyStore(), false);

        String alg = kmc.isSetFactoryAlgorithm()
                     ? kmc.getFactoryAlgorithm()
                     : KeyManagerFactory.getDefaultAlgorithm();

        char[] keyPass = getKeyPassword(kmc);

        KeyManagerFactory fac =
                     kmc.isSetProvider()
                     ? KeyManagerFactory.getInstance(alg, kmc.getProvider())
                     : KeyManagerFactory.getInstance(alg);

        try {
            fac.init(keyStore, keyPass);

            return fac.getKeyManagers();
        } catch (java.security.UnrecoverableKeyException uke) {
            //jsse has the restriction that different key in keystore
            //cannot has different password, use MultiKeyPasswordKeyManager
            //as fallback when this happen
            MultiKeyPasswordKeyManager manager
                = new MultiKeyPasswordKeyManager(keyStore, alias,
                                             new String(keyPass));
            return new KeyManager[]{manager};
        }
    }

    private static char[] getKeyPassword(KeyManagersType kmc) {
        char[] keyPass = kmc.isSetKeyPassword()
            ? deobfuscate(kmc.getKeyPassword())
            : null;

        if (keyPass != null) {
            return keyPass;
        }

        String callbackHandlerClass = kmc.getKeyPasswordCallbackHandler();
        if (callbackHandlerClass == null) {
            return null;
        }
        try {
            final CallbackHandler ch = (CallbackHandler) ClassLoaderUtils
                .loadClass(callbackHandlerClass, TLSParameterJaxBUtils.class).getDeclaredConstructor().newInstance();
            String prompt = kmc.getKeyStore().getFile();
            if (prompt == null) {
                prompt = kmc.getKeyStore().getResource();
            }
            PasswordCallback pwCb = new PasswordCallback(prompt, false);
            PasswordCallback[] callbacks = new PasswordCallback[] {pwCb};
            ch.handle(callbacks);
            keyPass = callbacks[0].getPassword();
        } catch (Exception e) {
            LOG.log(Level.WARNING,
                    "Cannot load key password from callback handler: " + e.getMessage(), e);
        }
        return keyPass;
    }

    /**
     * This method converts the JAXB TrustManagersType into a list of
     * JSSE TrustManagers.
     */
    @Deprecated
    public static TrustManager[] getTrustManagers(TrustManagersType tmc)
        throws GeneralSecurityException,
               IOException {
        return getTrustManagers(tmc, false);
    }

    public static TrustManager[] getTrustManagers(TrustManagersType tmc, boolean enableRevocation)
        throws GeneralSecurityException,
               IOException {

        final KeyStore keyStore =
            tmc.isSetKeyStore()
                ? getKeyStore(tmc.getKeyStore(), true)
                : (tmc.isSetCertStore()
                    ? getKeyStore(tmc.getCertStore())
                    : null);

        String alg = tmc.isSetFactoryAlgorithm()
                     ? tmc.getFactoryAlgorithm()
                     : TrustManagerFactory.getDefaultAlgorithm();

        TrustManagerFactory fac =
                     tmc.isSetProvider()
                     ? TrustManagerFactory.getInstance(alg, tmc.getProvider())
                     : TrustManagerFactory.getInstance(alg);

        if (enableRevocation) {
            PKIXBuilderParameters param = new PKIXBuilderParameters(keyStore, new X509CertSelector());
            param.setRevocationEnabled(true);

            fac.init(new CertPathTrustManagerParameters(param));
        } else {
            fac.init(keyStore);
        }

        return fac.getTrustManagers();
    }
}
