package com.auth0.android.authentication.storage;

import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.security.KeyPairGeneratorSpec;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.annotation.RequiresApi;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;

import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.ProviderException;
import java.security.UnrecoverableEntryException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.security.auth.x500.X500Principal;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.never;
import static org.powermock.api.mockito.PowerMockito.doReturn;
import static org.powermock.api.mockito.PowerMockito.doThrow;
import static org.powermock.api.mockito.PowerMockito.mock;

/**
 * Created by lbalmaceda on 8/24/17.
 */
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@RunWith(PowerMockRunner.class)
@PrepareForTest({CryptoUtil.class, KeyGenerator.class, TextUtils.class, Build.VERSION.class, Base64.class, Cipher.class, Log.class})
@Config(sdk = 22)
public class CryptoUtilTest {

    private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
    private static final String AES_TRANSFORMATION = "AES/GCM/NOPADDING";
    private static final String CERTIFICATE_PRINCIPAL = "CN=Auth0.Android,O=Auth0";
    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
    private static final String ALGORITHM_AES = "AES";
    private static final String ALGORITHM_RSA = "RSA";


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

    private Storage storage = PowerMockito.mock(Storage.class);
    private Cipher rsaCipher = PowerMockito.mock(Cipher.class);
    private Cipher aesCipher = PowerMockito.mock(Cipher.class);
    private KeyStore keyStore = PowerMockito.mock(KeyStore.class);
    private KeyPairGenerator keyPairGenerator = PowerMockito.mock(KeyPairGenerator.class);
    private KeyGenerator keyGenerator = PowerMockito.mock(KeyGenerator.class);

    private CryptoUtil cryptoUtil;

    private static final String KEY_ALIAS = "keyName";
    private Context context;

    //Android KeyStore not accessible using Robolectric
    //Must test using white-box approach
    //Ref: https://github.com/robolectric/robolectric/issues/1518

    @Before
    public void setUp() throws Exception {
        PowerMockito.mockStatic(Log.class);
        PowerMockito.mockStatic(TextUtils.class);
        PowerMockito.when(TextUtils.isEmpty(anyString())).then(new Answer<Boolean>() {
            @Override
            public Boolean answer(InvocationOnMock invocation) {
                String input = invocation.getArgumentAt(0, String.class);
                return input == null || input.isEmpty();
            }
        });

        context = mock(Context.class);
        cryptoUtil = newCryptoUtilSpy();
    }

    /*
     * GET RSA KEY tests
     */

    @Test
    public void shouldThrowWhenRSAKeyAliasIsInvalid() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("RSA and AES Key alias must be valid.");
        new CryptoUtil(RuntimeEnvironment.application, storage, " ");
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Test
    @Config(sdk = 19)
    public void shouldNotCreateProtectedRSAKeyPairIfMissingAndLockScreenEnabledOnAPI19() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 19);

        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
        KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);

        KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class);
        KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec);
        PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder);

        ArgumentCaptor<X500Principal> principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
        ArgumentCaptor<Date> startDateCaptor = ArgumentCaptor.forClass(Date.class);
        ArgumentCaptor<Date> endDateCaptor = ArgumentCaptor.forClass(Date.class);

        //Set LockScreen as Enabled
        KeyguardManager kService = PowerMockito.mock(KeyguardManager.class);
        PowerMockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService);
        PowerMockito.when(kService.isKeyguardSecure()).thenReturn(true);

        final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();

        Mockito.verify(builder).setKeySize(2048);
        Mockito.verify(builder).setSubject(principalCaptor.capture());
        Mockito.verify(builder).setAlias(KEY_ALIAS);
        Mockito.verify(builder).setSerialNumber(BigInteger.ONE);
        Mockito.verify(builder).setStartDate(startDateCaptor.capture());
        Mockito.verify(builder).setEndDate(endDateCaptor.capture());
        Mockito.verify(builder, never()).setEncryptionRequired();
        Mockito.verify(keyPairGenerator).initialize(spec);
        Mockito.verify(keyPairGenerator).generateKeyPair();

        assertThat(principalCaptor.getValue(), is(notNullValue()));
        assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));

        assertThat(startDateCaptor.getValue(), is(notNullValue()));
        long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
        long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(0L)); //Date is Today

        assertThat(endDateCaptor.getValue(), is(notNullValue()));
        diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
        days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days

        assertThat(entry, is(expectedEntry));
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Test
    @Config(sdk = 21)
    public void shouldCreateUnprotectedRSAKeyPairIfMissingAndLockScreenDisabledOnAPI21() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 21);

        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
        KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);

        KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class);
        KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec);
        PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder);

        ArgumentCaptor<X500Principal> principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
        ArgumentCaptor<Date> startDateCaptor = ArgumentCaptor.forClass(Date.class);
        ArgumentCaptor<Date> endDateCaptor = ArgumentCaptor.forClass(Date.class);

        //Set LockScreen as Disabled
        KeyguardManager kService = PowerMockito.mock(KeyguardManager.class);
        PowerMockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService);
        PowerMockito.when(kService.isKeyguardSecure()).thenReturn(false);
        PowerMockito.when(kService.createConfirmDeviceCredentialIntent(any(CharSequence.class), any(CharSequence.class))).thenReturn(null);

        final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();

        Mockito.verify(builder).setKeySize(2048);
        Mockito.verify(builder).setSubject(principalCaptor.capture());
        Mockito.verify(builder).setAlias(KEY_ALIAS);
        Mockito.verify(builder).setSerialNumber(BigInteger.ONE);
        Mockito.verify(builder).setStartDate(startDateCaptor.capture());
        Mockito.verify(builder).setEndDate(endDateCaptor.capture());
        Mockito.verify(builder, never()).setEncryptionRequired();
        Mockito.verify(keyPairGenerator).initialize(spec);
        Mockito.verify(keyPairGenerator).generateKeyPair();

        assertThat(principalCaptor.getValue(), is(notNullValue()));
        assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));

        assertThat(startDateCaptor.getValue(), is(notNullValue()));
        long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
        long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(0L)); //Date is Today

        assertThat(endDateCaptor.getValue(), is(notNullValue()));
        diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
        days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days

        assertThat(entry, is(expectedEntry));
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Test
    @Config(sdk = 21)
    public void shouldCreateProtectedRSAKeyPairIfMissingAndLockScreenEnabledOnAPI21() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 21);

        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
        KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);

        KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class);
        KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec);
        PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder);

        ArgumentCaptor<X500Principal> principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
        ArgumentCaptor<Date> startDateCaptor = ArgumentCaptor.forClass(Date.class);
        ArgumentCaptor<Date> endDateCaptor = ArgumentCaptor.forClass(Date.class);

        //Set LockScreen as Enabled
        KeyguardManager kService = PowerMockito.mock(KeyguardManager.class);
        PowerMockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService);
        PowerMockito.when(kService.isKeyguardSecure()).thenReturn(true);
        PowerMockito.when(kService.createConfirmDeviceCredentialIntent(any(CharSequence.class), any(CharSequence.class))).thenReturn(new Intent());

        final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();

        Mockito.verify(builder).setKeySize(2048);
        Mockito.verify(builder).setSubject(principalCaptor.capture());
        Mockito.verify(builder).setAlias(KEY_ALIAS);
        Mockito.verify(builder).setSerialNumber(BigInteger.ONE);
        Mockito.verify(builder).setStartDate(startDateCaptor.capture());
        Mockito.verify(builder).setEndDate(endDateCaptor.capture());
        Mockito.verify(builder).setEncryptionRequired();
        Mockito.verify(keyPairGenerator).initialize(spec);
        Mockito.verify(keyPairGenerator).generateKeyPair();

        assertThat(principalCaptor.getValue(), is(notNullValue()));
        assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));

        assertThat(startDateCaptor.getValue(), is(notNullValue()));
        long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
        long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(0L)); //Date is Today

        assertThat(endDateCaptor.getValue(), is(notNullValue()));
        diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
        days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days

        assertThat(entry, is(expectedEntry));
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    @Test
    @Config(sdk = 23)
    public void shouldCreateRSAKeyPairIfMissingOnAPI23AndUp() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 23);

        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
        KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);

        KeyGenParameterSpec spec = PowerMockito.mock(KeyGenParameterSpec.class);
        KeyGenParameterSpec.Builder builder = newKeyGenParameterSpecBuilder(spec);
        PowerMockito.whenNew(KeyGenParameterSpec.Builder.class).withArguments(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).thenReturn(builder);

        ArgumentCaptor<X500Principal> principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
        ArgumentCaptor<Date> startDateCaptor = ArgumentCaptor.forClass(Date.class);
        ArgumentCaptor<Date> endDateCaptor = ArgumentCaptor.forClass(Date.class);


        final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();


        Mockito.verify(builder).setKeySize(2048);
        Mockito.verify(builder).setCertificateSubject(principalCaptor.capture());
        Mockito.verify(builder).setCertificateSerialNumber(BigInteger.ONE);
        Mockito.verify(builder).setCertificateNotBefore(startDateCaptor.capture());
        Mockito.verify(builder).setCertificateNotAfter(endDateCaptor.capture());
        Mockito.verify(builder).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1);
        Mockito.verify(builder).setBlockModes(KeyProperties.BLOCK_MODE_ECB);
        Mockito.verify(keyPairGenerator).initialize(spec);
        Mockito.verify(keyPairGenerator).generateKeyPair();

        assertThat(principalCaptor.getValue(), is(notNullValue()));
        assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));

        assertThat(startDateCaptor.getValue(), is(notNullValue()));
        long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
        long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(0L)); //Date is Today

        assertThat(endDateCaptor.getValue(), is(notNullValue()));
        diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
        days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days

        assertThat(entry, is(expectedEntry));
    }

    @RequiresApi(api = Build.VERSION_CODES.P)
    @Test
    @Config(sdk = 28)
    public void shouldCreateRSAKeyPairIfMissingOnAPI28AndUp() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28);

        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
        KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);

        KeyGenParameterSpec spec = PowerMockito.mock(KeyGenParameterSpec.class);
        KeyGenParameterSpec.Builder builder = newKeyGenParameterSpecBuilder(spec);
        PowerMockito.whenNew(KeyGenParameterSpec.Builder.class).withArguments(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).thenReturn(builder);

        ArgumentCaptor<X500Principal> principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
        ArgumentCaptor<Date> startDateCaptor = ArgumentCaptor.forClass(Date.class);
        ArgumentCaptor<Date> endDateCaptor = ArgumentCaptor.forClass(Date.class);


        final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();

        Mockito.verify(builder).setKeySize(2048);
        Mockito.verify(builder).setCertificateSubject(principalCaptor.capture());
        Mockito.verify(builder).setCertificateSerialNumber(BigInteger.ONE);
        Mockito.verify(builder).setCertificateNotBefore(startDateCaptor.capture());
        Mockito.verify(builder).setCertificateNotAfter(endDateCaptor.capture());
        Mockito.verify(builder).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1);
        Mockito.verify(builder).setBlockModes(KeyProperties.BLOCK_MODE_ECB);
        Mockito.verify(keyPairGenerator).initialize(spec);
        Mockito.verify(keyPairGenerator).generateKeyPair();

        assertThat(principalCaptor.getValue(), is(notNullValue()));
        assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));

        assertThat(startDateCaptor.getValue(), is(notNullValue()));
        long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
        long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(0L)); //Date is Today

        assertThat(endDateCaptor.getValue(), is(notNullValue()));
        diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
        days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days

        assertThat(entry, is(expectedEntry));
    }

    @RequiresApi(api = Build.VERSION_CODES.P)
    @Test
    @Config(sdk = 28)
    public void shouldCreateNewRSAKeyPairWhenExistingRSAKeyPairCannotBeRebuiltOnAPI28AndUp() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28);
        PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);

        //This is required to trigger the fallback when alias is present but key is not
        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true);
        PowerMockito.when(keyStore.getKey(KEY_ALIAS, null)).thenReturn(privateKey).thenReturn(null);
        PowerMockito.when(keyStore.getCertificate(KEY_ALIAS)).thenReturn(null);
        //This is required to trigger finding the key after generating it
        KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry);

        //Tests no instantiation of PrivateKeyEntry
        PowerMockito.verifyZeroInteractions(KeyStore.PrivateKeyEntry.class);

        //Creation assertion
        KeyGenParameterSpec spec = PowerMockito.mock(KeyGenParameterSpec.class);
        KeyGenParameterSpec.Builder builder = newKeyGenParameterSpecBuilder(spec);
        PowerMockito.whenNew(KeyGenParameterSpec.Builder.class).withArguments(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).thenReturn(builder);

        ArgumentCaptor<X500Principal> principalCaptor = ArgumentCaptor.forClass(X500Principal.class);
        ArgumentCaptor<Date> startDateCaptor = ArgumentCaptor.forClass(Date.class);
        ArgumentCaptor<Date> endDateCaptor = ArgumentCaptor.forClass(Date.class);


        final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry();

        Mockito.verify(builder).setKeySize(2048);
        Mockito.verify(builder).setCertificateSubject(principalCaptor.capture());
        Mockito.verify(builder).setCertificateSerialNumber(BigInteger.ONE);
        Mockito.verify(builder).setCertificateNotBefore(startDateCaptor.capture());
        Mockito.verify(builder).setCertificateNotAfter(endDateCaptor.capture());
        Mockito.verify(builder).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1);
        Mockito.verify(builder).setBlockModes(KeyProperties.BLOCK_MODE_ECB);
        Mockito.verify(keyPairGenerator).initialize(spec);
        Mockito.verify(keyPairGenerator).generateKeyPair();

        assertThat(principalCaptor.getValue(), is(notNullValue()));
        assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL));

        assertThat(startDateCaptor.getValue(), is(notNullValue()));
        long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime();
        long days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(0L)); //Date is Today

        assertThat(endDateCaptor.getValue(), is(notNullValue()));
        diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime();
        days = TimeUnit.MILLISECONDS.toDays(diffMillis);
        assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days

        assertThat(entry, is(expectedEntry));
    }

    @RequiresApi(api = Build.VERSION_CODES.P)
    @Test
    @Config(sdk = 28)
    public void shouldUseExistingRSAKeyPairRebuildingTheEntryOnAPI28AndUp() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28);
        KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
        Certificate certificate = PowerMockito.mock(Certificate.class);

        ArgumentCaptor<Object> varargsCaptor = ArgumentCaptor.forClass(Object.class);
        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true);
        PowerMockito.when(keyStore.getKey(KEY_ALIAS, null)).thenReturn(privateKey);
        PowerMockito.when(keyStore.getCertificate(KEY_ALIAS)).thenReturn(certificate);
        PowerMockito.whenNew(KeyStore.PrivateKeyEntry.class).withAnyArguments().thenReturn(entry);

        KeyStore.PrivateKeyEntry rsaEntry = cryptoUtil.getRSAKeyEntry();
        PowerMockito.verifyNew(KeyStore.PrivateKeyEntry.class).withArguments(varargsCaptor.capture());
        assertThat(rsaEntry, is(notNullValue()));
        assertThat(rsaEntry, is(entry));
        assertThat(varargsCaptor.getAllValues(), is(notNullValue()));
        PrivateKey capturedPrivateKey = (PrivateKey) varargsCaptor.getAllValues().get(0);
        Certificate[] capturedCertificatesArray = (Certificate[]) varargsCaptor.getAllValues().get(1);
        assertThat(capturedPrivateKey, is(privateKey));
        assertThat(capturedCertificatesArray[0], is(certificate));
        assertThat(capturedCertificatesArray.length, is(1));
    }

    @RequiresApi(api = Build.VERSION_CODES.P)
    @Test
    @Config(sdk = 28)
    public void shouldUseExistingRSAKeyPairOnAPI28AndUp() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28);
        KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(entry);
        PrivateKey privateKey = null;
        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true);
        PowerMockito.when(keyStore.getKey(KEY_ALIAS, null)).thenReturn(privateKey);

        KeyStore.PrivateKeyEntry rsaEntry = cryptoUtil.getRSAKeyEntry();
        assertThat(rsaEntry, is(notNullValue()));
        assertThat(rsaEntry, is(entry));
    }

    @RequiresApi(api = Build.VERSION_CODES.O_MR1)
    @Test
    @Config(sdk = 27)
    public void shouldUseExistingRSAKeyPairOnAPI27AndDown() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 27);
        KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true);
        PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(entry);

        KeyStore.PrivateKeyEntry rsaEntry = cryptoUtil.getRSAKeyEntry();
        assertThat(rsaEntry, is(notNullValue()));
        assertThat(rsaEntry, is(entry));
    }

    @Test
    public void shouldThrowOnUnrecoverableEntryExceptionWhenTryingToObtainRSAKeys() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The existing RSA key pair could not be recovered and has been deleted. " +
                "This occasionally happens when the Lock Screen settings are changed. You can safely retry this operation.");

        KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true);
        PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null))
                .thenThrow(new UnrecoverableEntryException())
                .thenReturn(entry);

        cryptoUtil.getRSAKeyEntry();
    }

    @Test
    public void shouldDeleteRSAAndAESKeysAndThrowOnIOExceptionWhenTryingToObtainRSAKeys() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The existing RSA key pair could not be recovered and has been deleted. " +
                "This occasionally happens when the Lock Screen settings are changed. You can safely retry this operation.");

        doThrow(new IOException()).when(keyStore).load(any(KeyStore.LoadStoreParameter.class));

        cryptoUtil.getRSAKeyEntry();

        Mockito.verify(keyStore).deleteEntry(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS + "_iv");
    }

    @Test
    public void shouldThrowOnKeyStoreExceptionWhenTryingToObtainRSAKeys() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        PowerMockito.mockStatic(KeyStore.class);
        PowerMockito.when(KeyStore.getInstance(anyString()))
                .thenThrow(new KeyStoreException());

        cryptoUtil.getRSAKeyEntry();
    }

    @Test
    public void shouldThrowOnCertificateExceptionWhenTryingToObtainRSAKeys() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        doThrow(new CertificateException()).when(keyStore).load(any(KeyStore.LoadStoreParameter.class));

        cryptoUtil.getRSAKeyEntry();
    }

    @Test
    public void shouldThrowOnProviderExceptionWhenTryingToObtainRSAKeys() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        doThrow(new ProviderException()).when(keyStore).load(any(KeyStore.LoadStoreParameter.class));

        cryptoUtil.getRSAKeyEntry();
    }

    @Test
    public void shouldThrowOnNoSuchProviderExceptionWhenTryingToObtainRSAKeys() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 19);
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
        KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class);
        KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec);
        PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder);

        PowerMockito.mockStatic(KeyPairGenerator.class);
        PowerMockito.when(KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE))
                .thenThrow(new NoSuchProviderException());

        cryptoUtil.getRSAKeyEntry();
    }

    @Test
    public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToObtainRSAKeys() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 19);
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
        KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class);
        KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec);
        PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder);

        PowerMockito.mockStatic(KeyPairGenerator.class);
        PowerMockito.when(KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE))
                .thenThrow(new NoSuchAlgorithmException());

        cryptoUtil.getRSAKeyEntry();
    }

    @Test
    public void shouldThrowOnInvalidAlgorithmParameterExceptionWhenTryingToObtainRSAKeys() throws Exception {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 19);
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false);
        KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class);
        KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec);
        PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder);

        doThrow(new InvalidAlgorithmParameterException()).when(keyPairGenerator).initialize(any(AlgorithmParameterSpec.class));

        cryptoUtil.getRSAKeyEntry();
    }

    /*
     * GET AES KEY tests
     */

    @Test
    public void shouldCreateAESKeyIfMissing() {
        byte[] sampleBytes = new byte[]{0, 1, 2, 3, 4, 5};
        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.encode(sampleBytes, Base64.DEFAULT)).thenReturn("data".getBytes());
        PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(null);

        SecretKey secretKey = PowerMockito.mock(SecretKey.class);
        PowerMockito.when(keyGenerator.generateKey()).thenReturn(secretKey);
        PowerMockito.when(secretKey.getEncoded()).thenReturn(sampleBytes);
        doReturn(sampleBytes).when(cryptoUtil).RSAEncrypt(sampleBytes);


        final byte[] aesKey = cryptoUtil.getAESKey();

        Mockito.verify(keyGenerator).init(256);
        Mockito.verify(keyGenerator).generateKey();
        Mockito.verify(storage).store(KEY_ALIAS, "data");

        assertThat(aesKey, is(notNullValue()));
        assertThat(aesKey, is(sampleBytes));
    }

    @Test
    public void shouldCreateAESKeyIfStoredOneIsEmpty() throws BadPaddingException, IllegalBlockSizeException {
        String emptyString = "";
        byte[] sampleBytes = emptyString.getBytes();
        byte[] sampleOutput = new byte[]{99, 33, 11};
        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.decode(emptyString, Base64.DEFAULT)).thenReturn(sampleBytes);
        PowerMockito.when(Base64.encode(sampleBytes, Base64.DEFAULT)).thenReturn("data".getBytes());
        PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(emptyString);
        doReturn(sampleBytes).when(cryptoUtil).RSAEncrypt(sampleBytes);

        //Assume RSAKeyEntry exists
        PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(privateKey).when(privateKeyEntry).getPrivateKey();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        doReturn(sampleOutput).when(rsaCipher).doFinal(sampleBytes);

        SecretKey secretKey = PowerMockito.mock(SecretKey.class);
        PowerMockito.when(secretKey.getEncoded()).thenReturn(sampleBytes);
        PowerMockito.when(keyGenerator.generateKey()).thenReturn(secretKey);


        final byte[] aesKey = cryptoUtil.getAESKey();

        Mockito.verify(keyGenerator).init(256);
        Mockito.verify(keyGenerator).generateKey();
        Mockito.verify(storage).store(KEY_ALIAS, "data");

        assertThat(aesKey, is(notNullValue()));
        assertThat(aesKey, is(sampleBytes));
    }

    @Test
    public void shouldUseExistingAESKey() {
        final int AES_KEY_SIZE = 256;
        byte[] sampleBytes = new byte[AES_KEY_SIZE / 8];
        Arrays.fill(sampleBytes, (byte) 1);
        String aesString = "non null string";

        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.decode(aesString, Base64.DEFAULT)).thenReturn(sampleBytes);
        PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(aesString);
        doReturn(sampleBytes).when(cryptoUtil).RSADecrypt(sampleBytes);

        final byte[] aesKey = cryptoUtil.getAESKey();
        assertThat(aesKey, is(notNullValue()));
        assertThat(aesKey, is(sampleBytes));
    }

    @Test
    public void shouldThrowOnNoSuchAlgorithmExceptionWhenCreatingAESKey() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(null);
        PowerMockito.mockStatic(KeyGenerator.class);
        PowerMockito.when(KeyGenerator.getInstance(ALGORITHM_AES))
                .thenThrow(new NoSuchAlgorithmException());

        cryptoUtil.getAESKey();
    }

    /*
     * RSA ENCRYPT tests
     */

    @Test
    public void shouldRSAEncryptData() throws Exception {
        byte[] sampleInput = new byte[]{0, 1, 2, 3, 4, 5};
        byte[] sampleOutput = new byte[]{99, 33, 11};

        Certificate certificate = PowerMockito.mock(Certificate.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(certificate).when(privateKeyEntry).getCertificate();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        doReturn(sampleOutput).when(rsaCipher).doFinal(sampleInput);

        final byte[] output = cryptoUtil.RSAEncrypt(sampleInput);

        Mockito.verify(rsaCipher).init(Cipher.ENCRYPT_MODE, certificate);
        assertThat(output, is(sampleOutput));
    }

    @Test
    public void shouldThrowOnInvalidKeyExceptionWhenTryingToRSAEncrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        byte[] sampleBytes = new byte[0];
        Certificate certificate = PowerMockito.mock(Certificate.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(certificate).when(privateKeyEntry).getCertificate();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaCipher);
        doThrow(new InvalidKeyException()).when(rsaCipher).init(Cipher.ENCRYPT_MODE, certificate);

        cryptoUtil.RSAEncrypt(sampleBytes);
    }

    @Test
    public void shouldDeleteAESKeysAndThrowOnBadPaddingExceptionWhenTryingToRSAEncrypt() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The RSA decrypted input is invalid.");

        byte[] sampleBytes = new byte[0];
        Certificate certificate = PowerMockito.mock(Certificate.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(certificate).when(privateKeyEntry).getCertificate();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaCipher);
        PowerMockito.when(rsaCipher.doFinal(sampleBytes)).thenThrow(new BadPaddingException());

        cryptoUtil.RSAEncrypt(sampleBytes);

        Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS + "_iv");
    }

    @Test
    public void shouldDeleteAESKeysAndThrowOnIllegalBlockSizeExceptionWhenTryingToRSAEncrypt() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The RSA decrypted input is invalid.");

        Certificate certificate = PowerMockito.mock(Certificate.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(certificate).when(privateKeyEntry).getCertificate();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaCipher);
        PowerMockito.when(rsaCipher.doFinal(any(byte[].class))).thenThrow(new IllegalBlockSizeException());

        cryptoUtil.RSAEncrypt(new byte[0]);

        Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS + "_iv");
    }

    @Test
    public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToRSAEncrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        Certificate certificate = PowerMockito.mock(Certificate.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(certificate).when(privateKeyEntry).getCertificate();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException());

        cryptoUtil.RSAEncrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnNoSuchPaddingExceptionWhenTryingToRSAEncrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        Certificate certificate = PowerMockito.mock(Certificate.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(certificate).when(privateKeyEntry).getCertificate();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchPaddingException());

        cryptoUtil.RSAEncrypt(new byte[0]);
    }

    /*
     * RSA DECRYPT tests
     */

    @Test
    public void shouldRSADecryptData() throws Exception {
        byte[] sampleInput = new byte[]{0, 1, 2, 3, 4, 5};
        byte[] sampleOutput = new byte[]{99, 33, 11};

        PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(privateKey).when(privateKeyEntry).getPrivateKey();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        doReturn(sampleOutput).when(rsaCipher).doFinal(sampleInput);

        final byte[] output = cryptoUtil.RSADecrypt(sampleInput);

        Mockito.verify(rsaCipher).init(Cipher.DECRYPT_MODE, privateKey);
        assertThat(output, is(sampleOutput));
    }

    @Test
    public void shouldThrowOnInvalidKeyExceptionWhenTryingToRSADecrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        byte[] sampleBytes = new byte[0];
        PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(privateKey).when(privateKeyEntry).getPrivateKey();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaCipher);
        doThrow(new InvalidKeyException()).when(rsaCipher).init(Cipher.DECRYPT_MODE, privateKey);

        cryptoUtil.RSADecrypt(sampleBytes);
    }

    @Test
    public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToRSADecrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(privateKey).when(privateKeyEntry).getPrivateKey();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException());

        cryptoUtil.RSADecrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnNoSuchPaddingExceptionWhenTryingToRSADecrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(privateKey).when(privateKeyEntry).getPrivateKey();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchPaddingException());

        cryptoUtil.RSADecrypt(new byte[0]);
    }

    @Test
    public void shouldDeleteAESKeysAndThrowOnBadPaddingExceptionWhenTryingToRSADecrypt() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The RSA encrypted input is corrupted and cannot be recovered. Please discard it.");

        PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(privateKey).when(privateKeyEntry).getPrivateKey();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();

        doThrow(new BadPaddingException()).when(rsaCipher).doFinal(any(byte[].class));
        cryptoUtil.RSADecrypt(new byte[0]);

        Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS + "_iv");
    }

    @Test
    public void shouldDeleteAESKeysAndThrowOnIllegalBlockSizeExceptionWhenTryingToRSADecrypt() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The RSA encrypted input is corrupted and cannot be recovered. Please discard it.");

        PrivateKey privateKey = PowerMockito.mock(PrivateKey.class);
        KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class);
        doReturn(privateKey).when(privateKeyEntry).getPrivateKey();
        doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry();

        doThrow(new IllegalBlockSizeException()).when(rsaCipher).doFinal(any(byte[].class));
        cryptoUtil.RSADecrypt(new byte[0]);

        Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS);
        Mockito.verify(storage).remove(KEY_ALIAS + "_iv");
    }

    /*
     * MAIN ENCRYPT (AES) tests
     */

    @Test
    public void shouldAESEncryptData() throws Exception {
        ArgumentCaptor<SecretKey> secretKeyCaptor = ArgumentCaptor.forClass(SecretKey.class);
        byte[] aesKey = "aes-decrypted-key".getBytes();
        byte[] data = "data".getBytes();
        byte[] encryptedData = new byte[]{0, 1, 2, 3, 4, 5};
        byte[] iv = new byte[]{99, 99, 11, 11};
        byte[] encodedIv = "iv-data".getBytes();

        doReturn(aesKey).when(cryptoUtil).getAESKey();
        doReturn(encryptedData).when(aesCipher).doFinal(data);
        PowerMockito.when(aesCipher.doFinal(data)).thenReturn(encryptedData);
        PowerMockito.when(aesCipher.getIV()).thenReturn(iv);
        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.encode(iv, Base64.DEFAULT)).thenReturn(encodedIv);

        final byte[] encrypted = cryptoUtil.encrypt(data);


        Mockito.verify(aesCipher).init(eq(Cipher.ENCRYPT_MODE), secretKeyCaptor.capture());
        assertThat(secretKeyCaptor.getValue(), is(notNullValue()));
        assertThat(secretKeyCaptor.getValue().getAlgorithm(), is(ALGORITHM_AES));
        assertThat(secretKeyCaptor.getValue().getEncoded(), is(aesKey));

        Mockito.verify(storage).store(KEY_ALIAS + "_iv", "iv-data");
        assertThat(encrypted, is(encryptedData));
    }

    @Test
    public void shouldThrowOnCryptoExceptionOnRSAKeyReadingWhenTryingToAESEncrypt() {
        exception.expect(CryptoException.class);

        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]);
        PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key");

        doThrow(new CryptoException(null, null)).when(cryptoUtil).getRSAKeyEntry();
        cryptoUtil.encrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnCryptoExceptionOnAESKeyReadingWhenTryingToAESEncrypt() {
        exception.expect(CryptoException.class);

        doThrow(new CryptoException(null, null)).when(cryptoUtil).getAESKey();
        cryptoUtil.encrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnIncompatibleDeviceExceptionOnRSAKeyReadingWhenTryingToAESEncrypt() {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]);
        PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key");

        doThrow(new IncompatibleDeviceException(null)).when(cryptoUtil).getRSAKeyEntry();
        cryptoUtil.encrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnIncompatibleDeviceExceptionOnAESKeyReadingWhenTryingToAESEncrypt() {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        doThrow(new IncompatibleDeviceException(null)).when(cryptoUtil).getAESKey();
        cryptoUtil.encrypt(new byte[0]);
    }


    @Test
    public void shouldThrowOnNoSuchPaddingExceptionWhenTryingToAESEncrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey();

        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchPaddingException());

        cryptoUtil.encrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToAESEncrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey();

        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException());

        cryptoUtil.encrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnInvalidKeyExceptionWhenTryingToAESEncrypt() throws Exception {
        Exception exception = null;
        ArgumentCaptor<SecretKey> secretKeyArgumentCaptor = ArgumentCaptor.forClass(SecretKey.class);
        byte[] aesKeyBytes = new byte[]{11, 22, 33};
        try {
            doReturn(aesKeyBytes).when(cryptoUtil).getAESKey();

            PowerMockito.mockStatic(Cipher.class);
            PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher);
            doThrow(new InvalidKeyException()).when(aesCipher).init(eq(Cipher.ENCRYPT_MODE), secretKeyArgumentCaptor.capture());

            cryptoUtil.encrypt(new byte[0]);
        } catch (IncompatibleDeviceException e) {
            exception = e;
        }
        assertThat(exception, is(notNullValue()));
        assertThat(secretKeyArgumentCaptor.getValue().getAlgorithm(), is("AES"));
        assertThat(secretKeyArgumentCaptor.getValue().getEncoded(), is(aesKeyBytes));
    }


    @Test
    public void shouldDeleteAESKeysAndThrowOnBadPaddingExceptionWhenTryingToAESEncrypt() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The AES decrypted input is invalid.");

        doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey();
        doThrow(new BadPaddingException()).when(aesCipher)
                .doFinal(any(byte[].class));

        cryptoUtil.encrypt(new byte[0]);

        Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS);
        Mockito.verify(storage, never()).remove(KEY_ALIAS);
        Mockito.verify(storage, never()).remove(KEY_ALIAS + "_iv");
    }

    @Test
    public void shouldDeleteAESKeysAndThrowOnIllegalBlockSizeExceptionWhenTryingToAESEncrypt() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The AES decrypted input is invalid.");

        doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey();
        doThrow(new IllegalBlockSizeException()).when(aesCipher)
                .doFinal(any(byte[].class));

        cryptoUtil.encrypt(new byte[0]);

        Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS);
        Mockito.verify(storage, never()).remove(KEY_ALIAS);
        Mockito.verify(storage, never()).remove(KEY_ALIAS + "_iv");
    }


    /*
     * MAIN DECRYPT (AES) tests
     */

    @Test
    public void shouldAESDecryptData() throws Exception {
        ArgumentCaptor<SecretKey> secretKeyCaptor = ArgumentCaptor.forClass(SecretKey.class);
        ArgumentCaptor<IvParameterSpec> ivParameterSpecCaptor = ArgumentCaptor.forClass(IvParameterSpec.class);
        byte[] aesKey = "aes-decrypted-key".getBytes();
        byte[] data = "data".getBytes();
        byte[] decryptedData = new byte[]{0, 1, 2, 3, 4, 5};
        String encodedIv = "iv-data";

        doReturn(aesKey).when(cryptoUtil).getAESKey();
        doReturn(decryptedData).when(aesCipher).doFinal(data);
        PowerMockito.when(aesCipher.doFinal(data)).thenReturn(decryptedData);
        PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn(encodedIv);
        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.decode(encodedIv, Base64.DEFAULT)).thenReturn(encodedIv.getBytes());

        final byte[] decrypted = cryptoUtil.decrypt(data);


        Mockito.verify(aesCipher).init(eq(Cipher.DECRYPT_MODE), secretKeyCaptor.capture(), ivParameterSpecCaptor.capture());
        assertThat(secretKeyCaptor.getValue(), is(notNullValue()));
        assertThat(secretKeyCaptor.getValue().getAlgorithm(), is(ALGORITHM_AES));
        assertThat(secretKeyCaptor.getValue().getEncoded(), is(aesKey));
        assertThat(ivParameterSpecCaptor.getValue(), is(notNullValue()));
        assertThat(ivParameterSpecCaptor.getValue().getIV(), is(encodedIv.getBytes()));

        assertThat(decrypted, is(decryptedData));
    }

    @Test
    public void shouldThrowOnCryptoExceptionOnRSAKeyReadingWhenTryingToAESDecrypt() {
        exception.expect(CryptoException.class);

        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]);
        PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key");

        doThrow(new CryptoException(null, null)).when(cryptoUtil).getRSAKeyEntry();
        cryptoUtil.decrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnCryptoExceptionOnAESKeyReadingWhenTryingToAESDecrypt() {
        exception.expect(CryptoException.class);

        doThrow(new CryptoException(null, null)).when(cryptoUtil).getAESKey();
        cryptoUtil.decrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnIncompatibleDeviceExceptionOnRSAKeyReadingWhenTryingToAESDecrypt() {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]);
        PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key");

        doThrow(new IncompatibleDeviceException(null)).when(cryptoUtil).getRSAKeyEntry();
        cryptoUtil.decrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnIncompatibleDeviceExceptionOnAESKeyReadingWhenTryingToAESDecrypt() {
        exception.expect(IncompatibleDeviceException.class);
        exception.expectMessage("The device is not compatible with the CryptoUtil class");

        doThrow(new IncompatibleDeviceException(null)).when(cryptoUtil).getAESKey();
        cryptoUtil.decrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnNoSuchPaddingExceptionWhenTryingToAESDecrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey();

        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchPaddingException());

        cryptoUtil.decrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToAESDecrypt() throws Exception {
        exception.expect(IncompatibleDeviceException.class);
        doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey();

        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException());

        cryptoUtil.decrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnEmptyInitializationVectorWhenTryingToAESDecrypt() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The encryption keys changed recently. You need to re-encrypt something first.");

        doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher);
        PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("");


        cryptoUtil.decrypt(new byte[0]);
    }

    @Test
    public void shouldThrowOnInvalidKeyExceptionWhenTryingToAESDecrypt() throws Exception {
        Exception exception = null;
        byte[] aesKeyBytes = new byte[]{11, 22, 33};
        byte[] ivBytes = new byte[]{99, 22};
        ArgumentCaptor<SecretKey> secretKeyArgumentCaptor = ArgumentCaptor.forClass(SecretKey.class);
        ArgumentCaptor<IvParameterSpec> ivParameterSpecArgumentCaptor = ArgumentCaptor.forClass(IvParameterSpec.class);

        try {
            doReturn(aesKeyBytes).when(cryptoUtil).getAESKey();
            PowerMockito.mockStatic(Cipher.class);
            PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher);
            PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv");

            PowerMockito.mockStatic(Base64.class);
            PowerMockito.when(Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes);

            doThrow(new InvalidKeyException()).when(aesCipher).init(eq(Cipher.DECRYPT_MODE), secretKeyArgumentCaptor.capture(), ivParameterSpecArgumentCaptor.capture());

            cryptoUtil.decrypt(new byte[0]);
        } catch (IncompatibleDeviceException e) {
            exception = e;
        }

        assertThat(exception, is(notNullValue()));
        assertThat(secretKeyArgumentCaptor.getValue().getAlgorithm(), is("AES"));
        assertThat(secretKeyArgumentCaptor.getValue().getEncoded(), is(aesKeyBytes));
        assertThat(ivParameterSpecArgumentCaptor.getValue().getIV(), is(ivBytes));
    }

    @Test
    public void shouldThrowOnInvalidAlgorithmParameterExceptionWhenTryingToAESDecrypt() throws Exception {
        Exception exception = null;
        byte[] aesKeyBytes = new byte[]{11, 22, 33};
        byte[] ivBytes = new byte[]{99, 22};
        ArgumentCaptor<SecretKey> secretKeyArgumentCaptor = ArgumentCaptor.forClass(SecretKey.class);
        ArgumentCaptor<IvParameterSpec> ivParameterSpecArgumentCaptor = ArgumentCaptor.forClass(IvParameterSpec.class);

        try {
            doReturn(aesKeyBytes).when(cryptoUtil).getAESKey();
            PowerMockito.mockStatic(Cipher.class);
            PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher);
            PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv");

            PowerMockito.mockStatic(Base64.class);
            PowerMockito.when(Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes);

            doThrow(new InvalidAlgorithmParameterException()).when(aesCipher).init(eq(Cipher.DECRYPT_MODE), secretKeyArgumentCaptor.capture(), ivParameterSpecArgumentCaptor.capture());
            cryptoUtil.decrypt(new byte[0]);
        } catch (IncompatibleDeviceException e) {
            exception = e;
        }

        assertThat(exception, is(notNullValue()));
        assertThat(secretKeyArgumentCaptor.getValue().getAlgorithm(), is("AES"));
        assertThat(secretKeyArgumentCaptor.getValue().getEncoded(), is(aesKeyBytes));
        assertThat(ivParameterSpecArgumentCaptor.getValue().getIV(), is(ivBytes));
    }

    @Test
    public void shouldDeleteAESKeysAndThrowOnBadPaddingExceptionWhenTryingToAESDecrypt() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The AES encrypted input is corrupted and cannot be recovered. Please discard it.");

        byte[] aesKeyBytes = new byte[]{11, 22, 33};
        byte[] ivBytes = new byte[]{99, 22};

        doReturn(aesKeyBytes).when(cryptoUtil).getAESKey();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher);
        PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv");

        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes);

        doThrow(new BadPaddingException()).when(aesCipher).doFinal(any(byte[].class));

        cryptoUtil.decrypt(new byte[0]);

        Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS);
        Mockito.verify(storage, never()).remove(KEY_ALIAS);
        Mockito.verify(storage, never()).remove(KEY_ALIAS + "_iv");
    }

    @Test
    public void shouldDeleteAESKeysAndThrowOnIllegalBlockSizeExceptionWhenTryingToAESDecrypt() throws Exception {
        exception.expect(CryptoException.class);
        exception.expectMessage("The AES encrypted input is corrupted and cannot be recovered. Please discard it.");

        byte[] aesKeyBytes = new byte[]{11, 22, 33};
        doReturn(aesKeyBytes).when(cryptoUtil).getAESKey();
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher);
        PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv");

        byte[] ivBytes = new byte[]{99, 22};
        PowerMockito.mockStatic(Base64.class);
        PowerMockito.when(Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes);

        doThrow(new IllegalBlockSizeException()).when(aesCipher).doFinal(any(byte[].class));

        cryptoUtil.decrypt(new byte[0]);

        Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS);
        Mockito.verify(storage, never()).remove(KEY_ALIAS);
        Mockito.verify(storage, never()).remove(KEY_ALIAS + "_iv");
    }


    /*
     * Helper methods
     */
    private KeyPairGeneratorSpec.Builder newKeyPairGeneratorSpecBuilder(KeyPairGeneratorSpec expectedBuilderOutput) {
        KeyPairGeneratorSpec.Builder builder = PowerMockito.mock(KeyPairGeneratorSpec.Builder.class);
        PowerMockito.when(builder.setAlias(anyString())).thenReturn(builder);
        PowerMockito.when(builder.setSubject(any(X500Principal.class))).thenReturn(builder);
        PowerMockito.when(builder.setKeySize(anyInt())).thenReturn(builder);
        PowerMockito.when(builder.setSerialNumber(any(BigInteger.class))).thenReturn(builder);
        PowerMockito.when(builder.setStartDate(any(Date.class))).thenReturn(builder);
        PowerMockito.when(builder.setEndDate(any(Date.class))).thenReturn(builder);
        PowerMockito.when(builder.setEncryptionRequired()).thenReturn(builder);
        PowerMockito.when(builder.build()).thenReturn(expectedBuilderOutput);
        return builder;
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    private KeyGenParameterSpec.Builder newKeyGenParameterSpecBuilder(KeyGenParameterSpec expectedBuilderOutput) {
        KeyGenParameterSpec.Builder builder = PowerMockito.mock(KeyGenParameterSpec.Builder.class);
        PowerMockito.when(builder.setKeySize(anyInt())).thenReturn(builder);
        PowerMockito.when(builder.setCertificateSubject(any(X500Principal.class))).thenReturn(builder);
        PowerMockito.when(builder.setCertificateSerialNumber(any(BigInteger.class))).thenReturn(builder);
        PowerMockito.when(builder.setCertificateNotBefore(any(Date.class))).thenReturn(builder);
        PowerMockito.when(builder.setCertificateNotAfter(any(Date.class))).thenReturn(builder);
        //noinspection WrongConstant
        PowerMockito.when(builder.setEncryptionPaddings(anyString())).thenReturn(builder);
        //noinspection WrongConstant
        PowerMockito.when(builder.setBlockModes(anyString())).thenReturn(builder);
        PowerMockito.when(builder.build()).thenReturn(expectedBuilderOutput);
        return builder;
    }

    private CryptoUtil newCryptoUtilSpy() throws Exception {
        CryptoUtil cryptoUtil = PowerMockito.spy(new CryptoUtil(context, storage, KEY_ALIAS));
        PowerMockito.mockStatic(KeyStore.class);
        PowerMockito.when(KeyStore.getInstance(ANDROID_KEY_STORE)).thenReturn(keyStore);
        PowerMockito.mockStatic(KeyPairGenerator.class);
        PowerMockito.when(KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE)).thenReturn(keyPairGenerator);
        PowerMockito.mockStatic(KeyGenerator.class);
        PowerMockito.when(KeyGenerator.getInstance(ALGORITHM_AES)).thenReturn(keyGenerator);
        PowerMockito.mockStatic(Cipher.class);
        PowerMockito.when(Cipher.getInstance(anyString())).then(new Answer<Cipher>() {
            @Override
            public Cipher answer(InvocationOnMock invocation) {
                String transformation = invocation.getArgumentAt(0, String.class);
                if (RSA_TRANSFORMATION.equals(transformation)) {
                    return rsaCipher;
                } else if (AES_TRANSFORMATION.equals(transformation)) {
                    return aesCipher;
                }
                return null;
            }
        });
        return cryptoUtil;
    }
}