package ch.ge.ve.commons.crypto;

/*-
 * #%L
 * Common crypto utilities
 * %%
 * Copyright (C) 2015 - 2016 République et Canton de Genève
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

import ch.ge.ve.commons.crypto.utils.SecureRandomFactory;
import com.google.common.base.Joiner;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;

import javax.crypto.SecretKey;
import java.io.*;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Base64;
import java.util.Random;

import static ch.ge.ve.commons.crypto.SensitiveDataCryptoUtils.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertTrue;

/**
 * This test suit aims at covering the {@link SensitiveDataCryptoUtils} utility class.
 */
public class SensitiveDataCryptoUtilsTest {
    private static final int SECRETKEY_LENGTH = 256;
    private static final TestSensitiveDataCryptoUtilsConfiguration configuration = new TestSensitiveDataCryptoUtilsConfiguration();

    @BeforeClass
    public static void init() {
        Security.addProvider(new BouncyCastleProvider());
        SensitiveDataCryptoUtils.configure(configuration);
    }

    /**
     * saving a key and retrieving it from a file should give the same object
     */
    @Test
    public void saveAndRetrieveSecretKey() throws IOException, ClassNotFoundException {
        SecretKey sK = SensitiveDataCryptoUtils.buildSecretKey(SECRETKEY_LENGTH, configuration.getPbkdf2Algorithm());
        String secretKeyFingerPrint = Base64.getEncoder().encodeToString(sK.getEncoded());

        File keyFile = File.createTempFile("keyfile", "key");
        keyFile.deleteOnExit();

        saveInFile(keyFile, sK);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(keyFile));
        SecretKey readKey = (SecretKey) ois.readObject();
        ois.close();

        assertTrue(readKey.getEncoded().length * 8 == SECRETKEY_LENGTH);
        assertTrue(Base64.getEncoder().encodeToString(readKey.getEncoded()).equals(secretKeyFingerPrint));
    }

    private void saveInFile(File keyFile, Object sK) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(keyFile));
        oos.writeObject(sK);
        oos.flush();
        oos.close();
    }

    /**
     * buildMAC() should generate a correct MAC
     */
    @Test
    public void testMac() throws InvalidKeyException, NoSuchAlgorithmException, IOException, ClassNotFoundException {
        String inputData = "This is the text we want to get the MAC for";
        String hmac = Base64.getEncoder().encodeToString(buildMAC(inputData));
        assertTrue(hmac.equals("kJ5sdJg8C490B16kmZhE+druaVTWTUXvEKwL643w6dI="));
    }

    /**
     * verifyMAC() should validate a message with its correct MAC
     */
    @Test
    public void testMessageAuthenticated() {
        String message = "This is the text we want to get the MAC for";
        final String mac = buildMACAsBase64String(message);
        assertTrue("message should be authenticated", verifyMAC(message, mac));
    }

    /**
     * verifySaltedMAC() should validate a message with its correct salted MAC
     */
    @Test
    public void testMessageAuthenticatedWithSalt() {
        String message = "This is the text we want to get the MAC for";
        final String mac = buildSaltedMACAsBase64String(message);
        assertTrue("message should be authenticated", verifySaltedMAC(message, mac));
    }

    /**
     * encryption followed by a decryption should return the same object
     */
    @Test
    public void encrypt_then_decrypt_byte_array_should_be_identity() {
        byte[] input = "The lazy fox jumps over the ".getBytes();
        byte[] cipherText = encrypt(input);
        assertThat(decrypt(cipherText), is(input));
    }

    /**
     * encryption followed by a decryption of a large object should return the same object
     */
    @Test
    public void encrypt_then_decrypt_large_byte_array_should_be_identity() {
        byte[] input = new byte[4080];
        Random random = SecureRandomFactory.createPRNG();
        random.nextBytes(input);
        byte[] cipherText = encrypt(input);
        assertThat(decrypt(cipherText), is(input));
    }

    /**
     * encryption followed by a decryption of a long object should return the same object
     */
    @Test
    public void encrypt_then_decrypt_long_should_be_identity() {
        long input = 123456789L;
        String cipherText = encryptAsBase64String(input);
        assertThat(decryptAsLong(cipherText), is(input));
    }

    /**
     * encryption followed by a decryption of a string object should return the same object
     */
    @Test
    public void encrypt_then_decrypt_string_should_be_identity() {
        String input = "This is a very secret plaintext";
        String cipherText = encryptAsBase64String(input);
        assertThat(decryptAsString(cipherText), is(input));
    }

    /**
     * HMAC of a long representation and a sting representation of the same number should be the same
     */
    @Test
    public void hmacOfCardNumberShouldBeStable() {
        String cardNumberString = "1234567890123456";

        String hmac = buildMACAsBase64String(cardNumberString);

        long cardNumber = Long.valueOf(cardNumberString);

        assertThat(buildMACAsBase64String(String.valueOf(cardNumber)), is(hmac));
    }

    /**
     * encryption of the date of birth should be randomized
     */
    @Test
    public void encryptionOfDateOfBirthShouldBeRandomized() {
        Long dob = 19850101L;
        String encryptedDOB = encryptAsBase64String(dob);

        assertThat(encryptAsBase64String(dob), not(encryptedDOB));
    }

    /**
     * encryption followed by decryption of the date of birth should return the same object
     */
    @Test
    public void encryptThenDecryptDateOfBirthShouldBeStable() {
        Long dob = 19850101L;
        String encryptedDOB = encryptAsBase64String(dob);

        assertThat(decryptAsLong(encryptedDOB), is(dob));
    }

    /**
     * validation of a secure password should work
     */
    @Test
    public void securePasswordHashShouldValidate() {
        char[] passwd = "A random te$ting p#s$w0rd!!!".toCharArray();
        String passwordHash = generateStrongPasswordHash(passwd);

        assertThat("The validation of the password should work", validateStrongPasswordHash(passwd, passwordHash));
    }

    /**
     * validation of a password hash with the wrong password should fail
     */
    @Test
    public void securePasswordHashShouldNotValidateWrongPassword() {
        char[] passwd = "A random te$ting p#s$w0rd!!!".toCharArray();
        String passwordHash = generateStrongPasswordHash(passwd);
        char[] wrongPasswd = "Not this one!".toCharArray();

        assertThat("A wrong password should not be validated", !validateStrongPasswordHash(wrongPasswd, passwordHash));
    }

    /**
     * alidation of the password should fail when iteration count has been altered
     */
    @Test
    public void securePasswordHashWithWrongIterationsCountShouldNotValidate() {
        char[] passwd = "The lazy fox jumps over...".toCharArray();
        String passwordHash = generateStrongPasswordHash(passwd);

        String[] parts = passwordHash.split(":");

        int iterations = Integer.parseInt(parts[0]);

        String manipulatedIterationsHash = Joiner.on(":").join(iterations + 1, parts[1], parts[2]);
        assertThat("The validation of the password should fail when iteration count has been altered", !validateStrongPasswordHash(passwd, manipulatedIterationsHash));
    }

    /**
     * validation of the password should fail when the salt has been altered
     */
    @Test
    public void securePasswordHashWithWrongSaltShouldNotValidate() {
        char[] passwd = "A random te$ting p#s$w0rd!!!".toCharArray();
        String passwordHash = generateStrongPasswordHash(passwd);

        String[] parts = passwordHash.split(":");

        String salt = new BigInteger(parts[1].length() / 2, new SecureRandom()).toString(16);
        if (salt.length() < parts[1].length()) {
            salt = String.format("%0" + (parts[1].length() - salt.length()) + "d%s", 0, salt);
        }
        String manipulatedSaltHash = Joiner.on(":").join(parts[0], salt, parts[2]);

        assertThat("The validation of the password should fail when the salt has been altered", !validateStrongPasswordHash(passwd, manipulatedSaltHash));
    }

}