package org.bouncycastle.cms;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CRLException;
import org.bouncycastle.jce.cert.CertStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import org.bouncycastle.jce.cert.CollectionCertStoreParameters;
import java.util.ArrayList;
import java.util.Enumeration;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.ASN1Set;
import org.bouncycastle.asn1.cms.ContentInfo;
import org.bouncycastle.asn1.cms.SignedData;
import org.bouncycastle.asn1.cms.SignerInfo;

/**
 * general class for handling a pkcs7-signature message.
 *
 * A simple example of usage - note, in the example below the validity of
 * the certificate isn't verified, just the fact that one of the certs 
 * matches the given signer...
 *
 * <pre>
 *  CertStore               certs = s.getCertificatesAndCRLs("Collection", "BC");
 *  SignerInformationStore  signers = s.getSignerInfos();
 *  Collection              c = signers.getSigners();
 *  Iterator                it = c.iterator();
 *  
 *  while (it.hasNext())
 *  {
 *      SignerInformation   signer = (SignerInformation)it.next();
 *      Collection          certCollection = certs.getCertificates(signer.getSID());
 *  
 *      Iterator        certIt = certCollection.iterator();
 *      X509Certificate cert = (X509Certificate)certIt.next();
 *  
 *      if (signer.verify(cert.getPublicKey()))
 *      {
 *          verified++;
 *      }   
 *  }
 * </pre>
 */
public class CMSSignedData
{
    SignedData              signedData;
    ContentInfo             contentInfo;
    CMSProcessable          signedContent;
    CertStore               certStore;
    SignerInformationStore  signerInfoStore;

    private static ContentInfo readContentInfo(
        InputStream envelopedData)
        throws CMSException
    {
        try
        {
            ASN1InputStream in = new ASN1InputStream(envelopedData);

            return ContentInfo.getInstance(in.readObject());
        }
        catch (IOException e)
        {
            throw new CMSException("IOException reading content.", e);
        }
    }

	public CMSSignedData(
        byte[]      sigBlock)
        throws CMSException
    {
        this(readContentInfo(new ByteArrayInputStream(sigBlock)));
    }

	public CMSSignedData(
        CMSProcessable  signedContent,
        byte[]          sigBlock)
        throws CMSException
    {
        this(signedContent, readContentInfo(new ByteArrayInputStream(sigBlock)));
    }

    /**
     * base constructor
     *
     * @param signedContent the content that was signed.
     * @param sigData the signature object.
     */
	public CMSSignedData(
        CMSProcessable  signedContent,
        InputStream     sigData)
        throws CMSException
    {
        this(signedContent, readContentInfo(sigData));
    }

    /**
     * base constructor - with encapsulated content
     */
	public CMSSignedData(
        InputStream sigData)
        throws CMSException
    {
        this(readContentInfo(sigData));
    }

    public CMSSignedData(
        CMSProcessable  signedContent,
        ContentInfo     sigData)
    {
		this.signedContent = signedContent;
        this.contentInfo = sigData;
        this.signedData = SignedData.getInstance(contentInfo.getContent());
	}

    public CMSSignedData(
        ContentInfo sigData)
    {
        this.contentInfo = sigData;
        this.signedData = SignedData.getInstance(contentInfo.getContent());

        //
        // this can happen if the signed message is sent simply to send a
        // certificate chain.
        //
        if (signedData.getEncapContentInfo().getContent() != null)
        {
            this.signedContent = new CMSProcessableByteArray(
                    ((ASN1OctetString)(signedData.getEncapContentInfo()
                                                .getContent())).getOctets());
        }
        else
        {
            this.signedContent = null;
        }
	}

    /**
     * return the collection of signers that are associated with the
     * signatures for the message.
     */
    public SignerInformationStore getSignerInfos()
    {
        if (signerInfoStore == null)
        {
            ASN1Set     	s = signedData.getSignerInfos();
            ArrayList   	signerInfos = new ArrayList();

            for (int i = 0; i != s.size(); i++)
            {
                signerInfos.add(new SignerInformation(SignerInfo.getInstance(s.getObjectAt(i)), signedData.getEncapContentInfo().getContentType(), signedContent));
            }

            signerInfoStore = new SignerInformationStore(signerInfos);
        }

        return signerInfoStore;
    }

    /**
     * return a CertStore containing the certificates and CRLs associated with
     * this message.
     *
     * @exception NoProviderException if the provider requested isn't available.
     * @exception NoSuchAlgorithmException if the cert store isn't available.
     */
    public CertStore getCertificatesAndCRLs(
        String  type,
        String  provider)
        throws NoSuchAlgorithmException, NoSuchProviderException, CMSException
    {
        if (certStore == null)
        {
            ArrayList               certsAndcrls = new ArrayList();
            ByteArrayOutputStream   bOut = new ByteArrayOutputStream();
            ASN1OutputStream        aOut = new ASN1OutputStream(bOut);
            CertificateFactory      cf;

            try
            {
                cf = CertificateFactory.getInstance("X.509", provider);
            }
            catch (CertificateException ex)
            {
                throw new CMSException("can't get certificate factory.", ex);
            }

            //
            // load the certificates and revocation lists if we have any
            //
            ASN1Set s = signedData.getCertificates();

            if (s != null)
            {
                Enumeration e = s.getObjects();

                while (e.hasMoreElements())
                {
                    try
                    {
                        aOut.writeObject(e.nextElement());

                        certsAndcrls.add(cf.generateCertificate(
                            new ByteArrayInputStream(bOut.toByteArray())));
                    }
                    catch (IOException ex)
                    {
                        throw new CMSException(
                                "can't re-encode certificate!", ex);
                    }
                    catch (CertificateException ex)
                    {
                        throw new CMSException(
                                "can't re-encode certificate!", ex);
                    }

                    bOut.reset();
                }
            }

            s = signedData.getCRLs();

            if (s != null)
            {
                Enumeration e = s.getObjects();

                while (e.hasMoreElements())
                {
                    try
                    {
                        aOut.writeObject(e.nextElement());

                        certsAndcrls.add(cf.generateCRL(
                            new ByteArrayInputStream(bOut.toByteArray())));
                    }
                    catch (IOException ex)
                    {
                        throw new CMSException("can't re-encode CRL!", ex);
                    }
                    catch (CRLException ex)
                    {
                        throw new CMSException("can't re-encode CRL!", ex);
                    }

                    bOut.reset();
                }
            }

            try
            {
                certStore = CertStore.getInstance(type, 
                    new CollectionCertStoreParameters(certsAndcrls), provider);
            }
            catch (InvalidAlgorithmParameterException e)
            {
                throw new CMSException("can't setup the CertStore", e);
            }
        }

        return certStore;
    }

    /**
     * Return the a string representation of the OID associated with the
     * encapsulated content info structure carried in the signed data.
     * 
     * @return the OID for the content type.
     */
    public String getSignedContentTypeOID()
    {
        return signedData.getEncapContentInfo().getContentType().getId();
    }

    public CMSProcessable getSignedContent()
    {
        return signedContent;
    }

    /**
     * return the ASN.1 encoded representation of this object.
     */
    public byte[] getEncoded()
        throws IOException
    {
        ByteArrayOutputStream   bOut = new ByteArrayOutputStream();
        ASN1OutputStream        aOut = new ASN1OutputStream(bOut);

        aOut.writeObject(contentInfo);

        return bOut.toByteArray();
    }
}
