/*
 * Copyright (c) 2011-2012 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE file for licensing information.
 * 
 * Parts of this class are derived from the glite.security.util-java module, 
 * copyrighted as follows:
 *
 * Copyright (c) Members of the EGEE Collaboration. 2004. See
 * http://www.eu-egee.org/partners/ for details on the copyright holders.
 */
package eu.emi.security.authn.x509.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.nio.charset.Charset;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.crypto.BadPaddingException;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMEncryptor;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.OutputEncryptor;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemObjectGenerator;
import org.bouncycastle.util.io.pem.PemWriter;

import eu.emi.security.authn.x509.X509Credential;
import eu.emi.security.authn.x509.helpers.CachedPEMReader;
import eu.emi.security.authn.x509.helpers.CertificateHelpers;
import eu.emi.security.authn.x509.helpers.CertificateHelpers.PEMContentsType;
import eu.emi.security.authn.x509.helpers.CharArrayPasswordFinder;
import eu.emi.security.authn.x509.helpers.FlexiblePEMReader;
import eu.emi.security.authn.x509.helpers.KeyStoreHelper;
import eu.emi.security.authn.x509.helpers.PKCS8DERReader;
import eu.emi.security.authn.x509.helpers.PasswordSupplier;

/**
 * Utility class with methods simplifying typical certificate related operations.
 * 
 * @author K. Benedyczak
 * @author J. Hahkala
 */
public class CertificateUtils
{
	static 
	{
		configureSecProvider();
	}
	
	/**
	 * Definition of the encoding that can be used for reading or writing 
	 * certificates or keys.
	 */
	public static enum Encoding {PEM, DER}
	
	public static final String DEFAULT_KEYSTORE_ALIAS = "default";
	
	public static final Charset ASCII = Charset.forName("US-ASCII");
	
	/**
	 * Configures security providers which are used by the library. Can be called 
	 * multiple times (subsequent calls won't have any effect). 
	 * <p>
	 * This method must be called before any other usage of the code from canl API. 
	 */
	public static void configureSecProvider()
	{
		if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null)
			Security.addProvider(new BouncyCastleProvider());
	}
	
	/**
	 * Performs a trivial conversion by use of casting of a Certificate array
	 * into X509Certificate array 
	 * @param chain to convert
	 * @return converted chain
	 * @throws ClassCastException if at least one entry in the source chain is not
	 * an {@link X509Certificate}
	 */
	public static X509Certificate[] convertToX509Chain(Certificate []chain) 
		throws ClassCastException
	{
		X509Certificate[] ret = new X509Certificate[chain.length];
		for (int i=0; i<chain.length; i++)
			ret[i] = (X509Certificate) chain[i];
		return ret;
	}
	
	/**
	 * Produces a human readable text representation of the provided certificate.
	 * It uses {@link X509Formatter} internally.
	 * @param cert input certificate
	 * @param mode controls how detailed the string representation should be 
	 * @return the text representation
	 */
	public static String format(X509Certificate cert, FormatMode mode)
	{
		X509Formatter formatter = new X509Formatter(mode);
		return formatter.format(cert);
	}

	/**
	 * Produces a human readable text representation of the provided certificate chain. 
	 * It uses {@link X509Formatter} internally.
	 * @param certChain input certificates
	 * @param mode controls how detailed the string representation should be 
	 * @return the text representation
	 */
	public static String format(X509Certificate[] certChain, FormatMode mode)
	{
		X509Formatter formatter = new X509Formatter(mode);
		return formatter.format(certChain);
	}

	
	/**
	 * Loads a single certificate from the provided input stream. The stream is always closed afterwards.
	 * @param is input stream to read encoded certificate from
	 * @param format encoding type
	 * @return loaded certificate
	 * @throws IOException if certificate can not be read or parsed
	 */
	public static X509Certificate loadCertificate(InputStream is, Encoding format) 
			throws IOException
	{
		InputStream realIS = is;
		if (format.equals(Encoding.PEM))
		{
			ByteArrayOutputStream buffer = new ByteArrayOutputStream(4096);
			Reader br = new InputStreamReader(is, ASCII);
			FlexiblePEMReader pemReader = new FlexiblePEMReader(br);
			try
			{
				PemObject pem = pemReader.readPemObject();
				if (pem == null)
					throw new IOException("PEM data not found in the stream and its end was reached");
				PEMContentsType type = CertificateHelpers.getPEMType(pem.getType());
				if (!type.equals(PEMContentsType.CERTIFICATE))
					throw new IOException("Expected PEM encoded certificate but found: " + type);
				buffer.write(pem.getContent());

				realIS = new ByteArrayInputStream(buffer.toByteArray());
			} finally
			{
				pemReader.close();
			}
		}
		Certificate cert = CertificateHelpers.readDERCertificate(realIS);
		
		if (!(cert instanceof X509Certificate))
			throw new IOException("The DER input contains a certificate which is not a " +
					"X.509Certificate, it is " + cert.getClass().getName());
		return (X509Certificate)cert;
	}

	/**
	 * Loads a private key from the provided input stream. The input stream must be encoded
	 * in the PKCS8 format (PEM or DER). Additionally in case of PEM encoding the legacy 
	 * OpenSSL format for storing private keys is supported. Such PEM header names
	 * has algorithm {RSA|DSA|EC} placed before the PRIVATE KEY string.
	 * <p>
	 * Currently supported key encryption algorithms are DES and 3 DES. RC2 is unsupported.
	 * <p>
	 * NOTE: currently it is unsupported to load DER private keys which were encoded with openssl 
	 * legacy encoding (e.g. with @verbatim openssl rsa -outform der ... @endverbatim). PEM files
	 * in openssl legacy encoding are supported. 
	 * @param is input stream to read encoded key from
	 * @param format encoding type (PEM or DER)
	 * @param password key's encryption password (can be null is file is not encrypted)
	 * @return loaded key
	 * @throws IOException if key can not be read or parsed
	 */
	public static PrivateKey loadPrivateKey(InputStream is, Encoding format, 
			char[] password) throws IOException
	{
		if (format.equals(Encoding.PEM))
		{
			return loadPEMPrivateKey(is, getPF(password));
		} else
			return loadDERPrivateKey(is, password);
	}

	/**
	 * Loads a private key from the provided input stream. The input stream must be encoded
	 * in the PEM format. This method is a special purpose version of the 
	 * {@link #loadPrivateKey(InputStream, Encoding, char[])}. It allows to provide {@link PasswordFinder}
	 * instead of the actual password. The {@link PasswordFinder} implementation will be used only if
	 * the source is encrypted.
	 * <p>
	 * All other limitations and features are as in the {@link #loadPrivateKey(InputStream, Encoding, char[])}
	 * method.  
	 * @param is input stream to read encoded key from
	 * @param pf password finder used to discover key's encryption password. 
	 * It is used only if the password is actually needed.
	 * @return loaded key
	 * @throws IOException if key can not be read or parsed
	 */
	public static PrivateKey loadPEMPrivateKey(InputStream is, PasswordSupplier pf) throws IOException
	{
		Reader reader = new InputStreamReader(is, Charset.forName("US-ASCII"));
		FlexiblePEMReader pemReader = new FlexiblePEMReader(reader);
		return internalLoadPK(pemReader, "PEM", pf);
	}

	private static PrivateKey parsePEMPrivateKey(PemObject pem, PasswordSupplier pf) 
			throws IOException
	{
		CachedPEMReader pemReader = new CachedPEMReader(pem);
		return internalLoadPK(pemReader, "PEM", pf);
	}

	private static PrivateKey internalLoadPK(PEMParser pemReader, String type, PasswordSupplier pf) 
			throws IOException
	{
		Object ret = null;
		try
		{
			ret = pemReader.readObject();
			if (ret == null)
				throw new IOException("Can not load the " + type + 
						" private key: no input data (empty source?)");
		} catch (IOException e)
		{
			if (e.getCause() != null && e.getCause() instanceof BadPaddingException)
			{
				throw new IOException("Can not load " + type + " private key: the password is " +
						"incorrect or the " + type + " data is corrupted.", e);
			}
			throw new IOException("Can not load the " + type + " private key: " + e);
		}
		return convertToPrivateKey(ret, type, pf);
	}

	
	private static PrivateKey convertToPrivateKey(Object pemObject, String type, PasswordSupplier pf) throws IOException
	{
		PrivateKeyInfo pki;
		try
		{
			pki = resolvePK(type, pemObject, pf);
		} catch (OperatorCreationException e)
		{
			throw new IOException("Can't initialize decryption infrastructure", e);
		} catch (PKCSException e)
		{
			throw new IOException("Error decrypting private key: the password is " +
						"incorrect or the " + type + " data is corrupted.", e);
		}
		JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
		return converter.getPrivateKey(pki);
	}
	
	private static PrivateKeyInfo resolvePK(String type, Object src, PasswordSupplier pf) throws 
		IOException, OperatorCreationException, PKCSException
	{
		if (src instanceof PrivateKeyInfo)
			return (PrivateKeyInfo) src;
		
		if (src instanceof PEMKeyPair)
			return ((PEMKeyPair)src).getPrivateKeyInfo();
		
		if (src instanceof PKCS8EncryptedPrivateKeyInfo)
		{
			JceOpenSSLPKCS8DecryptorProviderBuilder provBuilder = new JceOpenSSLPKCS8DecryptorProviderBuilder();
			InputDecryptorProvider decProvider = provBuilder.build(pf.getPassword());
			return ((PKCS8EncryptedPrivateKeyInfo)src).decryptPrivateKeyInfo(decProvider);
		}
		
		if (src instanceof PEMEncryptedKeyPair)
		{
			JcePEMDecryptorProviderBuilder provBuilder = new JcePEMDecryptorProviderBuilder();
			PEMDecryptorProvider decProvider = provBuilder.build(pf.getPassword());
			PEMKeyPair keyPair = ((PEMEncryptedKeyPair)src).decryptKeyPair(decProvider);
			return keyPair.getPrivateKeyInfo();
		}
		
		throw new IOException("The " + type + " input does not contain a private key, " +
				"it was parsed as " + src.getClass().getName());
	}
	
	
	private static PrivateKey loadDERPrivateKey(InputStream is, char[] password) 
			throws IOException
	{
		String type = "DER";
		Object ret = null;
		PKCS8DERReader derReader = new PKCS8DERReader(is, password != null);
		try
		{
			ret = derReader.readObject();
			derReader.close();
			if (ret == null)
				throw new IOException("Can not load the " + type + 
						" private key: no input data (empty source?)");
		} catch (IOException e)
		{
			if (e.getCause() != null && e.getCause() instanceof BadPaddingException)
			{
				throw new IOException("Can not load " + type + " private key: the password is " +
						"incorrect or the " + type + " data is corrupted.", e);
			}
			throw new IOException("Can not load the " + type + " private key: ", e);
		}
		
		return convertToPrivateKey(ret, type, getPF(password));
	}

	/**
	 * Loads a set of (possibly unrelated to each other) certificates from the provided input stream. 
	 * The input stream is always closed afterwards.
	 * 
	 * @param is input stream to read encoded certificates from
	 * @param format encoding type
	 * @return loaded certificates array
	 * @throws IOException if certificates can not be read or parsed
	 */
	public static X509Certificate[] loadCertificates(InputStream is, Encoding format) throws IOException
	{
		InputStream realIS = is;
		if (format.equals(Encoding.PEM))
		{
			boolean readOne = false;
			ByteArrayOutputStream buffer = new ByteArrayOutputStream(4096);
			Reader br = new InputStreamReader(is, ASCII);
			FlexiblePEMReader pemReader = new FlexiblePEMReader(br);
			try
			{
				do
				{
					PemObject pem = pemReader.readPemObject();
					if (pem == null && readOne == false)
						throw new IOException("PEM data not found in the stream and its end was reached");
					if (pem == null)
						break;
					PEMContentsType type = CertificateHelpers.getPEMType(pem.getType());
					if (!type.equals(PEMContentsType.CERTIFICATE))
						throw new IOException("Expected PEM encoded certificate but found: " + type);
					readOne = true;
					buffer.write(pem.getContent());
				} while (true);
			} finally
			{
				pemReader.close();
			}
			realIS = new ByteArrayInputStream(buffer.toByteArray());
		}
		return loadDERCertificates(realIS);
	}
	
	/**
	 * Loads a chain of certificates from the provided input stream. The input stream is always closed afterwards.
	 * @param is input stream to read encoded certificates from
	 * @param format encoding type
	 * @return loaded certificates array
	 * @throws IOException if certificates can not be read or parsed
	 */
	public static X509Certificate[] loadCertificateChain(InputStream is, Encoding format) throws IOException
	{
		X509Certificate[] unsorted = loadCertificates(is, format);
		List<X509Certificate> unsortedList = new ArrayList<X509Certificate>();
		Collections.addAll(unsortedList, unsorted);
		return CertificateHelpers.sortChain(unsortedList);
	}

	private static X509Certificate[] loadDERCertificates(InputStream is) throws IOException
	{
		Collection<? extends Certificate> certs = CertificateHelpers.readDERCertificates(is);
		Iterator<? extends Certificate> iterator = certs.iterator();
		X509Certificate []ret = new X509Certificate[certs.size()];
		for (int i=0; i<ret.length; i++)
		{
			Certificate c = iterator.next();
			if (!(c instanceof X509Certificate))
				throw new IOException("The DER input contains a certificate which is not a " +
						"X.509Certificate, it is " + c.getClass().getName());
			ret[i] = (X509Certificate) c;
		}
		return ret;
	}

	/**
	 * Loads certificates and private keys from the PEM input stream 
	 * (usually from file). Order of entries is not relevant. However it is assumed 
	 * that the input contains:
	 * <ol>
	 *  <li> one private key K,
	 *  <li> one certificate C corresponding to the private key K,
	 *  <li> zero or more certificates that if present form a 
	 *  chain of the certificate C. 
	 * </ol>
	 * If more then one certificate is found then this method tries to sort them to
	 * form a consistent chain (inability to do so is thrown as an exception) and assumes
	 * that the last certificate in chain is the user's certificate corresponding 
	 * to the private key.
	 *  
	 * @param is input stream to read from
	 * @param password private key's encryption password or null if key is not encrypted.
	 * @param ksPassword password which is used to encrypt the private key in the keystore. 
	 * Can not be null.
	 * @return KeyStore with one private key typed entry, with alias 
	 * {@link #DEFAULT_KEYSTORE_ALIAS} of the JKS type. If password is != null then it is also
	 * used to crypt the key in the keystore. If it is null then #
	 * @throws IOException if input can not be read or parsed
	 */
	public static KeyStore loadPEMKeystore(InputStream is, char[] password, char[] ksPassword) throws IOException
	{
		return loadPEMKeystore(is, getPF(password), ksPassword);
	}
	
	/**
	 * As {@link #loadPEMKeystore(InputStream, char[], char[])} but this version allows for providing input
	 * key's encryption password only when needed. Input stream is always closed afterwards.
	 *  
	 * @param is input stream to read from
	 * @param pf implementation will be used to get the password needed to decrypt the private key 
	 * from the PEM keystore. Won't be used if the key happens to be unencrypted.
	 * @param ksPassword password which is used to encrypt the private key in the keystore. 
	 * Can not be null.
	 * @return KeyStore with one private key typed entry, with alias 
	 * {@link #DEFAULT_KEYSTORE_ALIAS} of the JKS type. If password is != null then it is also
	 * used to crypt the key in the keystore. If it is null then #
	 * @throws IOException if input can not be read or parsed
	 */
	public static KeyStore loadPEMKeystore(InputStream is, PasswordSupplier pf, char[] ksPassword) throws IOException
	{
		PrivateKey pk = null;
		List<X509Certificate> certChain = new ArrayList<X509Certificate>();
		Reader br = new InputStreamReader(is, ASCII);
		FlexiblePEMReader pemReader = new FlexiblePEMReader(br);
		try
		{
			do
			{
				PemObject pem = pemReader.readPemObject();
				if (pem == null)
					break;
				PEMContentsType type = CertificateHelpers.getPEMType(pem.getType());
				if (type.equals(PEMContentsType.PRIVATE_KEY) || 
						type.equals(PEMContentsType.LEGACY_OPENSSL_PRIVATE_KEY))
				{
					if (pk != null)
						throw new IOException("Multiple private keys were found");
					pk = parsePEMPrivateKey(pem, pf);
				} else if (type.equals(PEMContentsType.CERTIFICATE))
				{
					X509Certificate[] certs = loadDERCertificates(
							new ByteArrayInputStream(pem.getContent()));
					for (X509Certificate cert: certs)
						certChain.add(cert);
				} else
				{
					throw new IOException("Unsupported PEM object found in the input: " + type);
				}
			} while (true);
		} finally
		{
			pemReader.close();
		}
		
		if (pk == null)
		{
			throw new IOException("Private key was not found in the PEM keystore (" + 
					certChain.size() + " certificate(s) was (were) found).");
		}
		
		Certificate []chain = CertificateHelpers.sortChain(certChain);
		
		KeyStore ks;
		try
		{
			ks = KeyStoreHelper.getInstanceForCredential("JKS");
			ks.load(null, null);
			ks.setKeyEntry(DEFAULT_KEYSTORE_ALIAS, pk, ksPassword, chain);
		} catch (KeyStoreException e)
		{
			throw new IOException("Can't setup the JKS keystore", e);
		} catch (NoSuchAlgorithmException e)
		{
			throw new IOException("Can't setup the JKS keystore", e);
		} catch (CertificateException e)
		{
			throw new IOException("Can't setup the JKS keystore", e);
		}
		return ks;
	}

	/**
	 * Saves the provided certificate to the output file, using the requested encoding.
	 * <b> WARNING </b> The output stream IS NOT closed afterwards. This is on purpose,
	 * so it is possible to write additional output.
	 * @param os where to write the encoded certificate to 
	 * @param cert certificate to save
	 * @param format format to use
	 * @throws IOException if the data can not be written 
	 */
	public static void saveCertificate(OutputStream os, X509Certificate cert, 
			Encoding format) throws IOException
	{
		if (format.equals(Encoding.PEM))
		{
			@SuppressWarnings("resource")
			JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(os, ASCII));
			writer.writeObject(cert);
			writer.flush();
		} else
		{
			try
			{
				os.write(cert.getEncoded());
			} catch (CertificateEncodingException e)
			{
				throw new IOException("Can't encode the " +
						"certificate into ASN.1 DER format", e);
			}
			os.flush();
		}
	}

	/**
	 * As {@link #savePrivateKey(OutputStream, PrivateKey, Encoding, String, char[], boolean)} with
	 * the last argument equal to false 
	 * 
	 * @param os where to write the encoded key to 
	 * @param pk key to save
	 * @param format format to use
	 * @param encryptionAlg encryption algorithm to be used. 
	 * See {@link #savePrivateKey(OutputStream, PrivateKey, Encoding, String, char[], boolean)} documentation
	 * for details about allowed values.
	 * @param encryptionPassword encryption password to be used.
	 * @throws IOException if the data can not be written 
	 * @throws IllegalArgumentException if encryptionAlg is unsupported
	 */
	public static void savePrivateKey(OutputStream os, PrivateKey pk, 
			Encoding format, String encryptionAlg, char[] encryptionPassword) 
			throws IOException, IllegalArgumentException
	{
		savePrivateKey(os, pk, format, encryptionAlg, encryptionPassword, false);
	}
	
	/**
	 * Saves the provided private key to the output file, using the requested encoding.
	 * Allows for using PKCS #8 or the legacy openssl PKCS #1 encoding.
	 * <b> WARNING </b> The output stream IS NOT closed afterwards. This is on purpose,
	 * so it is possible to write additional output.
	 * 
	 * @param os where to write the encoded key to 
	 * @param pk key to save
	 * @param format format to use
	 * @param encryptionAlg encryption algorithm to be used. 
	 * Use null if output must not be encrypted.
	 * For PKCS8 output see {@link JceOpenSSLPKCS8EncryptorBuilder} constants for available names. 
	 * For the legacy openssl format, one can use the 
	 * algorithm names composed from 3 parts glued with hyphen. The first part determines algorithm,
	 * one of AES, DES, BF and RC2. The second part determines key bits and is used for AES and
	 * optionally for RC2. For AES it is possible to use values
	 * 128, 192 and 256. For RC2 64, 40 can be used or nothing - then value 128 is used.
	 * The last part determines the block mode: CFB, ECB, OFB, EDE and CBC. Additionally EDE3 
	 * can be used in combination with DES to use DES3 with EDE. Examples:
	 * AES-192-ECB or DES-EDE3.  
	 * @param encryptionPassword encryption password to be used.
	 * @param opensslLegacyFormat if true the key is saved in the legacy openssl format. Otherwise a 
	 * PKCS #8 is used.
	 * @throws IOException if the data can not be written 
	 * @throws IllegalArgumentException if encryptionAlg is unsupported
	 * @since 1.1.0
	 */
	public static void savePrivateKey(OutputStream os, PrivateKey pk, 
			Encoding format, String encryptionAlg, char[] encryptionPassword, 
			boolean opensslLegacyFormat) 
			throws IOException, IllegalArgumentException
	{
		PemObjectGenerator gen;
		if (encryptionAlg != null)
		{
			try
			{
				if (!opensslLegacyFormat)
				{
					JceOpenSSLPKCS8EncryptorBuilder encryptorBuilder = 
							new JceOpenSSLPKCS8EncryptorBuilder(
									new ASN1ObjectIdentifier(encryptionAlg));
				        encryptorBuilder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
				        encryptorBuilder.setPasssword(encryptionPassword);
				        
					OutputEncryptor oe = encryptorBuilder.build();
					gen = new JcaPKCS8Generator(pk, oe);
				} else
				{
					JcePEMEncryptorBuilder builder = new JcePEMEncryptorBuilder(encryptionAlg);
					builder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
					builder.setSecureRandom(new SecureRandom());
					PEMEncryptor encryptor = builder.build(encryptionPassword);
					gen = new JcaMiscPEMGenerator(pk, encryptor);
				}
			} catch (OperatorCreationException e)
			{
				throw new IllegalArgumentException("Can't setup encryption modules, " +
						"likely the parameters (as algorithm) are invalid", e);
			}
		} else
		{
			if (!opensslLegacyFormat)
				gen = new JcaPKCS8Generator(pk, null);
			else
			{
				gen = new JcaMiscPEMGenerator(pk);
			}
		}
		
		if (format.equals(Encoding.PEM))
		{
			@SuppressWarnings("resource")
			PemWriter writer = new PemWriter(new OutputStreamWriter(os, ASCII));
			
			writer.writeObject(gen);
			writer.flush();
		} else
		{
			if (encryptionAlg == null)
			{
				os.write(pk.getEncoded());
			} else
			{
				PemObject pemO = gen.generate();
				os.write(pemO.getContent());
			}
			os.flush();
		}
	}
	
	/**
	 * Saves the provided certificate chain to the output stream, using the requested 
	 * encoding.
 	 * <b> WARNING </b> The output stream IS NOT closed afterwards. This is on purpose,
	 * so it is possible to write additional output.
	 * @param os where to write the encoded certificate to 
	 * @param chain certificate chain to save
	 * @param format format to use
	 * @throws IOException if the data can not be written
	 */
	public static void saveCertificateChain(OutputStream os, X509Certificate[] chain, 
			Encoding format) throws IOException
	{
		if (format.equals(Encoding.PEM))
		{
			for (X509Certificate cert: chain)
				saveCertificate(os, cert, Encoding.PEM);
		} else
		{
			byte [][] der = new byte[chain.length][];
			for (int i=0; i<chain.length; i++)
			{
				try
				{
					der[i] = chain[i].getEncoded();
				} catch (CertificateEncodingException e)
				{
					throw new IOException("Can't encode the certificate into ASN1 DER format", e);
				}
			}
			
			for (int i=0; i<der.length; i++)
				os.write(der[i]);
			os.flush();
		}
	}
	
	/**
	 * See {@link #savePEMKeystore(OutputStream, KeyStore, String, String, char[], char[], boolean)}
	 * with the last argument equal to false.
	 *  
	 * @param os where to write the encoded data to
	 * @param ks keystore to read from
	 * @param alias alias of the private key entry in the keystore
	 * @param encryptionAlg encryption algorithm to be used.
	 * See {@link #savePrivateKey(OutputStream, PrivateKey, Encoding, String, char[], boolean)} documentation
	 * for details about allowed values.
	 * @param keyPassword password of the private key in the keystore
	 * @param encryptionPassword encryption password to be used.
	 * @throws IOException if the data can not be written
	 * @throws KeyStoreException if the provided alias does not exist in the keystore 
	 * or if it does not correspond to the private key entry.
	 * @throws IllegalArgumentException if encriptionAlg is unsupported or alias is wrong
	 * @throws NoSuchAlgorithmException if algorithm is not known
	 * @throws UnrecoverableKeyException if key can not be recovered
	 */
	public static void savePEMKeystore(OutputStream os, KeyStore ks, String alias,
			String encryptionAlg, char[] keyPassword, char[] encryptionPassword) 
		throws IOException, KeyStoreException, IllegalArgumentException, UnrecoverableKeyException, NoSuchAlgorithmException
	{
		savePEMKeystore(os, ks, alias, encryptionAlg, keyPassword, encryptionPassword, false);
	}

	/**
	 * See {@link #savePEMKeystore(OutputStream, KeyStore, String, String, char[], char[], boolean)}.
	 * This method allows for using the CANL {@link X509Credential} instead of low level
	 * {@link KeyStore} as argument.
	 *  
	 * @param os where to write the encoded data to
	 * @param toSave CANL X509Credential to read from
	 * @param encryptionAlg encryption algorithm to be used.
	 * See {@link #savePrivateKey(OutputStream, PrivateKey, Encoding, String, char[], boolean)} documentation
	 * for details about allowed values.
	 * @param encryptionPassword encryption password to be used.
	 * @param opensslLegacyFormat if true the key is saved in the legacy openssl format. Otherwise a 
	 * PKCS #8 is used.
	 * @throws IOException if the data can not be written
	 * @throws KeyStoreException if the provided alias does not exist in the keystore 
	 * or if it does not correspond to the private key entry.
	 * @throws IllegalArgumentException if encriptionAlg is unsupported or alias is wrong
	 * @throws NoSuchAlgorithmException if algorithm is not known
	 * @throws UnrecoverableKeyException if key can not be recovered
	 */
	public static void savePEMKeystore(OutputStream os, X509Credential toSave,
			String encryptionAlg, char[] encryptionPassword, boolean opensslLegacyFormat) 
		throws IOException, KeyStoreException, IllegalArgumentException, UnrecoverableKeyException, NoSuchAlgorithmException
	{
		savePEMKeystore(os, toSave.getKeyStore(), toSave.getKeyAlias(), 
				encryptionAlg, toSave.getKeyPassword(), 
				encryptionPassword, opensslLegacyFormat);
	}
	
	/**
	 * Saves the chosen private key entry from the provided keystore as a plain 
	 * text PEM data. The produced PEM contains the private key first and then all
	 * certificates which are stored in the provided keystore under the given alias.
	 * The order from the keystore is preserved. The output stream is closed afterwards 
	 * only if the write operation was successful (there was no exception).  
	 *  
	 * @param os where to write the encoded data to
	 * @param ks keystore to read from
	 * @param alias alias of the private key entry in the keystore
	 * @param encryptionAlg encryption algorithm to be used.
	 * See {@link #savePrivateKey(OutputStream, PrivateKey, Encoding, String, char[], boolean)} documentation
	 * for details about allowed values.
	 * @param keyPassword password of the private key in the keystore
	 * @param encryptionPassword encryption password to be used.
	 * @param opensslLegacyFormat if true the key is saved in the legacy openssl format. Otherwise a 
	 * PKCS #8 is used.
	 * @throws IOException if the data can not be written
	 * @throws KeyStoreException if the provided alias does not exist in the keystore 
	 * or if it does not correspond to the private key entry.
	 * @throws IllegalArgumentException if encriptionAlg is unsupported or alias is wrong
	 * @throws NoSuchAlgorithmException if algorithm is not known
	 * @throws UnrecoverableKeyException if key can not be recovered
	 */
	public static void savePEMKeystore(OutputStream os, KeyStore ks, String alias,
			String encryptionAlg, char[] keyPassword, char[] encryptionPassword, boolean opensslLegacyFormat) 
		throws IOException, KeyStoreException, IllegalArgumentException, UnrecoverableKeyException, NoSuchAlgorithmException
	{
		Key k = ks.getKey(alias, keyPassword);
		if (k == null)
			throw new IllegalArgumentException("The specified alias does not correspond to any key entry");
		if (!(k instanceof PrivateKey))
			throw new IllegalArgumentException("The alias corresponds to a secret key, not to the private key");

		savePrivateKey(os, (PrivateKey)k, Encoding.PEM, encryptionAlg, encryptionPassword, opensslLegacyFormat);
		X509Certificate[] certs = convertToX509Chain(ks.getCertificateChain(alias));
		saveCertificateChain(os, certs, Encoding.PEM);
		os.close();
	}
	
	
	public static PasswordSupplier getPF(char[] password)
	{
		return (password == null) ? null : new CharArrayPasswordFinder(password);
	}
}
