/*
 *     Copyright (C) 2015 ASIP Santé
 */
package fr.asipsante.mss.connectkit.connection;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStore.Builder;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.ProviderException;
import java.security.Security;
import java.security.Signature;
import java.security.UnrecoverableEntryException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.HashSet;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyStoreBuilderParameters;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link HttpsConnectionFactory} pour utiliser le module PKCS11 CPx
 * (authentification par carte CPx) sur JDK Sun.
 * 
 * @author ASIP Santé
 * 
 */
public class SunBasedPKCS11ClientAuthHttpsConnectionFactory extends
        HttpsConnectionFactory {
    private static final Logger LOGGER = LoggerFactory
            .getLogger(SunBasedPKCS11ClientAuthHttpsConnectionFactory.class);
    private static final String PROVIDER_NAME_CFG = "CPS";
    private static final int OFFSET_DIGITAL_SIGNATURE = 0;
    private static final int OFFSET_NON_REPUDIATION = 1;

    private KeyManagerFactory keyManagerFactory;


    /**
     * @param requestHeaders
     *            headers HTTP à positionner sur toutes les connexions.
     * @param truststoreURL
     * @see HttpsConnectionFactory#HttpsConnectionFactory(java.util.Set, URL,
     *      String)
     * @param trustpasswd
     * @throws SSLSecurityException
     * @see HttpsConnectionFactory#HttpsConnectionFactory(java.util.Set, URL,
     *      String)
     * 
     * */
    public SunBasedPKCS11ClientAuthHttpsConnectionFactory(URL truststoreURL,
            String trustpasswd) throws SSLSecurityException {
        super(new HashSet<RequestProperty>(), truststoreURL, trustpasswd);
        buildKeyManagerFactory();
    }

    /**
     * @return une fabrique de gestionnaire de clef basée sur le @Provider
     *         PKCS#11 du Sun.
     */
    @Override
    protected KeyManagerFactory getKeyManagerFactory() {
        return keyManagerFactory;
    }

    private void buildKeyManagerFactory() throws SSLSecurityException {
        try {
            if (keyManagerFactory == null) {
                @SuppressWarnings("restriction")
                // Volontaire: on choisit ici l'implémentation de Sun en sachant
                // qu'on se cantonnera aux JVM compatibles
                Provider pkcs11Provider = Security.getProvider("SunPKCS11-"
                        + PROVIDER_NAME_CFG);
                if (pkcs11Provider == null) {
                    final StringBuilder cardConfig = new StringBuilder();
                    cardConfig.append("name = " + PROVIDER_NAME_CFG + "\n");
                    cardConfig.append("library = " + findPkcs11Module() + "\n");

                    final InputStream is = new ByteArrayInputStream(cardConfig
                            .toString().getBytes());
                    pkcs11Provider = new sun.security.pkcs11.SunPKCS11(is);
                    Security.addProvider(pkcs11Provider);

                }

                Builder builder = Builder.newInstance("PKCS11", pkcs11Provider,
                        new PromptCallbackHandlerProtection(
                                new Pkcs11CallbackHandler()));
                KeyManagerFactory factory = KeyManagerFactory
                        .getInstance("NewSunX509");
                factory.init(new KeyStoreBuilderParameters(builder));

                KeyStore ks = builder.getKeyStore();
                Enumeration<String> aliases = ks.aliases();
                while (aliases.hasMoreElements()) {
                    String nextElement = aliases.nextElement();
                    LOGGER.debug("Alias: " + nextElement);

                    // ASIP: if non-repudiation usage, then this is a signature
                    // certificate
                    // ASIP: else this is the authentication certificate:

                    // ASIP: see
                    // ASIP-PUSC-PSCE_NP_Cryptolib-CPS-v5-Impacts-migration-CPS2Ter-CPS3_yyyyMMdd_vX.Y.Z.pdf

                    final PrivateKeyEntry currentPke = (PrivateKeyEntry) ks
                            .getEntry(nextElement, null);
                    final X509Certificate currentCert = (X509Certificate) currentPke
                            .getCertificate();
                    final boolean[] currentKeyUsages = currentCert
                            .getKeyUsage();
                    if (currentKeyUsages[OFFSET_DIGITAL_SIGNATURE] == true
                            && currentKeyUsages[OFFSET_NON_REPUDIATION] == false) {
                        // ASIP: authentication key found:
                        PrivateKey currentKey = currentPke.getPrivateKey();
                        LOGGER.debug(currentKey.toString());
                        Signature.getInstance("NONEwithRSA").initSign(
                                currentKey);
                        Signature.getInstance("MD5withRSA")
                                .initSign(currentKey);
                    }
                }
                keyManagerFactory = factory;
            }
        } catch (ProviderException | InvalidAlgorithmParameterException
                | UnrecoverableEntryException | InvalidKeyException pe) {

            throw new RuntimeException("Echec au chargement du module PKCS11",
                    pe);

        } catch (NoSuchAlgorithmException | KeyStoreException e) {

            throw new SSLSecurityException("Echec de configuration SSL", e);
        }
    }

    // get JVM architecture
    public final boolean is64bit() {
        return (System.getProperty("os.arch").indexOf("64") != -1);
    }

    //
    public final String findPkcs11Module() {

        final String sep = System.getProperty("file.separator");

        // use installed Cryptolib CPS first:
        // ASIP: google "manuel Cryptolib CPS" to find Cryptolib CPS Cryptoki
        // pathes:

        // first v5 then v4
        if (is64bit()) {
            // JVM is 64b then return 64b module
            // windows:

            final String path = System.getenv("windir") + sep + "System32"
                    + sep + "cps3_pkcs11_w64.dll";
            File f = new File(path);
            if (f.exists() && !f.isDirectory()) {
                return path;
            }
            // end windows.

            // TODO: add Mac OS X 64b
            // v5: /usr/lib/libcps3_pkcs11_osx.dylib
            // v4: /usr/lib/libcps_pkcs11_osx.dylib
            // v4 Full PC/SC: /usr/lib/libcps_pkcs11_pcsc_osx.dylib

            // no Linux 64b at this time
        } else {
        	
            // JVM is 32b then return 32b module
            // windows:
            // v5 32b:
            String path = System.getenv("windir") + sep + "SysWOW64" + sep
                    + "cps3_pkcs11_w32.dll";
            File f = new File(path);
            if (f.exists() && !f.isDirectory()) {
                // JVM is 32b then return the CPS3 32b module over x64 system
                return path;
            }
            path = System.getenv("windir") + sep + "System32" + sep
                    + "cps3_pkcs11_w32.dll";
            f = new File(path);
            if (f.exists() && !f.isDirectory()) {
                // JVM is 32b then return the CPS3 32b module over x32 system
                return path;
            }
            // v4 GALSS:
            path = System.getenv("windir") + sep + "cps_pkcs11_w32.dll";
            f = new File(path);
            if (f.exists() && !f.isDirectory()) {
                // JVM is 32b then return the CPS2Ter module over x64 system
                return path;
            }
            // v4 Full PC/SC:
            path = System.getenv("windir") + sep + "SysWOW64" + sep
                    + "cps_pkcs11_pcsc_w32.dll";
            f = new File(path);
            if (f.exists() && !f.isDirectory()) {
                // JVM is 32b then return the CPS2Ter module over x32 system
                // return path;
                return path;
            }
            path = System.getenv("windir") + sep + "System32" + sep
                    + "cps_pkcs11_pcsc_w32.dll";
            f = new File(path);
            if (f.exists() && !f.isDirectory()) {
                // JVM is 32b then return the CPS2Ter module over x32 system
                // return path;
                return path;
            }
            // end windows.

            // TODO: add Mac OS X 32b
            // v5: /usr/lib/libcps3_pkcs11_osx.dylib
            // v4: /usr/lib/libcps_pkcs11_osx.dylib
            // v4 Full PC/SC: /usr/lib/libcps_pkcs11_pcsc_osx.dylib

            // TODO: add Linux 32b:
            // v5: /usr/lib/libcps3_pkcs11_lux.so
            // v4: no v4 under linux
            // v3 GALSS: /usr/lib/libcps_pkcs11_lux.so
            // v3 Full PC/SC: /usr/lib/libcps_pkcs11_pcsc_lux.so
        }

        // ASIP: Cryptolib CPS Cryptoki not found at default pathes.
        // Second: use configured Cryptolib CPS
        // TODO: use configuration to retrieve Cryptolib CPS path

        return null;

    }

}
