diff --git a/app/src/main/java/org/tasks/data/CaldavAccount.kt b/app/src/main/java/org/tasks/data/CaldavAccount.kt index 6d665d43d..c05dc5b89 100644 --- a/app/src/main/java/org/tasks/data/CaldavAccount.kt +++ b/app/src/main/java/org/tasks/data/CaldavAccount.kt @@ -67,11 +67,11 @@ class CaldavAccount : Parcelable { } fun getPassword(encryption: KeyStoreEncryption): String { - return encryption.decrypt(password) + return encryption.decrypt(password) ?: "" } fun getEncryptionPassword(encryption: KeyStoreEncryption): String { - return encryption.decrypt(encryptionKey) + return encryption.decrypt(encryptionKey) ?: "" } val isCaldavAccount: Boolean diff --git a/app/src/main/java/org/tasks/security/KeyStoreEncryption.java b/app/src/main/java/org/tasks/security/KeyStoreEncryption.java deleted file mode 100644 index 8051500dc..000000000 --- a/app/src/main/java/org/tasks/security/KeyStoreEncryption.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.tasks.security; - -import static org.tasks.Strings.isNullOrEmpty; - -import android.annotation.SuppressLint; -import android.security.keystore.KeyGenParameterSpec; -import android.security.keystore.KeyProperties; -import android.util.Base64; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.KeyStore; -import java.security.KeyStore.Entry; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SecureRandom; -import java.security.UnrecoverableEntryException; -import java.security.cert.CertificateException; -import java.util.Arrays; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.KeyGenerator; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.GCMParameterSpec; -import javax.inject.Inject; -import org.tasks.injection.ApplicationScope; -import timber.log.Timber; - -@ApplicationScope -public class KeyStoreEncryption { - - private static final String ANDROID_KEYSTORE = "AndroidKeyStore"; - private static final String ALIAS = "passwords"; - private static final Charset ENCODING = StandardCharsets.UTF_8; - private static final int GCM_IV_LENGTH = 12; - private static final int GCM_TAG_LENGTH = 16; - - private KeyStore keyStore; - - @Inject - public KeyStoreEncryption() { - try { - keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); - keyStore.load(null); - } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) { - throw new IllegalStateException(); - } - } - - public String encrypt(String text) { - byte[] iv = new byte[GCM_IV_LENGTH]; - new SecureRandom().nextBytes(iv); - Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, iv); - try { - byte[] output = cipher.doFinal(text.getBytes(ENCODING)); - byte[] result = new byte[iv.length + output.length]; - System.arraycopy(iv, 0, result, 0, iv.length); - System.arraycopy(output, 0, result, iv.length, output.length); - return Base64.encodeToString(result, Base64.DEFAULT); - } catch (IllegalBlockSizeException | BadPaddingException e) { - Timber.e(e); - return null; - } - } - - public String decrypt(String text) { - if (isNullOrEmpty(text)) { - return null; - } - - byte[] decoded = Base64.decode(text, Base64.DEFAULT); - byte[] iv = Arrays.copyOfRange(decoded, 0, GCM_IV_LENGTH); - Cipher cipher = getCipher(Cipher.DECRYPT_MODE, iv); - try { - byte[] decrypted = cipher.doFinal(decoded, GCM_IV_LENGTH, decoded.length - GCM_IV_LENGTH); - return new String(decrypted, ENCODING); - } catch (IllegalBlockSizeException | BadPaddingException e) { - Timber.e(e); - return ""; - } - } - - private Cipher getCipher(int cipherMode, byte[] iv) { - try { - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(cipherMode, getSecretKey(), new GCMParameterSpec(GCM_TAG_LENGTH * Byte.SIZE, iv)); - return cipher; - } catch (NoSuchAlgorithmException - | NoSuchPaddingException - | InvalidAlgorithmParameterException - | InvalidKeyException e) { - throw new IllegalArgumentException(e); - } - } - - private SecretKey getSecretKey() { - try { - Entry entry = keyStore.getEntry(ALIAS, null); - return entry == null ? generateNewKey() : ((KeyStore.SecretKeyEntry) entry).getSecretKey(); - } catch (NoSuchAlgorithmException | KeyStoreException | UnrecoverableEntryException e) { - throw new IllegalStateException(); - } - } - - @SuppressLint("TrulyRandom") - private SecretKey generateNewKey() { - try { - final KeyGenerator keyGenerator = - KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE); - - keyGenerator.init( - new KeyGenParameterSpec.Builder( - ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setRandomizedEncryptionRequired(false) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .build()); - return keyGenerator.generateKey(); - } catch (NoSuchAlgorithmException - | InvalidAlgorithmParameterException - | NoSuchProviderException e) { - throw new IllegalStateException(); - } - } -} diff --git a/app/src/main/java/org/tasks/security/KeyStoreEncryption.kt b/app/src/main/java/org/tasks/security/KeyStoreEncryption.kt new file mode 100644 index 000000000..f9668bba6 --- /dev/null +++ b/app/src/main/java/org/tasks/security/KeyStoreEncryption.kt @@ -0,0 +1,97 @@ +package org.tasks.security + +import android.annotation.SuppressLint +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import org.tasks.Strings.isNullOrEmpty +import org.tasks.injection.ApplicationScope +import timber.log.Timber +import java.nio.charset.StandardCharsets +import java.security.KeyStore +import java.security.SecureRandom +import java.util.* +import javax.crypto.* +import javax.crypto.spec.GCMParameterSpec +import javax.inject.Inject + +@ApplicationScope +class KeyStoreEncryption @Inject constructor() { + + private val keyStore: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE) + + fun encrypt(text: String): String? { + val iv = ByteArray(GCM_IV_LENGTH) + SecureRandom().nextBytes(iv) + val cipher = getCipher(Cipher.ENCRYPT_MODE, iv) + return try { + val output = cipher.doFinal(text.toByteArray(ENCODING)) + val result = ByteArray(iv.size + output.size) + System.arraycopy(iv, 0, result, 0, iv.size) + System.arraycopy(output, 0, result, iv.size, output.size) + Base64.encodeToString(result, Base64.DEFAULT) + } catch (e: IllegalBlockSizeException) { + Timber.e(e) + null + } catch (e: BadPaddingException) { + Timber.e(e) + null + } + } + + fun decrypt(text: String?): String? { + if (isNullOrEmpty(text)) { + return null + } + val decoded = Base64.decode(text, Base64.DEFAULT) + val iv = Arrays.copyOfRange(decoded, 0, GCM_IV_LENGTH) + val cipher = getCipher(Cipher.DECRYPT_MODE, iv) + return try { + val decrypted = cipher.doFinal(decoded, GCM_IV_LENGTH, decoded.size - GCM_IV_LENGTH) + String(decrypted, ENCODING) + } catch (e: IllegalBlockSizeException) { + Timber.e(e) + "" + } catch (e: BadPaddingException) { + Timber.e(e) + "" + } + } + + private fun getCipher(cipherMode: Int, iv: ByteArray): Cipher { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(cipherMode, secretKey, GCMParameterSpec(GCM_TAG_LENGTH * java.lang.Byte.SIZE, iv)) + return cipher + } + + private val secretKey: SecretKey + get() { + val entry: KeyStore.Entry? = keyStore.getEntry(ALIAS, null) + return (entry as KeyStore.SecretKeyEntry?)?.secretKey ?: generateNewKey() + } + + @SuppressLint("TrulyRandom") + private fun generateNewKey(): SecretKey { + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE) + keyGenerator.init( + KeyGenParameterSpec.Builder( + ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setRandomizedEncryptionRequired(false) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()) + return keyGenerator.generateKey() + } + + init { + keyStore.load(null) + } + + companion object { + private const val ANDROID_KEYSTORE = "AndroidKeyStore" + private const val ALIAS = "passwords" + private val ENCODING = StandardCharsets.UTF_8 + private const val GCM_IV_LENGTH = 12 + private const val GCM_TAG_LENGTH = 16 + } +} \ No newline at end of file