package com.mobify.astro.security;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.security.KeyPairGeneratorSpec;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;

import com.mobify.astro.BuildConfig;

import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Calendar;
import java.util.Locale;

import javax.crypto.Cipher;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import javax.security.auth.x500.X500Principal;

/**
 * RSA Asymmetric Cryptosystem can encrypt/decrypt data up to a limited length. This cryptosystem
 * uses an asymmetric keypair which is stored in the Android Keystore. Each app using the Astro
 * framework will store it's own keypair based on the app's package name.
 *
 * * Support for APIs
 *
 * Google fully deprecated the apis to access the Android Keystore in API 22 and introduced
 * new apis in API 23. Thus we need to use the appropriate apis based on Android API level.
 *
 * We have different code path for [API 23 and above] and [API 22 and below].
 *
 * API 23 and above have the following differences:
 * - KeyPairGeneratorSpec is deprecated and is replaced by KeyPairGenerator
 * - New methods for obtaining the private and public keys stored in the Android KeyStore
 * - Different encryption/decryption algorithm support
 */
class RsaAsymmetricCryptosystem implements AsymmetricCryptosystem {
    private final static String TAG = RsaAsymmetricCryptosystem.class.getName();
    private final static String CHAR_SET = "UTF-8";
    private final static String KEYSTORE = "AndroidKeyStore";
    private final static String KEYSTORE_ALIAS = "KEYSTORE_ALIAS";
    private final static boolean API_LEVEL_LESS_THAN_23 = Build.VERSION.SDK_INT < 23;
    private final static String MESSAGE_DIGEST = "SHA-256";
    private final static String MASK_GENERATION_FUNCTION = "MGF1";

    private Context context;
    private KeyStore keystore;
    private String ASYMMETRIC_KEY_CIPHER_ALGORITHM;
    private String keystoreAlias;

    protected static String keystoreAlias(Context context) {
        return context.getPackageName() + "." + KEYSTORE_ALIAS + "." + BuildConfig.BUILD_TYPE;
    }

    RsaAsymmetricCryptosystem(Context context) throws Exception {
        this.context = context;
        this.keystoreAlias = keystoreAlias(context);

        if (API_LEVEL_LESS_THAN_23) {
            ASYMMETRIC_KEY_CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding";
        } else {
            ASYMMETRIC_KEY_CIPHER_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
        }

        keystore = KeyStore.getInstance(KEYSTORE);
        keystore.load(null);
    }

    @Override
    public void init() throws Exception {
        initKeystore();
    }

    @Override
    public boolean isInitialized() throws Exception {
        return keystore.containsAlias(keystoreAlias);
    }

    @TargetApi(23)
    private void initKeystore() throws Exception {
        // We are setting locale back to US because there is a bug with generating
        // a self-signed certificate when obtaining the date string involving
        // non-standard character sets
        Locale currentLocal = Locale.getDefault();
        Locale.setDefault(Locale.US);

        try {
            if (!isInitialized()) {

                KeyPairGenerator generator;
                AlgorithmParameterSpec spec;

                if (API_LEVEL_LESS_THAN_23) {
                    Calendar start = Calendar.getInstance();
                    Calendar end = Calendar.getInstance();
                    end.add(Calendar.YEAR, 10);

                    spec = new KeyPairGeneratorSpec.Builder(context)
                            .setAlias(keystoreAlias)
                            .setSubject(new X500Principal("CN=Astro, O=MOBIFY, C=CA"))
                            .setSerialNumber(BigInteger.ONE)
                            .setStartDate(start.getTime())
                            .setEndDate(end.getTime())
                            .setKeySize(2048)
                            .build();
                    generator = KeyPairGenerator.getInstance("RSA", KEYSTORE);
                } else {
                    generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEYSTORE);
                    spec = new KeyGenParameterSpec.Builder(keystoreAlias, KeyProperties.PURPOSE_DECRYPT)
                            .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
                            .build();
                }

                // This puts a key pair entry into Android KeyStore base on the specs we defined with the KeyPairGenerator
                generator.initialize(spec);
                generator.generateKeyPair();
            }
        } catch (Exception e) {
            Log.e(TAG, "Error initializing Keystore!", e);
            clear();
            throw e;
        } finally {
            Locale.setDefault(currentLocal);
        }
    }

    @Override
    public void clear() throws Exception {
        keystore.deleteEntry(keystoreAlias);
    }

    private Key getPublicKey() throws Exception {
        // Retrieve public key for encryption
        Key publicKey;

        if (API_LEVEL_LESS_THAN_23) {
            KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keystore.getEntry(keystoreAlias, null);
            publicKey = privateKeyEntry.getCertificate().getPublicKey();
        } else {
            PublicKey restrictedPublicKey = keystore.getCertificate(keystoreAlias).getPublicKey();

            // Need to generate restricted public key because bug in Android will
            // not return public key unless authorization has taken place
            // Issue documented at:
            // https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.html
            X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(restrictedPublicKey.getEncoded());
            publicKey = KeyFactory.getInstance(restrictedPublicKey.getAlgorithm()).generatePublic(x509EncodedKeySpec);
        }

        return publicKey;
    }

    private Key getPrivateKey() throws Exception {
        // Retrieve private key for decryption
        Key privateKey;

        if (API_LEVEL_LESS_THAN_23) {
            KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keystore.getEntry(keystoreAlias, null);
            privateKey = privateKeyEntry.getPrivateKey();
        } else {
            privateKey = keystore.getKey(keystoreAlias, null);
        }

        return privateKey;
    }

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

        // Initialize cipher for RSA encryption
        Cipher cipher = Cipher.getInstance(ASYMMETRIC_KEY_CIPHER_ALGORITHM);

        if (API_LEVEL_LESS_THAN_23) {
            cipher.init(Cipher.ENCRYPT_MODE, getPublicKey());
        } else {
            // When using the unrestricted public key the default Parameter Spec is not compatible
            // It is necessary to specify the exact compatible Parameter Spec
            // https://stackoverflow.com/a/36020975
            OAEPParameterSpec spec = new OAEPParameterSpec(
                    MESSAGE_DIGEST, MASK_GENERATION_FUNCTION, MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);
            cipher.init(Cipher.ENCRYPT_MODE, getPublicKey(), spec);
        }


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

        return Base64.encodeToString(encryptedValue, Base64.DEFAULT);
    }

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

        // Initialize Cipher for RSA decryption
        Cipher cipher = Cipher.getInstance(ASYMMETRIC_KEY_CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, getPrivateKey());

        // Decrypt string
        byte[] decryptedValue = cipher.doFinal(Base64.decode(valueToDecrypt, Base64.DEFAULT));

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