/////////////////////////////////////////////////////////////////////////
//
// © University of Southampton IT Innovation Centre, 2004
//
// Copyright in this library belongs to the IT Innovation Centre of
// 2 Venture Road, Chilworth Science Park, Southampton, SO16 7NP, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
//      Created by:             Darren Marvin
//      Created date:           2004/04/30
//      Created for project:    GEMSS
//
/////////////////////////////////////////////////////////////////////////
//
//      Dependencies: None
//
/////////////////////////////////////////////////////////////////////////
//
//      Last commit info:       $Author: $
//                              $Date: $
//                              $Revision: $
//
/////////////////////////////////////////////////////////////////////////

package uk.ac.soton.itinnovation.gemss.security.context.token.providers.javakeystore;

import java.util.logging.Level;
import java.util.logging.Logger;


import eu.gemss.components.security.token.TokenDescriptor;
import eu.gemss.components.security.token.SecurityToken;
import eu.gemss.components.security.token.types.UnsupportedTokenException;
import eu.gemss.components.security.token.types.key.asymmetric.AsymmetricKeyDescriptor;
import eu.gemss.components.security.token.types.pki.CertificateDescriptor;
import eu.gemss.signals.SignalHandler;
import eu.gemss.components.security.InvalidCredentialException;
import eu.gemss.components.security.InvalidPasswordException;
import eu.gemss.components.security.SecurityConfigurationException;
import eu.gemss.components.security.token.provision.TokenProviderException;
import eu.gemss.components.security.token.provision.SecurityTokenProvider;
import eu.gemss.components.security.SecurityProviderConfiguration;
import uk.ac.soton.itinnovation.gemss.security.context.token.types.PrivateKeyTokenImpl;
import uk.ac.soton.itinnovation.gemss.security.context.token.types.PublicKeyTokenImpl;
import uk.ac.soton.itinnovation.gemss.security.context.token.types.X509CertificateTokenImpl;
import eu.gemss.components.security.token.trust.SecurityTrustProvider;
import eu.gemss.components.security.token.SecurityTokenException;
import eu.gemss.components.security.token.trust.TrustProviderException;
import eu.gemss.components.security.token.types.pki.X509CertificateChainToken;
import eu.gemss.components.security.token.types.pki.X509CertificateChainDescriptor;
import uk.ac.soton.itinnovation.gemss.signal.SignalException;
import uk.ac.soton.itinnovation.gemss.security.context.token.types.X509CertificateChainTokenImpl;
import uk.ac.soton.itinnovation.gemss.utils.configuration.ConfigurationException;
import uk.ac.soton.itinnovation.gemss.security.context.*;


/**
 * JavaKeystoreProvider provides security tokens and trust verification against a keystore type of
 * security context
 */
public class JavaKeystoreProvider implements SecurityTokenProvider, SecurityTrustProvider {

    private static String KEYSTORE_PATH_PROPERTY_NAME = "gemss.security.context.javakeystore.keystorepath";
    private static String KEYSTORE_PASSWORD_PROPERTY_NAME = "gemss.security.context.javakeystore.keystorepass";
    private static String PRIVATE_KEY_ALIAS_PROPERTY_NAME = "gemss.security.context.javakeystore.privatekeyalias";
    private static String PRIVATE_KEY_PASSWORD_PROPERTY_NAME = "gemss.security.context.javakeystore.privatekeypass";
    private static String REVOCATION_LIST_PROPERTY_NAME = "gemss.security.context.javakeystore.crlfile";

    private static final long SLEEP_TIME = 1000;
    private static final long WAIT_TIME = 360000;//wait for 3 minutes for authentication
    private static KeystoreManager mKeystoreManager;
    private static SignalHandler mSignalHandler;
    private static TokenDescriptor[] mDescriptors;
    private static TokenDescriptor[] mTrustDescriptors;
    private static AsymmetricKeyDescriptor mPrivateKeyTokenDescriptor;
    private static AsymmetricKeyDescriptor mPublicKeyTokenDescriptor;
    private static CertificateDescriptor mCertificateTokenDescriptor;
    private static X509CertificateChainDescriptor mX509CertificateChainTokenDescriptorForAcceptedIssuers;
    private static X509CertificateChainDescriptor mX509CertificateChainTokenDescriptorForClient;
    private static X509CertificateChainDescriptor mTrustedX509CertificateChainTokenDescriptor;
    private static Logger mLogger = Logger.getLogger("uk.ac.soton.itinnovation.gemss.security.context.token.providers.javakeystore.JavaKeystoreTokenProvider");
    private static SecurityProviderConfiguration mTrustConfiguration;
    private static SecurityProviderConfiguration mTokenConfiguration;
    private static final int ATTEMPTS = 3;

    private String KEYSTORE_PASS;
    private String KEYSTORE_PATH;
    private String PRIVATE_KEY_ALIAS;
    private String PRIVATE_KEY_PASS;

    /**
     * public so that it can be loaded
     */
    public JavaKeystoreProvider() {}

       /**
        * Sets the signal channel where signals should be sent for forward to
        * the application or some other handler.
        * @param channel destination for signals
        */
    public void setSignalHandler(SignalHandler handler) {
           mSignalHandler = handler;
       }

    /**
     * Retrieve a list of all the tokens supported by the provider
     * @return list of token descriptors
     */
    public TokenDescriptor[] getSupportedTokenList() {
        if(mDescriptors == null)
            this.initialiseTokenDescriptorList();
        return mDescriptors;
    }

    /**
     * Generate an instance of the token of a particular type described
     * by the token descriptor
     * @param tokenDesc descriptor for required token
     * @return instance of the required security token
     */
    public SecurityToken generateSecurityToken(TokenDescriptor tokenDesc) throws UnsupportedTokenException, TokenProviderException, InvalidCredentialException {
        try{

            SecurityToken token = null;
            if(mDescriptors == null)
                this.initialiseTokenDescriptorList();
            //check that required token is supported, don't want to create keystore manager needlessly
            if(!mPrivateKeyTokenDescriptor.isEqual(tokenDesc) && !mPublicKeyTokenDescriptor.isEqual(tokenDesc) && !mCertificateTokenDescriptor.isEqual(tokenDesc)
               &&!this.mX509CertificateChainTokenDescriptorForAcceptedIssuers.isEqual(tokenDesc)
               &&!this.mX509CertificateChainTokenDescriptorForClient.isEqual(tokenDesc) ) {
                mLogger.log(Level.WARNING,"Requested security token '" + tokenDesc.getDescription() + "' is not supported.");
                throw new UnsupportedTokenException("Requested security token '" + tokenDesc.getDescription() + "' is not supported.");
            }
            //Keystore manager might not be initialised yet
            if(mKeystoreManager==null)
                createKeystoreManager();
            if(mPrivateKeyTokenDescriptor.isEqual(tokenDesc)) {
                //a private key token has been requested
                token = new PrivateKeyTokenImpl(mKeystoreManager.getPrivateKey());
            }
            else if(mPublicKeyTokenDescriptor.isEqual(tokenDesc)) {
                //a public key token has been requested
                token = new PublicKeyTokenImpl(mKeystoreManager.getPublicKey());
            }
            else if(mCertificateTokenDescriptor.isEqual(tokenDesc)) {
                //a certificate tokne has been requested
                token = new X509CertificateTokenImpl(mKeystoreManager.getX509Certificate());
            }
            else if(this.mX509CertificateChainTokenDescriptorForAcceptedIssuers.isEqual(tokenDesc)) {
                token = new X509CertificateChainTokenImpl(mKeystoreManager.getX509CertificateChain(),X509CertificateChainTokenImpl.AUTH_TYPE_RSA,true);//means only accepted issuer certificate chains are provided
            }
            else if(this.mX509CertificateChainTokenDescriptorForClient.isEqual(tokenDesc)) {
                token = new X509CertificateChainTokenImpl(mKeystoreManager.getX509CertificateChain(),X509CertificateChainTokenImpl.AUTH_TYPE_RSA,true);//means only accepted issuer certificate chains are provided
            }
            if(token==null) {
                mLogger.log(Level.SEVERE,"Unrecognised failure occured whilst obtaining security token described as '" + tokenDesc.getDescription() + "', please send a bug report including all log files.");
                throw new TokenProviderException("Unrecognised failure occured whilst obtaining security token described as '" + tokenDesc.getDescription() + "', please send a bug report including all log files.");
            }

            return token;
        }
        catch(UnsupportedTokenException ex) {
            throw (UnsupportedTokenException) ex;
        }
        catch(TokenProviderException ex) {
            throw (TokenProviderException) ex;
        }
        catch(Exception ex) {
            mLogger.log(Level.SEVERE,"Unrecognised failure occured whilst obtaining security token described as '" + tokenDesc.getDescription() + "', please send a bug report including all log files.",ex);
            throw new TokenProviderException("Unrecognised failure occured whilst obtaining security token described as '" + tokenDesc.getDescription() + "', please send a bug report including all log files.");
        }
    }

    /**
     * Retrieve a list of all the tokens supported by the provider
     * @return list of token descriptors
     */
    public TokenDescriptor[] getSupportedTokenTrustList() {
        if(mTrustDescriptors==null) {
            initialiseTrustTokenDescriptorList();
        }
        return mTrustDescriptors;
    }

    /**
     * Test if the supplied security token is trusted
     * @param secToken security token
     * @return true if trusted, false otherwise.
     */
    public boolean isTrusted(SecurityToken secToken) throws TrustProviderException {
        try{
            if(mTrustDescriptors==null)
                initialiseTrustTokenDescriptorList();
            //check if supported
            if(!mTrustedX509CertificateChainTokenDescriptor.isEqual(secToken.getTokenDescriptor())) {
                mLogger.log(Level.SEVERE,"The supplied security token described as '" + secToken.getTokenDescriptor().getDescription() + "' is not supported by provider '" + this.getClass().getName());
                throw new TrustProviderException("The supplied security token described as '" + secToken.getTokenDescriptor().getDescription() + "' is not supported by provider '" + this.getClass().getName());
            }
            //the security token is a X509CertificateChain
            X509CertificateChainToken certToken = (X509CertificateChainToken) secToken;

            //Keystore manager might not be initialised yet
            if(mKeystoreManager==null)
                createTruststoreManager();
            //might not have set a crl
            tryToSetCRLFile();

            return mKeystoreManager.isTrusted(certToken.getX509CertificateChain(),certToken.getAuthType());

        }
        catch(KeyManagerException ex) {
            throw new TrustProviderException(ex.getMessage());
        }
        catch(SecurityTokenException ex) {
            throw new TrustProviderException(ex.getMessage());
        }
    }

    /**
     * Set the security token provider configuration
     * @param configuration
     */
    public void setTokenProviderConfiguration(SecurityProviderConfiguration configuration) {
        mTokenConfiguration = configuration;
    }

    /**
     * Set the security token provider configuration
     * @param configuration
     */
    public void setTrustProviderConfiguration(SecurityProviderConfiguration configuration) {
        mTrustConfiguration = configuration;
    }

    private CredentialRequest createCredentialRequest() {
        //constructs a suitable credential request signal for retrieving the keystore credentials to use
        return new CredentialRequest(this);
    }

    private void createTruststoreManager() throws KeyManagerException {
        //don't actually need to have any passwords to check trust just need trust store and crl, I will
        //have to come back to this later
        try{
            int attempts = ATTEMPTS;
            while(mKeystoreManager==null && attempts>=0) {
                try{
                    String keystorePath = null;
                    String privateKeyAlias = null;
                    String privateKeyPass = null;
                    String keystorePass = null;
                    //first attempt to retrieve each keystore configuration from the
                    //configuration file, all should be there except passwords and aliases

                    keystorePath = mTrustConfiguration.getConfigurationValue(this.KEYSTORE_PATH_PROPERTY_NAME);
                    if(keystorePath==null || keystorePath.equals("")) {
                        mLogger.log(Level.SEVERE,"Cannot initialise keystore because configuration does not include path to keystore and CRL file");
                    }
                    //if cannot retrieve any of the alias or passwords from the configuration then signal user to get them
                    try{
                        privateKeyAlias = mTrustConfiguration.getConfigurationValue(this.PRIVATE_KEY_ALIAS_PROPERTY_NAME);
                        privateKeyPass = mTrustConfiguration.getConfigurationValue(this.PRIVATE_KEY_PASSWORD_PROPERTY_NAME);
                        keystorePass = mTrustConfiguration.getConfigurationValue(this.KEYSTORE_PASSWORD_PROPERTY_NAME);
                    }

                    catch(SecurityConfigurationException e) {
                        //signal for credentials
                        CredentialRequest credentialRequest = signalForCredentials();
                        //set the values
                        privateKeyAlias = credentialRequest.getPrivateKeyAlias();
                        privateKeyPass = credentialRequest.getPrivateKeyPassword();
                        keystorePass = credentialRequest.getKeystorePassword();
                    }

                    //have the credentials that require
                    if(keystorePath!=null && privateKeyAlias!=null && privateKeyPass!=null && keystorePass!=null) {
                        mKeystoreManager = new KeystoreManager(new KeystoreCredentials(keystorePath,
                                keystorePass,privateKeyAlias,privateKeyPass));

                    }
                    else {
                        mLogger.log(Level.SEVERE,"Unable to initialise keystore security context");
                        throw new KeyManagerException("Unable to initialise keystore security context");
                    }
                }
                catch(SignalException ex) {

                    attempts--;
                    if(attempts<0) {
                        throw new KeyManagerException("Unable to obtain the request credentials from signal handlers");
                    }
                    //else try again
                }
                catch(KeyManagerException ex) {
                    mLogger.log(Level.WARNING,ex.getMessage(),ex);
                    attempts--;
                    if(attempts<0) {
                        throw new KeyManagerException("Unable to obtain the request credentials from signal handlers");
                    }
                }

            }


        }
        catch(SecurityConfigurationException ex) {
            mLogger.log(Level.SEVERE,"Unable to initialise truststore from configuration '" + ex.getMessage() + "'",ex);
            throw new KeyManagerException("Unable to initialise security context");
        }
    }

    private void createKeystoreManager() throws KeyManagerException {
        try{
            int attempts = ATTEMPTS;
            while(mKeystoreManager==null && attempts>=0) {
                try{
                    String keystorePath = null;
                    String privateKeyAlias = null;
                    String privateKeyPass = null;
                    String keystorePass = null;
                    //first attempt to retrieve each keystore configuration from the
                    //configuration file, all should be there except passwords and aliases

                    keystorePath = mTokenConfiguration.getConfigurationValue(this.KEYSTORE_PATH_PROPERTY_NAME);
                    if(keystorePath==null || keystorePath.equals("")) {
                        mLogger.log(Level.SEVERE,"Cannot initialise keystore because configuration does not include path to keystore and CRL file");
                    }
                    //if cannot retrieve any of the alias or passwords from the configuration then signal user to get them
                    try{
                        privateKeyAlias = mTokenConfiguration.getConfigurationValue(this.PRIVATE_KEY_ALIAS_PROPERTY_NAME);
                        privateKeyPass = mTokenConfiguration.getConfigurationValue(this.PRIVATE_KEY_PASSWORD_PROPERTY_NAME);
                        keystorePass = mTokenConfiguration.getConfigurationValue(this.KEYSTORE_PASSWORD_PROPERTY_NAME);
                    }

                    catch(SecurityConfigurationException e) {
                        //signal for credentials
                        CredentialRequest credentialRequest = signalForCredentials();

                        //set the values
                        privateKeyAlias = credentialRequest.getPrivateKeyAlias();
                        privateKeyPass = credentialRequest.getPrivateKeyPassword();
                        keystorePass = credentialRequest.getKeystorePassword();
                    }

                    //have the credentials that require
                    if(keystorePath!=null && privateKeyAlias!=null && privateKeyPass!=null && keystorePass!=null) {
                        mKeystoreManager = new KeystoreManager(new KeystoreCredentials(keystorePath,
                                keystorePass,privateKeyAlias,privateKeyPass));

                    }
                    else {
                        mLogger.log(Level.SEVERE,"Unable to initialise keystore security context");
                        throw new KeyManagerException("Unable to initialise keystore security context");
                    }
                }
                catch(SignalException ex) {
                    attempts--;
                    if(attempts<0) {
                        throw new KeyManagerException("Unable to obtain the request credentials from signal handlers");
                    }
                    //else try again

                }
                catch(KeyManagerException ex) {
                    mLogger.log(Level.WARNING,ex.getMessage(),ex);
                    attempts--;
                    if(attempts<0) {
                        throw new KeyManagerException("Unable to obtain the request credentials from signal handlers");
                    }
                }
            }

        }
        catch(SecurityConfigurationException ex) {
            mLogger.log(Level.SEVERE,"Unable to initialise truststore from configuration '" + ex.getMessage() + "'",ex);
            throw new KeyManagerException("Unable to initialise security context");
        }

    }

    private void tryToSetCRLFile() throws KeyManagerException {
      String crlFile = null;
      try{
          crlFile = mTrustConfiguration.getConfigurationValue(this.REVOCATION_LIST_PROPERTY_NAME);
          if(!mKeystoreManager.CRLFileSet()) {
                mKeystoreManager.setCRLFile(crlFile);
            }
        }
        catch(SecurityConfigurationException ex) {
            mLogger.log(Level.WARNING,"Unable to set a certificate revocation list file to '" + crlFile + "'");
            throw new KeyManagerException("Unable to set a certificate revocation list file to '" + crlFile + "', please check your configuration");
        }
    }

    private CredentialRequest signalForCredentials() throws SignalException, KeyManagerException {
        CredentialRequest credentialRequest = null;
        //try to get the client to provide the values


        //Ask for the credentials
        credentialRequest = createCredentialRequest();
        //Want to pump a CredentialRequest out
        mSignalHandler.process(credentialRequest);
        //creates an instance of keystore manager using the credentials provided.
        //it will wait until the all credentials have been set or until the timeout
        //value has been reached.
        long commenceWait = System.currentTimeMillis();
        while(credentialRequest.getKeystorePassword()==null ||
              credentialRequest.getPrivateKeyAlias()==null ||
              credentialRequest.getPrivateKeyPassword()==null) {
            if(WAIT_TIME > System.currentTimeMillis()-commenceWait) {
                try{
                    Thread.sleep(SLEEP_TIME);
                }
                catch(InterruptedException ex) {
                    //fine
                }
            }
            else {
                mLogger.log(Level.SEVERE,"No keystore credentials were supplied, either application is not responding or has not been supplied with the signal");
                throw new KeyManagerException("No keystore credentials were supplied, either application is not responding or has not been supplied with the signal");
            }
        }



        return credentialRequest;
    }

    private void initialiseTokenDescriptorList() {
        //this is quite simple, only 3 types are supported
        mDescriptors = new TokenDescriptor[5];
        //add descriptor for private key token of type RSA.

        mPrivateKeyTokenDescriptor = new AsymmetricKeyDescriptor(AsymmetricKeyDescriptor.PRIVATE_KEY, AsymmetricKeyDescriptor.CYPHER_ALGO_RSA);
        mDescriptors[0] = mPrivateKeyTokenDescriptor;
        //add descriptor for public key token of type RSA.
        mPublicKeyTokenDescriptor = new AsymmetricKeyDescriptor(AsymmetricKeyDescriptor.PUBLIC_KEY, AsymmetricKeyDescriptor.CYPHER_ALGO_RSA);
        mDescriptors[1] = mPublicKeyTokenDescriptor;
        //add descriptor for X.509 certificate
        mCertificateTokenDescriptor = new CertificateDescriptor(CertificateDescriptor.PKI_X509V3, AsymmetricKeyDescriptor.CYPHER_ALGO_RSA);
        mDescriptors[2] = mCertificateTokenDescriptor;

        mX509CertificateChainTokenDescriptorForAcceptedIssuers = new X509CertificateChainDescriptor(true);
        mDescriptors[3] = mX509CertificateChainTokenDescriptorForAcceptedIssuers;
        mX509CertificateChainTokenDescriptorForClient = new X509CertificateChainDescriptor(false);
        mDescriptors[4] = mX509CertificateChainTokenDescriptorForClient;
    }

   private void initialiseTrustTokenDescriptorList() {
       //only trust X509Certificates
       mTrustDescriptors = new TokenDescriptor[1];
       mTrustedX509CertificateChainTokenDescriptor = new X509CertificateChainDescriptor(false);
       mTrustDescriptors[0] = mTrustedX509CertificateChainTokenDescriptor;
   }

}

