/*
 * 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.asymmetric.cipher.factory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.Security;
import java.util.Arrays;
import java.util.Base64;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.cipher.configuration.AsymmetricCipherPolicy;
import com.scytl.cryptolib.asymmetric.cipher.configuration.ConfigAsymmetricCipherAlgorithmAndSpec;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.commons.constants.Constants;
import com.scytl.cryptolib.commons.utils.system.OperatingSystem;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.cryptolib.primitives.securerandom.configuration.ConfigSecureRandomAlgorithmAndProvider;
import com.scytl.cryptolib.primitives.securerandom.configuration.SecureRandomPolicyFromProperties;
import com.scytl.cryptolib.primitives.securerandom.factory.CryptoRandomBytes;
import com.scytl.cryptolib.primitives.securerandom.factory.SecureRandomFactory;
import com.scytl.cryptolib.symmetric.cipher.configuration.ConfigSymmetricCipherAlgorithmAndSpec;
import com.scytl.cryptolib.symmetric.key.configuration.ConfigSecretKeyAlgorithmAndSpec;

public class CryptoAsymmetricCipherTest {

    private static final int DATA_BYTE_LENGTH = 100;

    private static final String PRIVATE_KEY_PEM_FOR_JS_JAVA_INTEROP_TEST =
        "-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAglypZU45bnf2opnRI+Y51VqouBpvDT33xIB/OtbwKzwVpi+JrjBFtfk33tE9t/dSRs79CK94HRhCWcOiLa2qWPrjeZ9SBiEyScrhIvRZVBF41zBgwQNuRvJCsKmAqlZaFNJDZxEP4repmlBn1CfVFmfrXmOKqwP5F7l9ZtucveRzsfmF1yVPFkW8TMuB3YqMiyymyqHlS8ujCsu5I8tpgPbwuxdMOY94fNhSXrYkY8IuX1g1zdq/Z1jluOaR/UqK4UpnbuJaH/F0VgDNiWh6cTD0DFGEk0b70i5wU4Q3L/S6XZQRvSuADoCbhwBKuFL5pW5n865oLVb5S3wuVdWaGwIDAQABAoIBAC/tn34Wf3kE9BGeGc1oFLVDaqqdVVz5/oEpeR2J7q0GnzMFYUpAhzC7WvY52cYsUPyll1Q9Jx0TUTmteo/uvKWQQFfz4nVMeS+2PoXabolBDzuWlsv/1eiRo0FOYHa/3siu8YcQN9X0DpAkpbfTmT1uoZOHZ3EuucMmOFu7vGn38Grw8bSxpR0uvTtnb8ygC+aB51y38RMyhzQQanrM8FMeAfDAy6IB0Yo7b0c50Cxa6Ax4nqn9LXyGakr5WeAMkgTIOA/GId9SZD4e5eRpq+628pOeR4O9datFltgl6r1+A4ii2VrJsDqeatGtODlX6KRKqwFHoGIa2TjgSZLuorECgYEAxeSZDOOgFsI5mB7RkRzZaQ9znJ15sgdyZiAFZAOUah4hSGdAXNAnZTlrdacduXEu3EfkpuPToX7xZSv5FRYwfBwMwCLeytlGLPjQzWejZGbo4+KqgzWb9fECDYVtDPlJ/+yLih9nt67BHweJKxYydl18rVigdVyy22X86NijSykCgYEAqKPUrXZAo+TJvmTw4tgsibJgvXBYBhmsej8mGNQw+Nyp2gV28sgm61ifIeXKS8teq+MFwGA6cHQedbsCqhMHokdhESZmlbWxhSFLihQcewBxwvrBwbaxI23yXRzwMewznZFL032PpcbqrmwFmcSSEZ3nmbvTH6ShqLW+pzDNp6MCgYBQLzdgxJ7qedqSa/JohTMG4e7rh9d2rpPJE7J7ewPZF8pOpx+qO+Gqn2COdJ+Ts2vUcAETKn9nEaPIZc/wnmQY9dioxbhWo0FPGaaphBPtq9Ez/XUv4zoFppk5V1X/isdUPsmvttf00oeIBiqrXbwmv+yz5JRn2Z7TTXjz9Ev+OQKBgQCUuoCMRzl1EgcXIqEL/0kwW6BUEqufHa9u1Ri9Vw6lvL8T6DPipMEmWK9nzuid9gtVns/ovTVtDgv7GuabplLaPQePf4WDzY11c0rSyS/hDyBFrK+LL5uEOqhAlJAGB2HyOj1clWVF+GvrTpuV5LZKUS/79pmZU7G7QCaX/0Ow7wKBgC/kDH7cmWQnWvvJ5izrx/7PogQVPOLELeUIGLu/hjsSdDKiFCxCUZ948+9NuG+DnpXDWzw//r8mPBRRGGsqFws5Aipp7yjQ3kRDCCzGelPCVhHyfmKqA+8ewXPulKS3/wIyHIvaXmsuAtTfurHtpRyzjKmCBK1Y6WQ3trIXvo7s-----END RSA PRIVATE KEY-----";

    private static final String DATA_STRING_FOR_JS_JAVA_INTEROP_TEST =
        "Ox2fUJq1gAbX";

    private static final String ENCRYPTED_DATA_BASE64_FOR_JS_JAVA_INTEROP_TEST =
        "YncLoL2eLyPOvPja4omKcSDLBv4fi8edHKbn6Y5Tu8H/sZ07IrH+w4yZRemp4iFfmikljU9kUnZq0i+vptoSUTmcJu4bP3jOqowrSdi/cWKKxtXkzaZfCP6qPAOpt2es+sLiUz/UO98LNO0mhyXSrkohWcSQdLMZtF5MxTOHUQfHZEJrlyTZ0os2na6/a+R7ZRmHG6mCzoUsQZUYY6sXJkvv7ZU1BbFmmGlR5MQ8QCLjNrIeymyZq+dd+G28/7UpZffr5AXRpEoMRbNWsRTWtLuzfifPHztdIpsf6pWmz33EVYsgcX8b4FzN4b7veHpopzq2qbLObfPb8fIzhVrvjLGW16F/8hee/qOGXlYxlqt0xIB/a+mURCYDS9kJUwSNO/9LmF61cKM=";

    private static KeyPair _keyPair;

    private static CryptoRandomBytes _randomByteGenerator;

    private static byte[] _data;

    private static CryptoAsymmetricCipher _cryptoAsymmetricCipher;

    private static PrivateKey _privateKeyJsJavaInterop;

    private static byte[] _dataJsJavaInterop;

    private static byte[] _encryptedDataJsJavaInterop;

    @BeforeClass
    public static void setUp() throws GeneralCryptoLibException {

        Security.addProvider(new BouncyCastleProvider());

        AsymmetricService asymmetricServiceFromDefaultConstructor =
            new AsymmetricService();

        _keyPair =
            asymmetricServiceFromDefaultConstructor
                .getKeyPairForEncryption();

        _randomByteGenerator =
            new SecureRandomFactory(new SecureRandomPolicyFromProperties(
                Constants.CRYPTOLIB_POLICY_PROPERTIES_FILE_PATH))
                .createByteRandom();

        _data = _randomByteGenerator.nextRandom(DATA_BYTE_LENGTH);

        _privateKeyJsJavaInterop =
            PemUtils
                .privateKeyFromPem(PRIVATE_KEY_PEM_FOR_JS_JAVA_INTEROP_TEST);
        _dataJsJavaInterop =
            DATA_STRING_FOR_JS_JAVA_INTEROP_TEST.getBytes();
        _encryptedDataJsJavaInterop =
            Base64.getDecoder()
                .decode(ENCRYPTED_DATA_BASE64_FOR_JS_JAVA_INTEROP_TEST);
    }

    @AfterClass
    public static void tearDown() {

        Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
    }

    @Test
    public void testEncryptAndDecryptDataWithRsaKemWithKdf1AndSha256AndBc()
            throws GeneralCryptoLibException {

        _cryptoAsymmetricCipher =
            new AsymmetricCipherFactory(
                getAsymmetricCipherPolicyRsaKemWithKdf1AndSha256AndBc())
                .create();

        byte[] encryptedData =
            _cryptoAsymmetricCipher.encrypt(_keyPair.getPublic(), _data);

        byte[] decryptedData =
            _cryptoAsymmetricCipher.decrypt(_keyPair.getPrivate(),
                encryptedData);

        Assert.assertTrue(Arrays.equals(decryptedData, _data));
    }

    @Test
    public void testEncryptAndDecryptDataWithRsaKemWithKdf2AndSha256AndBc()
            throws GeneralCryptoLibException {

        _cryptoAsymmetricCipher =
            new AsymmetricCipherFactory(
                getAsymmetricCipherPolicyRsaKemWithKdf2AndSha256AndBc())
                .create();

        byte[] encryptedData =
            _cryptoAsymmetricCipher.encrypt(_keyPair.getPublic(), _data);

        byte[] decryptedData =
            _cryptoAsymmetricCipher.decrypt(_keyPair.getPrivate(),
                encryptedData);

        Assert.assertTrue(Arrays.equals(decryptedData, _data));
    }
    
    @Test(expected = GeneralCryptoLibException.class)
    public void tectDecryptShortWithRsaKem()
            throws GeneralCryptoLibException {
        _cryptoAsymmetricCipher = new AsymmetricCipherFactory(
            getAsymmetricCipherPolicyRsaKemWithKdf2AndSha256AndBc())
                .create();
        byte[] data = {1, 2, 3 };
        _cryptoAsymmetricCipher.decrypt(_keyPair.getPrivate(), data);
    }

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    private static AsymmetricCipherPolicy getAsymmetricCipherPolicyRsaKemWithKdf1AndSha256AndBc() {
        return new AsymmetricCipherPolicy() {

            @Override
            public ConfigAsymmetricCipherAlgorithmAndSpec getAsymmetricCipherAlgorithmAndSpec() {
                return ConfigAsymmetricCipherAlgorithmAndSpec.RSAwithRSAKEMANDKDF1ANDSHA256_BC;
            }

            @Override
            public ConfigSecureRandomAlgorithmAndProvider getSecureRandomAlgorithmAndProvider() {
                return getSecureRandomConfig();
            }

            @Override
            public ConfigSecretKeyAlgorithmAndSpec getSecretKeyAlgorithmAndSpec() {
                return ConfigSecretKeyAlgorithmAndSpec.AES_128_SUNJCE;
            }

            @Override
            public ConfigSymmetricCipherAlgorithmAndSpec getSymmetricCipherAlgorithmAndSpec() {
                return ConfigSymmetricCipherAlgorithmAndSpec.AESwithGCMandNOPADDING_96_128_BC;
            }
        };
    }

    private static AsymmetricCipherPolicy getAsymmetricCipherPolicyRsaKemWithKdf2AndSha256AndBc() {
        return new AsymmetricCipherPolicy() {

            @Override
            public ConfigAsymmetricCipherAlgorithmAndSpec getAsymmetricCipherAlgorithmAndSpec() {
                return ConfigAsymmetricCipherAlgorithmAndSpec.RSAwithRSAKEMANDKDF2ANDSHA256_BC;
            }

            @Override
            public ConfigSecureRandomAlgorithmAndProvider getSecureRandomAlgorithmAndProvider() {
                return getSecureRandomConfig();
            }

            @Override
            public ConfigSecretKeyAlgorithmAndSpec getSecretKeyAlgorithmAndSpec() {
                return ConfigSecretKeyAlgorithmAndSpec.AES_128_SUNJCE;
            }

            @Override
            public ConfigSymmetricCipherAlgorithmAndSpec getSymmetricCipherAlgorithmAndSpec() {
                return ConfigSymmetricCipherAlgorithmAndSpec.AESwithGCMandNOPADDING_96_128_BC;
            }
        };
    }

    private static ConfigSecureRandomAlgorithmAndProvider getSecureRandomConfig() {

        switch (OperatingSystem.current()) {

        case WINDOWS:
            return ConfigSecureRandomAlgorithmAndProvider.PRNG_SUN_MSCAPI;
        case LINUX:
            return ConfigSecureRandomAlgorithmAndProvider.NATIVE_PRNG_SUN;
        default:
            throw new CryptoLibException("OS not supported");
        }
    }

    private byte[] generateData(int rest)
            throws IOException, GeneralCryptoLibException {

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        outputStream.write(_randomByteGenerator.nextRandom(100));
        outputStream.write(_randomByteGenerator.nextRandom(100));
        outputStream.write(_randomByteGenerator.nextRandom(rest));
        byte[] generatedData = outputStream.toByteArray();
        outputStream.close();

        return generatedData;
    }
}
