From e42bf0285bc0a01eb4aec2b44244968a1d4a633a Mon Sep 17 00:00:00 2001 From: Jordan Haven Date: Tue, 7 Apr 2026 11:35:32 -0400 Subject: [PATCH 1/2] Introduce new UserAuthenticationRequired biometric status to handle UserNotAuthenticatedException --- .../biometrics/BiometricAvailability.kt | 33 +++++++++++++++---- .../sdk/consumer/biometrics/BiometricsImpl.kt | 20 +++++++++-- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/source/sdk/src/main/java/com/stytch/sdk/consumer/biometrics/BiometricAvailability.kt b/source/sdk/src/main/java/com/stytch/sdk/consumer/biometrics/BiometricAvailability.kt index 86e5e1f49..8dcf8bf72 100644 --- a/source/sdk/src/main/java/com/stytch/sdk/consumer/biometrics/BiometricAvailability.kt +++ b/source/sdk/src/main/java/com/stytch/sdk/consumer/biometrics/BiometricAvailability.kt @@ -67,20 +67,33 @@ public sealed class BiometricAvailability { fun fromReason(reason: Int) = Unavailable( when (reason) { - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { Reason.BIOMETRIC_ERROR_NO_HARDWARE - BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> + } + + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { Reason.BIOMETRIC_ERROR_HW_UNAVAILABLE - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> + } + + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { Reason.BIOMETRIC_ERROR_NONE_ENROLLED - BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> + } + + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { Reason.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED - BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> + } + + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { Reason.BIOMETRIC_ERROR_UNSUPPORTED - BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> + } + + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { Reason.BIOMETRIC_STATUS_UNKNOWN - else -> + } + + else -> { Reason.BIOMETRIC_STATUS_UNKNOWN + } }, ) } @@ -93,6 +106,12 @@ public sealed class BiometricAvailability { @JacocoExcludeGenerated public data object RegistrationRevoked : BiometricAvailability() + /** + * Indicates that a cryptographic operation could not be performed because the user has not been + * authenticated recently enough. Authenticating the user will resolve this issue. + */ + @JacocoExcludeGenerated public data object UserAuthenticationRequired : BiometricAvailability() + /** * Status indicating that biometrics are available, but no registrations have been made yet */ diff --git a/source/sdk/src/main/java/com/stytch/sdk/consumer/biometrics/BiometricsImpl.kt b/source/sdk/src/main/java/com/stytch/sdk/consumer/biometrics/BiometricsImpl.kt index a22b0c24e..fa1c1bf62 100644 --- a/source/sdk/src/main/java/com/stytch/sdk/consumer/biometrics/BiometricsImpl.kt +++ b/source/sdk/src/main/java/com/stytch/sdk/consumer/biometrics/BiometricsImpl.kt @@ -2,6 +2,7 @@ package com.stytch.sdk.consumer.biometrics import android.os.Build import android.security.keystore.KeyPermanentlyInvalidatedException +import android.security.keystore.UserNotAuthenticatedException import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL @@ -31,6 +32,7 @@ import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException import java.util.concurrent.CompletableFuture internal const val LAST_USED_BIOMETRIC_REGISTRATION_ID = "last_used_biometric_registration_id" @@ -78,7 +80,13 @@ internal class BiometricsImpl internal constructor( var errorEncounteredWhenGeneratingKey = false try { biometricsProvider.ensureSecretKeyIsAvailable(allowedAuthenticators) - } catch (_: KeyPermanentlyInvalidatedException) { + } catch (_: UserNotAuthenticatedException) { + // Indicates that a cryptographic operation could not be performed because the user has not been + // authenticated recently enough. Authenticating the user will resolve this issue. + return BiometricAvailability.UserAuthenticationRequired + } catch (_: InvalidKeyException) { + // This covers all other instances of invalid keys, where a registration WAS available + // but the key is not valid, so we should revoke the registration and have them re-register externalScope.launch(dispatchers.io) { removeRegistration() } @@ -104,7 +112,10 @@ internal class BiometricsImpl internal constructor( false -> BiometricAvailability.AvailableNoRegistrations } } - else -> BiometricAvailability.Unavailable.fromReason(result) + + else -> { + BiometricAvailability.Unavailable.fromReason(result) + } } } @@ -121,7 +132,10 @@ internal class BiometricsImpl internal constructor( removeLocalRegistrationOnly() true } - else -> false + + else -> { + false + } } } From 81f52838b9f555768af2789a08a95c9286247ea9 Mon Sep 17 00:00:00 2001 From: Jordan Haven Date: Tue, 7 Apr 2026 11:36:04 -0400 Subject: [PATCH 2/2] Bump version --- source/sdk/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/sdk/build.gradle.kts b/source/sdk/build.gradle.kts index 824c62d4d..cfd1481ea 100644 --- a/source/sdk/build.gradle.kts +++ b/source/sdk/build.gradle.kts @@ -16,7 +16,7 @@ plugins { } val publishGroupId = "com.stytch.sdk" -val publishVersion = "0.64.0" +val publishVersion = "0.65.0" val publishArtifactId = "sdk" android {