/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.apksig.internal.util;

import com.android.apksig.internal.asn1.Asn1BerParser;
import com.android.apksig.internal.asn1.Asn1DecodingException;
import com.android.apksig.internal.asn1.Asn1DerEncoder;
import com.android.apksig.internal.asn1.Asn1EncodingException;
import com.android.apksig.internal.x509.Certificate;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;

/**
 * Provides methods to generate {@code X509Certificate}s from their encoded form. These methods
 * can be used to generate certificates that would be rejected by the Java {@code
 * CertificateFactory}.
 */
public class X509CertificateUtils {

    private static CertificateFactory sCertFactory = null;

    // The PEM certificate header and footer as specified in RFC 7468:
    //   There is exactly one space character (SP) separating the "BEGIN" or
    //   "END" from the label.  There are exactly five hyphen-minus (also
    //   known as dash) characters ("-") on both ends of the encapsulation
    //   boundaries, no more, no less.
    public static final byte[] BEGIN_CERT_HEADER = "-----BEGIN CERTIFICATE-----".getBytes();
    public static final byte[] END_CERT_FOOTER = "-----END CERTIFICATE-----".getBytes();

    private static void buildCertFactory() {
        if (sCertFactory != null) {
            return;
        }
        try {
            sCertFactory = CertificateFactory.getInstance("X.509");
        } catch (CertificateException e) {
            throw new RuntimeException("Failed to create X.509 CertificateFactory", e);
        }
    }

    /**
     * Generates an {@code X509Certificate} from the {@code InputStream}.
     *
     * @throws CertificateException if the {@code InputStream} cannot be decoded to a valid
     *                              certificate.
     */
    public static X509Certificate generateCertificate(InputStream in) throws CertificateException {
        byte[] encodedForm;
        try {
            encodedForm = ByteStreams.toByteArray(in);
        } catch (IOException e) {
            throw new CertificateException("Failed to parse certificate", e);
        }
        return generateCertificate(encodedForm);
    }

    /**
     * Generates an {@code X509Certificate} from the encoded form.
     *
     * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
     */
    public static X509Certificate generateCertificate(byte[] encodedForm)
            throws CertificateException {
        if (sCertFactory == null) {
            buildCertFactory();
        }
        return generateCertificate(encodedForm, sCertFactory);
    }

    /**
     * Generates an {@code X509Certificate} from the encoded form using the provided
     * {@code CertificateFactory}.
     *
     * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
     */
    public static X509Certificate generateCertificate(byte[] encodedForm,
            CertificateFactory certFactory) throws CertificateException {
        X509Certificate certificate;
        try {
            certificate = (X509Certificate) certFactory.generateCertificate(
                    new ByteArrayInputStream(encodedForm));
            return certificate;
        } catch (CertificateException e) {
            // This could be expected if the certificate is encoded using a BER encoding that does
            // not use the minimum number of bytes to represent the length of the contents; attempt
            // to decode the certificate using the BER parser and re-encode using the DER encoder
            // below.
        }
        try {
            // Some apps were previously signed with a BER encoded certificate that now results
            // in exceptions from the CertificateFactory generateCertificate(s) methods. Since
            // the original BER encoding of the certificate is used as the signature for these
            // apps that original encoding must be maintained when signing updated versions of
            // these apps and any new apps that may require capabilities guarded by the
            // signature. To maintain the same signature the BER parser can be used to parse
            // the certificate, then it can be re-encoded to its DER equivalent which is
            // accepted by the generateCertificate method. The positions in the ByteBuffer can
            // then be used with the GuaranteedEncodedFormX509Certificate object to ensure the
            // getEncoded method returns the original signature of the app.
            ByteBuffer encodedCertBuffer = getNextDEREncodedCertificateBlock(
                    ByteBuffer.wrap(encodedForm));
            int startingPos = encodedCertBuffer.position();
            Certificate reencodedCert = Asn1BerParser.parse(encodedCertBuffer, Certificate.class);
            byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
            certificate = (X509Certificate) certFactory.generateCertificate(
                    new ByteArrayInputStream(reencodedForm));
            // If the reencodedForm is successfully accepted by the CertificateFactory then copy the
            // original encoding from the ByteBuffer and use that encoding in the Guaranteed object.
            byte[] originalEncoding = new byte[encodedCertBuffer.position() - startingPos];
            encodedCertBuffer.position(startingPos);
            encodedCertBuffer.get(originalEncoding);
            GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
                    new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
            return guaranteedEncodedCert;
        } catch (Asn1DecodingException | Asn1EncodingException | CertificateException e) {
            throw new CertificateException("Failed to parse certificate", e);
        }
    }

    /**
     * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
     * InputStream}.
     *
     * @throws CertificateException if the InputStream cannot be decoded to zero or more valid
     *                              {@code Certificate} objects.
     */
    public static Collection<? extends java.security.cert.Certificate> generateCertificates(
            InputStream in) throws CertificateException {
        if (sCertFactory == null) {
            buildCertFactory();
        }
        return generateCertificates(in, sCertFactory);
    }

    /**
     * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
     * InputStream} using the provided {@code CertificateFactory}.
     *
     * @throws CertificateException if the InputStream cannot be decoded to zero or more valid
     *                              {@code Certificates} objects.
     */
    public static Collection<? extends java.security.cert.Certificate> generateCertificates(
            InputStream in, CertificateFactory certFactory) throws CertificateException {
        // Since the InputStream is not guaranteed to support mark / reset operations first read it
        // into a byte array to allow using the BER parser / DER encoder if it cannot be read by
        // the CertificateFactory.
        byte[] encodedCerts;
        try {
            encodedCerts = ByteStreams.toByteArray(in);
        } catch (IOException e) {
            throw new CertificateException("Failed to read the input stream", e);
        }
        try {
            return certFactory.generateCertificates(new ByteArrayInputStream(encodedCerts));
        } catch (CertificateException e) {
            // This could be expected if the certificates are encoded using a BER encoding that does
            // not use the minimum number of bytes to represent the length of the contents; attempt
            // to decode the certificates using the BER parser and re-encode using the DER encoder
            // below.
        }
        try {
            Collection<X509Certificate> certificates = new ArrayList<>(1);
            ByteBuffer encodedCertsBuffer = ByteBuffer.wrap(encodedCerts);
            while (encodedCertsBuffer.hasRemaining()) {
                ByteBuffer certBuffer = getNextDEREncodedCertificateBlock(encodedCertsBuffer);
                int startingPos = certBuffer.position();
                Certificate reencodedCert = Asn1BerParser.parse(certBuffer, Certificate.class);
                byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
                X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(
                        new ByteArrayInputStream(reencodedForm));
                byte[] originalEncoding = new byte[certBuffer.position() - startingPos];
                certBuffer.position(startingPos);
                certBuffer.get(originalEncoding);
                GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
                        new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
                certificates.add(guaranteedEncodedCert);
            }
            return certificates;
        } catch (Asn1DecodingException | Asn1EncodingException e) {
            throw new CertificateException("Failed to parse certificates", e);
        }
    }

    /**
     * Parses the provided ByteBuffer to obtain the next certificate in DER encoding. If the buffer
     * does not begin with the PEM certificate header then it is returned with the assumption that
     * it is already DER encoded. If the buffer does begin with the PEM certificate header then the
     * certificate data is read from the buffer until the PEM certificate footer is reached; this
     * data is then base64 decoded and returned in a new ByteBuffer.
     *
     * If the buffer is in PEM format then the position of the buffer is moved to the end of the
     * current certificate; if the buffer is already DER encoded then the position of the buffer is
     * not modified.
     *
     * @throws CertificateException if the buffer contains the PEM certificate header but does not
     *                              contain the expected footer.
     */
    private static ByteBuffer getNextDEREncodedCertificateBlock(ByteBuffer certificateBuffer)
            throws CertificateException {
        if (certificateBuffer == null) {
            throw new NullPointerException("The certificateBuffer cannot be null");
        }
        // if the buffer does not contain enough data for the PEM cert header then just return the
        // provided buffer.
        if (certificateBuffer.remaining() < BEGIN_CERT_HEADER.length) {
            return certificateBuffer;
        }
        certificateBuffer.mark();
        for (int i = 0; i < BEGIN_CERT_HEADER.length; i++) {
            if (certificateBuffer.get() != BEGIN_CERT_HEADER[i]) {
                certificateBuffer.reset();
                return certificateBuffer;
            }
        }
        StringBuilder pemEncoding = new StringBuilder();
        while (certificateBuffer.hasRemaining()) {
            char encodedChar = (char) certificateBuffer.get();
            // if the current character is a '-' then the beginning of the footer has been reached
            if (encodedChar == '-') {
                break;
            } else if (Character.isWhitespace(encodedChar)) {
                continue;
            } else {
                pemEncoding.append(encodedChar);
            }
        }
        // start from the second index in the certificate footer since the first '-' should have
        // been consumed above.
        for (int i = 1; i < END_CERT_FOOTER.length; i++) {
            if (!certificateBuffer.hasRemaining()) {
                throw new CertificateException(
                        "The provided input contains the PEM certificate header but does not "
                                + "contain sufficient data for the footer");
            }
            if (certificateBuffer.get() != END_CERT_FOOTER[i]) {
                throw new CertificateException(
                        "The provided input contains the PEM certificate header without a "
                                + "valid certificate footer");
            }
        }
        byte[] derEncoding = Base64.getDecoder().decode(pemEncoding.toString());
        // consume any trailing whitespace in the byte buffer
        int nextEncodedChar = certificateBuffer.position();
        while (certificateBuffer.hasRemaining()) {
            char trailingChar = (char) certificateBuffer.get();
            if (Character.isWhitespace(trailingChar)) {
                nextEncodedChar++;
            } else {
                break;
            }
        }
        certificateBuffer.position(nextEncodedChar);
        return ByteBuffer.wrap(derEncoding);
    }
}
