/*
    BEEM is a videoconference application on the Android Platform.

    Copyright (C) 2009-2012 by Frederic-Charles Barthelery,
                               Nikita Kozlov,
                               Vincent Veronis.

    This file is part of BEEM.

    BEEM is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    BEEM is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with BEEM.  If not, see <http://www.gnu.org/licenses/>.

    Please send bug reports with examples or suggestions to
    contact@beem-project.com or http://www.beem-project.com/

*/
package com.beem.project.beem.utils;

import static com.google.android.apps.iosched.util.LogUtils.LOGD;
import static com.google.android.apps.iosched.util.LogUtils.LOGW;
import static com.google.android.apps.iosched.util.LogUtils.makeLogTag;


import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;

import android.annotation.TargetApi;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;

import org.jivesoftware.smack.util.Base64;

/**
 * Allows to encrypt and decrypt some strings using the Android Keystore.
 */
public class EncryptionManager {

    private static final String TAG = makeLogTag(EncryptionManager.class);
    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
    private static final String CIPHER_TRANSFORMATION = String.format("%s/%s/%s",
            KeyProperties.KEY_ALGORITHM_AES, KeyProperties.BLOCK_MODE_CBC,
            KeyProperties.ENCRYPTION_PADDING_PKCS7);
    private boolean isEncryptionAvailable;
    private KeyStore keystore;
    private KeyGenerator keyGenerator;
    private Cipher aesCipher;
    private byte[] latestEncryptionIv;

    /**
     * Create an EncryptionManager.
     */
    public EncryptionManager() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                keystore = KeyStore.getInstance(ANDROID_KEY_STORE);
                keystore.load(null);
                aesCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
                keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
                isEncryptionAvailable = true;
            } else {
                isEncryptionAvailable = false;
            }
        } catch (NoSuchPaddingException | KeyStoreException | IOException
                | NoSuchProviderException | NoSuchAlgorithmException | CertificateException e) {
            LOGW(TAG, "Unable to load AndroidKeyStore", e);
            isEncryptionAvailable = false;
        }
    }

    /**
     * Test if the device supports encryption.
     *
     * @return true if encryption is available, false otherwise
     */
    public boolean isEncryptionAvailable() {
        return isEncryptionAvailable;
    }

    /**
     * Test if there is an encryption key stored with the alias.
     *
     * @param alias the alias to test for
     * @return true if a key is available, false otherwise
     */
    public boolean hasEncryptionKey(String alias) {
        if (!isEncryptionAvailable) {
            return false;
        }
        try {
            return keystore.getKey(alias, null) != null;
        } catch (GeneralSecurityException e) {
            LOGD(TAG, "Unable to get key", e);
            return false;
        }
    }

    /**
     * Generate a random encryption key and store it in the keystore under alias.
     *
     * @param alias the alias use to retrieve the key
     * @return true if the key was generate, false otherwise
     */
    @TargetApi(Build.VERSION_CODES.M)
    public boolean generateEncryptionKey(String alias) {
	if (!isEncryptionAvailable)
	    return false;
	KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
	    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
	    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
	    .setUserAuthenticationRequired(false)
	    .build();
	try {
	    keyGenerator.init(spec);
	    return keyGenerator.generateKey() != null;
	} catch (InvalidAlgorithmParameterException e) {
	    LOGW(TAG, "Unable to generate key", e);
	    return false;
	}
    }

    /**
     * Encrypt a string with the key stored with keyAlias.
     *
     * @param cleartext the clear text to encrypt
     * @param keyAlias the alias of the key to use
     * @return the encrypted string
     */
    public String encryptString(String cleartext, String keyAlias) {
        if (!isEncryptionAvailable())
            return null;
        try {
            Key key = keystore.getKey(keyAlias, null);
            aesCipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] encrypted = aesCipher.doFinal(cleartext.getBytes());
            latestEncryptionIv = aesCipher.getIV();
            return Base64.encodeBytes(encrypted);
        } catch (GeneralSecurityException e) {
            LOGW(TAG, "Unable to encrypt text", e);
            return null;
        }
    }

    /**
     * Get the encryption IV used with the latest encryption operation.
     *
     * @return the encryption IV or null if no encryption operation has been done
     */
    public byte[] getLatestEncryptionIv() {
        return latestEncryptionIv;
    }

    /**
     * Decrypt an encrypted text using the key stored with alias keyAlias and the specified encryption IV.
     *
     * @param encryptedText the encrypted text to decrypt
     * @param keyAlias the alias of the key to use
     * @param encryptionIv the encryption IV
     * @return the clear text
     */
    public String decryptString(String encryptedText, String keyAlias, byte[] encryptionIv) {
        if (!isEncryptionAvailable())
            return null;
        try {
            Key key = keystore.getKey(keyAlias, null);
            byte[] passwordByte = Base64.decode(encryptedText);
            aesCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(encryptionIv));
            byte[] clear = aesCipher.doFinal(passwordByte);
            return new String(clear);
        } catch (GeneralSecurityException e) {
            LOGW(TAG, "Unable to decrypt text", e);
            return null;
        }
    }
}
