package org.bouncycastle.openpgp;

import java.io.InputStream;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchProviderException;

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

import org.bouncycastle.bcpg.BCPGInputStream;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.bcpg.InputStreamPacket;
import org.bouncycastle.bcpg.PacketTags;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.PublicKeyEncSessionPacket;
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket;
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;

/**
 * A public key encrypted data object.
 */
public class PGPPublicKeyEncryptedData
	implements PacketTags, PublicKeyAlgorithmTags, SymmetricKeyAlgorithmTags, HashAlgorithmTags
{
	PublicKeyEncSessionPacket			keyData;
	InputStreamPacket						encData;
	
	PGPPublicKeyEncryptedData(
		PublicKeyEncSessionPacket	keyData,
		InputStreamPacket				encData)
	{
		this.keyData = keyData;
		this.encData = encData;
	}
	
	private static Cipher getKeyCipher(
		int		algorithm,
		String	provider)
		throws NoSuchProviderException, PGPException
	{
		try
		{
			switch (algorithm)
			{
			case RSA_ENCRYPT:
			case RSA_GENERAL:
				return Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
			case ELGAMAL_ENCRYPT:
			case ELGAMAL_GENERAL:
				return Cipher.getInstance("ElGamal/ECB/PKCS1Padding", provider);
			default:
				throw new PGPException("unknown asymmetric algorithm: " + algorithm);
			}
		}
		catch (NoSuchProviderException e)
		{
			throw e;
		}
		catch (PGPException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new PGPException("Exception creating cipher", e);
		}
	}
	
	/**
	 * Return the keyID for the key used to encrypt the data.
	 * 
	 * @return long
	 */
	public long getKeyID()
	{
		return keyData.getKeyID();
	}
	
	/**
	 * Return the raw input stream for the data stream.
	 * 
	 * @return InputStream
	 */
	public InputStream getInputStream()
	{
		return encData.getInputStream();
	}
	
	/**
	 * Return the decrypted data stream for the packet.
	 * 
	 * @param privKey
	 * @param provider
	 * @return InputStream
	 * @throws PGPException
	 * @throws NoSuchProviderException
	 */
	public InputStream getDataStream(
		PGPPrivateKey		privKey,
		String					provider)
		throws PGPException, NoSuchProviderException
	{		
		Cipher	c1 = getKeyCipher(keyData.getAlgorithm(), provider);
		
		try
        {
            c1.init(Cipher.DECRYPT_MODE, privKey.getKey());
        }
        catch (InvalidKeyException e)
        {
            throw new PGPException("error setting asymmetric cipher", e);
        }
		
		BigInteger[]	keyD = keyData.getEncSessionKey();
			
		for (int i = 0; i != keyD.length; i++)
		{
			byte[]	bi = keyD[i].toByteArray();

			if (bi[0] == 0)
			{
				c1.update(bi, 1, bi.length - 1);
			}
			else
			{
				c1.update(bi);
			}
		}
		
		byte[] plain;
        try
        {
            plain = c1.doFinal();
        }
        catch (Exception e)
        {
            throw new PGPException("exception decrypting secret key", e);
        }
        
		Cipher c2;
        try
        {
			if (encData instanceof SymmetricEncIntegrityPacket)
			{
            	c2 =
                	Cipher.getInstance(
                    	PGPUtil.getSymmetricCipherName(plain[0]) + "/CFB/NoPadding",
                   	 	provider);
			}
			else
			{
				c2 =
					Cipher.getInstance(
						PGPUtil.getSymmetricCipherName(plain[0]) + "/PGPCFBwithIV/NoPadding",
						provider);
			}
        }
        catch (NoSuchProviderException e)
        {
           throw e;
        }
        catch (PGPException e)
        {
			throw e;
        }
        catch (Exception e)
        {
			throw new PGPException("exception creating cipher", e);
        }
		
		if (c2 != null)
		{
			try
			{
				SecretKey	key = new SecretKeySpec(plain, 1, plain.length - 3, PGPUtil.getSymmetricCipherName(plain[0]));
				BCPGInputStream	encStream = encData.getInputStream();
				byte[]				iv = new byte[c2.getBlockSize()];
				
				if (encData instanceof SymmetricEncIntegrityPacket)
				{
					c2.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
					
					encStream = new BCPGInputStream(new CipherInputStream(encStream, c2));
					encStream.readFully(iv);
					encStream.read();
					encStream.read();
					
					return encStream;
				}
				else
				{
					c2.init(Cipher.DECRYPT_MODE, key);
					
					return new CipherInputStream(encStream, c2);
				}
			}
			catch (Exception e)
			{
				throw new PGPException("Exception starting decryption", e);
			}
		}
		else
		{
			return encData.getInputStream();
		}
	}
}
