/////////////////////////////////////////////////////////////////////////
//
// © 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.io.FileInputStream;
import java.io.File;
import java.io.IOException;

import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.security.PublicKey;
import java.security.PrivateKey;
import java.util.logging.Level;
import java.util.logging.Logger;

import eu.gemss.components.security.token.provision.TokenProviderException;
import eu.gemss.components.security.token.trust.TrustProviderException;
import eu.gemss.components.security.InvalidCredentialException;
import eu.gemss.components.security.InvalidPasswordException;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.Certificate;
import java.io.InputStream;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import uk.ac.soton.itinnovation.gemss.security.context.*;
import java.io.*;


/**
 * A instance of KeystoreManager handles access to a particular keystore for a particular session.
 * It will cache the security credentials for a particular session.
 * It will enable access to private, public keys and certificates.
 */
public class KeystoreManager {

    private static final String KEYSTORE_TYPE = "JKS";
    private KeystoreCredentials mCredentials = null;
    private KeyStore mKeystore = null;
    private X509TrustManager mTrustManager = null;
    private String mCrlFile = null;
    private Logger mLogger = Logger.getLogger("uk.ac.soton.itinnovation.gemss.security.context.token.providers.KeystoreManager");

    /**
     * Creates a new instance of KeystoreManager
     * @param keystore credentials
     */
    public KeystoreManager(KeystoreCredentials creds) throws KeyManagerException {
        //try to load the keystore using the keystore credentials
        FileInputStream inStream = null;
        try{
            inStream = new FileInputStream(new File(creds.getKeystorePath()));
            mKeystore = KeyStore.getInstance(KEYSTORE_TYPE);
            mKeystore.load(inStream,creds.getKeystorePassword().toCharArray());//password only checks integrity, what is stupid is that there is no exception for signalling integrity failure?
            //loading succeeded, not going to check if private key correct until try to retrieve it.
            mCredentials = creds;
        }
        catch(FileNotFoundException ex) {
            mLogger.log(Level.SEVERE,"Unable to find or read keystore file '" + creds.getKeystorePath() + "'",ex);
            throw new KeyManagerException("Unable to find or read keystore file ' " + creds.getKeystorePath() + "'");
        }
        catch(IOException ex) {
            mLogger.log(Level.SEVERE,"Unable to find or read keystore file '" + creds.getKeystorePath() + "'",ex);
            throw new KeyManagerException("Unable to find or read keystore file ' " + creds.getKeystorePath() + "'");
        }
        catch(java.security.NoSuchAlgorithmException ex) {
            mLogger.log(Level.SEVERE,"No supported algorithm when loading keystore '" + creds.getKeystorePath() + "'",ex);
            throw new KeyManagerException("No supported algorithm when loading keystore '" + creds.getKeystorePassword() + "'");
        }
        catch(java.security.KeyStoreException ex) {
            mLogger.log(Level.SEVERE,"Unrecognised keystore failure",ex);
            throw new KeyManagerException(ex.getMessage());
        }
        catch(java.security.cert.CertificateException ex) {
            mLogger.log(Level.SEVERE,"Unrecognised problem with certificate when loading keystore '" + creds.getKeystorePath() + "'",ex);
            throw new KeyManagerException(ex.getMessage());
        }
        finally {
            /*
            try{
                if(inStream!=null)
                   inStream.close();
                            }
            catch(java.io.IOException ex) {
                mLogger.log(Level.WARNING,"Unable to close input stream for keystore '" + creds.getKeystorePath() +"'",ex);
            }
            */
        }
    }

    /**
     * Set the crlFile
     * @param crlFile
     */
    public void setCRLFile(String crlFile) {
        mCrlFile = crlFile;
    }


    /**
     * Check to see if the CRL file is set
     * @return
     */
    public boolean CRLFileSet() {
        if(mCrlFile==null)
           return false;
        else
            return true;
    }

    /**
     * Retrieve the public Key associated with the cached credentials
     * @return public key
     */
    public PublicKey getPublicKey() throws TokenProviderException {
       try{
            PublicKey pubKey = null;
            if(mCredentials!=null) {
                X509Certificate cert = (X509Certificate) mKeystore.getCertificate(mCredentials.getPrivateKeyAlias());
                pubKey = cert.getPublicKey();
            }
            else{
                throw new TokenProviderException("No authentication credentials exist please reauthenticate");
            }
            if(pubKey==null) {
                mLogger.log(Level.SEVERE,"Could not retrieve the private key for the alias '" + mCredentials.getPrivateKeyAlias() + "' from '" + mCredentials.getKeystorePath()+ "'");
                throw new TokenProviderException("Could not retrieve the private key for the alias '" + mCredentials.getPrivateKeyAlias() + "' from '" + mCredentials.getKeystorePath()+ "'");
            }
            return pubKey;
       }
       catch(ClassCastException ex) {
           mLogger.log(Level.SEVERE,"Could not obtain correct security token type from keystore",ex);
           throw new TokenProviderException("The alias '" + mCredentials.getPrivateKeyAlias()  + "' does not refer to a key entry in keystore '" + mCredentials.getKeystorePath()+ "'");
       }
       catch(java.security.KeyStoreException ex) {
            mLogger.log(Level.SEVERE,"Unrecognised keystore failure",ex);
            throw new TokenProviderException(ex.getMessage());
        }

    }

    /**
     * Retrieve the private key associated with the cached credentials
     * @return private key
     */
    public PrivateKey getPrivateKey() throws TokenProviderException, InvalidCredentialException {
        try {
            PrivateKey pK = null;
            if(mCredentials!=null) {
                pK = (PrivateKey) mKeystore.getKey(mCredentials.getPrivateKeyAlias(),mCredentials.getPrivateKeyPassword().toCharArray());
            }
            else {
                throw new TokenProviderException("No authentication credentials exist please reauthenticate");
            }
            if(pK==null) {
                mLogger.log(Level.SEVERE,"Could not retrieve the private key for the alias '" + mCredentials.getPrivateKeyAlias() + "' from '" + mCredentials.getKeystorePath()+ "'");
                throw new TokenProviderException("Could not retrieve the private key for the alias '" + mCredentials.getPrivateKeyAlias() + "' from '" + mCredentials.getKeystorePath()+ "'");
            }
            return pK;
        }
        catch(ClassCastException ex) {
            mLogger.log(Level.SEVERE,"Could not obtain correct security token type from keystore",ex);
            throw new TokenProviderException("The alias '" + mCredentials.getPrivateKeyAlias() + "' does not refer to a private key entry in keystore '" + mCredentials.getKeystorePath() + "'");
        }
        catch(java.security.KeyStoreException ex) {
            mLogger.log(Level.SEVERE,"Unrecognised keystore failure",ex);
            throw new TokenProviderException(ex.getMessage());
        }
        catch(java.security.NoSuchAlgorithmException ex) {
            mLogger.log(Level.SEVERE,"No supported algorithm when loading keystore '" + mCredentials.getKeystorePath() + "'",ex);
            throw new TokenProviderException("No supported algorithm when loading keystore '" + mCredentials.getKeystorePassword() + "'");
        }
        catch(java.security.UnrecoverableKeyException ex) {
            mLogger.log(Level.SEVERE,"Unable to recover key with alias '" + mCredentials.getPrivateKeyAlias() + "' from keystore '" + mCredentials.getKeystorePath() + "'",ex);
            throw new InvalidCredentialException("Unable to recover key with alias '" + mCredentials.getPrivateKeyAlias() + "' from keystore '" + mCredentials.getKeystorePath() + "'","Unable to recover key with alias '" + mCredentials.getPrivateKeyAlias() + "' from keystore '" + mCredentials.getKeystorePath() + "'");

        }
    }
    /**
     * Retrieve the X.509 Certificate associated with the cached credentials
     * @return public key
     */
    public X509Certificate getX509Certificate() throws TokenProviderException {
        try{
            X509Certificate cert = null;
            if(mCredentials!=null) {
                cert = (X509Certificate) mKeystore.getCertificate(mCredentials.getPrivateKeyAlias());

            }
            else{
                throw new TokenProviderException("No authentication credentials exist please reauthenticate");
            }
            if(cert==null) {
                mLogger.log(Level.SEVERE,"Could not retrieve the private key for the alias '" + mCredentials.getPrivateKeyAlias() + "' from '" + mCredentials.getKeystorePath()+ "'");
                throw new TokenProviderException("Could not retrieve the private key for the alias '" + mCredentials.getPrivateKeyAlias()  + "' from '" + mCredentials.getKeystorePath()+ "'");
            }
            return cert;
       }
       catch(ClassCastException ex) {
           mLogger.log(Level.SEVERE,"Could not obtain correct security token type from keystore",ex);
           throw new TokenProviderException("Could not retrieve the public key for the alias '" + mCredentials.getPrivateKeyAlias()  + "' from '" + mCredentials.getKeystorePath()+ "'");
       }
        catch(java.security.KeyStoreException ex) {
            mLogger.log(Level.SEVERE,"Unrecognised keystore failure",ex);
            throw new TokenProviderException(ex.getMessage());
        }
    }

    public X509Certificate[] getX509CertificateChain() throws TokenProviderException {
        X509Certificate[] X509certs = null;
        try{
            if(mCredentials!=null) {
                Certificate[] certs = (Certificate[]) mKeystore.getCertificateChain(mCredentials.getPrivateKeyAlias());
                //how do I check for the true type? Cast will fail for now
                X509certs = new X509Certificate[certs.length];
                for(int i=0;i<certs.length;i++) {
                    X509certs[i] = (X509Certificate) certs[i];
                }
            }
            else{
                throw new TokenProviderException("No authentication credentials exist please reauthenticate");
            }
            if(X509certs==null) {
                mLogger.log(Level.SEVERE,"Could not retrieve the private key for the alias '" + mCredentials.getPrivateKeyAlias() + "' from '" + mCredentials.getKeystorePath()+ "'");
                throw new TokenProviderException("Could not retrieve the private key for the alias '" + mCredentials.getPrivateKeyAlias()  + "' from '" + mCredentials.getKeystorePath()+ "'");
            }
            return X509certs;
        }
        catch(java.security.KeyStoreException ex) {
            mLogger.log(Level.SEVERE,"Unrecognised keystore failure",ex);
            throw new TokenProviderException(ex.getMessage());
        }
    }

    public boolean isTrusted(X509Certificate[] certChain,String authType) throws TrustProviderException {
        try{
            //create a trust factory if one does not already exist
            if(mTrustManager == null) {
                TrustManagerFactory trustFactory = TrustManagerFactory.getInstance("SunX509","SunJSSE");
                trustFactory.init(mKeystore);
                TrustManager[] tManagers = trustFactory.getTrustManagers();
                for(int i=0;i<tManagers.length;i++) {
                    if(tManagers[i] instanceof X509TrustManager) {
                        mTrustManager = (X509TrustManager) tManagers[0];
                        break;
                    }
                }
                if(mTrustManager == null) {
                    mLogger.severe("Could not configure keystore as a truststore. Please send a bug report including all logfiles");
                    throw new TrustProviderException("Could not configure keystore as a truststore. Please send a bug report including all logfiles");
                }
            }
            //read crl file
            // Generate CRL
            if(mCrlFile!=null) {
                X509CRL crl = null;
                try {
                    InputStream inStream = new FileInputStream(mCrlFile);
                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
                    crl = (X509CRL)cf.generateCRL(inStream);
                    inStream.close();
                }
                catch(java.io.FileNotFoundException ex) {
                    mLogger.log(Level.SEVERE,"Problem accessing CRL file '" + mCrlFile + "', please check it is OK",ex);
                    throw new TrustProviderException("Problem accessing CRL file '" + mCrlFile + "', please check it is OK","Problem accessing CRL file '" + mCrlFile + "', please check it is OK: " + ex.getMessage());
                }
                catch(java.io.IOException ex) {
                    mLogger.log(Level.SEVERE,"Problem accessing CRL file '" + mCrlFile + "', please check it is OK",ex);
                    throw new TrustProviderException("Problem accessing CRL file '" + mCrlFile + "', please check it is OK","Problem accessing CRL file '" + mCrlFile + "', please check it is OK: " + ex.getMessage());
                }
                catch(java.security.cert.CRLException ex) {
                    mLogger.log(Level.SEVERE,"Problem accessing CRL file '" + mCrlFile + "', please check it is OK",ex);
                    throw new TrustProviderException("Problem accessing CRL file '" + mCrlFile + "', please check it is OK","Problem accessing CRL file '" + mCrlFile + "', please check it is OK: " + ex.getMessage());
                }

                if (crl == null) {
                    mLogger.severe("Problem accessing CRL file, please check it is OK");
                    throw new TrustProviderException("Problem accessing CRL file, please check it is OK");
                }
                for(int i=0;i<certChain.length;i++) {
                    if(crl.isRevoked(certChain[i])) {
                        mLogger.severe("Certificate with subject DN '" + certChain[i].getSubjectDN().getName() +"' has been revoked");
                        throw new TrustProviderException("Certificate with subject DN '" + certChain[i].getSubjectDN().getName() +"' has been revoked");
                    }
                }
            }
            else {
                mLogger.log(Level.WARNING,"Problem accessing CRL file '" + mCrlFile + "', please check it is OK");
                throw new TrustProviderException("Problem accessing CRL file '" + mCrlFile + "', please check it is OK");
            }
            //following will throw CertificateException if chain is not trusted.
            mTrustManager.checkServerTrusted(certChain,authType); //auth type is actually dynamic
            return true;
        }
        catch(java.security.KeyStoreException ex) {
            mLogger.log(Level.SEVERE,"Problem accessing your keystore, details:",ex);
            throw new TrustProviderException("Problem accessing your keystore, please see the logs for details");
        }
        catch(java.security.NoSuchAlgorithmException ex) {
            mLogger.log(Level.SEVERE,"Keystore does not support required algorithm, details:",ex);
            throw new TrustProviderException("Keystore does not support required algorithm, please see the logs for details");
        }
        catch(java.security.NoSuchProviderException ex) {
            mLogger.log(Level.SEVERE,"No suitable Java JSSE provider exists, details:",ex);
            throw new TrustProviderException("No suitable Java JSSE provider exists, please see the logs for details");
        }
        catch(java.security.cert.CertificateException ex) {
            //certificate is not trusted
             mLogger.log(Level.INFO,"A Certificate Chain was rejected");
            return false;
        }
    }
}
