From 95ebfbe95e733456dff4c53ced3cf2e17ae991c1 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 2 Nov 2026 18:01:43 +0100 Subject: fix(ssh): unlock screen-lock protected SSH keys with a fingerprint A Keystore SSH key generated with "Protect with screen lock credential" was bound to AUTH_DEVICE_CREDENTIAL only, while the unlock prompt also accepted biometrics. A fingerprint passed the prompt, then signing the SSH auth request threw UserNotAuthenticatedException, sshj logged "User not authenticated" and the clone failed with "Socket closed". Bind new keys to AUTH_DEVICE_CREDENTIAL or AUTH_BIOMETRIC_STRONG, and allow only strong biometrics in the prompt on Android 11+, since weak ones never unlock Keystore keys (the androidx MasterKey keys already use credential or strong biometric). Android 9 and 10 cannot combine BIOMETRIC_STRONG with DEVICE_CREDENTIAL, so they keep the weak class. Existing protected keys still need the PIN until regenerated. Verified on the emulator (Class 3 fingerprint): a protected ECDSA key unlocked with a fingerprint authenticates and clones over SSH. Co-Authored-By: Claude Opus 5.5 --- .../app/passwordstore/util/auth/BiometricAuthenticator.kt | 15 +++++++++++++-- .../main/java/app/passwordstore/util/git/sshj/SshKey.kt | 5 ++++- 2 files changed, 17 insertions(+), 3 deletions(-) (limited to 'app/src/main/java') diff --git a/app/src/main/java/app/passwordstore/util/auth/BiometricAuthenticator.kt b/app/src/main/java/app/passwordstore/util/auth/BiometricAuthenticator.kt index 9458e8f4..4aad4588 100644 --- a/app/src/main/java/app/passwordstore/util/auth/BiometricAuthenticator.kt +++ b/app/src/main/java/app/passwordstore/util/auth/BiometricAuthenticator.kt @@ -5,6 +5,7 @@ package app.passwordstore.util.auth import android.app.KeyguardManager +import android.os.Build import androidx.annotation.StringRes import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators @@ -18,8 +19,18 @@ import logcat.logcat object BiometricAuthenticator { private const val TAG = "BiometricAuthenticator" - private const val VALID_AUTHENTICATORS = - Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK + + /** + * Keystore keys that require user authentication are unlocked only by the device credential or a + * Class 3 (strong) biometric, so a weak biometric would pass the prompt and then fail to sign or + * decrypt with `User not authenticated`. Android 9 and 10 do not support combining + * [Authenticators.BIOMETRIC_STRONG] with [Authenticators.DEVICE_CREDENTIAL], so they keep the + * weak class. + */ + private val VALID_AUTHENTICATORS = + Authenticators.DEVICE_CREDENTIAL or + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Authenticators.BIOMETRIC_STRONG + else Authenticators.BIOMETRIC_WEAK /** * Sealed class to wrap [BiometricPrompt]'s [Int]-based return codes into more easily-interpreted diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt index a7127ec6..99e600bf 100644 --- a/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt +++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt @@ -282,7 +282,10 @@ object SshKey { if (requireAuthentication) { setUserAuthenticationRequired(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - setUserAuthenticationParameters(30, KeyProperties.AUTH_DEVICE_CREDENTIAL) + setUserAuthenticationParameters( + 30, + KeyProperties.AUTH_DEVICE_CREDENTIAL or KeyProperties.AUTH_BIOMETRIC_STRONG, + ) } else { @Suppress("DEPRECATION") setUserAuthenticationValidityDurationSeconds(30) } -- cgit v1.2.3