package com.mobify.astro.security;

import android.util.Base64;
import android.util.Log;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;

/**
 * AES/CBC with HMAC is an authenticated mode of encryption that is non-deterministic
 * and does not suffer the confidentiality problems of ECB mode.
 *
 * AES/CBC with HMAC ensures that all ciphertext values are random and no patterns
 * will be observable to an attacker, that the ciphertext cannot be modified in any way
 * (either by re-arranging portions or flipping bits), and that the ciphertext cannot
 * be copy-and-pasted between settings.
 */
class AesCbcHmacSymmetricCryptosystem implements SymmetricCryptosystem {
    private final static String TAG = AesCbcHmacSymmetricCryptosystem.class.getName();

    private final static String CHAR_SET = "UTF-8";
    private final static int IV_LENGTH = 16;

    // 256 Bits in Bytes
    private final static int HMAC_LENGTH = 256/8;
    private final static String AES_CBC_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
    private final static String HMAC_MAC_ALGORITHM = "HmacSHA256";

    private SymmetricKeyService symmetricKeyService;
    private String sharedPrefSymmetricKey;
    private String sharedPrefHmacSymmetricKey;
    private IvGenerator ivGenerator;

    AesCbcHmacSymmetricCryptosystem(SymmetricKeyService symmetricKeyService, String sharedPrefSymmetricKey, String sharePrefHmacSymmetricKey) {
        this.symmetricKeyService = symmetricKeyService;
        this.sharedPrefSymmetricKey = sharedPrefSymmetricKey;
        this.sharedPrefHmacSymmetricKey = sharePrefHmacSymmetricKey;
        this.ivGenerator = new IvGenerator(IV_LENGTH);
    }

    @Override
    public String encrypt(String keyToAssociate, String valueToEncrypt) throws Exception {
        if(keyToAssociate == null || valueToEncrypt == null) {
            Log.e(TAG, "Key and value cannot be null");
            return "";
        }

        // Generate a random IV (Initial Vector)
        byte[] iv = ivGenerator.generate();

        // Prepare cipher for AES/CBC encrypting
        Cipher cipher = Cipher.getInstance(AES_CBC_CIPHER_ALGORITHM);
        IvParameterSpec ivParamSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, symmetricKeyService.getSymmetricKey(sharedPrefSymmetricKey), ivParamSpec);

        // Encrypt string
        byte[] encryptedValue = cipher.doFinal(valueToEncrypt.getBytes(CHAR_SET));

        // Compute HMAC
        byte[] hmac = computeHmac(keyToAssociate, iv, encryptedValue);

        // Prepending HMAC and IV with ciphered text
        byte[] encryptedData = new byte[hmac.length + iv.length + encryptedValue.length];
        System.arraycopy(hmac, 0, encryptedData, 0, hmac.length);
        System.arraycopy(iv, 0, encryptedData, hmac.length, iv.length);
        System.arraycopy(encryptedValue, 0, encryptedData, hmac.length + iv.length, encryptedValue.length);

        // Return encrypted string
        return Base64.encodeToString(encryptedData, Base64.DEFAULT);
    }

    @Override
    public String decrypt(String keyToAssociate, String valueToDecrypt) throws Exception {
        if(keyToAssociate == null || valueToDecrypt == null) {
            Log.e(TAG, "Key and value cannot be null");
            return null;
        }

        byte[] encryptedData = Base64.decode(valueToDecrypt, Base64.DEFAULT);
        byte[] storedHmac = new  byte[HMAC_LENGTH];
        byte[] iv = new byte[IV_LENGTH];
        byte[] encryptedValue = new byte[encryptedData.length - storedHmac.length - iv.length];

        // Separate HMAC, IV and ciphered text
        System.arraycopy(encryptedData, 0, storedHmac, 0, storedHmac.length);
        System.arraycopy(encryptedData, storedHmac.length, iv, 0, iv.length);
        System.arraycopy(encryptedData, storedHmac.length + iv.length, encryptedValue, 0, encryptedValue.length);

        byte[] recomputedHmac = computeHmac(keyToAssociate, iv, encryptedValue);

        // Compare stored HMAC and recalculated HMAC to ensure nothing has been tampered with
        if (!areArraysEqualConstantTime(storedHmac, recomputedHmac)) {
            return "";
        }

        // Prepare cipher for AES/CBC decrypting
        Cipher cipher = Cipher.getInstance(AES_CBC_CIPHER_ALGORITHM);
        IvParameterSpec ivParamSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.DECRYPT_MODE, symmetricKeyService.getSymmetricKey(sharedPrefSymmetricKey), ivParamSpec);

        // Decrypt value
        byte[] decryptedValue = cipher.doFinal(encryptedValue);

        // Return decrypted string
        return new String(decryptedValue, 0, decryptedValue.length, CHAR_SET);
    }

    private byte[] computeHmac(String keyToAssociate, byte[] iv, byte[] encryptedValue) throws Exception {
        // Compute the Message Authentication Code using the HMAC-SHA256 algorithm
        Mac mac = Mac.getInstance(HMAC_MAC_ALGORITHM);

        // Initialize MAC with symmetric key created for all HMAC calculations
        mac.init(symmetricKeyService.getSymmetricKey(sharedPrefHmacSymmetricKey));

        // Add the keyToAssociate, iv and encryptedValue to the mac for processing
        mac.update(keyToAssociate.getBytes(CHAR_SET));
        mac.update(iv);
        mac.update(encryptedValue);

        // Return the resulting HMAC value
        return mac.doFinal();
    }

    // A security issue occurs if the byte arrays for the HMAC aren't compared in constant time
    // Arrays.equals method is not constant time
    // Implemented based on: https://cryptocoding.net/index.php/Coding_rules#Compare_secret_strings_in_constant_time
    private static boolean areArraysEqualConstantTime(byte[] array1, byte[] array2) {
        if (array1.length != array2.length) {
            return false;
        }

        int length = array1.length;
        byte result = 0;

        for (int i = 0; i < length; i++) {
            result |= array1[i] ^ array2[i];
        }

        return (result == 0);
    }
}
