package com.codemonkeylabs.encryptionexample.app;


import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.AlgorithmParameters;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * AES Helper Encryption Class
 */
public class AESEncryptDecrypt {

    //32 byte key -> 256 bit.....getBytes defaults to utf-8
    public static final String NOT_SECRET_ENCRYPTION_KEY = "12345678123456781234567812345678";
    //type of aes key that will be created
    public static final String SECRET_KEY_TYPE = "PBKDF2WithHmacSHA1";
    //value used for salting....can be anything
    public static final String salt = "some_salt";
    //length of key
    public static final int KEY_LENGTH = 256;
    //number of times the password is hashed
    public static final int ITERATION_COUNT = 65536;
    //main family of aes
    public static final String AES = "AES";
    //spongy castle security provider
    public static String SECURITY_PROVIDER = "SC";

    /*
     * YOU HAVE TO CALL THIS BEFORE ENCRYPT/DECRYPT TO SET PROVIDER
     * i added it this way for ease of distinction when unit testing...
     * unit test requires bouncyCastle and on android we want
     * spongyCastle....in the real world you would inject this
     * dependency via dagger..
     */
    public static void setProvider(Provider provider, String providerName) {
        SECURITY_PROVIDER = providerName;
        Security.insertProviderAt(provider, 1);
    }

    /*
     * helper enum class that contains the aes ciphers that we
     * support
     */
    public enum AESCipherType {
        AES_CIPHER_CTR_NOPADDING("AES/CTR/NOPADDING"),
        AES_CIPHER_ECB_PKCS5PADDING("AES/ECB/PKCS5PADDING"),
        AES_CBC_PKCS5PADDING("AES/CBC/PKCS5Padding"),
        AES_CBC_PKCS7Padding("AES/CBC/PKCS7Padding");

        private final String value;

        AESCipherType(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }

    /**
     * main aes encryption method.  takes in the unencrypted data
     * as an inputstream and writes out the encrypted data
     * within the provided outputstream
     *
     * @param inData inputStream that represents the plaintext data
     * @param key unencoded and unencrypted aes key
     * @param aesCipherType enum representing what ciper to use boils down to a string
     * @param outData  outputstream where we write the encrypted data
     * @return IV generated from key creation
     */
    public static byte[] aesEncrypt(InputStream inData,
                                    char[] key,
                                    AESEncryptDecrypt.AESCipherType aesCipherType,
                                    OutputStream outData)
    {
        CipherOutputStream cos = null;
        try
        {
            //create the cipher from the passed in type using the specified security provider
            Cipher cipher = Cipher.getInstance(aesCipherType.getValue(), SECURITY_PROVIDER);
            //generate secret key
            SecretKey secret = getSecretKey(key);
            //init the cipher
            cipher.init(Cipher.ENCRYPT_MODE, secret);
            //create the cipher outputstream
            cos = new CipherOutputStream(outData, cipher);
            //copy the plaintext inputstream to the encrypted outputstream
            Util.copy(inData, cos);
            //query parameters for iv
            AlgorithmParameters params = cipher.getParameters();
            //check to see if we have an IV to return
            //some ciphers (ECB) don't create IV
            return params == null ? null : params.getParameterSpec(IvParameterSpec.class).getIV();
        }
        catch (Exception e)
        {
            Log.e(AESEncryptDecrypt.class.getName(), e.getMessage(), e);
            throw new RuntimeException(e);
        }
        finally
        {
            if(cos != null)
                try
                {
                    //close the stream
                    cos.close();
                } catch (IOException e) {
                    Log.e(AESEncryptDecrypt.class.getName(), e.getMessage(), e);
                    throw new RuntimeException(e);
                }
        }
    }

    /**
     * generates a secret key from the passed in raw key value
     * we create a 256 bit key that is salted using our example
     * salt value above
     *
     * @param key input key in a char array
     * @return a salted key of the type SECRET_KEY_TYPE
     * @throws NoSuchAlgorithmException
     * @throws UnsupportedEncodingException
     * @throws InvalidKeySpecException
     */
    private static SecretKey getSecretKey(char[] key) throws NoSuchAlgorithmException,
            UnsupportedEncodingException,
            InvalidKeySpecException,
            NoSuchProviderException
    {
        SecretKeyFactory factory = null;
        factory = SecretKeyFactory.getInstance(SECRET_KEY_TYPE,
                SECURITY_PROVIDER);

        KeySpec spec = new PBEKeySpec(key,
                salt.getBytes("UTF-8"),
                ITERATION_COUNT,
                KEY_LENGTH);

        SecretKey tmp = factory.generateSecret(spec);
        return new SecretKeySpec(tmp.getEncoded(), AES);
    }

    /**
     * main aes decryption method. takes in the encrypted data
     * inputstream and writes the decrypted data to the provided
     * outputstream.
     *
     * @param inData inputStream that represents the encrypted data
     * @param key unencoded and unencrypted aes key
     * @param ivs unencoded and unencrypted iv
     * @param aesCipherType enum representing what ciper to use boils down to a string
     * @param outData outputstream where we write the decrypted data
     */
    public static void aesDecrypt(InputStream inData,
                                  char[] key,
                                  byte[] ivs,
                                  AESEncryptDecrypt.AESCipherType aesCipherType,
                                  OutputStream outData)
    {
        CipherInputStream cis = null;
        try
        {
            //create the cipher from the passed in type using the specified security provider
            Cipher cipher = Cipher.getInstance(aesCipherType.getValue(), SECURITY_PROVIDER);
            //generate secret key
            SecretKey secret = getSecretKey(key);
            //if ivs is passed in then we should use it to create the
            //cipher
            if( ivs == null)
            {
                cipher.init(Cipher.DECRYPT_MODE, secret);
            } else {
                IvParameterSpec ivps = new IvParameterSpec(ivs);
                cipher.init(Cipher.DECRYPT_MODE, secret, ivps);
            }
            //create the cipher inputstream
            cis = new CipherInputStream(inData, cipher);
            //copy the encrypted inputstream to the plaintext outputstream
            Util.copy(cis, outData);
        }
        catch (Exception e)
        {
            Log.e(AESEncryptDecrypt.class.getName(), e.getMessage(), e);
            throw new RuntimeException(e);
        }
        finally
        {
            if(cis != null)
                try
                {
                    //close the stream
                    cis.close();
                } catch (IOException e) {
                    Log.e(AESEncryptDecrypt.class.getName(), e.getMessage(), e);
                    throw new RuntimeException(e);
                }
        }
    }

}