mirror of
https://github.com/twofas/2fas-android.git
synced 2024-11-21 17:59:50 +01:00
Improve error handling on BiometricsPrompt
This commit is contained in:
parent
c85d30420c
commit
44229aad6c
@ -1,82 +1,106 @@
|
||||
package com.twofasapp.feature.security.ui.biometric
|
||||
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.widget.Toast
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.biometric.BiometricPrompt.PromptInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.twofasapp.designsystem.ktx.currentActivity
|
||||
import com.twofasapp.designsystem.ktx.toastLong
|
||||
import com.twofasapp.feature.security.biometric.BiometricKeyProvider
|
||||
import com.twofasapp.locale.R
|
||||
import java.util.concurrent.Executor
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
internal class BiometricDialog(
|
||||
private val activity: FragmentActivity,
|
||||
private val fragment: Fragment? = null,
|
||||
private val titleRes: Int = R.string.biometric_dialog_auth_title,
|
||||
private val subtitleRes: Int = R.string.biometric_dialog_auth_subtitle,
|
||||
private val cancelRes: Int = R.string.biometric_dialog_auth_cancel,
|
||||
private val onSuccess: () -> Unit,
|
||||
private val onFailed: () -> Unit,
|
||||
private val onError: () -> Unit,
|
||||
private val onDismiss: () -> Unit = {},
|
||||
private val onBiometricInvalidated: () -> Unit = {},
|
||||
private val biometricKeyProvider: BiometricKeyProvider,
|
||||
@Composable
|
||||
fun BiometricDialog(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
negative: String,
|
||||
requireKeyValidation: Boolean,
|
||||
onSuccess: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onBiometricInvalidated: () -> Unit = {},
|
||||
biometricKeyProvider: BiometricKeyProvider,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = LocalContext.currentActivity as? FragmentActivity
|
||||
|
||||
private lateinit var executor: Executor
|
||||
private lateinit var biometricPrompt: BiometricPrompt
|
||||
private lateinit var promptInfo: BiometricPrompt.PromptInfo
|
||||
if (activity == null) {
|
||||
context.toastLong("Could not find FragmentActivity. Restart the app and try again.")
|
||||
onDismiss()
|
||||
return
|
||||
}
|
||||
|
||||
fun show() {
|
||||
executor = ContextCompat.getMainExecutor(activity)
|
||||
val executor = ContextCompat.getMainExecutor(activity)
|
||||
|
||||
val callback: BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke()
|
||||
}
|
||||
val promptInfo = PromptInfo.Builder()
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setNegativeButtonText(negative)
|
||||
.build()
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailed.invoke()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
if (errorCode == 13 || errorCode == 10) {
|
||||
// Cancel action
|
||||
onDismiss.invoke()
|
||||
return
|
||||
}
|
||||
|
||||
onError.invoke()
|
||||
Toast.makeText(activity, "$errString", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
biometricPrompt = if (fragment != null) {
|
||||
BiometricPrompt(fragment, executor, callback)
|
||||
} else {
|
||||
BiometricPrompt(activity, executor, callback)
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
// Failed eg. due to wrong fingerprint
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(activity.getString(titleRes))
|
||||
.setSubtitle(activity.getString(subtitleRes))
|
||||
.setNegativeButtonText(activity.getString(cancelRes))
|
||||
.build()
|
||||
|
||||
try {
|
||||
val cipher = Cipher.getInstance(BiometricKeyProvider.TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, biometricKeyProvider.getSecretKey())
|
||||
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||
onBiometricInvalidated()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onError()
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
if (errorCode == 13 || errorCode == 10) {
|
||||
// Cancel action
|
||||
onDismiss()
|
||||
return
|
||||
}
|
||||
context.toastLong("Error: $errString")
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val biometricPrompt = BiometricPrompt(activity, executor, callback)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
authenticate(
|
||||
biometricPrompt = biometricPrompt,
|
||||
promptInfo = promptInfo,
|
||||
secretKey = biometricKeyProvider.getSecretKey()
|
||||
)
|
||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||
if (requireKeyValidation) {
|
||||
onBiometricInvalidated()
|
||||
} else {
|
||||
biometricKeyProvider.deleteSecretKey()
|
||||
|
||||
authenticate(
|
||||
biometricPrompt = biometricPrompt,
|
||||
promptInfo = promptInfo,
|
||||
secretKey = biometricKeyProvider.getSecretKey()
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
context.toastLong("Error: ${e.message}")
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticate(
|
||||
biometricPrompt: BiometricPrompt,
|
||||
promptInfo: PromptInfo,
|
||||
secretKey: SecretKey,
|
||||
) {
|
||||
val cipher = Cipher.getInstance(BiometricKeyProvider.TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
package com.twofasapp.feature.security.ui.biometric
|
||||
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.widget.Toast
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.twofasapp.feature.security.biometric.BiometricKeyProvider
|
||||
import com.twofasapp.locale.R
|
||||
import java.util.concurrent.Executor
|
||||
import javax.crypto.Cipher
|
||||
|
||||
internal class OldBiometricDialog(
|
||||
private val activity: FragmentActivity,
|
||||
private val fragment: Fragment? = null,
|
||||
private val titleRes: Int = R.string.biometric_dialog_auth_title,
|
||||
private val subtitleRes: Int = R.string.biometric_dialog_auth_subtitle,
|
||||
private val cancelRes: Int = R.string.biometric_dialog_auth_cancel,
|
||||
private val onSuccess: () -> Unit,
|
||||
private val onFailed: () -> Unit,
|
||||
private val onError: () -> Unit,
|
||||
private val onDismiss: () -> Unit = {},
|
||||
private val onBiometricInvalidated: () -> Unit = {},
|
||||
private val biometricKeyProvider: BiometricKeyProvider,
|
||||
) {
|
||||
|
||||
private lateinit var executor: Executor
|
||||
private lateinit var biometricPrompt: BiometricPrompt
|
||||
private lateinit var promptInfo: BiometricPrompt.PromptInfo
|
||||
|
||||
fun show() {
|
||||
executor = ContextCompat.getMainExecutor(activity)
|
||||
|
||||
val callback: BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailed.invoke()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
if (errorCode == 13 || errorCode == 10) {
|
||||
// Cancel action
|
||||
onDismiss.invoke()
|
||||
return
|
||||
}
|
||||
|
||||
onError.invoke()
|
||||
Toast.makeText(activity, "$errString", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
biometricPrompt = if (fragment != null) {
|
||||
BiometricPrompt(fragment, executor, callback)
|
||||
} else {
|
||||
BiometricPrompt(activity, executor, callback)
|
||||
}
|
||||
|
||||
promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(activity.getString(titleRes))
|
||||
.setSubtitle(activity.getString(subtitleRes))
|
||||
.setNegativeButtonText(activity.getString(cancelRes))
|
||||
.build()
|
||||
|
||||
try {
|
||||
val cipher = Cipher.getInstance(BiometricKeyProvider.TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, biometricKeyProvider.getSecretKey())
|
||||
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||
onBiometricInvalidated()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
@ -23,16 +23,16 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.twofasapp.feature.security.biometric.BiometricKeyProvider
|
||||
import com.twofasapp.designsystem.TwTheme
|
||||
import com.twofasapp.designsystem.common.TwCircularProgressIndicator
|
||||
import com.twofasapp.feature.security.biometric.BiometricKeyProvider
|
||||
import com.twofasapp.feature.security.ui.biometric.BiometricDialog
|
||||
import com.twofasapp.locale.R
|
||||
|
||||
internal sealed interface PinScreenState {
|
||||
object Loading : PinScreenState
|
||||
@ -132,19 +132,21 @@ internal fun PinScreen(
|
||||
|
||||
if (showBiometricDialog && showBiometrics && biometricKeyProvider != null) {
|
||||
BiometricDialog(
|
||||
activity = LocalContext.current as FragmentActivity,
|
||||
title = stringResource(id = R.string.biometric_dialog_auth_title),
|
||||
subtitle = stringResource(id = R.string.biometric_dialog_auth_subtitle),
|
||||
negative = stringResource(id = R.string.biometric_dialog_auth_cancel),
|
||||
onSuccess = {
|
||||
onBiometricsVerified.invoke()
|
||||
showBiometricDialog = false
|
||||
},
|
||||
onFailed = { showBiometricDialog = false },
|
||||
onError = { showBiometricDialog = false },
|
||||
onDismiss = { showBiometricDialog = false },
|
||||
onDismiss = {
|
||||
showBiometricDialog = false
|
||||
},
|
||||
onBiometricInvalidated = { onBiometricsInvalidated() },
|
||||
biometricKeyProvider = biometricKeyProvider,
|
||||
).show()
|
||||
requireKeyValidation = true,
|
||||
biometricKeyProvider = biometricKeyProvider
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
@ -18,8 +18,9 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.twofasapp.feature.security.biometric.BiometricKeyProvider
|
||||
import com.twofasapp.data.session.domain.LockMethod
|
||||
import com.twofasapp.data.session.domain.PinTimeout
|
||||
import com.twofasapp.data.session.domain.PinTrials
|
||||
import com.twofasapp.designsystem.TwIcons
|
||||
import com.twofasapp.designsystem.TwTheme
|
||||
import com.twofasapp.designsystem.common.TwTopAppBar
|
||||
@ -29,11 +30,9 @@ import com.twofasapp.designsystem.settings.SettingsDivider
|
||||
import com.twofasapp.designsystem.settings.SettingsHeader
|
||||
import com.twofasapp.designsystem.settings.SettingsLink
|
||||
import com.twofasapp.designsystem.settings.SettingsSwitch
|
||||
import com.twofasapp.locale.R
|
||||
import com.twofasapp.data.session.domain.LockMethod
|
||||
import com.twofasapp.data.session.domain.PinTimeout
|
||||
import com.twofasapp.data.session.domain.PinTrials
|
||||
import com.twofasapp.feature.security.biometric.BiometricKeyProvider
|
||||
import com.twofasapp.feature.security.ui.biometric.BiometricDialog
|
||||
import com.twofasapp.locale.R
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@ -232,19 +231,19 @@ internal fun SecurityScreen(
|
||||
|
||||
if (showBiometricDialog) {
|
||||
BiometricDialog(
|
||||
activity = activity!! as FragmentActivity,
|
||||
titleRes = R.string.biometric_dialog_setup_title,
|
||||
cancelRes = R.string.biometric_dialog_setup_cancel,
|
||||
title = stringResource(id = R.string.biometric_dialog_setup_title),
|
||||
subtitle = stringResource(id = R.string.biometric_dialog_auth_subtitle),
|
||||
negative = stringResource(id = R.string.biometric_dialog_setup_cancel),
|
||||
onSuccess = {
|
||||
viewModel.updateBiometricLock(true)
|
||||
showBiometricDialog = false
|
||||
},
|
||||
onFailed = { showBiometricDialog = false },
|
||||
onError = { showBiometricDialog = false },
|
||||
onDismiss = { showBiometricDialog = false },
|
||||
onBiometricInvalidated = {},
|
||||
biometricKeyProvider = biometricKeyProvider,
|
||||
).show()
|
||||
onDismiss = {
|
||||
showBiometricDialog = false
|
||||
},
|
||||
requireKeyValidation = false,
|
||||
biometricKeyProvider = biometricKeyProvider
|
||||
)
|
||||
}
|
||||
|
||||
if (showScreenshotsConfirmDialog) {
|
||||
|
@ -30,7 +30,7 @@ activityX = { module = "androidx.activity:activity-ktx", version.ref = "composeA
|
||||
apacheCommonsCodec = "commons-codec:commons-codec:1.15"
|
||||
appcompat = "androidx.appcompat:appcompat:1.6.1"
|
||||
barcodeScanning = "com.google.mlkit:barcode-scanning:17.2.0"
|
||||
biometric = "androidx.biometric:biometric:1.1.0"
|
||||
biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraX" }
|
||||
camera2Lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraX" }
|
||||
camera2View = { module = "androidx.camera:camera-view", version.ref = "cameraX" }
|
||||
|
Loading…
Reference in New Issue
Block a user