I am trying to set up the BiometricPrompt, but I need an authentication with a CryptoObject, which seems to not be possible when the https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.Builder.html#setDeviceCredentialAllowed(boolean) is set to true.
try {
KeyGeneratorUtil.generateKeyPair("1", null);
} catch (Exception e) {
e.printStackTrace();
}
PrivateKey privateKey;
try {
privateKey = KeyGeneratorUtil.getPrivateKeyReference("test");
} catch (Exception e) {
return;
}
final Signature signature;
try {
signature = initSignature(privateKey);
} catch (Exception e) {
return;
}
final BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(signature);
final BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(context)
.setTitle("Title")
.setDescription("Description")
.setDeviceCredentialAllowed(true)
.build();
...
biometricPrompt.authenticate(cryptoObject, new CancellationSignal(), executor, callback);
When I run this I get the following exception.
2019-07-03 13:50:45.140 13715-13715/kcvetano.com.biometricpromptpoc E/AndroidRuntime: FATAL EXCEPTION: main
Process: kcvetano.com.biometricpromptpoc, PID: 13715
java.lang.IllegalArgumentException: Device credential not supported with crypto
at android.hardware.biometrics.BiometricPrompt.authenticate(BiometricPrompt.java:556)
at kcvetano.com.biometricpromptpoc.BiometryAPI29.handleBiometry(BiometryAPI29.java:65)
at kcvetano.com.biometricpromptpoc.MainActivity$1.onClick(MainActivity.java:56)
at android.view.View.performClick(View.java:7251)
at android.view.View.performClickInternal(View.java:7228)
at android.view.View.access$3500(View.java:802)
at android.view.View$PerformClick.run(View.java:27843)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7116)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:925)
This should do the trick:
biometricPrompt.authenticate(null, new CancellationSignal(), executor, callback);
It's more or less written (one could say hidden) in the error message: when setDeviceCredentialAllowed(true)
is used don't use the crypto object.
It all breaks down to how your private key for the crypto operation inside the CryptoObject
is configured.
I assume your private key used to initialize the signature object is built with setUserAuthenticationRequired(true)
. Keys build with that option are meant to be used for one crypto operation only. Additionally they have to be unlocked using biometrics, using either BiometricPrompt.authenticate
or FingerprintManager.authenticate
(now deprecated in favor of BiometricPrompt).
The official documentation talks about two modes if keys are authorized to be used only if the user has been authenticated, i.e. :
setUserAuthenticationRequired(true)
have to be unlocked using FingerprintManager.authenticate
(now BiometricPrompt.authenticate
)setUserAuthenticationValidityDurationSeconds
have to be unlocked with the KeyguardManager.createConfirmDeviceCredentialIntent
flowA note at the end of the official biometric auth training guide suggests to switch from the KeyguardManager.createConfirmDeviceCredentialIntent
flow to the new BiometricPrompt
with setDeviceCredentialAllowed(true)
.
But it's not as simple as setting the UserAuthenticationValidityDuration of the key to a non zero value as this will trigger an UserNotAuthenticatedException
inside your initSignature(privateKey)
call as soon as initialize the signature object. And there are even more caveats... See the two examples below
Biometric key auth
fun biometric_auth() {
val myKeyStore = KeyStore.getInstance("AndroidKeyStore")
myKeyStore.load(null)
val keyGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
"AndroidKeyStore"
)
// build MY_BIOMETRIC_KEY
val keyAlias = "MY_BIOMETRIC_KEY"
val keyProperties = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
val builder = KeyGenParameterSpec.Builder(keyAlias, keyProperties)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setUserAuthenticationRequired(true)
keyGenerator.run {
initialize(builder.build())
generateKeyPair()
}
val biometricKeyEntry: KeyStore.Entry = myKeyStore.getEntry(keyAlias, null)
if (biometricKeyEntry !is KeyStore.PrivateKeyEntry) {
return
}
// create signature object
val signature = Signature.getInstance("SHA256withECDSA")
// init signature else "IllegalStateException: Crypto primitive not initialized" is thrown
signature.initSign(biometricKeyEntry.privateKey)
val cryptoObject = BiometricPrompt.CryptoObject(signature)
// create biometric prompt
// NOTE: using androidx.biometric.BiometricPrompt here
val prompt = BiometricPrompt(
this,
AsyncTask.THREAD_POOL_EXECUTOR,
object : BiometricPrompt.AuthenticationCallback() {
// override the required methods...
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.w(TAG, "onAuthenticationError $errorCode $errString")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "onAuthenticationSucceeded" + result.cryptoObject)
val sigBytes = signature.run {
update("hello world".toByteArray())
sign()
}
Log.d(TAG, "sigStr " + Base64.encodeToString(sigBytes, 0))
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.w(TAG, "onAuthenticationFailed")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock your device")
.setSubtitle("Please authenticate to ...")
// negative button option required for biometric auth
.setNegativeButtonText("Cancel")
.build()
prompt.authenticate(promptInfo, cryptoObject)
}
PIN/Password/Pattern auth
fun password_auth() {
val myKeyStore = KeyStore.getInstance("AndroidKeyStore")
myKeyStore.load(null)
val keyGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
"AndroidKeyStore"
)
// build MY_PIN_PASSWORD_PATTERN_KEY
val keyAlias = "MY_PIN_PASSWORD_PATTERN_KEY"
val keyProperties = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
val builder = KeyGenParameterSpec.Builder(keyAlias, keyProperties)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
// this would trigger an UserNotAuthenticatedException: User not authenticated when using the fingerprint
// .setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(10)
keyGenerator.run {
initialize(builder.build())
generateKeyPair()
}
val keyEntry: KeyStore.Entry = myKeyStore.getEntry(keyAlias, null)
if (keyEntry !is KeyStore.PrivateKeyEntry) {
return
}
// create signature object
val signature = Signature.getInstance("SHA256withECDSA")
// this would fail with UserNotAuthenticatedException: User not authenticated
// signature.initSign(keyEntry.privateKey)
// create biometric prompt
// NOTE: using androidx.biometric.BiometricPrompt here
val prompt = BiometricPrompt(
this,
AsyncTask.THREAD_POOL_EXECUTOR,
object : BiometricPrompt.AuthenticationCallback() {
// override the required methods...
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.w(TAG, "onAuthenticationError $errorCode $errString")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "onAuthenticationSucceeded " + result.cryptoObject)
// now it's safe to init the signature using the password key
signature.initSign(keyEntry.privateKey)
val sigBytes = signature.run {
update("hello password/pin/pattern".toByteArray())
sign()
}
Log.d(TAG, "sigStr " + Base64.encodeToString(sigBytes, 0))
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.w(TAG, "onAuthenticationFailed")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock your device")
.setDeviceCredentialAllowed(true)
.build()
prompt.authenticate(promptInfo)
}