package com.mobify.astro.security;

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

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;

/**
 * Encrypting with AES/GCM/NoPadding algorithm.
 *
 * GCM allows encrypted text to be associated with the key it is paired with. This prevents
 * attackers from copying encrypted text to another key. For example, say you have the
 * following:
 *
 *      Key 1: true
 *      Key 2: false
 *
 * Without GCM, an attacker can simply copy the encrypted false text to Key 1 and Key 1 will have the
 * resulting value of false since we use the same symmetric key to encrypt and decrypt.
 *
 * AES/GCM is an authenticated mode of encryption that is non-deterministic
 * and does not suffer the confidentiality problems of ECB mode.
 *
 * AES/GCM 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 AesGcmSymmetricCryptosystem implements SymmetricCryptosystem {
    private final static String TAG = AesGcmSymmetricCryptosystem.class.getName();

    private final static String CHAR_SET = "UTF-8";
    private final static int GCM_IV_LENGTH = 16;
    private final static int GCM_TAG_LENGTH = 128;
    public final static String AES_GCM_CIPHER_ALGORITHM = "AES/GCM/NoPadding";

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

    AesGcmSymmetricCryptosystem(SymmetricKeyService symmetricKeyService, String sharedPrefSymmetricKey) {
        this.symmetricKeyService = symmetricKeyService;
        this.sharedPrefSymmetricKey = sharedPrefSymmetricKey;
        this.ivGenerator = new IvGenerator(GCM_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 GCM encrypting
       Cipher cipher = Cipher.getInstance(AES_GCM_CIPHER_ALGORITHM);
       GCMParameterSpec gcmParamSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
       cipher.init(Cipher.ENCRYPT_MODE, symmetricKeyService.getSymmetricKey(sharedPrefSymmetricKey), gcmParamSpec);

       // Associate key-value pair with cipher
       cipher.updateAAD(keyToAssociate.getBytes(CHAR_SET));

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

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

       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[] iv = new byte[GCM_IV_LENGTH];
        byte[] encryptedValue = new byte[encryptedData.length - iv.length];

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

        // Prepare cipher for GCM decrypting
        Cipher cipher = Cipher.getInstance(AES_GCM_CIPHER_ALGORITHM);
        GCMParameterSpec gcmParamSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        cipher.init(Cipher.DECRYPT_MODE, symmetricKeyService.getSymmetricKey(sharedPrefSymmetricKey), gcmParamSpec);

        // Associate key-value pair with cipher
        cipher.updateAAD(keyToAssociate.getBytes(CHAR_SET));

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

        return new String(decryptedValue, 0, decryptedValue.length, CHAR_SET);
    }
}
