Implemented encrypting biometric password
This commit is contained in:
parent
32c71fcb39
commit
2cab245600
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue