Implemented encrypting biometric password

This commit is contained in:
dankito 2020-10-07 23:06:36 +02:00
parent 32c71fcb39
commit 2cab245600
10 changed files with 191 additions and 13 deletions

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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))
}
}
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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"

View File

@ -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 <T : Any> deserializeObject(serializedObject: String, objectClass: KClass<T>, vararg genericParameterTypes: KClass<*>): T? {
return serializer.deserializeObject(serializedObject, objectClass.java, *genericParameterTypes.map { it.java }.toTypedArray())
}
override fun <T : Any> deserializeObject(serializedObjectFile: File, objectClass: KClass<T>,
vararg genericParameterTypes: KClass<*>): T? {
return serializer.deserializeObject(serializedObjectFile, objectClass.java, *genericParameterTypes.map { it.java }.toTypedArray())
}
override fun <T : Any> deserializeListOr(serializedObject: String, genericListParameterType: KClass<T>, defaultValue: List<T>): List<T> {
return serializer.deserializeListOr(serializedObject, genericListParameterType.java, defaultValue) ?: defaultValue
}
override fun <T : Any> deserializeListOr(serializedObjectFile: File, genericListParameterType: KClass<T>, defaultValue: List<T>): List<T> {
return serializer.deserializeListOr(serializedObjectFile, genericListParameterType.java, defaultValue)
}

View File

@ -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
}