Move KeyStoreEncryption to kmp

pull/3864/head
Alex Baker 3 months ago
parent e400594e5b
commit 3ff4a2339b

@ -39,6 +39,8 @@ import org.tasks.jobs.WorkManager
import org.tasks.kmp.createDataStore import org.tasks.kmp.createDataStore
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.preferences.TasksPreferences import org.tasks.preferences.TasksPreferences
import org.tasks.security.AndroidKeyStoreEncryption
import org.tasks.security.KeyStoreEncryption
import java.util.Locale import java.util.Locale
import javax.inject.Singleton import javax.inject.Singleton
@ -179,4 +181,8 @@ class ApplicationModule {
@Singleton @Singleton
fun providesVtodoCache(caldavDao: CaldavDao, fileStorage: FileStorage) = fun providesVtodoCache(caldavDao: CaldavDao, fileStorage: FileStorage) =
VtodoCache(caldavDao, fileStorage) VtodoCache(caldavDao, fileStorage)
@Provides
@Singleton
fun providesKeyStoreEncryption(): KeyStoreEncryption = AndroidKeyStoreEncryption()
} }

@ -1,97 +0,0 @@
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 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
import javax.inject.Singleton
@Singleton
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
}
}

@ -0,0 +1,42 @@
package org.tasks.security
import android.annotation.SuppressLint
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
class AndroidKeyProvider : KeyProvider {
private val keyStore: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
init {
keyStore.load(null)
}
override fun getKey(): SecretKey {
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()
}
companion object {
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val ALIAS = "passwords"
}
}

@ -0,0 +1,18 @@
package org.tasks.security
import java.util.Base64
class AndroidKeyStoreEncryption() : KeyStoreEncryption(AndroidKeyProvider()) {
override fun decodeBase64(text: String): ByteArray {
return try {
Base64.getDecoder().decode(text)
} catch (_: IllegalArgumentException) {
try {
android.util.Base64.decode(text, android.util.Base64.DEFAULT)
} catch (e: Exception) {
logger.e(e) { "Failed to decode Base64 data with both decoders" }
ByteArray(0)
}
}
}
}

@ -0,0 +1,7 @@
package org.tasks.security
import javax.crypto.SecretKey
interface KeyProvider {
fun getKey(): SecretKey
}

@ -0,0 +1,78 @@
package org.tasks.security
import co.touchlab.kermit.Logger
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
import java.util.Base64
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.spec.GCMParameterSpec
open class KeyStoreEncryption(
private val keyProvider: KeyProvider
) {
protected val logger = Logger.withTag("KeyStoreEncryption")
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.getEncoder().encodeToString(result)
} catch (e: IllegalBlockSizeException) {
logger.e(e) { "Failed to encrypt data" }
null
} catch (e: BadPaddingException) {
logger.e(e) { "Failed to encrypt data" }
null
}
}
fun decrypt(text: String?): String? {
if (text.isNullOrBlank()) {
return null
}
val decoded = decodeBase64(text)
if (decoded.isEmpty()) {
return ""
}
val iv = decoded.copyOfRange(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) {
logger.e(e) { "Failed to decrypt data" }
""
} catch (e: BadPaddingException) {
logger.e(e) { "Failed to decrypt data" }
""
}
}
protected open fun decodeBase64(text: String): ByteArray {
return try {
Base64.getDecoder().decode(text)
} catch (e: IllegalArgumentException) {
logger.e(e) { "Failed to decode Base64 data" }
ByteArray(0)
}
}
private fun getCipher(cipherMode: Int, iv: ByteArray): Cipher {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(cipherMode, keyProvider.getKey(), GCMParameterSpec(GCM_TAG_LENGTH * java.lang.Byte.SIZE, iv))
return cipher
}
companion object {
private val ENCODING = StandardCharsets.UTF_8
private const val GCM_IV_LENGTH = 12
private const val GCM_TAG_LENGTH = 16
}
}
Loading…
Cancel
Save