package org.bouncycastle.openpgp;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.ArrayList;
import java.util.Date;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

import org.bouncycastle.bcpg.*;
import org.bouncycastle.jce.interfaces.ElGamalPrivateKey;
import org.bouncycastle.jce.interfaces.ElGamalPublicKey;
import org.bouncycastle.jce.spec.ElGamalParameterSpec;
import org.bouncycastle.jce.spec.ElGamalPrivateKeySpec;

import sun.security.provider.DSAPrivateKey;

/**
 * general class to handle a PGP secret key object.
 */
public class PGPSecretKey
{
	private long 		keyID;
	private byte[]	fingerPrint;
	
    SecretKeyPacket	secret;
	ArrayList			ids;
	ArrayList			idSigs;
	PGPPublicKey		pub;
	
	PGPSecretKey(
		SecretKeyPacket	secret,
		MessageDigest	sha,
		ArrayList			ids,
		ArrayList			idSigs)
		throws IOException
	{
		this.secret = secret;
		this.ids = ids;
		this.idSigs = idSigs;
		this.pub = new PGPPublicKey(secret.getPublicKeyPacket(), sha, ids, idSigs);
	}
	
	public PGPSecretKey(
		int										certificationLevel,
		int										algorithm,
		PublicKey								pubKey,
		PrivateKey								privKey,
		Date										time,
		String									id,
		int										encAlgorithm,
		char[]									passPhrase,
		PGPSignatureSubpacketVector	hashedPcks,
		PGPSignatureSubpacketVector	unhashedPcks,
		SecureRandom						rand,
		String									provider)
		throws PGPException, NoSuchProviderException
	{
		PublicKeyPacket			pubPk;
		BCPGObject				secKey;
		MessageDigest 			sha;
		
		try
		{
			sha = MessageDigest.getInstance("SHA1");
		}
		catch (NoSuchAlgorithmException e)
		{
			throw new PGPException("can't find SHA1 digest");
		}
		
		if (pubKey instanceof RSAPublicKey)
		{
			RSAPublicKey		rK = (RSAPublicKey)pubKey;
			RSAPrivateCrtKey	rsK = (RSAPrivateCrtKey)privKey;
			
			pubPk = new PublicKeyPacket(algorithm, time, new RSAPublicBCPGKey(rK.getModulus(), rK.getPublicExponent()));
			secKey = new RSASecretBCPGKey(rsK.getPrivateExponent(), rsK.getPrimeP(), rsK.getPrimeQ());
		}
		else if (pubKey instanceof DSAPublicKey)
		{
			DSAPublicKey	dK = (DSAPublicKey)pubKey;
			DSAParams		dP = dK.getParams();
			DSAPrivateKey	dsK = (DSAPrivateKey)privKey;
			
			pubPk = new PublicKeyPacket(algorithm, time, new DSAPublicBCPGKey(dP.getP(), dP.getQ(), dP.getG(),  dK.getY()));
			secKey = new DSASecretBCPGKey(dsK.getX());
		}
		else if (pubKey instanceof ElGamalPublicKey)
		{
			ElGamalPublicKey			eK = (ElGamalPublicKey)pubKey;
			ElGamalParameterSpec	eS = eK.getParams();
			ElGamalPrivateKey			esK = (ElGamalPrivateKey)privKey;
			
			pubPk = new PublicKeyPacket(algorithm, time, new ElGamalPublicBCPGKey(eS.getP(), eS.getG(), eK.getY()));
			secKey = new ElGamalSecretBCPGKey(esK.getX());
		}
		else
		{
			throw new PGPException("unknown key class");
		}

		String	cName = PGPUtil.getSymmetricCipherName(encAlgorithm);
		
		if (cName == null)
		{
			throw new PGPException("null cipher specified");
		}
		
		Cipher										c = null;
		
		try
		{
			c = Cipher.getInstance(cName + "/CFB/NoPadding", provider);
		}
		catch (NoSuchProviderException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new PGPException("Exception creating cipher", e);
		}
		
		byte[]										iv = new byte[8];
		
		rand.nextBytes(iv);
		
		S2K											s2k = new S2K(HashAlgorithmTags.SHA1, iv, 0x60);

		try
		{
			ByteArrayOutputStream	bOut = new ByteArrayOutputStream();
			BCPGOutputStream			pOut = new BCPGOutputStream(bOut);
			
			SecretKey	key = PGPUtil.makeKeyFromPassPhrase(encAlgorithm, s2k, passPhrase, provider);

			c.init(Cipher.ENCRYPT_MODE, key, rand);
		
			iv = c.getIV();
			
			pOut.writeObject(secKey);
			
			byte[]	keyData = bOut.toByteArray();
			int		checkSum = 0;
			
			for (int i = 0; i != keyData.length; i++)
			{
				checkSum += keyData[i] & 0xff;
			}
			
			pOut.write(checkSum >> 8);
			pOut.write(checkSum);
			
			byte[]	encData = c.doFinal(bOut.toByteArray());

			this.secret = new SecretKeyPacket(pubPk, encAlgorithm, s2k, iv, encData);
			
			this.ids = new ArrayList();
			ids.add(id);
			this.idSigs = new ArrayList();
			
			this.pub = new PGPPublicKey(pubPk, sha, ids, idSigs);
			
			PGPSignatureGenerator	sGen = new PGPSignatureGenerator(pubPk.getAlgorithm(), HashAlgorithmTags.SHA1, provider);
			
			//
			// prepare the key for signing
			//
			byte[]	keyBytes = pubPk.getEncodedContents();

			sGen.initSign(certificationLevel, new PGPPrivateKey(privKey, pub.getKeyID()));
	
			sGen.update((byte)0x99);
			sGen.update((byte)(keyBytes.length >> 8));
			sGen.update((byte)(keyBytes.length));
			sGen.update(keyBytes);
			
			//
			// hash in the id
			//
			byte[]	idBytes = new byte[id.length()];
			
			for (int i = 0; i != idBytes.length; i++)
			{
				idBytes[i] = (byte)id.charAt(i);
			}
			
			sGen.update((byte)0xb4);
			sGen.update((byte)(idBytes.length >> 24));
			sGen.update((byte)(idBytes.length >> 16));
			sGen.update((byte)(idBytes.length >> 8));
			sGen.update((byte)(idBytes.length));
			sGen.update(idBytes);
			
			sGen.setHashedSubpackets(hashedPcks);
			sGen.setUnhashedSubpackets(unhashedPcks);
			
			ArrayList sigList = new ArrayList();
			
			sigList.add(sGen.generate());
			idSigs.add(sigList);
		}
		catch (PGPException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new PGPException("Exception encrypting key", e);
		}
	}

	/**
	 * Return the keyID associated with this key.
	 * 
	 * @return long
	 */
	public long getKeyID()
	{
		return pub.getKeyID();
	}
	
	/**
	 * Return the public key associated with this key.
	 * 
	 * @return PGPPublicKey
	 */
    public PGPPublicKey getPublicKey()
    {
    	return pub;
    }
    
    /**
     * Extract a PGPPrivate key from the SecretKey's encrypted contents.
     * 
     * @param passPhrase
     * @param provider
     * @return PGPPrivateKey
     * @throws PGPException
     * @throws NoSuchProviderException
     */
	public  PGPPrivateKey extractPrivateKey(
		char[]				passPhrase,
		String				provider)
		throws PGPException, NoSuchProviderException
	{
		PublicKeyPacket			pub = secret.getPublicKeyPacket();
		BCPGKey					sKey = null;
	
		String	cName = PGPUtil.getSymmetricCipherName(secret.getEncAlgorithm());
	
		if (cName == null)
		{
			throw new PGPException("null cipher specified");
		}
	
		Cipher						c = null;
	
		try
		{
			c = Cipher.getInstance(cName + "/CFB/NoPadding", provider);
		}
		catch (NoSuchProviderException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new PGPException("Exception creating cipher", e);
		}
	
		byte[]	encData = secret.getSecretKeyData();
		byte[]	data = null;
	
		if (c != null)
		{
			try
			{
				IvParameterSpec ivSpec = new IvParameterSpec(secret.getIV());

				SecretKey	key = PGPUtil.makeKeyFromPassPhrase(secret.getEncAlgorithm(), secret.getS2K(), passPhrase, provider);

				c.init(Cipher.DECRYPT_MODE, key, ivSpec);
			
				data = c.doFinal(encData, 0, encData.length);
			}
			catch (PGPException e)
			{
				throw e;
			}
			catch (Exception e)
			{
				throw new PGPException("Exception decrypting key", e);
			}
		}
		else
		{
			data = encData;
		}
	
		KeyFactory		fact;
		BCPGInputStream	in = new BCPGInputStream(new ByteArrayInputStream(data));
	
		try
		{
			switch (pub.getAlgorithm())
			{
			case PGPPublicKey.RSA_ENCRYPT:
			case PGPPublicKey.RSA_GENERAL:
				RSAPublicBCPGKey   		rsaPub = (RSAPublicBCPGKey)pub.getKey();
				RSASecretBCPGKey		rsaPriv = new RSASecretBCPGKey(in);
				RSAPrivateCrtKeySpec	rsaPrivSpec = new RSAPrivateCrtKeySpec(
													rsaPriv.getModulus(), 
													rsaPub.getPublicExponent(),
													rsaPriv.getPrivateExponent(),
													rsaPriv.getPrimeP(),
													rsaPriv.getPrimeQ(),
													rsaPriv.getPrimeExponentP(),
													rsaPriv.getPrimeExponentQ(),
													rsaPriv.getCrtCoefficient());
            						
				fact = KeyFactory.getInstance("RSA", provider);

				return new PGPPrivateKey(fact.generatePrivate(rsaPrivSpec), this.getKeyID());	
			case PGPPublicKey.DSA:
				DSAPublicBCPGKey  	dsaPub = (DSAPublicBCPGKey)pub.getKey();
				DSASecretBCPGKey   dsaPriv = new DSASecretBCPGKey(in);
				DSAPrivateKeySpec	dsaPrivSpec =
					new DSAPrivateKeySpec(dsaPriv.getX(), dsaPub.getP(), dsaPub.getQ(), dsaPub.getG());

				fact = KeyFactory.getInstance("DSA", provider);

				return new PGPPrivateKey(fact.generatePrivate(dsaPrivSpec), this.getKeyID());
			case PGPPublicKey.ELGAMAL_ENCRYPT:
			case PGPPublicKey.ELGAMAL_GENERAL:
				ElGamalPublicBCPGKey  	elPub = (ElGamalPublicBCPGKey)pub.getKey();
				ElGamalSecretBCPGKey	elPriv = new ElGamalSecretBCPGKey(in);
				ElGamalPrivateKeySpec	elSpec = new ElGamalPrivateKeySpec(elPriv.getX(), new ElGamalParameterSpec(elPub.getP(), elPub.getG()));
			
				fact = KeyFactory.getInstance("ElGamal", provider);
			
				return new PGPPrivateKey(fact.generatePrivate(elSpec), this.getKeyID());
			default:
				throw new PGPException("unknown public key algorithm encountered");
			}
		}
		catch (PGPException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new PGPException("Exception constructing key", e);
		}
	}
	
	public byte[] getEncoded() 
		throws IOException
	{
		ByteArrayOutputStream	bOut = new ByteArrayOutputStream();
		
		this.encode(bOut);
		
		return bOut.toByteArray();
	}
	
	public void encode(
		OutputStream	outStream) 
		throws IOException
	{
		BCPGOutputStream	out;
		
		if (outStream instanceof BCPGOutputStream)
		{
			out = (BCPGOutputStream)outStream;
		}
		else
		{
			out = new BCPGOutputStream(outStream);
		}

		out.writePacket(secret);
        
		for (int i = 0; i != ids.size(); i++)
		{
			String	id = (String)ids.get(i);
        	
			out.writePacket(new UserIDPacket(id));
        	
			ArrayList	sigs = (ArrayList)idSigs.get(i);
			
			for (int j = 0; j != sigs.size(); j++)
			{
				((PGPSignature)sigs.get(j)).encode(out);
			}
		}
    }
}