package com.amaze.filemanager.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.security.KeyPairGeneratorSpec;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.annotation.RequiresApi;
import android.util.Base64;
import com.amaze.filemanager.filesystem.BaseFile;
import com.amaze.filemanager.filesystem.FileUtil;
import com.amaze.filemanager.filesystem.HFile;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.UnrecoverableEntryException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Calendar;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.x500.X500Principal;
/**
* Created by vishal on 6/4/17.
*
* Class provide helper methods to encrypt/decrypt various type of files, or passwords
* We take the password from user before encrypting file. First, the password is encrypted against
* the key created in keystore in android {@see #encryptPassword(String)}.
* We're using AES encryption with GCM as the processor algorithm.
* The encrypted password is mapped against the file path to be encrypted in database for later use.
* This is handled by the service invoking this instance.
* The service then calls the constructor which fires up the subsequent encryption/decryption process.
*
* We differentiate between already encrypted files from <i>new ones</i> by encrypting the plaintext
* {@link com.amaze.filemanager.fragments.preference_fragments.Preffrag#ENCRYPT_PASSWORD_MASTER}
* and {@link com.amaze.filemanager.fragments.preference_fragments.Preffrag#ENCRYPT_PASSWORD_FINGERPRINT}
* against the path in database. At the time of decryption, we check for these values
* and either retrieve master password from preferences or fire up the fingerprint sensor authentication.
*
* From <i>new ones</i> we mean the ones when were encrypted after user changed preference
* for master password/fingerprint sensor from settings.
*
* We use buffered streams to process files, usage of NIO will probably mildly effect the performance.
*
* Be sure to use constructors to encrypt/decrypt files only, and to call service through
* {@link ServiceWatcherUtil} and to initialize watchers beforehand
*/
public class CryptUtil {
private static final String ALGO_AES = "AES/GCM/NoPadding";
private static final String ALGO_RSA = "RSA/ECB/PKCS1Padding";
private static final String KEY_STORE_ANDROID = "AndroidKeyStore";
private static final String KEY_ALIAS_AMAZE = "AmazeKey";
private static final String PREFERENCE_KEY = "aes_key";
// TODO: Generate a random IV every time, and keep track of it (in database against encrypted files)
private static final String IV = "LxbHiJhhUXcj"; // 12 byte long IV supported by android for GCM
public static final String CRYPT_EXTENSION = ".aze";
private ProgressHandler progressHandler;
private ArrayList<HFile> failedOps;
/**
* Constructor will start encryption process serially. Make sure to call with background thread.
* The result file of encryption will be in the same directory with a {@link #CRYPT_EXTENSION} extension
*
* Make sure you're done with encrypting password for this file and map it with this file in database
*
* Be sure to use constructors to encrypt/decrypt files only, and to call service through
* {@link ServiceWatcherUtil} and to initialize watchers beforehand
*
* @param context
* @param sourceFile the file to encrypt
*/
public CryptUtil(Context context, BaseFile sourceFile, ProgressHandler progressHandler,
ArrayList<HFile> failedOps)
throws IOException, CertificateException,
NoSuchAlgorithmException, UnrecoverableEntryException, InvalidKeyException,
InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchProviderException,
BadPaddingException, KeyStoreException, IllegalBlockSizeException {
this.progressHandler = progressHandler;
this.failedOps = failedOps;
// target encrypted file
HFile hFile = new HFile(sourceFile.getMode(), sourceFile.getParent(context));
encrypt(context, sourceFile, hFile);
}
/**
* Decrypt the file in specified path. Can be used to open the file (decrypt in cache) or
* simply decrypt the file in the same (or in a custom preference) directory
* Make sure to decrypt and check user provided passwords beforehand from database
*
* Be sure to use constructors to encrypt/decrypt files only, and to call service through
* {@link ServiceWatcherUtil} and to initialize watchers beforehand
*
* @param context
* @param baseFile the encrypted file
* @param targetPath the directory in which file is to be decrypted
* the source's parent in normal case
*/
public CryptUtil(Context context, BaseFile baseFile, String targetPath,
ProgressHandler progressHandler, ArrayList<HFile> failedOps)
throws IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableEntryException,
InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException,
NoSuchProviderException, BadPaddingException, KeyStoreException, IllegalBlockSizeException {
this.progressHandler = progressHandler;
this.failedOps = failedOps;
HFile targetDirectory = new HFile(OpenMode.FILE, targetPath);
if (!targetPath.equals(context.getExternalCacheDir())) {
// same file system as of base file
targetDirectory.setMode(baseFile.getMode());
}
decrypt(context, baseFile, targetDirectory);
}
/**
* Wrapper around handling decryption for directory tree
* @param context
* @param sourceFile the source file to decrypt
* @param targetDirectory the target directory inside which we're going to decrypt
* @throws IOException
* @throws CertificateException
* @throws NoSuchAlgorithmException
* @throws UnrecoverableEntryException
* @throws InvalidKeyException
* @throws InvalidAlgorithmParameterException
* @throws NoSuchPaddingException
* @throws NoSuchProviderException
* @throws BadPaddingException
* @throws KeyStoreException
* @throws IllegalBlockSizeException
*/
private void decrypt(Context context, BaseFile sourceFile, HFile targetDirectory) throws IOException,
CertificateException, NoSuchAlgorithmException, UnrecoverableEntryException,
InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException,
NoSuchProviderException, BadPaddingException, KeyStoreException, IllegalBlockSizeException {
if (sourceFile.isDirectory()) {
HFile hFile = new HFile(targetDirectory.getMode(), targetDirectory.getPath(),
sourceFile.getName().replace(CRYPT_EXTENSION, ""), sourceFile.isDirectory());
FileUtil.mkdirs(context, hFile);
for (BaseFile baseFile : sourceFile.listFiles(context, sourceFile.isRoot())) {
decrypt(context, baseFile, hFile);
}
} else {
if (!sourceFile.getPath().endsWith(CRYPT_EXTENSION)) {
failedOps.add(sourceFile);
return;
}
BufferedInputStream inputStream = new BufferedInputStream(sourceFile.getInputStream(context),
GenericCopyUtil.DEFAULT_BUFFER_SIZE);
HFile targetFile = new HFile(targetDirectory.getMode(),
targetDirectory.getPath(), sourceFile.getName().replace(CRYPT_EXTENSION, ""),
sourceFile.isDirectory());
progressHandler.setFileName(sourceFile.getName());
BufferedOutputStream outputStream = new BufferedOutputStream(targetFile.getOutputStream(context),
GenericCopyUtil.DEFAULT_BUFFER_SIZE);
if (progressHandler.getCancelled()) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
aesDecrypt(inputStream, outputStream);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
rsaDecrypt(context, inputStream, outputStream);
}
}
}
/**
* Wrapper around handling encryption in directory tree
* @param context
* @param sourceFile the source file to encrypt
* @param targetDirectory the target directory in which we're going to encrypt
* @throws IOException
* @throws CertificateException
* @throws NoSuchAlgorithmException
* @throws UnrecoverableEntryException
* @throws InvalidKeyException
* @throws InvalidAlgorithmParameterException
* @throws NoSuchPaddingException
* @throws NoSuchProviderException
* @throws BadPaddingException
* @throws KeyStoreException
* @throws IllegalBlockSizeException
*/
private void encrypt(Context context, BaseFile sourceFile, HFile targetDirectory) throws IOException,
CertificateException, NoSuchAlgorithmException, UnrecoverableEntryException,
InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException,
NoSuchProviderException, BadPaddingException, KeyStoreException, IllegalBlockSizeException {
if (sourceFile.isDirectory()) {
// succeed #CRYPT_EXTENSION at end of directory/file name
HFile hFile = new HFile(targetDirectory.getMode(),
targetDirectory.getPath(), sourceFile.getName() + CRYPT_EXTENSION,
sourceFile.isDirectory());
FileUtil.mkdirs(context, hFile);
for (BaseFile baseFile : sourceFile.listFiles(context, sourceFile.isRoot())) {
encrypt(context, baseFile, hFile);
}
} else {
if (sourceFile.getName().endsWith(CRYPT_EXTENSION)) {
failedOps.add(sourceFile);
return;
}
BufferedInputStream inputStream = new BufferedInputStream(sourceFile.getInputStream(context),
GenericCopyUtil.DEFAULT_BUFFER_SIZE);
// succeed #CRYPT_EXTENSION at end of directory/file name
HFile targetFile = new HFile(targetDirectory.getMode(),
targetDirectory.getPath(), sourceFile.getName() + CRYPT_EXTENSION,
sourceFile.isDirectory());
progressHandler.setFileName(sourceFile.getName());
BufferedOutputStream outputStream = new BufferedOutputStream(targetFile.getOutputStream(context),
GenericCopyUtil.DEFAULT_BUFFER_SIZE);
if (progressHandler.getCancelled()) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
aesEncrypt(inputStream, outputStream);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
rsaEncrypt(context, inputStream, outputStream);
}
}
}
/**
* Helper method to encrypt plain text password
* @param plainTextPassword
* @return
* @throws CertificateException
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
* @throws IOException
* @throws NoSuchPaddingException
* @throws UnrecoverableKeyException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static String aesEncryptPassword(String plainTextPassword)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
NoSuchProviderException, InvalidAlgorithmParameterException, IOException,
NoSuchPaddingException, UnrecoverableKeyException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(ALGO_AES);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), gcmParameterSpec);
byte[] encodedBytes = cipher.doFinal(plainTextPassword.getBytes());
return Base64.encodeToString(encodedBytes, Base64.DEFAULT);
}
/**
* Helper method to decrypt cipher text password
* @param cipherPassword
* @return
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws UnrecoverableKeyException
* @throws KeyStoreException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
* @throws IOException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static String aesDecryptPassword(String cipherPassword) throws NoSuchPaddingException,
NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException,
KeyStoreException, NoSuchProviderException, InvalidAlgorithmParameterException,
IOException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(ALGO_AES);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), gcmParameterSpec);
byte[] decryptedBytes = cipher.doFinal(Base64.decode(cipherPassword, Base64.DEFAULT));
return new String(decryptedBytes);
}
/**
* Helper method to encrypt a file
* @param inputStream stream associated with the file to be encrypted
* @param outputStream stream associated with new output encrypted file
* @throws CertificateException
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
* @throws IOException
* @throws NoSuchPaddingException
* @throws UnrecoverableKeyException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static void aesEncrypt(BufferedInputStream inputStream, BufferedOutputStream outputStream)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
NoSuchProviderException, InvalidAlgorithmParameterException, IOException,
NoSuchPaddingException, UnrecoverableKeyException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(ALGO_AES);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), gcmParameterSpec);
byte[] buffer = new byte[GenericCopyUtil.DEFAULT_BUFFER_SIZE];
int count;
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
try {
while ((count = inputStream.read(buffer)) != -1) {
cipherOutputStream.write(buffer, 0, count);
ServiceWatcherUtil.POSITION+=count;
}
} finally {
cipherOutputStream.flush();
cipherOutputStream.close();
inputStream.close();
}
}
/**
* Helper method to decrypt file
* @param inputStream stream associated with encrypted file
* @param outputStream stream associated with new output decrypted file
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws UnrecoverableKeyException
* @throws KeyStoreException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
* @throws IOException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static void aesDecrypt(BufferedInputStream inputStream, BufferedOutputStream outputStream)
throws NoSuchPaddingException, NoSuchAlgorithmException, CertificateException,
UnrecoverableKeyException, KeyStoreException, NoSuchProviderException,
InvalidAlgorithmParameterException, IOException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(ALGO_AES);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), gcmParameterSpec);
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
byte[] buffer = new byte[GenericCopyUtil.DEFAULT_BUFFER_SIZE];
int count;
try {
while ((count = cipherInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, count);
ServiceWatcherUtil.POSITION+=count;
}
} finally {
outputStream.flush();
cipherInputStream.close();
outputStream.close();
}
}
/**
* Gets a secret key from Android key store.
* If no key has been generated with a given alias then generate a new one
* @return
* @throws KeyStoreException
* @throws CertificateException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
* @throws UnrecoverableKeyException
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static Key getSecretKey() throws KeyStoreException, CertificateException,
NoSuchAlgorithmException, IOException, NoSuchProviderException,
InvalidAlgorithmParameterException,
UnrecoverableKeyException {
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_ANDROID);
keyStore.load(null);
if (!keyStore.containsAlias(KEY_ALIAS_AMAZE)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE_ANDROID);
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEY_ALIAS_AMAZE,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT);
builder.setBlockModes(KeyProperties.BLOCK_MODE_GCM);
builder.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE);
builder.setRandomizedEncryptionRequired(false);
keyGenerator.init(builder.build());
return keyGenerator.generateKey();
} else {
return keyStore.getKey(KEY_ALIAS_AMAZE, null);
}
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static void rsaEncrypt(Context context, BufferedInputStream inputStream, BufferedOutputStream outputStream)
throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException,
CertificateException, BadPaddingException, InvalidAlgorithmParameterException,
KeyStoreException, UnrecoverableEntryException, IllegalBlockSizeException,
InvalidKeyException, IOException {
Cipher cipher = Cipher.getInstance(ALGO_AES, "BC");
RSAKeygen keygen = new RSAKeygen(context);
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keygen.getSecretKey(), ivParameterSpec);
byte[] buffer = new byte[GenericCopyUtil.DEFAULT_BUFFER_SIZE];
int count;
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
try {
while ((count = inputStream.read(buffer)) != -1) {
cipherOutputStream.write(buffer, 0, count);
ServiceWatcherUtil.POSITION+=count;
}
} finally {
cipherOutputStream.flush();
cipherOutputStream.close();
inputStream.close();
}
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static void rsaDecrypt(Context context, BufferedInputStream inputStream,
BufferedOutputStream outputStream) throws NoSuchPaddingException,
NoSuchAlgorithmException, NoSuchProviderException, CertificateException,
BadPaddingException, InvalidAlgorithmParameterException, KeyStoreException,
UnrecoverableEntryException, IllegalBlockSizeException, InvalidKeyException, IOException {
Cipher cipher = Cipher.getInstance(ALGO_AES, "BC");
RSAKeygen keygen = new RSAKeygen(context);
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keygen.getSecretKey(), ivParameterSpec);
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
byte[] buffer = new byte[GenericCopyUtil.DEFAULT_BUFFER_SIZE];
int count;
try {
while ((count = cipherInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, count);
ServiceWatcherUtil.POSITION+=count;
}
} finally {
outputStream.flush();
outputStream.close();
cipherInputStream.close();
}
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static String rsaEncryptPassword(Context context, String password) throws
NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException,
CertificateException, BadPaddingException, InvalidAlgorithmParameterException,
KeyStoreException, UnrecoverableEntryException, IllegalBlockSizeException,
InvalidKeyException, IOException {
Cipher cipher = Cipher.getInstance(ALGO_AES, "BC");
RSAKeygen keygen = new RSAKeygen(context);
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keygen.getSecretKey(), ivParameterSpec);
return Base64.encodeToString(cipher.doFinal(password.getBytes()), Base64.DEFAULT);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static String rsaDecryptPassword(Context context, String cipherText) throws
NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException,
CertificateException, BadPaddingException, InvalidAlgorithmParameterException,
KeyStoreException, UnrecoverableEntryException, IllegalBlockSizeException,
InvalidKeyException, IOException {
Cipher cipher = Cipher.getInstance(ALGO_AES, "BC");
RSAKeygen keygen = new RSAKeygen(context);
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keygen.getSecretKey(), ivParameterSpec);
byte[] decryptedBytes = cipher.doFinal(Base64.decode(cipherText, Base64.DEFAULT));
return decryptedBytes.toString();
}
/**
* Method handles encryption of plain text on various APIs
* @param context
* @param plainText
* @return
*/
public static String encryptPassword(Context context, String plainText) throws IOException,
CertificateException, NoSuchAlgorithmException, UnrecoverableEntryException,
InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException,
NoSuchProviderException, BadPaddingException, KeyStoreException, IllegalBlockSizeException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return CryptUtil.aesEncryptPassword(plainText);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return CryptUtil.rsaEncryptPassword(context, plainText);
} else return plainText;
}
/**
* Method handles decryption of cipher text on various APIs
* @param context
* @param cipherText
* @return
*/
public static String decryptPassword(Context context, String cipherText) throws IOException,
CertificateException, NoSuchAlgorithmException, UnrecoverableEntryException,
InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException,
NoSuchProviderException, BadPaddingException, KeyStoreException, IllegalBlockSizeException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return CryptUtil.aesDecryptPassword(cipherText);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return CryptUtil.rsaDecryptPassword(context, cipherText);
} else return cipherText;
}
/**
* Method initializes a Cipher to be used by {@link android.hardware.fingerprint.FingerprintManager}
* @param context
* @return
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws UnrecoverableEntryException
* @throws KeyStoreException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
* @throws IOException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
public static Cipher initCipher(Context context) throws NoSuchPaddingException, NoSuchAlgorithmException,
CertificateException, UnrecoverableEntryException, KeyStoreException,
NoSuchProviderException, InvalidAlgorithmParameterException, IOException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
cipher = Cipher.getInstance(ALGO_AES);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), gcmParameterSpec);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
cipher = Cipher.getInstance(ALGO_AES, "BC");
RSAKeygen keygen = new RSAKeygen(context);
cipher.init(Cipher.ENCRYPT_MODE, keygen.getSecretKey());
}
return cipher;
}
/**
* Class responsible for generating key for API lower than M
*/
static class RSAKeygen {
private Context context;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
RSAKeygen(Context context) {
this.context = context;
try {
generateKeyPair(context);
setKeyPreference();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (UnrecoverableEntryException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
}
/**
* Generates a RSA public/private key pair to encrypt AES key
* @param context
* @throws KeyStoreException
* @throws CertificateException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private void generateKeyPair(Context context) throws KeyStoreException,
CertificateException, NoSuchAlgorithmException, IOException, NoSuchProviderException,
InvalidAlgorithmParameterException {
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_ANDROID);
keyStore.load(null);
if (!keyStore.containsAlias(KEY_ALIAS_AMAZE)) {
// generate a RSA key pair to encrypt/decrypt AES key from preferences
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 30);
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", KEY_STORE_ANDROID);
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
.setAlias(KEY_ALIAS_AMAZE)
.setSubject(new X500Principal("CN=" + KEY_ALIAS_AMAZE))
.setSerialNumber(BigInteger.TEN)
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.build();
keyPairGenerator.initialize(spec);
keyPairGenerator.generateKeyPair();
}
}
/**
* Encrypts AES key and set into preference
*/
private void setKeyPreference() throws IOException, CertificateException,
NoSuchAlgorithmException, InvalidKeyException, UnrecoverableEntryException,
NoSuchPaddingException, NoSuchProviderException, BadPaddingException,
KeyStoreException, IllegalBlockSizeException {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String encodedAesKey = preferences.getString(PREFERENCE_KEY, null);
if (encodedAesKey==null) {
// generate encrypted aes key and save to preference
byte[] key = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(key);
byte[] encryptedKey = encryptAESKey(key);
encodedAesKey = Base64.encodeToString(encryptedKey, Base64.DEFAULT);
preferences.edit().putString(PREFERENCE_KEY, encodedAesKey).apply();
}
}
/**
* Encrypts randomly generated AES key using RSA public key
* @param secretKey
* @return
*/
private byte[] encryptAESKey(byte[] secretKey) throws KeyStoreException,
UnrecoverableEntryException, NoSuchAlgorithmException, IOException,
CertificateException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_ANDROID);
keyStore.load(null);
KeyStore.PrivateKeyEntry keyEntry = (KeyStore.PrivateKeyEntry)
keyStore.getEntry(KEY_ALIAS_AMAZE, null);
Cipher cipher = Cipher.getInstance(ALGO_RSA, "AndroidOpenSSL");
cipher.init(Cipher.ENCRYPT_MODE, keyEntry.getCertificate().getPublicKey());
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
CipherOutputStream outputStream = new CipherOutputStream(byteArrayOutputStream, cipher);
outputStream.write(secretKey);
outputStream.close();
return byteArrayOutputStream.toByteArray();
}
/**
* Decodes encrypted AES key from preference and decrypts using RSA private key
* @return
* @throws CertificateException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws NoSuchProviderException
* @throws UnrecoverableEntryException
* @throws IOException
* @throws InvalidAlgorithmParameterException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public Key getSecretKey() throws CertificateException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, KeyStoreException, NoSuchProviderException, UnrecoverableEntryException, IOException, InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String encodedString = preferences.getString(PREFERENCE_KEY, null);
if (encodedString != null) {
return new SecretKeySpec(decryptAESKey(Base64.decode(encodedString, Base64.DEFAULT)),
"AES");
} else {
generateKeyPair(context);
setKeyPreference();
return getSecretKey();
}
}
/**
* Decrypts AES decoded key from preference using RSA private key
* @param encodedBytes
* @return
* @throws KeyStoreException
* @throws CertificateException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws UnrecoverableEntryException
* @throws NoSuchProviderException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
*/
private byte[] decryptAESKey(byte[] encodedBytes) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, UnrecoverableEntryException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException {
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_ANDROID);
keyStore.load(null);
KeyStore.PrivateKeyEntry keyEntry = (KeyStore.PrivateKeyEntry)
keyStore.getEntry(KEY_ALIAS_AMAZE, null);
Cipher cipher = Cipher.getInstance(ALGO_RSA, "AndroidOpenSSL");
cipher.init(Cipher.DECRYPT_MODE, keyEntry.getPrivateKey());
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encodedBytes);
CipherInputStream inputStream = new CipherInputStream(byteArrayInputStream, cipher);
ArrayList<Byte> bytes = new ArrayList<>();
int nextByte;
while ((nextByte = inputStream.read()) != -1) {
bytes.add((byte) nextByte);
}
byte[] decryptedBytes = new byte[bytes.size()];
for (int i=0; i<bytes.size(); i++) {
decryptedBytes[i] = bytes.get(i).byteValue();
}
return decryptedBytes;
}
}
}