Search code examples
androidandroid-biometric-prompt

java.lang.IllegalArgumentException: Device credential not supported with crypto


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)

Solution

  • 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. :

    • Keys built with setUserAuthenticationRequired(true) have to be unlocked using FingerprintManager.authenticate (now BiometricPrompt.authenticate)
    • Keys built with setUserAuthenticationValidityDurationSeconds have to be unlocked with the KeyguardManager.createConfirmDeviceCredentialIntent flow

    A 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)
    }