For passwordless authentication, the app creates a public-private key pair. If the phone has a Strongbox the keypair is stored within the Strongbox. During the login process, a message is signed with the private key. For user-friendly reasons the key is not being secured with user authentication, only an unlocked device is required.
Some Android users, mostly Samsung Galaxy phones, are sometimes facing an UserNotAuthenticatedException when the app is trying to sign a message with the private key. Most of the time the exception is thrown when the user opens the app right after unlocking the lock screen. If the user waits for a minute or two everything is going well.
This is weird because the private key doesn't require user authentication. I have confirmed that user authentication is not required, also not by the Secure Hardware, by logging the KeyInfo. Here are the logs:
isInsideSecureHardware: true isUserAuthenticationRequired: false isUserAuthenticationRequirementEnforcedBySecureHardware: false isInvalidatedByBiometricEnrollment: true isUnlockedDeviceRequired: true
I have tried adding a high timeout to the user authentication by setUserAuthenticationParameters, but it hasn't been successful.
Here is the code for creating a key.
private const val keyStoreName: String = "AndroidKeyStore"
private const val algorithm: String = "SHA256withECDSA"
fun hasStrongBox(context: Context) : Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) else
false
}
fun isDeviceSecure(context: Context) : Boolean {
val keyguardManager: KeyguardManager? = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
return keyguardManager?.isDeviceSecure ?: false
}
fun generateKeyPair(context: Context, tag: String) : String {
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
keyStoreName
)
val isDeviceSecure = isDeviceSecure(context)
val publicKey: PublicKey = try {
initializeKeyPair(kpg, tag, hasStrongBox(context), isDeviceSecure);
} catch (e: Exception) {
// if strongbox isn't available the key will be generated without strongbox
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && e is StrongBoxUnavailableException)
initializeKeyPair(kpg, tag, false, isDeviceSecure)
else
throw e
}
return Base64.encodeToString(publicKey.encoded, Base64.NO_WRAP)
}
private fun initializeKeyPair(kpg: KeyPairGenerator, tag: String, useStrongBox: Boolean, isDeviceSecure: Boolean) : PublicKey {
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
tag,
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
).run {
setDigests(KeyProperties.DIGEST_SHA256)
setKeySize(256)
setUserAuthenticationRequired(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setUnlockedDeviceRequired(isDeviceSecure)
setIsStrongBoxBacked(useStrongBox)
}
build()
}
kpg.initialize(parameterSpec)
return kpg.generateKeyPair().public
}
Here is the code for signing a message with the Keypair:
fun signMessage(tag: String, message: String) : String {
val keyPair: KeyPair = loadKeyPair(tag)
val messageData: ByteArray = message.encodeToByteArray()
val signature: ByteArray = Signature.getInstance(algorithm).run {
initSign(keyPair.private)
update(messageData)
sign()
}
return Base64.encodeToString(signature, Base64.NO_WRAP)
}
private fun loadKeyPair(tag: String) : KeyPair {
val ks: KeyStore = KeyStore.getInstance(keyStoreName).apply {
load(null)
}
val entry: KeyStore.Entry = ks.getEntry(tag, null) ?: throw KeyDoesNotExistsException("Key doesn't exists")
if (entry !is KeyStore.PrivateKeyEntry) {
throw KeyNotPrivateKeyException("Key isn't private key")
}
return KeyPair(entry.certificate.publicKey, entry.privateKey)
}
Here is the error log:
android.security.keystore.UserNotAuthenticatedException: User not authenticated at android.security.keystore2.KeyStoreCryptoOperationUtils.getInvalidKeyException(KeyStoreCryptoOperationUtils.java:128) at android.security.keystore2.AndroidKeyStoreSignatureSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreSignatureSpiBase.java:217) at android.security.keystore2.AndroidKeyStoreSignatureSpiBase.engineInitSign(AndroidKeyStoreSignatureSpiBase.java:123) at android.security.keystore2.AndroidKeyStoreSignatureSpiBase.engineInitSign(AndroidKeyStoreSignatureSpiBase.java:101) at java.security.Signature$Delegate.init(Signature.java:1357) at java.security.Signature$Delegate.chooseProvider(Signature.java:1310) at java.security.Signature$Delegate.engineInitSign(Signature.java:1385) at java.security.Signature.initSign(Signature.java:679) at com.app.passwordless.KeyKt.signMessage(Key.kt:98)