Refactor BiometricAuthenticator and add proper support for retries (#1627)

This commit is contained in:
Harsh Shandilya 2021-12-29 16:05:19 +05:30 committed by GitHub
parent 8b5be3f785
commit 4c9413709d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 58 additions and 16 deletions

View file

@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
- .gpg-id file generated by APS did not work with pass CLI
- All but the latest launcher shortcut would have an empty icon
- When prompted to select a GPG key during onboarding, the app would crash if the user did not make a selection in OpenKeychain
- Biometric authentication prompts no longer inexplicably dismiss when an incorrect biometric is entered
### Changed

View file

@ -14,6 +14,7 @@ import dev.msfjarvis.aps.ui.crypto.BasePgpActivity
import dev.msfjarvis.aps.ui.crypto.DecryptActivity
import dev.msfjarvis.aps.ui.passwords.PasswordStore
import dev.msfjarvis.aps.util.auth.BiometricAuthenticator
import dev.msfjarvis.aps.util.auth.BiometricAuthenticator.Result
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.settings.PreferenceKeys
@ -23,18 +24,19 @@ class LaunchActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
val prefs = sharedPrefs
if (prefs.getBoolean(PreferenceKeys.BIOMETRIC_AUTH, false)) {
BiometricAuthenticator.authenticate(this) {
when (it) {
is BiometricAuthenticator.Result.Success -> {
BiometricAuthenticator.authenticate(this) { result ->
when (result) {
is Result.Success -> {
startTargetActivity(false)
}
is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
is Result.HardwareUnavailableOrDisabled -> {
prefs.edit { remove(PreferenceKeys.BIOMETRIC_AUTH) }
startTargetActivity(false)
}
is BiometricAuthenticator.Result.Failure, BiometricAuthenticator.Result.Cancelled -> {
is Result.Failure, Result.Cancelled -> {
finish()
}
is Result.Retry -> {}
}
}
} else {

View file

@ -17,6 +17,7 @@ import de.Maxr1998.modernpreferences.helpers.singleChoice
import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem
import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.util.auth.BiometricAuthenticator
import dev.msfjarvis.aps.util.auth.BiometricAuthenticator.Result
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.settings.PreferenceKeys
@ -73,11 +74,12 @@ class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider
activity.sharedPrefs.edit {
BiometricAuthenticator.authenticate(activity) { result ->
when (result) {
is BiometricAuthenticator.Result.Success -> {
is Result.Success -> {
// Apply the changes
putBoolean(PreferenceKeys.BIOMETRIC_AUTH, checked)
enabled = true
}
is Result.Retry -> {}
else -> {
// If any error occurs, revert back to the previous
// state. This

View file

@ -22,6 +22,7 @@ import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.databinding.ActivitySshKeygenBinding
import dev.msfjarvis.aps.injection.prefs.GitPreferences
import dev.msfjarvis.aps.util.auth.BiometricAuthenticator
import dev.msfjarvis.aps.util.auth.BiometricAuthenticator.Result
import dev.msfjarvis.aps.util.extensions.keyguardManager
import dev.msfjarvis.aps.util.extensions.viewBinding
import dev.msfjarvis.aps.util.git.sshj.SshKey
@ -121,17 +122,17 @@ class SshKeyGenActivity : AppCompatActivity() {
if (requireAuthentication) {
val result =
withContext(Dispatchers.Main) {
suspendCoroutine<BiometricAuthenticator.Result> { cont ->
suspendCoroutine<Result> { cont ->
BiometricAuthenticator.authenticate(
this@SshKeyGenActivity,
R.string.biometric_prompt_title_ssh_keygen
) {
) { result ->
// Do not cancel on failed attempts as these are handled by the authenticator UI.
if (it !is BiometricAuthenticator.Result.Failure) cont.resume(it)
if (result !is Result.Retry) cont.resume(result)
}
}
}
if (result !is BiometricAuthenticator.Result.Success)
if (result !is Result.Success)
throw UserNotAuthenticatedException(getString(R.string.biometric_auth_generic_failure))
}
keyGenType.generateKey(requireAuthentication)

View file

@ -21,10 +21,28 @@ object BiometricAuthenticator {
private const val validAuthenticators =
Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
/**
* Sealed class to wrap [BiometricPrompt]'s [Int]-based return codes into more easily-interpreted
* types.
*/
sealed class Result {
/** Biometric authentication was a success. */
data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
/** Biometric authentication has irreversibly failed. */
data class Failure(val code: Int?, val message: CharSequence) : Result()
/**
* An incorrect biometric was entered, but the prompt UI is offering the option to retry the
* operation.
*/
object Retry : Result()
/** The biometric hardware is unavailable or disabled on a software or hardware level. */
object HardwareUnavailableOrDisabled : Result()
/** The prompt was dismissed. */
object Cancelled : Result()
}
@ -56,18 +74,35 @@ object BiometricAuthenticator {
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
Result.HardwareUnavailableOrDisabled
}
else ->
BiometricPrompt.ERROR_LOCKOUT,
BiometricPrompt.ERROR_LOCKOUT_PERMANENT,
BiometricPrompt.ERROR_NO_SPACE,
BiometricPrompt.ERROR_TIMEOUT,
BiometricPrompt.ERROR_VENDOR -> {
Result.Failure(
errorCode,
activity.getString(R.string.biometric_auth_error_reason, errString)
)
}
BiometricPrompt.ERROR_UNABLE_TO_PROCESS -> {
Result.Retry
}
// We cover all guaranteed values above, but [errorCode] is still an Int at the end of
// the day so a
// catch-all else will always be required.
else -> {
Result.Failure(
errorCode,
activity.getString(R.string.biometric_auth_error_reason, errString)
)
}
}
)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
callback(Result.Failure(null, activity.getString(R.string.biometric_auth_error)))
callback(Result.Retry)
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {

View file

@ -22,6 +22,7 @@ import dev.msfjarvis.aps.data.repo.PasswordRepository
import dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity
import dev.msfjarvis.aps.ui.sshkeygen.SshKeyImportActivity
import dev.msfjarvis.aps.util.auth.BiometricAuthenticator
import dev.msfjarvis.aps.util.auth.BiometricAuthenticator.Result.*
import dev.msfjarvis.aps.util.git.GitCommandExecutor
import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity
import dev.msfjarvis.aps.util.git.sshj.SshAuthMethod
@ -172,17 +173,17 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
BiometricAuthenticator.authenticate(
callingActivity,
R.string.biometric_prompt_title_ssh_auth
) { if (it !is BiometricAuthenticator.Result.Failure) cont.resume(it) }
) { result -> if (result !is Failure) cont.resume(result) }
}
}
when (result) {
is BiometricAuthenticator.Result.Success -> {
is Success -> {
registerAuthProviders(SshAuthMethod.SshKey(authActivity))
}
is BiometricAuthenticator.Result.Cancelled -> {
is Cancelled -> {
return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER))
}
is BiometricAuthenticator.Result.Failure -> {
is Failure -> {
throw IllegalStateException("Biometric authentication failures should be ignored")
}
else -> {