diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt index b0f91ce6..87eb715e 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt @@ -4,6 +4,8 @@ import android.os.Bundle import android.view.View import android.widget.Toast import kotlinx.android.synthetic.main.activity_login.* +import kotlinx.android.synthetic.main.activity_login.btnBiometricAuthentication +import kotlinx.android.synthetic.main.view_biometric_authentication_button.* import net.dankito.banking.ui.android.MainActivity import net.dankito.banking.ui.android.R import net.dankito.banking.ui.android.authentication.AuthenticationService @@ -47,7 +49,15 @@ open class LoginActivity : BaseActivity() { else { lytPasswordAuthentication.visibility = View.GONE - btnBiometricAuthentication.authenticationSuccessful = { biometricAuthenticationSuccessful() } + btnBiometricAuthentication.customButtonClickHandler = { + authenticationService.authenticateUserWithBiometric { result -> + if (result) { + btnStartBiometricAuthentication.isEnabled = false + + biometricAuthenticationSuccessful() + } + } + } btnBiometricAuthentication.showBiometricPrompt() } diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt index 58ec5d06..9920ad82 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt @@ -1,22 +1,28 @@ package net.dankito.banking.ui.android.authentication +import android.util.Base64 import at.favre.lib.crypto.bcrypt.BCrypt import net.dankito.banking.persistence.IBankingPersistence +import net.dankito.banking.ui.android.security.CryptographyManager import net.dankito.banking.util.ISerializer import net.dankito.utils.multiplatform.File import org.slf4j.LoggerFactory +import javax.crypto.Cipher open class AuthenticationService( protected open val biometricAuthenticationService: IBiometricAuthenticationService, protected open val persistence: IBankingPersistence, protected open val dataFolder: File, - protected open val serializer: ISerializer + protected open val serializer: ISerializer, + protected open val cryptographyManager: CryptographyManager = CryptographyManager() ) { companion object { private const val AuthenticationSettingsFilename = "s" + private const val EncryptionKeyName = "BankingAndroidKey" + private val log = LoggerFactory.getLogger(AuthenticationService::class.java) } @@ -27,6 +33,8 @@ open class AuthenticationService( open var authenticationType: AuthenticationType = AuthenticationType.None protected set + protected open var encryptionCipherForBiometric: Cipher? = null + init { val settings = loadAuthenticationSettings() @@ -62,6 +70,40 @@ open class AuthenticationService( return false } + open fun authenticateUserWithBiometricToSetAsNewAuthenticationMethod(authenticationResult: (AuthenticationResult) -> Unit) { + val cipher = cryptographyManager.getInitializedCipherForEncryption(EncryptionKeyName) + + biometricAuthenticationService.authenticate(cipher) { result -> + if (result.successful) { + this.encryptionCipherForBiometric = cipher + } + + authenticationResult(result) + } + } + + open fun authenticateUserWithBiometric(result: (Boolean) -> Unit) { + loadAuthenticationSettings()?.let { settings -> + val iv = decodeFromBase64(settings.initializationVector ?: "") + + val cipher = cryptographyManager.getInitializedCipherForDecryption(EncryptionKeyName, iv) + biometricAuthenticationService.authenticate(cipher) { authenticationResult -> + if (authenticationResult.successful) { + settings.encryptedUserPassword?.let { + val encryptedUserPassword = decodeFromBase64(it) + val decrypted = cryptographyManager.decryptData(encryptedUserPassword, cipher) + + result(openDatabase(decrypted)) + } + } + else { + result(false) + } + } + } + ?: run { result(false) } + } + protected open fun openDatabase(authenticationSettings: AuthenticationSettings) { openDatabase(authenticationSettings.userPassword) } @@ -88,17 +130,26 @@ open class AuthenticationService( val settings = loadOrCreateDefaultAuthenticationSettings() if (type == settings.type && - ((type != AuthenticationType.Password && settings.userPassword == newPassword) - || (type == AuthenticationType.Password && isCorrectUserPassword(newPassword)))) { // nothing changed + ((type == AuthenticationType.Password && isCorrectUserPassword(newPassword)) || settings.userPassword == newPassword)) { // nothing changed return true } settings.type = type settings.hashedUserPassword = if (type == AuthenticationType.Password) BCrypt.withDefaults().hashToString(12, newPassword.toCharArray()) else null - settings.userPassword = if (type == AuthenticationType.Password) null else newPassword + settings.userPassword = if (type == AuthenticationType.None) newPassword else null + + if (type == AuthenticationType.Biometric) { + encryptionCipherForBiometric?.let { encryptionCipher -> + val encryptedPassword = cryptographyManager.encryptData(newPassword, encryptionCipher) + settings.encryptedUserPassword = encodeToBase64(encryptedPassword) + settings.initializationVector = encodeToBase64(encryptionCipher.iv) + } + } if (saveAuthenticationSettings(settings)) { this.authenticationType = type + this.encryptionCipherForBiometric = null + persistence.changePassword(newPassword) // TODO: actually this is bad. If changing password fails then password is saved in AuthenticationSettings but DB has a different password return true } @@ -158,4 +209,13 @@ open class AuthenticationService( return passwordBuilder.toString() } + + open fun encodeToBase64(data: ByteArray): String { + return Base64.encodeToString(data, Base64.DEFAULT) + } + + open fun decodeFromBase64(data: String): ByteArray { + return Base64.decode(data, Base64.DEFAULT) + } + } \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt index aa29ec06..e3596e3f 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt @@ -4,7 +4,9 @@ package net.dankito.banking.ui.android.authentication open class AuthenticationSettings( open var type: AuthenticationType, open var hashedUserPassword: String? = null, - open var userPassword: String? = null + open var userPassword: String? = null, + open var encryptedUserPassword: String? = null, + open var initializationVector: String? = null ) { internal constructor() : this(AuthenticationType.None) // for object deserializers diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/BiometricAuthenticationService.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/BiometricAuthenticationService.kt index f7cdc4a1..740f7d28 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/BiometricAuthenticationService.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/BiometricAuthenticationService.kt @@ -6,6 +6,7 @@ import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import net.dankito.banking.ui.android.R import net.dankito.banking.ui.android.util.CurrentActivityTracker +import javax.crypto.Cipher open class BiometricAuthenticationService( @@ -18,7 +19,7 @@ open class BiometricAuthenticationService( get() = biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS - override fun authenticate(authenticationResult: (AuthenticationResult) -> Unit) { + override fun authenticate(cipher: Cipher?, authenticationResult: (AuthenticationResult) -> Unit) { activityTracker.currentOrNextActivity { activity -> val executor = ContextCompat.getMainExecutor(context) @@ -44,10 +45,15 @@ open class BiometricAuthenticationService( val promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle(context.getString(R.string.activity_login_authenticate_with_biometrics_prompt)) - .setDeviceCredentialAllowed(true) + .setNegativeButtonText(context.getString(android.R.string.cancel)) .build() - biometricPrompt.authenticate(promptInfo) + if (cipher == null) { + biometricPrompt.authenticate(promptInfo) + } + else { + biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) + } } } diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/IBiometricAuthenticationService.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/IBiometricAuthenticationService.kt index 0c32d3b2..1b890988 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/IBiometricAuthenticationService.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/IBiometricAuthenticationService.kt @@ -1,11 +1,13 @@ package net.dankito.banking.ui.android.authentication +import javax.crypto.Cipher + interface IBiometricAuthenticationService { val supportsBiometricAuthentication: Boolean - fun authenticate(authenticationResult: (AuthenticationResult) -> Unit) + fun authenticate(cipher: Cipher? = null, authenticationResult: (AuthenticationResult) -> Unit) } \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/security/CryptographyManager.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/security/CryptographyManager.kt new file mode 100644 index 00000000..ad786a87 --- /dev/null +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/security/CryptographyManager.kt @@ -0,0 +1,78 @@ +package net.dankito.banking.ui.android.security + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + + +open class CryptographyManager { + + companion object { + + const val AndroidKeyStore = "AndroidKeyStore" + + val PasswordCharset = Charsets.UTF_8 + + private const val KeySize: Int = 256 + private const val EncryptionBlockMode = KeyProperties.BLOCK_MODE_GCM + private const val EncryptionPadding = KeyProperties.ENCRYPTION_PADDING_NONE + private const val EncryptionAlgorithm = KeyProperties.KEY_ALGORITHM_AES + + private const val CipherTransformation = "$EncryptionAlgorithm/$EncryptionBlockMode/$EncryptionPadding" + + } + + + open fun getInitializedCipherForEncryption(keyName: String): Cipher { + return getInitializedCipher(keyName, Cipher.ENCRYPT_MODE) + } + + open fun getInitializedCipherForDecryption(keyName: String, initializationVector: ByteArray): Cipher { + return getInitializedCipher(keyName, Cipher.DECRYPT_MODE, initializationVector) + } + + protected open fun getInitializedCipher(keyName: String, cipherMode: Int, initializationVector: ByteArray? = null): Cipher { + val cipher = Cipher.getInstance(CipherTransformation) + val secretKey = getOrCreateSecretKey(keyName) + + cipher.init(cipherMode, secretKey, initializationVector?.let { GCMParameterSpec(128, initializationVector) }) + + return cipher + } + + + open fun encryptData(plaintext: String, cipher: Cipher): ByteArray { + return cipher.doFinal(plaintext.toByteArray(PasswordCharset)) + } + + open fun decryptData(cipherText: ByteArray, cipher: Cipher): String { + val plaintext = cipher.doFinal(cipherText) + return String(plaintext, PasswordCharset) + } + + protected open fun getOrCreateSecretKey(keyName: String): SecretKey { + val keyStore = KeyStore.getInstance(AndroidKeyStore) + keyStore.load(null) + keyStore.getKey(keyName, null)?.let { return it as SecretKey } + + val paramsBuilder = KeyGenParameterSpec.Builder(keyName, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + paramsBuilder.apply { + setBlockModes(EncryptionBlockMode) + setEncryptionPaddings(EncryptionPadding) + setKeySize(KeySize) + setUserAuthenticationRequired(true) + } + + val keyGenParams = paramsBuilder.build() + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, + AndroidKeyStore) + keyGenerator.init(keyGenParams) + return keyGenerator.generateKey() + } + +} \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/BiometricAuthenticationButton.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/BiometricAuthenticationButton.kt index e0d9538c..d06e27f9 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/BiometricAuthenticationButton.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/BiometricAuthenticationButton.kt @@ -21,6 +21,8 @@ open class BiometricAuthenticationButton @JvmOverloads constructor( open var authenticationSuccessful: (() -> Unit)? = null + open var customButtonClickHandler: (() -> Unit)? = null + init { BankingComponent.component.inject(this) @@ -33,7 +35,9 @@ open class BiometricAuthenticationButton @JvmOverloads constructor( val rootView = inflater.inflate(R.layout.view_biometric_authentication_button, this, true) rootView.apply { - btnStartBiometricAuthentication.setOnClickListener { doBiometricAuthenticationAndLogIn() } + btnStartBiometricAuthentication.setOnClickListener { + customButtonClickHandler?.invoke() ?: doBiometricAuthenticationAndLogIn() + } } } @@ -47,7 +51,7 @@ open class BiometricAuthenticationButton @JvmOverloads constructor( open fun showBiometricPrompt() { - doBiometricAuthenticationAndLogIn() + btnStartBiometricAuthentication.performClick() } } \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/res/layout/view_biometric_authentication_button.xml b/ui/BankingAndroidApp/src/main/res/layout/view_biometric_authentication_button.xml index 5c19a899..fc4d165c 100644 --- a/ui/BankingAndroidApp/src/main/res/layout/view_biometric_authentication_button.xml +++ b/ui/BankingAndroidApp/src/main/res/layout/view_biometric_authentication_button.xml @@ -13,7 +13,7 @@ android:layout_width="@dimen/view_biometric_authentication_button_icon_size" android:layout_height="@dimen/view_biometric_authentication_button_icon_size" android:padding="@dimen/view_biometric_authentication_button_padding" - android:background="@color/colorAccent" + android:background="@drawable/conditionally_disabled_view_background" android:tint="#FFFFFF" app:srcCompat="@drawable/ic_baseline_fingerprint_24" android:scaleType="fitCenter" diff --git a/ui/BankingUiCommon/src/jvmMain/kotlin/net/dankito/banking/util/JacksonJsonSerializer.kt b/ui/BankingUiCommon/src/jvmMain/kotlin/net/dankito/banking/util/JacksonJsonSerializer.kt index 40690cb6..751fd2ca 100644 --- a/ui/BankingUiCommon/src/jvmMain/kotlin/net/dankito/banking/util/JacksonJsonSerializer.kt +++ b/ui/BankingUiCommon/src/jvmMain/kotlin/net/dankito/banking/util/JacksonJsonSerializer.kt @@ -17,15 +17,28 @@ open class JacksonJsonSerializer( } ) : ISerializer { + override fun serializeObjectToString(obj: Any): String? { + return serializer.serializeObject(obj) + } + override fun serializeObject(obj: Any, outputFile: File) { return serializer.serializeObject(obj, outputFile) } + + override fun deserializeObject(serializedObject: String, objectClass: KClass, vararg genericParameterTypes: KClass<*>): T? { + return serializer.deserializeObject(serializedObject, objectClass.java, *genericParameterTypes.map { it.java }.toTypedArray()) + } + override fun deserializeObject(serializedObjectFile: File, objectClass: KClass, vararg genericParameterTypes: KClass<*>): T? { return serializer.deserializeObject(serializedObjectFile, objectClass.java, *genericParameterTypes.map { it.java }.toTypedArray()) } + override fun deserializeListOr(serializedObject: String, genericListParameterType: KClass, defaultValue: List): List { + return serializer.deserializeListOr(serializedObject, genericListParameterType.java, defaultValue) ?: defaultValue + } + override fun deserializeListOr(serializedObjectFile: File, genericListParameterType: KClass, defaultValue: List): List { return serializer.deserializeListOr(serializedObjectFile, genericListParameterType.java, defaultValue) } diff --git a/ui/BankingUiCommon/src/jvmMain/kotlin/net/dankito/banking/util/persistence/JacksonClassNameIdResolver.kt b/ui/BankingUiCommon/src/jvmMain/kotlin/net/dankito/banking/util/persistence/JacksonClassNameIdResolver.kt index 91e5fe29..59276653 100644 --- a/ui/BankingUiCommon/src/jvmMain/kotlin/net/dankito/banking/util/persistence/JacksonClassNameIdResolver.kt +++ b/ui/BankingUiCommon/src/jvmMain/kotlin/net/dankito/banking/util/persistence/JacksonClassNameIdResolver.kt @@ -14,10 +14,13 @@ import kotlin.reflect.jvm.jvmName open class JacksonClassNameIdResolver : ClassNameIdResolver(SimpleType.construct(JobParameters::class.java), TypeFactory.defaultInstance()) { override fun idFromValue(value: Any?): String { + println("value class is ${value?.javaClass?.simpleName}") + if (value is RetrieveAccountTransactionsParameters) { return RetrieveAccountTransactionsParameters::class.jvmName } else if (value is SepaAccountInfoParameters) { + println("Returning SepaAccountInfoParameters") return SepaAccountInfoParameters::class.jvmName }