/*
 * 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.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Base64;

import javax.crypto.SecretKey;
import javax.script.ScriptException;

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

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.utils.AsymmetricTestDataGenerator;
import com.scytl.cryptolib.certificates.utils.X509CertificateTestDataGenerator;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.cryptolib.elgamal.bean.ElGamalKeyPair;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.elgamal.utils.ElGamalTestDataGenerator;
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.ElGamalConstants;
import com.scytl.cryptolib.interoperation.js.constants.StoreConstants;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.mathematical.groups.utils.MathematicalTestDataGenerator;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.cryptolib.scytl.keystore.service.ScytlKeyStoreService;
import com.scytl.cryptolib.scytl.keystore.utils.SksTestDataGenerator;
import com.scytl.cryptolib.symmetric.utils.SymmetricTestDataGenerator;

/**
 * Tests the Java-JavaScript interoperation for Scytl key store applications.
 */
public class TestScytlKeyStore extends Command {

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

    private final Object _namespace;

    private final ScytlKeyStoreService _scytlKeyStoreService;

    private final ElGamalService _elGamalService;

    private static String _secretKeyEntryPassword;

    private static String _secretKeyEntryPasswordB64;

    private static SecretKey _secretKeyForEncryption;

    private static String _secretKeyForEncryptionB64;

    private static SecretKey _secretKeyForHmac;

    private static String _secretKeyForHmacB64;

    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;

    private static String _elGamalPrivateKeyEntryPassword;

    private static String _elGamalPrivateKeyEntryPasswordB64;

    private static ElGamalPrivateKey _elGamalPrivateKey;

    private static String _elGamalPrivateKeyJson;

    private static ElGamalPublicKey _elGamalPublicKey;

    private static String _elGamalPublicKeyJson;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate the Scytl key store
     *             interoperation tests implemented in JavaScript.
     */
    public TestScytlKeyStore() throws CommandException {
        super(StoreConstants.JS_SCYTL_KEY_STORE_TEST_PATH);

        _namespace =
            _engine.get(StoreConstants.JS_SCYTL_KEY_STORE_TEST_NAMESPACE);

        clearExceptionData();

        try {
            _scytlKeyStoreService = new ScytlKeyStoreService();

            _elGamalService = new ElGamalService();

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

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

        String scytlKeyStoreJson = generateUsingJava();

        openUsingJavaScript(scytlKeyStoreJson);

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

    private void generateInputData() throws GeneralCryptoLibException {

        _secretKeyEntryPassword =
            new String(SksTestDataGenerator.getPassword());
        _secretKeyEntryPasswordB64 = Base64.getEncoder().encodeToString(
            _secretKeyEntryPassword.getBytes(StandardCharsets.UTF_8));
        putExceptionData(
            "Secret key entry password (Base64 encoded)_[Java]",
            _secretKeyEntryPasswordB64);

        putExceptionData("Secret key for encryption alias",
            StoreConstants.SECRET_KEY_FOR_ENCRYPTION_ALIAS);
        _secretKeyForEncryption =
            SymmetricTestDataGenerator.getSecretKeyForEncryption();
        _secretKeyForEncryptionB64 = Base64.getEncoder()
            .encodeToString(_secretKeyForEncryption.getEncoded());
        putExceptionData(
            "Secret key for encryption (Base64 encoded) [Java]",
            _secretKeyForEncryptionB64);

        putExceptionData("Secret key for HMAC alias",
            StoreConstants.SECRET_KEY_FOR_HMAC_ALIAS);
        _secretKeyForHmac =
            SymmetricTestDataGenerator.getSecretKeyForHmac();
        _secretKeyForHmacB64 = Base64.getEncoder()
            .encodeToString(_secretKeyForHmac.getEncoded());
        putExceptionData("Secret key for HMAC (Base64 encoded) [Java]",
            _secretKeyForHmacB64);

        _privateKeyEntryPassword =
            new String(SksTestDataGenerator.getPassword());
        _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 (PEM format) [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);

        _elGamalPrivateKeyEntryPassword =
            new String(SksTestDataGenerator.getPassword());
        _elGamalPrivateKeyEntryPasswordB64 = Base64.getEncoder()
            .encodeToString(_elGamalPrivateKeyEntryPassword
                .getBytes(StandardCharsets.UTF_8));
        putExceptionData(
            "ElGamal private key entry password (Base64 encoded) [Java]",
            _elGamalPrivateKeyEntryPasswordB64);

        putExceptionData("ElGamal private key alias",
            StoreConstants.ELGAMAL_PRIVATE_KEY_ALIAS);
        ZpSubgroup zpSubgroup =
            MathematicalTestDataGenerator.getZpSubgroup();
        ElGamalEncryptionParameters elGamalEncryptionParameters =
            ElGamalTestDataGenerator
                .getElGamalEncryptionParameters(zpSubgroup);
        ElGamalKeyPair elGamalKeyPair =
            _elGamalService.getElGamalKeyPairGenerator().generateKeys(
                elGamalEncryptionParameters,
                ElGamalConstants.NUM_ZP_GROUP_ELEMENTS);
        _elGamalPrivateKey = elGamalKeyPair.getPrivateKeys();
        _elGamalPrivateKeyJson = _elGamalPrivateKey.toJson();
        putExceptionData("ElGamal private key (JSON format) [Java]",
            _elGamalPrivateKeyJson);
        _elGamalPublicKey = elGamalKeyPair.getPublicKeys();
        _elGamalPublicKeyJson = _elGamalPublicKey.toJson();
        putExceptionData("ElGamal public key (JSON format) [Java]",
            _elGamalPublicKeyJson);
    }

    private String generateUsingJava() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating Scytl key store, using Java...");

        try {
            CryptoAPIScytlKeyStore scyltKeyStore =
                _scytlKeyStoreService.createKeyStore();

            try {
                scyltKeyStore.setSecretKeyEntry(
                    StoreConstants.SECRET_KEY_FOR_ENCRYPTION_ALIAS,
                    _secretKeyForEncryption,
                    _secretKeyEntryPassword.toCharArray());
            } catch (CryptoLibException e) {
                throw new GeneralCryptoLibException(
                    "Could not set secret key for encryption entry in Scytl key store.",
                    e);
            }

            try {
                scyltKeyStore.setSecretKeyEntry(
                    StoreConstants.SECRET_KEY_FOR_HMAC_ALIAS,
                    _secretKeyForHmac,
                    _secretKeyEntryPassword.toCharArray());
            } catch (CryptoLibException e) {
                throw new GeneralCryptoLibException(
                    "Could not set secret key for HMAC entry in Scytl key store.",
                    e);
            }

            Certificate[] certificateChain = new X509Certificate[1];
            certificateChain[0] = _rootCertificate;
            try {
                scyltKeyStore.setPrivateKeyEntry(
                    StoreConstants.ROOT_PRIVATE_KEY_ALIAS, _rootPrivateKey,
                    _privateKeyEntryPassword.toCharArray(),
                    certificateChain);
            } catch (CryptoLibException e) {
                throw new GeneralCryptoLibException(
                    "Could not set root private key entry in Scytl key store.",
                    e);
            }

            certificateChain = new X509Certificate[2];
            certificateChain[0] = _intermediateCertificate;
            certificateChain[1] = _rootCertificate;
            try {
                scyltKeyStore.setPrivateKeyEntry(
                    StoreConstants.INTERMEDIATE_PRIVATE_KEY_ALIAS,
                    _intermediatePrivateKey,
                    _privateKeyEntryPassword.toCharArray(),
                    certificateChain);
            } catch (CryptoLibException e) {
                throw new GeneralCryptoLibException(
                    "Could not set intermediate private key entry in Scytl key store.",
                    e);
            }

            certificateChain = new X509Certificate[3];
            certificateChain[0] = _leafCertificate;
            certificateChain[1] = _intermediateCertificate;
            certificateChain[2] = _rootCertificate;
            try {
                scyltKeyStore.setPrivateKeyEntry(
                    StoreConstants.LEAF_PRIVATE_KEY_ALIAS, _leafPrivateKey,
                    _privateKeyEntryPassword.toCharArray(),
                    certificateChain);
            } catch (CryptoLibException e) {
                throw new GeneralCryptoLibException(
                    "Could not set leaf private key entry in Scytl key store.",
                    e);
            }

            try {
                scyltKeyStore.setElGamalPrivateKeyEntry(
                    StoreConstants.ELGAMAL_PRIVATE_KEY_ALIAS,
                    _elGamalPrivateKey,
                    _elGamalPrivateKeyEntryPassword.toCharArray());
            } catch (CryptoLibException e) {
                throw new GeneralCryptoLibException(
                    "Could not set ElGamal private key entry in Scytl key store.");
            }

            return scyltKeyStore
                .toJSON(_privateKeyEntryPassword.toCharArray());
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate Scytl key store, using Java.", e);
        }
    }

    private void openUsingJavaScript(final String scytlKeyStoreJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Opening Scytl key store, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                StoreConstants.JS_SCYTL_KEY_STORE_OPENING_FUNCTION,
                scytlKeyStoreJson, _secretKeyEntryPasswordB64,
                StoreConstants.SECRET_KEY_FOR_ENCRYPTION_ALIAS,
                _secretKeyForEncryptionB64,
                StoreConstants.SECRET_KEY_FOR_HMAC_ALIAS,
                _secretKeyForHmacB64, _privateKeyEntryPasswordB64,
                StoreConstants.ROOT_PRIVATE_KEY_ALIAS, _rootPrivateKeyPem,
                _rootCertificatePem,
                StoreConstants.INTERMEDIATE_PRIVATE_KEY_ALIAS,
                _intermediatePrivateKeyPem, _intermediateCertificatePem,
                StoreConstants.LEAF_PRIVATE_KEY_ALIAS, _leafPrivateKeyPem,
                _leafCertificatePem, _elGamalPrivateKeyEntryPasswordB64,
                StoreConstants.ELGAMAL_PRIVATE_KEY_ALIAS,
                _elGamalPrivateKeyJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + StoreConstants.JS_SCYTL_KEY_STORE_TEST_NAMESPACE
                        + "."
                        + StoreConstants.JS_SCYTL_KEY_STORE_OPENING_FUNCTION
                        + " in resources file "
                        + StoreConstants.JS_SCYTL_KEY_STORE_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not open Java generated Scytl key store, using JavaScript.",
                e);
        }
    }
}
