/*
 * 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.primitives;

import java.nio.charset.StandardCharsets;
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.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.PrimitiveConstants;
import com.scytl.cryptolib.primitives.derivation.constants.DerivationConstants;
import com.scytl.cryptolib.primitives.primes.utils.PrimitivesTestDataGenerator;
import com.scytl.cryptolib.primitives.securerandom.constants.SecureRandomConstants;
import com.scytl.cryptolib.primitives.service.PrimitivesService;
import com.scytl.cryptolib.test.tools.utils.CommonTestDataGenerator;

/**
 * Tests the Java-JavaScript interoperation for PBKDF key deriver applications.
 * NOTE: For the purpose of performance, the PBKDF deriver applications on the
 * JavaScript side have been upgraded to use Stanford Javascript Crypto Library
 * (SJCL) instead of Forge as the cryptographic service provider (CSP). However,
 * due to a limitation of the Nashorn JavaScript engine, the PBKDF
 * interoperation tests fail for certain passwords, when using SJCL. For this
 * reason, the password is not randomly generated for these tests. However,
 * another suite of interoperation tests is included here to verify that there
 * are no failures when using randomly generated passwords, with Forge as the
 * JavaScript CSP. For cryptolib-js, there is also a test, using randomly
 * generated passwords, that verifies that SJCL and Forge always produce the
 * same results for PBKDF applications. The combination of these two test suites
 * should be sufficient to verify that Java and SJCL always produce the same
 * results for PBKDF applications.
 */
public class TestPbkdfKeyDeriver extends Command {

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

    private static final String PW =
        "lITjwvhmMHYya505gXobNG39mxjaca3G0qjYF4QUJCeeBfz5AzEtSdce6sp7aUkkGdBOcdeX9suPtlzNQkuLXRR+11 yR+LnjSpM";

    private final Object _namespace;

    private final PrimitivesService _primitivesService;

    private static String _passwordB64;

    private static String _passwordForge;

    private static String _passwordForgeB64;

    private static byte[] _salt;

    private static String _saltB64;

    private static String _derivedKeyB64;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate the PBKDF key deriver
     *             interoperation tests implemented in JavaScript.
     */
    public TestPbkdfKeyDeriver() throws CommandException {
        super(PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_PATH);

        _namespace = _engine
            .get(PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_NAMESPACE);

        try {
            _primitivesService = new PrimitivesService();

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

    /**
     * Derives a PBKDF key using Java, verifies the result using JavaScript,
     * derives the same key using JavaScript and verifies the result using Java.
     * If no exceptions are thrown and the same PBKDF key is derived both times,
     * then the interoperation test for PBKDF key deriver applications was
     * successful.
     *
     * @throws CommandException
     *             if a JavaScript method cannot be found, a cryptographic
     *             operation fails or a derived PBKDF key cannot be verified.
     */
    @Override
    public void execute() throws CommandException {

        String derivedKeyB64 = deriveUsingJava(PW);

        verifyUsingJavaScript(derivedKeyB64);

        derivedKeyB64 = deriveUsingJavaScript();

        verifyUsingJava(derivedKeyB64);

        derivedKeyB64 = deriveUsingJava(_passwordForge);

        verifyUsingJavaScriptWithForge(derivedKeyB64);

        derivedKeyB64 = deriveUsingJavaScriptWithForge();

        verifyUsingJava(derivedKeyB64);

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

    private void generateInputData() throws GeneralCryptoLibException {

        _passwordB64 = Base64.getEncoder()
            .encodeToString(PW.getBytes(StandardCharsets.UTF_8));
        putExceptionData("Password to derive key (Base64 encoded) [Java]",
            _passwordB64);

        _passwordForge = _primitivesService
            .get64CharAlphabetCryptoRandomString()
            .nextRandom(CommonTestDataGenerator.getInt(
                DerivationConstants.MINIMUM_PBKDF_PASSWORD_LENGTH,
                SecureRandomConstants.MAXIMUM_GENERATED_BYTE_ARRAY_LENGTH));
        _passwordForgeB64 = Base64.getEncoder()
                .encodeToString(_passwordForge.getBytes(StandardCharsets.UTF_8));
        putExceptionData(
            "Password to derive key, using Forge (Base64 encoded) [Java]",
            _passwordForgeB64);

        _salt = PrimitivesTestDataGenerator
            .getByteArray(CommonTestDataGenerator.getInt(
                DerivationConstants.MINIMUM_PBKDF_SALT_LENGTH_IN_BYTES,
                SecureRandomConstants.MAXIMUM_GENERATED_BYTE_ARRAY_LENGTH));
        _saltB64 = Base64.getEncoder().encodeToString(_salt);
        putExceptionData("Salt to derive key (Base64 encoded) [Java]",
            _saltB64);
    }

    private String deriveUsingJava(final String password)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Deriving PBKDF key, using Java...");

        try {
            _derivedKeyB64 = Base64.getEncoder()
                .encodeToString(_primitivesService.getPBKDFDeriver()
                    .deriveKey(password.toCharArray(), _salt)
                    .getEncoded());
            putExceptionData("Derived key (Base64 encoded) [Java]",
                _derivedKeyB64);

            return _derivedKeyB64;
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not derive PBKDF key, using Java.", e);
        }
    }

    private void verifyUsingJavaScript(final String derivedKeyB64)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying derived PBKDF key, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                PrimitiveConstants.JS_PBKDF_KEY_DERIVER_VERIFICATION_FUNCTION,
                derivedKeyB64, _passwordB64, _saltB64);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_NAMESPACE
                        + "."
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_VERIFICATION_FUNCTION
                        + " in resources file "
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not verify derived PBKDF key, using JavaScript.",
                e);
        }
    }

    private String deriveUsingJavaScript() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Deriving PBKDF key, using JavaScript...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                PrimitiveConstants.JS_PBKDF_KEY_DERIVER_DERIVATION_FUNCTION,
                _passwordB64, _saltB64);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_NAMESPACE
                        + "."
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_DERIVATION_FUNCTION
                        + " in resources file "
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not derive PBKDF key, using JavaScript.", e);
        }
    }

    private void verifyUsingJava(final String derivedKeyB64)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying derived PBKDF key, using Java...");

        putExceptionData("Derived key (Base64 encoded) [JS]",
            derivedKeyB64);

        try {
            if (!derivedKeyB64.equals(_derivedKeyB64)) {
                throw new GeneralCryptoLibException(
                    "Expected derived key (Base64 encoded): "
                        + _derivedKeyB64 + " ; Found: " + derivedKeyB64);
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not verify derived PBKDF key, using Java.", e);
        }
    }

    private void verifyUsingJavaScriptWithForge(final String derivedKeyB64)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying derived PBKDF key, using JavaScript with Forge...");

        try {
            getInvocable().invokeMethod(_namespace,
                PrimitiveConstants.JS_PBKDF_KEY_DERIVER_VERIFICATION_WITH_FORGE_FUNCTION,
                derivedKeyB64, _passwordForgeB64, _saltB64);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_NAMESPACE
                        + "."
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_VERIFICATION_WITH_FORGE_FUNCTION
                        + " in resources file "
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not verify derived PBKDF key, using JavaScript with Forge.",
                e);
        }
    }

    private String deriveUsingJavaScriptWithForge()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Deriving PBKDF key, using JavaScript with Forge...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                PrimitiveConstants.JS_PBKDF_KEY_DERIVER_DERIVATION_WITH_FORGE_FUNCTION,
                _passwordForgeB64, _saltB64);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_NAMESPACE
                        + "."
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_DERIVATION_WITH_FORGE_FUNCTION
                        + " in resources file "
                        + PrimitiveConstants.JS_PBKDF_KEY_DERIVER_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not derive PBKDF key, using JavaScript with Forge.",
                e);
        }
    }
}
