package com.secspace.lib.common.cryptology;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;

import com.sec.lib.cryptology.utils.AESUtil;
import com.secspace.lib.common.base.BaseApplication;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.spec.KeySpec;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

@TargetApi(Build.VERSION_CODES.KITKAT)
public class SecKeyManager {
    private static final String CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";// AES是加密方式
    private static SecKeyManager mInstance = null;
    private final int IV_SIZE = 16;
    private final String SECURE_ENCRYPTION_INDICATOR_FILE_NAME = "encrypted_with_secure_key";
    private Context mContext;

    private SecKeyManager() {
        mContext = BaseApplication.getBase().getApplicationContext();
    }

    public static SecKeyManager getInstance() {
        if (mInstance == null) {
            synchronized (SecKeyManager.class) {
                if (mInstance == null) {
                    mInstance = new SecKeyManager();
                }
            }
        }
        return mInstance;
    }


    /**
     * Method used to derive an <b>insecure</b> key by emulating the SHA1PRNG algorithm from the
     * deprecated Crypto provider.
     * <p>
     * Do not use it to encrypt new data, just to decrypt encrypted data that would be unrecoverable
     * otherwise.
     */
    public SecretKey deriveKeyInsecurely(String password, int keySizeInBytes) {
        byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
        return new SecretKeySpec(
                InsecureSHA1PRNGKeyDerivator.deriveInsecureKey(passwordBytes, keySizeInBytes),
                "AES");
    }

    /**
     * Example use of a key derivation function, derivating a key securely from a password.
     */
    public SecretKey deriveKeySecurely(String password, int keySizeInBytes) {
        // Use this to derive the key from the password:
        KeySpec keySpec = new PBEKeySpec(password.toCharArray(), retrieveSalt(keySizeInBytes),
                100 /* iterationCount */, keySizeInBytes * 8 /* key size in bits */);
        try {
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
            return new SecretKeySpec(keyBytes, "AES");
        } catch (Exception e) {
            throw new RuntimeException("Deal with exceptions properly!", e);
        }
    }

    /**
     * Retrieve encrypted data using a password. If data is stored with an insecure key, re-encrypt
     * with a secure key.
     */
    private String retrieveData(String password, int keySizeInBytes) {
        String decryptedString;
        if (isDataStoredWithInsecureKey()) {
            SecretKey insecureKey = deriveKeyInsecurely(password, keySizeInBytes);
            byte[] decryptedData = AESUtil.decrypt(retrieveEncryptedData(), retrieveIvSpec(), insecureKey, CBC_PKCS5_PADDING);
            SecretKey secureKey = deriveKeySecurely(password, keySizeInBytes);
            storeDataEncryptedWithSecureKey(AESUtil.encrypt(decryptedData, retrieveIvSpec(), secureKey, CBC_PKCS5_PADDING));
            decryptedString = "Warning: data was encrypted with insecure key\n"
                    + new String(decryptedData, StandardCharsets.UTF_8);
        } else {
            SecretKey secureKey = deriveKeySecurely(password, keySizeInBytes);
            byte[] decryptedData = AESUtil.decrypt(retrieveEncryptedData(), retrieveIvSpec(), secureKey, CBC_PKCS5_PADDING);
            decryptedString = "Great!: data was encrypted with secure key\n"
                    + new String(decryptedData, StandardCharsets.UTF_8);
        }
        return decryptedString;
    }

    /*
     ***********************************************************************************************
     * Everything below this comment is a succession of mocks that would rarely interest someone on
     * Earth. They are merely intended to make the example self contained.
     ***********************************************************************************************
     */
    private boolean isDataStoredWithInsecureKey() {
        // Your app should have a way to tell whether the data has been re-encrypted in a secure
        // fashion, in this mock we use the existence of a file with a certain name to indicate
        // that.
        return !fileExists("encrypted_with_secure_key");
    }

    private byte[] retrieveIv() {
        byte[] iv = new byte[IV_SIZE];
        // Ideally your data should have been encrypted with a random iv. This creates a random iv
        // if not present, in order to encrypt our mock data.
        readFromFileOrCreateRandom("iv", iv);
        return iv;
    }

    private IvParameterSpec retrieveIvSpec() {
        byte[] iv = new byte[IV_SIZE];
        // Ideally your data should have been encrypted with a random iv. This creates a random iv
        // if not present, in order to encrypt our mock data.
        readFromFileOrCreateRandom("iv", iv);
        return new IvParameterSpec(iv);
    }

    private byte[] retrieveSalt(int keySizeInBytes) {
        // Salt must be at least the same size as the key.
        byte[] salt = new byte[keySizeInBytes];
        // Create a random salt if encrypting for the first time, and save it for future use.
        readFromFileOrCreateRandom("salt", salt);
        return salt;
    }

    private byte[] encryptedData = null;

    private byte[] retrieveEncryptedData() {
        return encryptedData;
    }

    private void storeDataEncryptedWithSecureKey(byte[] encryptedData) {
        // Mock implementation.
        this.encryptedData = encryptedData;
        writeToFile(SECURE_ENCRYPTION_INDICATOR_FILE_NAME, new byte[1]);
    }

    /**
     * Read from file or return random bytes in the given array.
     * <p>
     * <p>Save to file if file didn't exist.
     */
    private void readFromFileOrCreateRandom(String fileName, byte[] bytes) {
        if (fileExists(fileName)) {
            readBytesFromFile(fileName, bytes);
            return;
        }
        SecureRandom sr = new SecureRandom();
        sr.nextBytes(bytes);
        writeToFile(fileName, bytes);
    }


    private boolean fileExists(String fileName) {
        File file = new File(mContext.getFilesDir(), fileName);
        return file.exists();
    }

    private void removeFile(String fileName) {
        File file = new File(mContext.getFilesDir(), fileName);
        file.delete();
    }

    private void writeToFile(String fileName, byte[] bytes) {
        try (FileOutputStream fos = mContext.openFileOutput(fileName, Context.MODE_PRIVATE)) {
            fos.write(bytes);
        } catch (IOException e) {
            throw new RuntimeException("Couldn't write to " + fileName, e);
        }
    }

    private void readBytesFromFile(String fileName, byte[] bytes) {
        try (FileInputStream fis = mContext.openFileInput(fileName)) {
            int numBytes = 0;
            while (numBytes < bytes.length) {
                int n = fis.read(bytes, numBytes, bytes.length - numBytes);
                if (n <= 0) {
                    throw new RuntimeException("Couldn't read from " + fileName);
                }
                numBytes += n;
            }
        } catch (IOException e) {
            throw new RuntimeException("Couldn't read from " + fileName, e);
        }
    }
}
