Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 7 additions & 5 deletions app/src/main/java/dev/msfjarvis/aps/ui/main/LaunchActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 -> {
Expand Down