// --- BEGIN COPYRIGHT BLOCK ---
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// (C) 2007 Red Hat, Inc.
// All rights reserved.
// --- END COPYRIGHT BLOCK ---
package org.mozilla.jss.netscape.security.pkcs;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Vector;

import org.mozilla.jss.netscape.security.util.BigInt;
import org.mozilla.jss.netscape.security.util.DerInputStream;
import org.mozilla.jss.netscape.security.util.DerOutputStream;
import org.mozilla.jss.netscape.security.util.DerValue;
import org.mozilla.jss.netscape.security.util.ObjectIdentifier;
import org.mozilla.jss.netscape.security.util.Utils;
import org.mozilla.jss.netscape.security.x509.AlgorithmId;
import org.mozilla.jss.netscape.security.x509.X500Name;
import org.mozilla.jss.netscape.security.x509.X509CertImpl;

/**
 * PKCS7 as defined in RSA Laboratories PKCS7 Technical Note. Profile
 * Supports only <code>SignedData</code> ContentInfo
 * type, where to the type of data signed is plain Data.
 * For signedData, <code>crls</code>, <code>attributes</code> and
 * PKCS#6 Extended Certificates are not supported.
 *
 * @version 1.33 97/12/10
 * @author Benjamin Renaud
 */
public class PKCS7 {

    public final static String HEADER = "-----BEGIN PKCS7-----";
    public final static String FOOTER = "-----END PKCS7-----";

    private ObjectIdentifier contentType;

    // the ASN.1 members for a signedData (and other) contentTypes
    private BigInt version;
    private AlgorithmId[] digestAlgorithmIds;
    private ContentInfo contentInfo;
    private X509Certificate[] certificates;
    private SignerInfo[] signerInfos;

    /**
     * Unmarshals a PKCS7 block from its encoded form, parsing the
     * encoded bytes from the InputStream.
     *
     * @param in an input stream holding at least one PKCS7 block.
     * @exception ParsingException on parsing errors.
     * @exception IOException on other errors.
     */
    public PKCS7(InputStream in) throws ParsingException, IOException {
        DataInputStream dis = new DataInputStream(in);

        int len = 0;
        byte[] newbuf = new byte[len];
        byte[] oldbuf = new byte[len];
        byte[] data = new byte[len];

        do {
            newbuf = new byte[dis.available()];
            len += dis.available();
            dis.readFully(newbuf);
            data = new byte[len];

            System.arraycopy(oldbuf, 0, data, 0, oldbuf.length);
            System.arraycopy(newbuf, 0, data, oldbuf.length, newbuf.length);
            oldbuf = new byte[len];
            System.arraycopy(data, 0, oldbuf, 0, data.length);

        } while (dis.available() > 0);

        parse(new DerInputStream(data));
    }

    /**
     * Unmarshals a PKCS7 block from its encoded form, parsing the
     * encoded bytes from the DerInputStream.
     *
     * @param derin a DerInputStream holding at least one PKCS7 block.
     * @exception ParsingException on parsing errors.
     */
    public PKCS7(DerInputStream derin) throws ParsingException {
        parse(derin);
    }

    /**
     * Unmarshals a PKCS7 block from its encoded form, parsing the
     * encoded bytes.
     *
     * @param bytes the encoded bytes.
     * @exception ParsingException on parsing errors.
     */
    public PKCS7(byte[] bytes) throws ParsingException {
        DerInputStream derin = new DerInputStream(bytes);
        parse(derin);
    }

    /**
     * Unmarshals a PKCS7 block from PEM format.
     *
     * @param input the PKCS7 block in PEM format.
     * @exception ParsingException on parsing errors.
     */
    public PKCS7(String input) throws ParsingException, IOException {

        StringBuilder sb = new StringBuilder();

        try (StringReader sr = new StringReader(input.trim());
                BufferedReader in = new BufferedReader(sr)) {

            String line;

            // skip everything up to header
            String header = null;
            while ((line = in.readLine()) != null) {
                if (HEADER.equals(line)) {
                    header = line;
                    break;
                }
            }

            if (header == null) {
                throw new ParsingException("Missing PKCS #7 header");
            }

            // store everything up to footer
            String footer = null;
            while ((line = in.readLine()) != null) {
                if (FOOTER.equals(line)) {
                    footer = line;
                    break;
                }
                sb.append(line);
            }

            if (footer == null) {
                throw new ParsingException("Missing PKCS #7 footer");
            }
        }

        byte[] bytes = Utils.base64decode(sb.toString());
        parse(new DerInputStream(bytes));
    }

    private void parse(DerInputStream derin) throws ParsingException {
        try {
            ContentInfo contentInfo = new ContentInfo(derin);
            contentType = contentInfo.contentType;
            if (contentType.equals(ContentInfo.SIGNED_DATA_OID)) {
                parseSignedData(contentInfo.getContent());
            } else {
                throw new ParsingException("content type " + contentType +
                        " not supported.");
            }
        } catch (IOException e) {
            ParsingException pe =
                    new ParsingException("IOException: " + e.getMessage());
            pe.fillInStackTrace();
            throw pe;
        }
    }

    /**
     * Construct an initialized PKCS7 block.
     *
     * @param digestAlgorithmIds the message digest algorithm identifiers.
     * @param contentInfo the content information.
     * @param certificates an array of X.509 certificates.
     * @param signerInfos an array of signer information.
     */
    public PKCS7(AlgorithmId[] digestAlgorithmIds,
            ContentInfo contentInfo,
            X509Certificate[] certificates,
            SignerInfo[] signerInfos) {

        version = new BigInt(1);
        this.digestAlgorithmIds = digestAlgorithmIds;
        this.contentInfo = contentInfo;
        this.certificates = certificates;
        this.signerInfos = signerInfos;
    }

    /**
     * Construct PKCS7 from an array of certificates.
     *
     * @param certs Array of certificates.
     */
    public PKCS7(X509Certificate[] certs) {
        this(new AlgorithmId[0], new ContentInfo(new byte[0]), certs, new SignerInfo[0]);
    }

    private void parseSignedData(DerValue val)
            throws ParsingException, IOException {

        DerInputStream dis = val.toDerInputStream();

        // Version
        version = dis.getInteger();

        // digestAlgorithmIds
        DerValue[] digestAlgorithmIdVals = dis.getSet(1);
        int len = digestAlgorithmIdVals.length;
        digestAlgorithmIds = new AlgorithmId[len];
        try {
            for (int i = 0; i < len; i++) {
                DerValue oid = digestAlgorithmIdVals[i];
                digestAlgorithmIds[i] = AlgorithmId.parse(oid);
            }

        } catch (IOException e) {
            ParsingException pe =
                    new ParsingException("Error parsing digest AlgorithmId IDs: " +
                            e.getMessage());
            pe.fillInStackTrace();
            throw pe;
        }
        // contentInfo
        contentInfo = new ContentInfo(dis);

        /*
         * check if certificates (implicit tag) are provided
         * (certificates are OPTIONAL)
         */
        if ((byte) (dis.peekByte()) == (byte) 0xA0) {
            DerValue[] certificateVals = dis.getSet(2, true);

            len = certificateVals.length;
            certificates = new X509Certificate[len];

            for (int i = 0; i < len; i++) {
                try {
                    X509Certificate cert = new
                                           X509CertImpl(certificateVals[i]);
                    certificates[i] = cert;
                } catch (CertificateException e) {
                    ParsingException pe =
                            new ParsingException("CertificateException: " +
                                    e.getMessage());
                    pe.fillInStackTrace();
                    throw pe;
                }
            }
        }

        // check if crls (implicit tag) are provided (crls are OPTIONAL)
        if ((byte) (dis.peekByte()) == (byte) 0xA1) {
            dis.getSet(0, true);
        }

        // signerInfos
        DerValue[] signerInfoVals = dis.getSet(1);

        len = signerInfoVals.length;
        signerInfos = new SignerInfo[len];

        for (int i = 0; i < len; i++) {
            DerInputStream in = signerInfoVals[i].toDerInputStream();
            signerInfos[i] = new SignerInfo(in);
        }

    }

    /**
     * Encodes the signed data to an output stream.
     *
     * @param out the output stream to write the encoded data to.
     * @exception IOException on encoding errors.
     */
    public void encodeSignedData(OutputStream out) throws IOException {
        DerOutputStream derout = new DerOutputStream();
        encodeSignedData(derout, true);
        out.write(derout.toByteArray());
    }

    /**
     * Like method above but not sorted.
     */
    public void encodeSignedData(OutputStream out, boolean sort)
            throws IOException {
        DerOutputStream derout = new DerOutputStream();
        encodeSignedData(derout, sort);
        out.write(derout.toByteArray());
    }

    /**
     * encode signed data, sort certs by default.
     */
    public void encodeSignedData(DerOutputStream out)
            throws IOException {
        encodeSignedData(out, true);
    }

    /**
     * Encodes the signed data to a DerOutputStream.
     *
     * @param out the DerOutputStream to write the encoded data to.
     * @exception IOException on encoding errors.
     */
    public void encodeSignedData(DerOutputStream out, boolean sort)
            throws IOException {

        DerOutputStream signedData = new DerOutputStream();

        // version
        signedData.putInteger(version);

        // digestAlgorithmIds
        signedData.putOrderedSetOf(DerValue.tag_Set, digestAlgorithmIds);

        // contentInfo
        contentInfo.encode(signedData);

        // cast to X509CertImpl[] since X509CertImpl implements DerEncoder
        X509CertImpl implCerts[] = new X509CertImpl[certificates.length];
        try {
            for (int i = 0; i < certificates.length; i++) {
                implCerts[i] = (X509CertImpl) certificates[i];
            }
        } catch (ClassCastException e) {
            throw new IOException(
                    "Certificates in PKCS7 must be of class " +
                    "org.mozilla.jss.netscape.security.X509CertImpl: " + e.getMessage(), e);
        }

        // Add the certificate set (tagged with [0] IMPLICIT)
        // to the signed data
        if (sort) {
            signedData.putOrderedSetOf((byte) 0xA0, implCerts);
        } else {
            signedData.putSet((byte) 0xA0, implCerts);
        }

        // no crls (OPTIONAL field)

        // signerInfos
        signedData.putOrderedSetOf(DerValue.tag_Set, signerInfos);

        // making it a signed data block
        DerValue signedDataSeq = new DerValue(DerValue.tag_Sequence,
                          signedData.toByteArray());

        // making it a content info sequence
        ContentInfo block = new ContentInfo(ContentInfo.SIGNED_DATA_OID,
                        signedDataSeq);

        // writing out the contentInfo sequence
        block.encode(out);
    }

    /**
     * This verifies a given SignerInfo.
     *
     * @param info the signer information.
     * @param bytes the DER encoded content information.
     *
     * @exception NoSuchAlgorithmException on unrecognized algorithms.
     * @exception SignatureException on signature handling errors.
     */
    public SignerInfo verify(SignerInfo info, byte[] bytes)
            throws NoSuchAlgorithmException, SignatureException {
        return info.verify(this, bytes);
    }

    /**
     * Returns all signerInfos which self-verify.
     *
     * @param bytes the DER encoded content information.
     *
     * @exception NoSuchAlgorithmException on unrecognized algorithms.
     * @exception SignatureException on signature handling errors.
     */
    public SignerInfo[] verify(byte[] bytes)
            throws NoSuchAlgorithmException, SignatureException {

        Vector<SignerInfo> intResult = new Vector<>();
        for (int i = 0; i < signerInfos.length; i++) {

            SignerInfo signerInfo = verify(signerInfos[i], bytes);
            if (signerInfo != null) {
                intResult.addElement(signerInfo);
            }
        }
        if (intResult.size() != 0) {

            SignerInfo[] result = new SignerInfo[intResult.size()];
            intResult.copyInto(result);
            return result;
        }
        return null;
    }

    /**
     * Returns all signerInfos which self-verify.
     *
     * @exception NoSuchAlgorithmException on unrecognized algorithms.
     * @exception SignatureException on signature handling errors.
     */
    public SignerInfo[] verify()
            throws NoSuchAlgorithmException, SignatureException {
        return verify(null);
    }

    /**
     * Returns the version number of this PKCS7 block.
     */
    public BigInt getVersion() {
        return version;
    }

    /**
     * Returns the message digest algorithms specified in this PKCS7 block.
     */
    public AlgorithmId[] getDigestAlgorithmIds() {
        return digestAlgorithmIds;
    }

    /**
     * Returns the content information specified in this PKCS7 block.
     */
    public ContentInfo getContentInfo() {
        return contentInfo;
    }

    /**
     * Returns the X.509 certificates listed in this PKCS7 block.
     */
    public X509Certificate[] getCertificates() {
        return certificates;
    }

    /**
     * Returns the signer's information specified in this PKCS7 block.
     */
    public SignerInfo[] getSignerInfos() {
        return signerInfos;
    }

    /**
     * Returns the X.509 certificate listed in this PKCS7 block
     * which has a matching serial number and Issuer name, or
     * null if one is not found.
     *
     * @param serial the serial number of the certificate to retrieve.
     * @param name the Distinguished Name of the Issuer.
     */
    public X509Certificate getCertificate(BigInt serial, X500Name name) {

        for (int i = 0; i < certificates.length; i++) {
            X509Certificate cert = certificates[i];
            X500Name thisName = (X500Name) cert.getIssuerDN();
            BigInteger tmpSerial = cert.getSerialNumber();
            BigInt thisSerial = new BigInt(tmpSerial);
            if (serial.equals(thisSerial) && name.equals(thisName)) {
                return cert;
            }
        }
        return null;
    }

    public byte[] getBytes() throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        encodeSignedData(out);
        return out.toByteArray();
    }

    public String toPEMString() throws IOException {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw, true);
        pw.println(HEADER);
        pw.print(Utils.base64encode(getBytes(), true));
        pw.println(FOOTER);
        return sw.toString();
    }

    /**
     * Returns the PKCS7 block in a printable string form.
     */
    @Override
    public String toString() {
        StringBuffer out = new StringBuffer("PKCS7 :: version: " + version + "\n");

        out.append("PKCS7 :: digest AlgorithmIds: " + "\n");
        for (int i = 0; i < digestAlgorithmIds.length; i++) {
            out.append("\t" + digestAlgorithmIds[i] + "\n");
        }
        out.append(contentInfo + "\n");
        out.append("PKCS7 :: certificates: " + "\n");
        for (int i = 0; i < certificates.length; i++) {
            out.append("\t" + i + ".   " + certificates[i] + "\n");
        }
        out.append("PKCS7 :: signer infos: " + "\n");
        for (int i = 0; i < signerInfos.length; i++) {
            out.append("\t" + i + ".  " + signerInfos[i] + "\n");
        }
        return out.toString();
    }
}
