/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.cryptolib.interoperation.js.commands.stores;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Base64;

import javax.script.ScriptException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.stores.bean.KeyStoreType;
import com.scytl.cryptolib.asymmetric.utils.AsymmetricTestDataGenerator;
import com.scytl.cryptolib.certificates.utils.X509CertificateTestDataGenerator;
import com.scytl.cryptolib.interoperation.js.commands.Command;
import com.scytl.cryptolib.interoperation.js.commands.CommandException;
import com.scytl.cryptolib.interoperation.js.constants.CommonConstants;
import com.scytl.cryptolib.interoperation.js.constants.StoreConstants;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.cryptolib.primitives.primes.utils.PrimitivesTestDataGenerator;
import com.scytl.cryptolib.primitives.securerandom.constants.SecureRandomConstants;
import com.scytl.cryptolib.stores.service.StoresService;
import com.scytl.cryptolib.test.tools.utils.CommonTestDataGenerator;

/**
 * Tests the Java-JavaScript interoperation for PKCS12 applications.
 */
public class TestPkcs12 extends Command {

    private static final Logger LOGGER =
        LoggerFactory.getLogger(TestPkcs12.class);

    private final Object _namespace;

    private final StoresService _storesService;

    private static String _privateKeyEntryPassword;

    private static String _privateKeyEntryPasswordB64;

    private static PrivateKey _rootPrivateKey;

    private static String _rootPrivateKeyPem;

    private static Certificate _rootCertificate;

    private static String _rootCertificatePem;

    private static PrivateKey _intermediatePrivateKey;

    private static String _intermediatePrivateKeyPem;

    private static Certificate _intermediateCertificate;

    private static String _intermediateCertificatePem;

    private static PrivateKey _leafPrivateKey;

    private static String _leafPrivateKeyPem;

    private static Certificate _leafCertificate;

    private static String _leafCertificatePem;

    /**
     * Default constructor.
     *
     * @throws CommandbException
     *             if the script engine cannot evaluate the PKCS12
     *             interoperation tests implemented in JavaScript.
     */
    public TestPkcs12() throws CommandException {
        super(StoreConstants.JS_PKCS12_TEST_PATH);

        _namespace = _engine.get(StoreConstants.JS_PKCS12_TEST_NAMESPACE);

        clearExceptionData();

        try {
            _storesService = new StoresService();

            generateInputData();
        } catch (GeneralCryptoLibException e) {
            throw new CommandException.Builder().addCause(e).build();
        }
    }

    /**
     * Generates a PKCS12 using Java and opens the result using JavaScript. If
     * no exceptions are thrown, and the content of the PKCS12 can be retrieved
     * then the interoperation test for PKCS12 applications was successful.
     *
     * @throws CommandException
     *             if a JavaScript method cannot be found, a cryptographic
     *             operation fails or the content of the PKCS12 cannot be
     *             retrieved.
     */
    @Override
    public void execute() throws CommandException {

        String pkcs12B64 = generateUsingJava();

        openUsingJavaScript(pkcs12B64);

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "INTEROPERATION TEST FOR PKCS12 WAS SUCCESSFUL.");
    }

    private void generateInputData() throws GeneralCryptoLibException {

        _privateKeyEntryPassword = PrimitivesTestDataGenerator
            .getString64(CommonTestDataGenerator.getInt(1,
                SecureRandomConstants.MAXIMUM_GENERATED_STRING_LENGTH));
        _privateKeyEntryPasswordB64 = Base64.getEncoder().encodeToString(
            _privateKeyEntryPassword.getBytes(StandardCharsets.UTF_8));
        putExceptionData(
            "Private key entry password (Base64 encoded) [Java]",
            _privateKeyEntryPasswordB64);

        putExceptionData("Root private key alias",
            StoreConstants.ROOT_PRIVATE_KEY_ALIAS);
        KeyPair rootKeyPair =
            AsymmetricTestDataGenerator.getKeyPairForSigning();
        _rootPrivateKey = rootKeyPair.getPrivate();
        _rootPrivateKeyPem = PemUtils.privateKeyToPem(_rootPrivateKey);
        putExceptionData("Root private key (Base64 encoded) [Java]",
            _rootPrivateKeyPem);
        _rootCertificate = X509CertificateTestDataGenerator
            .getRootAuthorityX509Certificate(rootKeyPair).getCertificate();
        _rootCertificatePem = PemUtils.certificateToPem(_rootCertificate);
        putExceptionData("Root certificate (PEM format) [Java]",
            _rootCertificatePem);

        putExceptionData("Intermediate private key alias",
            StoreConstants.INTERMEDIATE_PRIVATE_KEY_ALIAS);
        KeyPair intermediateKeyPair =
            AsymmetricTestDataGenerator.getKeyPairForSigning();
        _intermediatePrivateKey = intermediateKeyPair.getPrivate();
        _intermediatePrivateKeyPem =
            PemUtils.privateKeyToPem(_intermediatePrivateKey);
        putExceptionData("Intermediate private key (PEM format) [Java]",
            _intermediatePrivateKeyPem);
        _intermediateCertificate = X509CertificateTestDataGenerator
            .getIntermediateAuthorityX509Certificate(intermediateKeyPair,
                rootKeyPair)
            .getCertificate();
        _intermediateCertificatePem =
            PemUtils.certificateToPem(_intermediateCertificate);
        putExceptionData("Intermediate certificate (PEM format) [Java]",
            _intermediateCertificatePem);

        putExceptionData("Leaf private key alias",
            StoreConstants.LEAF_PRIVATE_KEY_ALIAS);
        KeyPair leafKeyPair =
            AsymmetricTestDataGenerator.getKeyPairForSigning();
        _leafPrivateKey = leafKeyPair.getPrivate();
        _leafPrivateKeyPem = PemUtils.privateKeyToPem(_leafPrivateKey);
        putExceptionData("Leaf private key (PEM format) [Java]",
            _leafPrivateKeyPem);
        _leafCertificate = X509CertificateTestDataGenerator
            .getLeafX509Certificate(leafKeyPair, intermediateKeyPair)
            .getCertificate();
        _leafCertificatePem = PemUtils.certificateToPem(_leafCertificate);
        putExceptionData("Leaf certificate (PEM format) [Java]",
            _leafCertificatePem);
    }

    private String generateUsingJava() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating PKCS12, using Java...");

        try {
            KeyStore pkcs12 =
                _storesService.createKeyStore(KeyStoreType.PKCS12);

            Certificate[] rootCertificateChain = new X509Certificate[1];
            rootCertificateChain[0] = _rootCertificate;
            try {
                pkcs12.setKeyEntry(StoreConstants.ROOT_PRIVATE_KEY_ALIAS,
                    _rootPrivateKey,
                    _privateKeyEntryPassword.toCharArray(),
                    rootCertificateChain);
            } catch (KeyStoreException e) {
                throw new GeneralCryptoLibException(
                    "Could not set root private key entry in PKCS12.", e);
            }

            Certificate[] intermediateCertificateChain =
                new X509Certificate[2];
            intermediateCertificateChain[0] = _intermediateCertificate;
            intermediateCertificateChain[1] = _rootCertificate;
            try {
                pkcs12.setKeyEntry(
                    StoreConstants.INTERMEDIATE_PRIVATE_KEY_ALIAS,
                    _intermediatePrivateKey,
                    _privateKeyEntryPassword.toCharArray(),
                    intermediateCertificateChain);
            } catch (KeyStoreException e) {
                throw new GeneralCryptoLibException(
                    "Could not set intermediate private key entry in PKCS12.",
                    e);
            }

            Certificate[] leafCertificateChain = new X509Certificate[3];
            leafCertificateChain[0] = _leafCertificate;
            leafCertificateChain[1] = _intermediateCertificate;
            leafCertificateChain[2] = _rootCertificate;
            try {
                pkcs12.setKeyEntry(StoreConstants.LEAF_PRIVATE_KEY_ALIAS,
                    _leafPrivateKey,
                    _privateKeyEntryPassword.toCharArray(),
                    leafCertificateChain);
            } catch (KeyStoreException e) {
                throw new GeneralCryptoLibException(
                    "Could not set leaf private key entry in PKCS12.", e);
            }

            try (ByteArrayOutputStream byteArrayOutStream =
                new ByteArrayOutputStream()) {
                try {
                    pkcs12.store(byteArrayOutStream,
                        _privateKeyEntryPassword.toCharArray());

                    return Base64.getEncoder()
                        .encodeToString(byteArrayOutStream.toByteArray());
                } catch (KeyStoreException | NoSuchAlgorithmException
                        | CertificateException e) {
                    throw new GeneralCryptoLibException(
                        "Could not write PKCS12 to byte array output stream.",
                        e);
                }
            } catch (IOException e) {
                throw new GeneralCryptoLibException(
                    "Could not close PKCS12 byte array output stream.", e);
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate PKCS12, using Java.", e);
        }
    }

    private void openUsingJavaScript(final String pkcs12B64)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Opening PKCS12, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                StoreConstants.JS_PKCS12_OPENING_FUNCTION, pkcs12B64,
                _privateKeyEntryPasswordB64,
                StoreConstants.ROOT_PRIVATE_KEY_ALIAS, _rootPrivateKeyPem,
                _rootCertificatePem,
                StoreConstants.INTERMEDIATE_PRIVATE_KEY_ALIAS,
                _intermediatePrivateKeyPem, _intermediateCertificatePem,
                StoreConstants.LEAF_PRIVATE_KEY_ALIAS, _leafPrivateKeyPem,
                _leafCertificatePem);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + StoreConstants.JS_PKCS12_TEST_NAMESPACE + "."
                        + StoreConstants.JS_PKCS12_OPENING_FUNCTION
                        + " in resources file "
                        + StoreConstants.JS_PKCS12_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not open Java generated PKCS12, using JavaScript.",
                e);
        }
    }
}
