/************************************************************************
*
* Copyright (c) 2003-2004, C&C Research Laboratories, NEC Europe Ltd.
*
* Copyright in this library belongs to C&C Research Laboratories,
* Rathausallee 10, 53757 Sankt Augustin, Germany.
*
* 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 :           Federico Crazzolara
*  Created for Project :  GEMSS (IST-2001-37153)
*
************************************************************************/


package de.nece.ccrl.securitycontext.tokenprovider;

import eu.gemss.components.security.SecurityProviderConfiguration;
import eu.gemss.signals.SignalHandler;
import eu.gemss.components.security.token.*;
import eu.gemss.components.security.token.store.*;
import de.nece.ccrl.securitycontext.token.types.pki.CertificateDescriptor;
import de.nece.ccrl.securitycontext.token.types.key.KeyDescriptor;
import de.nece.ccrl.securitycontext.token.types.pki.X509CertificateTokenImpl;
import de.nece.ccrl.securitycontext.token.types.key.asymmetric.PrivateKeyTokenImpl;
import de.nece.ccrl.securitycontext.token.types.contexttoken.SecurityContextTokenImpl;
import de.nece.ccrl.util.*;
import eu.gemss.components.security.token.types.key.asymmetric.PrivateKeyToken;
import eu.gemss.components.security.token.types.key.symmetric.SecretKeyToken;
import eu.gemss.components.security.token.types.contexttoken.SecurityContextToken;
import eu.gemss.components.security.token.types.pki.X509CertificateToken;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.KeyStore;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.Security;
import java.security.Key;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import javax.crypto.SecretKey;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;
import java.util.List; 
import java.util.Set;
import java.util.Collection;
import java.io.*;



public class NECTokenStoreProvider implements SecurityTokenStoreProvider{

    Hashtable token_types;
    Hashtable cert_types;
    KeyStore cert_ks;
    KeyStore token_ks;
    char[] pwd;
    String keystore;

   


    // perhaps mutual exclusion problem for keystore and descriptor list access if parallel instances exist.
    // make all methods synchronized.


    private Hashtable load_descriptors(KeyStore ks) {

	Hashtable types = new Hashtable();
	TokenDescriptor hashtablekey;
	HashSet aliaslist;
	
	
	try{
	    
	    Enumeration en = ks.aliases();
	    
	    while (en.hasMoreElements()) {
		String alias = (String) en.nextElement();
		
		if (ks.isKeyEntry(alias)) {
		    Key t = ks.getKey(alias,pwd);
		    if (t instanceof SecretKey) {			
			// deserialize first -- it is for sure a SecretKeyToken or SecurityContextToken
			ByteArrayInputStream bis = new ByteArrayInputStream(t.getEncoded());
			ObjectInputStream ois = new ObjectInputStream(bis);
			Object o = ois.readObject();
			hashtablekey = ((SecurityToken)o).getTokenDescriptor();
			aliaslist = (HashSet) types.get(hashtablekey);
			if (aliaslist==null) aliaslist = new HashSet();
			aliaslist.add(alias);
			types.put(hashtablekey,aliaslist);	
		    // secret key tokens are, as well as context tokens, stored directly. This limits
		    // the possibility of manually storing them in the keystore. Otherwise need to 
		    // have a way of representing owner list as alias string.
		    }
		    if (t instanceof PrivateKey) {
			// here the owner of private key is left out. In this case one can handle only
			// one private key. For more general approach get DN from associated certificate
			// chain. The private keys can be distinguished using DN or altSubjectname as owner.
			hashtablekey = new KeyDescriptor(KeyDescriptor.PRIV_KEY, t.getAlgorithm());
			aliaslist = (HashSet) types.get(hashtablekey);
			if (aliaslist==null) aliaslist = new HashSet();
			aliaslist.add(alias);
			types.put(hashtablekey,aliaslist);
			
			// TO LIST YOUR OWN CERTIFICATE WHICH IS HIDDEN BY PRIVATE KEY
			// WE EXPOSE IT HERE BELOW

			Certificate[] chain = ks.getCertificateChain(alias);
			String owner = null;
			String altnames = new String();
			if (chain != null) {
			    if (!(chain[0] instanceof X509Certificate)) 
				throw new StoreProviderException("Unsupported certificate type for private key");
			    owner = (((X509Certificate) chain[0]).getSubjectX500Principal()).toString();
			    
			    Collection col = ((X509Certificate) chain[0]).getSubjectAlternativeNames();
			    if (col!=null){
				Iterator sit = col.iterator();
				//Use only the first altname in the list
				if (sit.hasNext()){
				    altnames = (String) ((List)sit.next()).get(1);
				}
				//System.out.println("ALTNAMES " + altnames);
			    }

			}
			//System.out.println("OWNER" + owner +"ALTNAMES " + altnames);
			hashtablekey = new CertificateDescriptor(CertificateDescriptor.X509,owner,altnames);
			aliaslist = (HashSet) types.get(hashtablekey);
			if (aliaslist==null) aliaslist = new HashSet();
			aliaslist.add(alias);
			types.put(hashtablekey,aliaslist);			
		    }

		} else if (ks.isCertificateEntry(alias)) {
		    Certificate c = ks.getCertificate(alias);
		    if (c instanceof X509Certificate) {
			String owner = (((X509Certificate) c).getSubjectX500Principal()).toString();
			Collection alt = ((X509Certificate) c).getSubjectAlternativeNames();
			String altnames = null;
			if (alt!=null) {
			    Iterator i = alt.iterator();
			    altnames = new String();
			    if (i.hasNext()) {
				// use only the first altname in the list
				altnames = (String)((List) i.next()).get(1);
				// if not string, the cast exception goes wrong! wrong certificate type
				// cos you will ask alternative names to be only strings i.e URIs
			    }
			    //System.out.println("ALTNAMES " + altnames);
			}
			hashtablekey = new CertificateDescriptor(CertificateDescriptor.X509,owner,altnames);
			aliaslist = (HashSet) types.get(hashtablekey);
			if (aliaslist==null) aliaslist = new HashSet();
			aliaslist.add(alias);
			types.put(hashtablekey,aliaslist);
		    }
		}
	    }  
	} catch (Exception e) {e.printStackTrace();}
	
	return types;	
    }



    
    public NECTokenStoreProvider() {
	pwd = null;
	keystore = null;
	cert_ks = null;
	token_ks = null;
	token_types = null;
	cert_types = null;
	Security.addProvider(new BouncyCastleProvider());
    }


    public String getInitiator() throws StoreProviderException {

	KeyDescriptor d = new KeyDescriptor(KeyDescriptor.PRIV_KEY,KeyDescriptor.RSA);
	// get initiator from private key information (certchain associated with it see NECSecurityTokenStoreProvider)
	PrivateKeyToken k = null;	
	k = (PrivateKeyToken) this.getSecurityToken(d);
	try {
	    d = (KeyDescriptor) k.getTokenDescriptor();
	} catch (Exception e) {e.printStackTrace();};
	List owners = d.getOwners();
	if (owners == null) throw new StoreProviderException("Private key has no corresponding certificate");
	return (String) owners.get(0);
    }


    public void setSignalHandler(SignalHandler channel) {
	// If you implement gets password from user input
    }

    public void setStoreProviderConfiguration(SecurityProviderConfiguration conf) {
	// TODO
	//means set the keystore parameterS ... INSTEAD OF SEE BELOW

    }

	
    public void setKeystore(String location, String password) {
	pwd = password.toCharArray();
	keystore = location;
	try{
	    cert_ks = KeyStore.getInstance("JKS"); 
	    FileInputStream inputstream = new FileInputStream(location);
	    cert_ks.load(inputstream, pwd);
	    inputstream.close();

	    token_ks = KeyStore.getInstance("BKS","BC");
	    inputstream = null;
	    NameSynch.get(location+".bks");
	    try{
		inputstream = new FileInputStream(location+".bks");
	    } catch (Exception e){} // this is necessary to allow good functionin if bks keystore is not present
	    token_ks.load(inputstream, pwd);
	    if (inputstream != null) inputstream.close();
	    NameSynch.release(location+".bks");

	    cert_types = this.load_descriptors(cert_ks);
	    token_types = this.load_descriptors(token_ks);	
  
  	} catch (Exception e) {
	    System.out.println(e.getMessage());
	    e.printStackTrace();
	}
    }


    static public int isKeystore(String location, String password){	
	int OK = 0;
	int WRONG_PWD = 1;
	int WRONG_FILE = 2;
	char[] pwd = password.toCharArray();
	String keystore = location;
	FileInputStream inputstream = null;
	KeyStore cert_ks;
	NameSynch.get(location);
	try{
	    cert_ks = KeyStore.getInstance("JKS"); 
	    inputstream = new FileInputStream(location);
	} catch (Exception e){return WRONG_FILE;}
	try{
	    cert_ks.load(inputstream, pwd);
	    inputstream.close();
	} catch (Exception e) {return WRONG_PWD;}	
	NameSynch.release(location);
	return OK;
    }


    public SecurityToken getSecurityToken(TokenDescriptor tokenDesc) throws StoreProviderException {
	SecurityToken sect = this.getSecurityToken(tokenDesc,token_ks,token_types);
	if (sect!=null) return sect;
	else return this.getSecurityToken(tokenDesc,cert_ks,cert_types); 
    }


    public SecurityToken getSecurityToken(TokenDescriptor tokenDesc, KeyStore ks, Hashtable tokentypes) throws StoreProviderException {
     
	// the descriptor that is passed may be less specified than the tokendescriptor in the store
	// determine to what tokendescriptor it matches and use that one to retrieve the token.

	Object[] darray = (tokentypes.keySet()).toArray();
	int j = 0;

	while ((j < darray.length) && (!(((TokenDescriptor) darray[j]).isEqual(tokenDesc)))) {	
	    j = j+1;
	}



	if (j == darray.length) {return null; }// the token was not found
	
	// determine the alias
	
	Object aliaslist = null;
	TokenDescriptor tablekey = (TokenDescriptor) darray[j];
	
	aliaslist = tokentypes.get(tablekey);

	if (aliaslist==null) return null;
	Iterator i = ((HashSet) aliaslist).iterator();
	String alias = (String) i.next();
	
	//System.out.println(tokenDesc.getDescription() + " is in store with alias " + alias);

	// map to my token representation
	Key t = null;
	Certificate c = null;
	Certificate[] chain = null;

	try{
	    if (ks.isKeyEntry(alias)) {
		t = ks.getKey(alias,pwd);
		chain = ks.getCertificateChain(alias);
	    }
	    if (ks.isCertificateEntry(alias)) 
		c = ks.getCertificate(alias);
	} catch (Exception e) {throw new StoreProviderException("unexpected keystore error");}


	if (t instanceof SecretKey) { 
	    try{
		// deserialize first -- it is for sure a SecretKeyToken or SecurityContextToken
		ByteArrayInputStream bis = new ByteArrayInputStream(t.getEncoded());
		ObjectInputStream ois = new ObjectInputStream(bis);
		Object o = ois.readObject();
		if (o instanceof SecurityContextToken) 
		    
		    System.out.println(tokenDesc.getDescription() + 
				       " expiration date: " + 
				       ((SecurityContextToken) o).getExpirationDate());
		return (SecurityToken) o;
	    } catch (Exception e) {} 
	}
	    
	if (t instanceof PrivateKey) {
	    
	    String owner = null;
	    if (chain != null) {
		if (!(chain[0] instanceof X509Certificate)) 
		    throw new StoreProviderException("Unsupported certificate type for private key");
		owner = (((X509Certificate) chain[0]).getSubjectDN()).toString();
	    }
	    // I might want the certificate associated with the key
	    if (tokenDesc instanceof CertificateDescriptor) 
		return new X509CertificateTokenImpl((X509Certificate) chain[0]);
	    else return new PrivateKeyTokenImpl((PrivateKey) t,owner);
	}
	
	if (c instanceof X509Certificate) 
	    return new X509CertificateTokenImpl((X509Certificate)c);
	
	return null;
    }


    public boolean contains(TokenDescriptor desc) {
	SecurityToken token = null;
	try {
	    token = this.getSecurityToken(desc);
	} catch (Exception e) {return false;} // you should really throw an exeption here.
	return !(token==null);
    }



    public boolean addSecurityToken(SecurityToken token) throws StoreProviderException {
	
	boolean flag = false;
	String storename = keystore + ".bks";

	NameSynch.get(storename);

	// the keystore should be reloaded to awoid conflict. in this case can write only in temp keystore
	KeyStore ks = null;
	try{
	    ks = KeyStore.getInstance("BKS","BC");
	    FileInputStream inputstream = null;
	    try{
		inputstream = new FileInputStream(storename);
	    } catch (Exception e){} // this is necessary to allow good functionin if bks keystore is not present
	    ks.load(inputstream, pwd);
	    if (inputstream != null) inputstream.close();
	}catch(Exception e1) {
	    e1.printStackTrace();
	}
	Hashtable tokentypes = this.load_descriptors(ks);
	


	TokenDescriptor desc = null;
	try {
	    desc = token.getTokenDescriptor();	    
	} catch (Exception e) {}

	Object[] darray = (tokentypes.keySet()).toArray();
	int j = 0;

	while ((j < darray.length) && (!(((TokenDescriptor) darray[j]).isEqual(desc)))) {	
	    j = j+1;
	}


	// determine the aliaslist
	
	Object aliaslist = null;

	if (j < darray.length) {
	    TokenDescriptor tablekey = (TokenDescriptor) darray[j];
	    aliaslist = tokentypes.get(tablekey);
	}

	if (aliaslist==null) {
	    aliaslist = new HashSet();
	    tokentypes.put(desc,aliaslist);
	}
	

	// remove token first

	Iterator i = ((HashSet) aliaslist).iterator();
	String alias = null;

	try{
	    
	    while (i.hasNext()) {
		alias = (String) i.next();
		if (ks.isKeyEntry(alias)){
		    Key t = ks.getKey(alias,pwd);
		    if (t instanceof SecretKey) {
			
			// deserialize first -- it is for sure a SecretKeyToken or SecurityContextToken
			ByteArrayInputStream bis = new ByteArrayInputStream(t.getEncoded());
			ObjectInputStream ois = new ObjectInputStream(bis);
			Object o = ois.readObject();
			//System.out.println("REMOVING ....");
			if (
			    ((token instanceof SecurityContextToken) 
			     && (((SecurityContextToken) token).equals((SecurityContextToken)o)))
			    ||
			    ((token instanceof SecretKeyToken) 
			     && (((SecretKeyToken) token).equals((SecretKeyToken)o)))
			    ) {
			    ks.deleteEntry(alias);	
			    ((HashSet) aliaslist).remove(alias);
			    
			}
		    }
		    if (t instanceof PrivateKey) {
			PrivateKeyTokenImpl st = new PrivateKeyTokenImpl((PrivateKey) t);
			if (st.equals(token)) {
			    ks.deleteEntry(alias);	
			    ((HashSet) aliaslist).remove(alias);
			    
			}
		    }
		}
		else if (ks.isCertificateEntry(alias)) {
		    Certificate c = ks.getCertificate(alias);
		    if (c instanceof X509Certificate) {
			X509CertificateTokenImpl st = new X509CertificateTokenImpl((X509Certificate)c);
			if (st.equals(token)) {
			    ks.deleteEntry(alias);	  
			    ((HashSet) aliaslist).remove(alias);
			}		
		    }		    
		}
	    }


	    // generate a random alias for the token to be inserted

	    Random generator = new Random();
	    alias = String.valueOf(generator.nextInt());

	    // inserts the alias in table 
        
	    ((HashSet) aliaslist).add(alias);
	
	    // insert token in keystore
	    
	    if ((token instanceof SecretKeyToken) || (token instanceof SecurityContextTokenImpl)) {
		ks.setKeyEntry(alias,(Key) token,pwd,null);
		flag = true;
		
	    } else 
		if (token instanceof X509CertificateToken) {
		    
		    // maybe test if there is a ca certificate that authenticates it in the keystore,
		    // otherwise throw an exception *** TODO .... is it done automatically? ****
		    // better done in ISTRUSTED provider of TRUST PROVIDER
		    ks.setCertificateEntry(alias,((X509CertificateToken) token).getX509Certificate());
		    flag = true;
		}
	
	    // record the changes
	    if (flag) {
		
		FileOutputStream outputstream = new FileOutputStream(storename);
		ks.store(outputstream, pwd);
		outputstream.close();
		

		//System.out.println(desc.getDescription() + " has been stored with alias " + alias);
	    }
	    
	} catch (Exception e) {
	    e.printStackTrace();
	    new StoreProviderException("unexpeced error while attempting to add a token");
	}

	NameSynch.release(storename);
	return flag;
    }


    public boolean removeSecurityToken(SecurityToken token) throws StoreProviderException {


	boolean flag = false;
	String storename = keystore + ".bks";

	NameSynch.get(storename);

	// the keystore should be reloaded to awoid conflict. in this case can write only in temp keystore
	KeyStore ks = null;
	try{
	    ks = KeyStore.getInstance("BKS","BC");
	    FileInputStream inputstream = null;
	    try{
		inputstream = new FileInputStream(storename);
	    } catch (Exception e){} // this is necessary to allow good functionin if bks keystore is not present
	    ks.load(inputstream, pwd);
	    if (inputstream != null) inputstream.close();
	}catch(Exception e1) {
	    e1.printStackTrace();
	}
	Hashtable tokentypes = this.load_descriptors(ks);



	TokenDescriptor desc = null;
	try {
	    desc = token.getTokenDescriptor();	    
	} catch (Exception e) {}

	Object[] darray = (tokentypes.keySet()).toArray();
	int j = 0;

	while ((j < darray.length) && (!(((TokenDescriptor) darray[j]).isEqual(desc)))) {	
	    j = j+1;
	}

	if (j == darray.length) return false; // the token was not found

	// determine the aliaslist
	
	Object aliaslist = null;
	TokenDescriptor tablekey = (TokenDescriptor) darray[j];
	
	aliaslist = tokentypes.get(tablekey);


	if (aliaslist==null) return false;
	Iterator i = ((HashSet) aliaslist).iterator();
	String alias = null;

	try{

	    //deletes the token
	    while (i.hasNext()) {
		alias = (String) i.next();
		if (ks.isKeyEntry(alias)){
		    Key t = ks.getKey(alias,pwd);
		    if (t instanceof SecretKey) {
			
			// deserialize first -- it is for sure a SecretKeyToken or SecurityContextToken
			ByteArrayInputStream bis = new ByteArrayInputStream(t.getEncoded());
			ObjectInputStream ois = new ObjectInputStream(bis);
			Object o = ois.readObject();
			//System.out.println("REMOVING ....");
			if (
			    ((token instanceof SecurityContextToken) 
			     && (((SecurityContextToken) token).equals((SecurityContextToken)o)))
			    ||
			    ((token instanceof SecretKeyToken) 
			     && (((SecretKeyToken) token).equals((SecretKeyToken)o)))
			    ) {
			    ks.deleteEntry(alias);	
			    ((HashSet) aliaslist).remove(alias);
			    flag = true;
			}
		    }
		    if (t instanceof PrivateKey) {
			PrivateKeyTokenImpl st = new PrivateKeyTokenImpl((PrivateKey) t);
			if (st.equals(token)) {
			    ks.deleteEntry(alias);	
			    ((HashSet) aliaslist).remove(alias);
			    flag = true;
			}
		    }
		}
		else if (ks.isCertificateEntry(alias)) {
		    Certificate c = ks.getCertificate(alias);
		    if (c instanceof X509Certificate) {
			X509CertificateTokenImpl st = new X509CertificateTokenImpl((X509Certificate)c);
			if (st.equals(token)) {
			    ks.deleteEntry(alias);	
			    ((HashSet) aliaslist).remove(alias);
			    flag = true;
			}		
		    }		    
		}
	    }

	    if (aliaslist == null) tokentypes.remove(token.getTokenDescriptor());

	    // record the modified keystore
	    if (flag) {
		FileOutputStream outputstream = new FileOutputStream(storename);
		ks.store(outputstream, pwd);
		outputstream.close();
	    }
	
	    
	} catch (Exception e) {
	    e.printStackTrace();
	    new StoreProviderException("unexpected error while attempting to remove a token");
	}
	
	NameSynch.release(storename);
	return flag;
    }



    public TokenDescriptor[] getSupportedTokenList() {
	Set supported = token_types.keySet();
	supported.addAll(cert_types.keySet());
	return (TokenDescriptor[]) supported.toArray();
    }

}
