/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software License
 * version 1.1, a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 */
package org.apache.avalon.cornerstone.blocks.sockets;

import com.sun.net.ssl.KeyManagerFactory;
import com.sun.net.ssl.SSLContext;
import com.sun.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.Contextualizable;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.phoenix.BlockContext;
import org.apache.avalon.framework.configuration.ConfigurationException;
import java.util.Arrays;
import javax.net.ssl.SSLSocketFactory;

/**
 * Builds SSLContexts with desired properties. Hides all the gory
 * details of SSLContext productions behind nice Avalon
 * interfaces. Married to Sun JCA implementation.
 * <p>
 * Configuration looks like:
 * <pre>
 * &lt;ssl-context&gt;
 *    &lt;keystore&gt;
 *      &lt;file&gt;conf/keystore&lt;/file&gt; &lt;!-- keystore file location relative to .sar base directory, defaults to conf/keystore --&gt;
 *      &lt;password&gt;&lt;/password&gt; &lt;!-- Key Store file password, only used to check keystore integrity --&gt;
 *      &lt;key-password&gt;&lt;/key-password&gt; &lt;!-- Only required when you need to decrypt a private key --&gt;
 *     &lt;type&gt;JKS&lt;/type&gt; &lt;!-- Key Store file format, defaults to JKS --&gt;
 *     &lt;algorithm&gt;SunX509&lt;/algorithm&gt; &lt;!-- Cryptography provider ID, defaults to SunX509 --&gt;
 *   &lt;/keystore&gt;
 *   &lt;protocol&gt;TLS&lt;/protocol&gt; &lt;!-- SSL protocol to use, defaults to TLS, another possible value is SSL --&gt;
 * &lt;/ssl-context&gt;
 * </pre>
 * </p>
 *
 * @author <a href="mailto:greg-avalon-apps at nest.cx">Greg Steuck</a>
 */
public class SSLFactoryBuilder extends AbstractLogEnabled
    implements Configurable, Contextualizable, Disposable
{
    private File m_baseDirectory;
    private File m_keystoreFile;

    private String m_keystorePassword;
    private String m_keyPassword;
    private String m_protocol;
    private String m_provider;
    private String m_keystoreFormat;

    static {
        // Registers Sun's providers
        java.security.Security.addProvider( new sun.security.provider.Sun() );
        java.security.Security.addProvider( new com.sun.net.ssl.internal.ssl.Provider() );
    }

    /**
     * Requires a BlockContext. We'll see how we end up expressing
     * these dependencies.
     */
    public void contextualize( final Context context )
    {
        final BlockContext blockContext = (BlockContext)context;
        m_baseDirectory = blockContext.getBaseDirectory();
    }

    public void configure( final Configuration configuration )
        throws ConfigurationException
    {
        final Configuration storeConfig = configuration.getChild( "keystore" );
        m_keystoreFile =
            new File ( m_baseDirectory,
                       storeConfig.getChild( "file" ).getValue( "conf/keystore" ));
        m_keystorePassword = storeConfig.getChild( "password" ).getValue( null );
        m_keyPassword = storeConfig.getChild( "key-password" ).getValue( null );
        // key is named incorrectly, left as is for compatibility
        m_provider = storeConfig.getChild( "algorithm" ).getValue( "SunX509" );
        // key is named incorrectly, left as is for compatibility
        m_keystoreFormat = storeConfig.getChild( "type" ).getValue( "JKS" );
        // ugly compatibility workaround follows
        m_protocol = configuration.getChild( "protocol" ).
            getValue( storeConfig.getChild( "protocol" ).getValue( "TLS" ) );
    }

    public SSLSocketFactory buildFactory()
        throws IOException, GeneralSecurityException
    {
        final FileInputStream keyStream = new FileInputStream( m_keystoreFile );
        final SSLContext ctx;
        try
        {
            ctx = makeContext( keyStream, m_keystorePassword,
                               m_keyPassword, m_protocol,
                               m_provider, m_keystoreFormat );
        }
        finally
        {
            try
            {
                keyStream.close();
            }
            catch ( IOException e )
            {
                // avoids hiding exceptions from makeContext
                // by catching this IOException
                getLogger().error( "Error keyStream.close failed", e );
            }
        }
        return ctx.getSocketFactory();
    }

    public void dispose()
    {
        m_keystorePassword = null;
        m_keyPassword = null;
    }

    /**
     * Creates an SSL context which uses the keys and certificates
     * provided by the given <tt>keyStream</tt>.  For simplicity the
     * same key stream (keystore) is used for both key and trust
     * factory.
     *
     * @param keyStream to read the keys from
     * @param keystorePassword password for the keystore, can be null
     *                      if integrity verification is not desired
     * @param keyPassword passphrase which unlocks the keys in the key file
     *        (should really be a char[] so that it can be cleaned after use)
     * @param protocol the standard name of the requested protocol
     * @param provider the standard name of the requested algorithm
     * @param keystoreFormat the type of keystore
     *
     * @return context configured with these keys and certificates
     * @throws IOException if files can't be read
     * @throws GeneralSecurityException is something goes wrong inside
     *                                  cryptography framework
     */
    public static SSLContext makeContext (InputStream keyStream,
                                          String keystorePassword,
                                          String keyPassword,
                                          String protocol,
                                          String provider,
                                          String keystoreFormat )
        throws IOException, GeneralSecurityException
    {
        final KeyStore keystore = loadKeystore( keyStream,
                                                keystorePassword,
                                                keystoreFormat );
        final KeyManagerFactory kmf = KeyManagerFactory.getInstance( provider );
        final char [] passChars = keyPassword.toCharArray();
        try
        {
            kmf.init( keystore, passChars );
        }
        finally
        {
            Arrays.fill( passChars, '\u0000' );
        }

        final TrustManagerFactory tmf =
            TrustManagerFactory.getInstance( provider );
        tmf.init( keystore );

        final SSLContext result = SSLContext.getInstance( protocol );
        result.init( kmf.getKeyManagers(),
                     tmf.getTrustManagers(),
                     new java.security.SecureRandom() );
        return result;
    }

    /**
     * Builds a keystore loaded from the given stream. The passphrase
     * is used to verify the keystore file integrity.
     * @param file to load from
     * @param passphrase for the store integrity verification (or null if
     *                   integrity check is not wanted)
     * @param keystoreFormat the type of keystore
     * @return loaded key store
     * @throws IOException if file can not be read
     * @throws GeneralSecurityException if key store can't be built
     */
    private static KeyStore loadKeystore( InputStream keyStream,
                                          String passphrase,
                                          String keystoreFormat )
        throws GeneralSecurityException, IOException
    {
        final KeyStore ks = KeyStore.getInstance( keystoreFormat );

        if ( passphrase != null )
        {
            final char [] passChars = passphrase.toCharArray();
            try
            {
                ks.load( keyStream, passChars );
            }
            finally
            {
                Arrays.fill( passChars, '\u0000' );
            }
        }
        else
        {
            ks.load( keyStream, null );
        }

        return ks;
    }
}
