/*-------------------------------------------------------------------------
*
* Copyright (c) 2011, PostgreSQL Global Development Group
*
*
*-------------------------------------------------------------------------
*/
package org.postgresql.ssl;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;



/**
 * Provide an implementation of SSLSocketFactory that allows client authentication.
 * 
 * This code creates and wraps a SSLSocketFactory using the paths and passwords
 * obtained from calls to abstract methods that must be defined by a subclass.
 * You cannot use this class as-is. See SysPropCertAuthFactory for one you can
 * use unmodified.
 * 
 * Subclasses <b>must</b> call buildSSLSocketFactory() at the end of their
 * constructors, and <b>must</b> call the superclass constructors at the beginning
 * of their constructors. Subclasses must also implement the abstract methods
 * so that appropriate configuration details are returned.
 * 
 * @author Marc-André Laverdière (marc-andre@atc.tcs.com / marcandre.laverdiere@tcs.com)
 */

////
//// TODO: Delegate trust checks to system trustmanager if 
////       no match in our own trustmanager?
////
//// TODO: way to override keymanager/trustmanager supply without
////      haivng to override those abstracts?

public abstract class AbstractCertAuthFactory extends WrappedFactory {

    protected final static String DEFAULT_SSL_PROTOCOL_NAME = "SSL";
	public final static String KEYSTORE_TYPE_PKCS12 = "PKCS12";
	public final static String KEYSTORE_TYPE_JKS = "JKS";
    
    protected AbstractCertAuthFactory() {
    }

    protected AbstractCertAuthFactory(String ignored) {
    }

    /**
     * Builds an SSLContext with a trust store and key store constructed
     * using the parameters provided by the subclass's implementation of 
     * AbstractCertAuthFactory's abstract methods.
     * 
     * If a null value is passed for any argument, the system default will
     * be used. For example, a null keyManagers array will cause
     * the SSLSocketFactory to use an SSLContext with a system-default KeyStore.
     * On the Sun JRE, this is created using the JSSE javax.net.ssl system 
     * properties. Similar rules apply for the TrustStore and SecureRandom instance.
     * 
     * See http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html .
     *
     * @param keyManagers KeyManager(s) to obtain key material from, or null for JSSE default keystore
     * @param trustManagers TrustManager(s) to obtain trust material from, or null for JSSE default truststore
     * @param secureRandom Secure random generator. Unless you know you need to do otherwise, leave this null to use the default.
     * 
     */
    protected void buildSSLSocketFactory(KeyManager[] keyManagers, TrustManager[] trustManagers, SecureRandom secureRandom) throws IOException, GeneralSecurityException{
        if (_factory != null) {
            throw new IllegalStateException("buildSSLSocketFactory() already called!");
        }

        //Create + Initialize TLS context
        SSLContext context = SSLContext.getInstance(getSSLProtocolName());
        context.init(keyManagers, trustManagers, secureRandom);
        _factory = context.getSocketFactory();
    }

    /**
     * Create and return KeyManager(s) using the passed information. 
     * 
     * If keyStoreType is null, the default keystore type for the system default
     * JSSE provider will be used. If the keyPass is null, the keyStorePass will
     * also be used to decode the key. No other arguments may be null.
     * 
     * @param keyStorePath Location of keystore on file system. Non-null.
     * @param keyStorePass Password/passphrase to decrypt keystore with. Non-null.
     * @param keyPass Password/passphrase to decrypt key(s) of interest within KeyStore with. If null, keyStorePass used.
     * @param keyStoreType JSSE keystore type. If null, JSSE provider default is used.
     * @return KeyStore(s) created
     * @throws IOException if the KeyStore could not be read
     * @throws GeneralSecurityException (subtypes of) for most crypto errors, passphrase errors, etc.
     */
    protected KeyManager[] createKeyManagers(String keyStorePath, char[] keyStorePass, char[] keyPass, String keyStoreType) throws IOException, GeneralSecurityException {
        //Load the Key Managers
        if (keyStoreType == null) {
            keyStoreType = KeyStore.getDefaultType();
        }
        if (keyPass == null) {
            keyPass = keyStorePass;
        }
        KeyStore ks = loadKeyStore(keyStorePath, keyStorePass);
        KeyManagerFactory managerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        managerFactory.init(ks, keyPass);
        return managerFactory.getKeyManagers();
    }

    /**
     * Create and return TrustManager(s) using the passed information.
     * 
     * If trustStoreType is null, the JSSE default keystore type is used.
     * No other arguments may be null.
     * 
     * If you want to use the Java default truststore, do not call this method
     * at all. Just pass a null TrustManager[] as the trustManagers argument to
     * buildSSLSocketFactory(...).
     * 
     * @param trustStorePath Location of trust store on file system. Non-null.
     * @param trustStorePass Password to decrypt truststore.
     * @param trustStoreType JSSE TrustStore type. If null, default JSSE keystore type is used.
     * @return TrustStore(s) created
     * @throws IOException If the TrustStore cannot be read
     * @throws GeneralSecurityException (subtypes of) for most crypto errors, password errors, etc.
     */
    protected TrustManager[] createTrustManagers(String trustStorePath, char[] trustStorePass, String trustStoreType) throws IOException, GeneralSecurityException {
        // Load the trust store
        if (trustStoreType == null) {
            trustStoreType = KeyStore.getDefaultType();
        }
        KeyStore trustKs = loadKeyStore(trustStorePath, trustStorePass);
        TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustFactory.init(trustKs);
        return trustFactory.getTrustManagers();
    }
    
    /**
     * If you wish to override SSL protocol selection, you may do so by
     * overriding this method. The defualt simply returns 
     * AbstractCertAuthFactory.DEFAULT_SSL_PROTOCOL_NAME .
     *  .
     * @return JSSE ssl/tls protocol name AbstractCertAuthFactory.DEFAULT_SSL_PROTOCOL_NAME
     */
    protected String getSSLProtocolName() {
        return DEFAULT_SSL_PROTOCOL_NAME;
    }

    
	// -------------------------------------- Internal Helper Methods
	/**
	 * Load a keystore at the path specified. It will try both JKS and PKCS12 keystores
	 * @param path the path to the keystore
	 * @param password the keystore password
	 * */
	protected static KeyStore loadKeyStore(String path, char[] password) throws IOException, GeneralSecurityException{
		if (path == null || "".equals(path)) throw new IllegalArgumentException("Path is empty or null");
		if (password == null) throw new IllegalArgumentException("Password is null");
		
		//first try with JKS
		try{
			return loadKeyStore(path, password, KEYSTORE_TYPE_JKS);
		} catch (IOException e){ //docs say that this is what is loaded if the file format is wrong
			//try loading PKCS instead
			return loadKeyStore(path, password, KEYSTORE_TYPE_PKCS12);
		}
		
	}
	
	/**
	 * Tries to open a keystore of the given type
	 * @param path the path to the keystore
	 * @param password the keystore password
	 * @param type the keystore type
	 * @return a valid keystore
	 * @throws IOException If there is any error loading the keystore
	 * @throws GeneralSecurityException if the certificates cannot be loaded or the type specified is invalid
	 */
	protected static KeyStore loadKeyStore(String path, char[] password, String type) throws IOException, GeneralSecurityException{
		FileInputStream fIn = null;
		try{
			KeyStore ks = KeyStore.getInstance(type);
			fIn = new FileInputStream(path);
			ks.load(fIn, password);
			return ks;
		} finally{
			if (fIn != null)
				fIn.close();
		}
	}

    
}
