package net.lightbody.bmp.mitm.tools;

import com.google.common.net.InetAddresses;

import net.lightbody.bmp.mitm.CertificateAndKey;
import net.lightbody.bmp.mitm.CertificateInfo;
import net.lightbody.bmp.mitm.exception.CertificateCreationException;
import net.lightbody.bmp.mitm.exception.ExportException;
import net.lightbody.bmp.mitm.exception.ImportException;
import net.lightbody.bmp.mitm.util.EncryptionUtil;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.bc.BcX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMEncryptor;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;

import javax.net.ssl.KeyManager;

public class BouncyCastleSecurityProviderTool implements SecurityProviderTool {
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * The size of certificate serial numbers, in bits.
     */
    private static final int CERTIFICATE_SERIAL_NUMBER_SIZE = 160;

    @Override
    public CertificateAndKey createServerCertificate(CertificateInfo certificateInfo,
                                                     X509Certificate caRootCertificate,
                                                     PrivateKey caPrivateKey,
                                                     KeyPair serverKeyPair,
                                                     String messageDigest) {
        // make sure certificateInfo contains all fields necessary to generate the certificate
        if (certificateInfo.getCommonName() == null) {
            throw new IllegalArgumentException("Must specify CN for server certificate");
        }

        if (certificateInfo.getNotBefore() == null) {
            throw new IllegalArgumentException("Must specify Not Before for server certificate");
        }

        if (certificateInfo.getNotAfter() == null) {
            throw new IllegalArgumentException("Must specify Not After for server certificate");
        }

        // create the subject for the new server certificate. when impersonating an upstream server, this should contain
        // the hostname of the server we are trying to impersonate in the CN field
        X500Name serverCertificateSubject = createX500NameForCertificate(certificateInfo);

        // get the algorithm that will be used to sign the new certificate, which is a combination of the message digest
        // and the digital signature from the CA's private key
        String signatureAlgorithm = EncryptionUtil.getSignatureAlgorithm(messageDigest, caPrivateKey);

        // get a ContentSigner with our CA private key that will be used to sign the new server certificate
        ContentSigner signer = getCertificateSigner(caPrivateKey, signatureAlgorithm);

        // generate a serial number for the new certificate. serial numbers only need to be unique within our
        // certification authority; a large random integer will satisfy that requirement.
        BigInteger serialNumber = EncryptionUtil.getRandomBigInteger(CERTIFICATE_SERIAL_NUMBER_SIZE);

        // create the X509Certificate using Bouncy Castle. the BC X509CertificateHolder can be converted to a JCA X509Certificate.
        X509CertificateHolder certificateHolder;
        try {
            certificateHolder = new JcaX509v3CertificateBuilder(caRootCertificate,
                    serialNumber,
                    certificateInfo.getNotBefore(),
                    certificateInfo.getNotAfter(),
                    serverCertificateSubject,
                    serverKeyPair.getPublic())
                    .addExtension(Extension.subjectAlternativeName, false, getDomainNameSANsAsASN1Encodable(certificateInfo.getSubjectAlternativeNames()))
                    .addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(serverKeyPair.getPublic()))
                    .addExtension(Extension.basicConstraints, false, new BasicConstraints(false))
                    .build(signer);
        } catch (CertIOException e) {
            throw new CertificateCreationException("Error creating new server certificate", e);
        }

        // convert the Bouncy Castle certificate holder into a JCA X509Certificate
        X509Certificate serverCertificate = convertToJcaCertificate(certificateHolder);

        return new CertificateAndKey(serverCertificate, serverKeyPair.getPrivate());
    }

    @Override
    public KeyStore createServerKeyStore(String keyStoreType, CertificateAndKey serverCertificateAndKey, X509Certificate rootCertificate, String privateKeyAlias, String password) {
        throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method");
    }

    @Override
    public KeyStore createRootCertificateKeyStore(String keyStoreType, CertificateAndKey rootCertificateAndKey, String privateKeyAlias, String password) {
        throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method");
    }

    @Override
    public CertificateAndKey createCARootCertificate(CertificateInfo certificateInfo,
                                                     KeyPair keyPair,
                                                     String messageDigest) {
        if (certificateInfo.getNotBefore() == null) {
            throw new IllegalArgumentException("Must specify Not Before for server certificate");
        }

        if (certificateInfo.getNotAfter() == null) {
            throw new IllegalArgumentException("Must specify Not After for server certificate");
        }

        // create the X500Name that will be both the issuer and the subject of the new root certificate
        X500Name issuer = createX500NameForCertificate(certificateInfo);

        BigInteger serial = EncryptionUtil.getRandomBigInteger(CERTIFICATE_SERIAL_NUMBER_SIZE);

        PublicKey rootCertificatePublicKey = keyPair.getPublic();

        String signatureAlgorithm = EncryptionUtil.getSignatureAlgorithm(messageDigest, keyPair.getPrivate());

        // this is a CA root certificate, so it is self-signed
        ContentSigner selfSigner = getCertificateSigner(keyPair.getPrivate(), signatureAlgorithm);

        ASN1EncodableVector extendedKeyUsages = new ASN1EncodableVector();
        extendedKeyUsages.add(KeyPurposeId.id_kp_serverAuth);
        extendedKeyUsages.add(KeyPurposeId.id_kp_clientAuth);
        extendedKeyUsages.add(KeyPurposeId.anyExtendedKeyUsage);

        X509CertificateHolder certificateHolder;
        try {
            certificateHolder = new JcaX509v3CertificateBuilder(
                    issuer,
                    serial,
                    certificateInfo.getNotBefore(),
                    certificateInfo.getNotAfter(),
                    issuer,
                    rootCertificatePublicKey)
                    .addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(rootCertificatePublicKey))
                    .addExtension(Extension.basicConstraints, true, new BasicConstraints(true))
                    .addExtension(Extension.keyUsage, false, new KeyUsage(
                            KeyUsage.keyCertSign
                                    | KeyUsage.digitalSignature
                                    | KeyUsage.keyEncipherment
                                    | KeyUsage.dataEncipherment
                                    | KeyUsage.cRLSign))
                    .addExtension(Extension.extendedKeyUsage, false, new DERSequence(extendedKeyUsages))
                    .build(selfSigner);
        } catch (CertIOException e) {
            throw new CertificateCreationException("Error creating root certificate", e);
        }

        // convert the Bouncy Castle X590CertificateHolder to a JCA cert
        X509Certificate cert = convertToJcaCertificate(certificateHolder);

        return new CertificateAndKey(cert, keyPair.getPrivate());
    }

    @Override
    public String encodePrivateKeyAsPem(PrivateKey privateKey, String passwordForPrivateKey, String encryptionAlgorithm) {
        if (passwordForPrivateKey == null) {
            throw new IllegalArgumentException("You must specify a password when serializing a private key");
        }

        PEMEncryptor encryptor = new JcePEMEncryptorBuilder(encryptionAlgorithm)
                .build(passwordForPrivateKey.toCharArray());

        return encodeObjectAsPemString(privateKey, encryptor);
    }

    @Override
    public String encodeCertificateAsPem(Certificate certificate) {
        return encodeObjectAsPemString(certificate, null);
    }

    @Override
    public PrivateKey decodePemEncodedPrivateKey(Reader privateKeyReader, String password) {
        try (PEMParser pemParser = new PEMParser(privateKeyReader)) {
            Object keyPair = pemParser.readObject();

            // retrieve the PrivateKeyInfo from the returned keyPair object. if the key is encrypted, it needs to be
            // decrypted using the specified password first.
            PrivateKeyInfo keyInfo;
            if (keyPair instanceof PEMEncryptedKeyPair) {
                if (password == null) {
                    throw new ImportException("Unable to import private key. Key is encrypted, but no password was provided.");
                }

                PEMDecryptorProvider decryptor = new JcePEMDecryptorProviderBuilder().build(password.toCharArray());

                PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptor);

                keyInfo = decryptedKeyPair.getPrivateKeyInfo();
            } else {
                keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
            }

            return new JcaPEMKeyConverter().getPrivateKey(keyInfo);
        } catch (IOException e) {
            throw new ImportException("Unable to read PEM-encoded PrivateKey", e);
        }
    }

    @Override
    public X509Certificate decodePemEncodedCertificate(Reader certificateReader) {
        // JCA provides this functionality already, but it can be easily implemented using BC as well
        throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method");
    }

    @Override
    public KeyStore loadKeyStore(File file, String keyStoreType, String password) {
        throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method");
    }

    @Override
    public void saveKeyStore(File file, KeyStore keyStore, String keystorePassword) {
        throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method");
    }

    @Override
    public KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword) {
        return new KeyManager[0];
    }


    /**
     * Creates an X500Name based on the specified certificateInfo.
     *
     * @param certificateInfo information to populate the X500Name with
     * @return a new X500Name object for use as a subject or issuer
     */
    private static X500Name createX500NameForCertificate(CertificateInfo certificateInfo) {
        X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE);

        if (certificateInfo.getCommonName() != null) {
            x500NameBuilder.addRDN(BCStyle.CN, certificateInfo.getCommonName());
        }

        if (certificateInfo.getOrganization() != null) {
            x500NameBuilder.addRDN(BCStyle.O, certificateInfo.getOrganization());
        }

        if (certificateInfo.getOrganizationalUnit() != null) {
            x500NameBuilder.addRDN(BCStyle.OU, certificateInfo.getOrganizationalUnit());
        }

        if (certificateInfo.getEmail() != null) {
            x500NameBuilder.addRDN(BCStyle.E, certificateInfo.getEmail());
        }

        if (certificateInfo.getLocality() != null) {
            x500NameBuilder.addRDN(BCStyle.L, certificateInfo.getLocality());
        }

        if (certificateInfo.getState() != null) {
            x500NameBuilder.addRDN(BCStyle.ST, certificateInfo.getState());
        }

        if (certificateInfo.getCountryCode() != null) {
            x500NameBuilder.addRDN(BCStyle.C, certificateInfo.getCountryCode());
        }

        // TODO: Add more X.509 certificate fields as needed

        return x500NameBuilder.build();
    }

    /**
     * Converts a list of domain name Subject Alternative Names into ASN1Encodable GeneralNames objects, for use with
     * the Bouncy Castle certificate builder.
     *
     * @param subjectAlternativeNames domain name SANs to convert
     * @return a GeneralNames instance that includes the specifie dsubjectAlternativeNames as DNS name fields
     */
    private static GeneralNames getDomainNameSANsAsASN1Encodable(List<String> subjectAlternativeNames) {
        List<GeneralName> encodedSANs = new ArrayList<>(subjectAlternativeNames.size());
        for (String subjectAlternativeName : subjectAlternativeNames) {
            // IP addresses use the IP Address tag instead of the DNS Name tag in the SAN list
            boolean isIpAddress = InetAddresses.isInetAddress(subjectAlternativeName);
            GeneralName generalName = new GeneralName(isIpAddress ? GeneralName.iPAddress : GeneralName.dNSName, subjectAlternativeName);
            encodedSANs.add(generalName);
        }

        return new GeneralNames(encodedSANs.toArray(new GeneralName[encodedSANs.size()]));
    }

    /**
     * Creates a ContentSigner that can be used to sign certificates with the given private key and signature algorithm.
     *
     * @param certAuthorityPrivateKey the private key to use to sign certificates
     * @param signatureAlgorithm      the algorithm to use to sign certificates
     * @return a ContentSigner
     */
    private static ContentSigner getCertificateSigner(PrivateKey certAuthorityPrivateKey, String signatureAlgorithm) {
        try {
            return new JcaContentSignerBuilder(signatureAlgorithm)
                    .build(certAuthorityPrivateKey);
        } catch (OperatorCreationException e) {
            throw new CertificateCreationException("Unable to create ContentSigner using signature algorithm: " + signatureAlgorithm, e);
        }
    }

    /**
     * Converts a Bouncy Castle X509CertificateHolder into a JCA X590Certificate.
     *
     * @param bouncyCastleCertificate BC X509CertificateHolder
     * @return JCA X509Certificate
     */
    private static X509Certificate convertToJcaCertificate(X509CertificateHolder bouncyCastleCertificate) {
        try {
            return new JcaX509CertificateConverter()
                    .getCertificate(bouncyCastleCertificate);
        } catch (CertificateException e) {
            throw new CertificateCreationException("Unable to convert X590CertificateHolder to JCA X590Certificate", e);
        }
    }

    /**
     * Creates the SubjectKeyIdentifier for a Bouncy Castle X590CertificateHolder.
     *
     * @param key public key to identify
     * @return SubjectKeyIdentifier for the specified key
     */
    private static SubjectKeyIdentifier createSubjectKeyIdentifier(Key key) {
        SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(key.getEncoded());

        return new BcX509ExtensionUtils().createSubjectKeyIdentifier(publicKeyInfo);
    }

    /**
     * Encodes the specified security object in PEM format, using the specified encryptor. If the encryptor is null,
     * the object will not be encrypted in the generated String.
     *
     * @param object    object to encrypt (certificate, private key, etc.)
     * @param encryptor engine to encrypt the resulting PEM String, or null if no encryption should be used
     * @return a PEM-encoded String
     */
    private static String encodeObjectAsPemString(Object object, PEMEncryptor encryptor) {
        StringWriter stringWriter = new StringWriter();

        try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
            pemWriter.writeObject(object, encryptor);
            pemWriter.flush();
        } catch (IOException e) {
            throw new ExportException("Unable to generate PEM string representing object", e);
        }

        return stringWriter.toString();
    }
}
