3

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)
K.Cvetanovska
  • 73
  • 1
  • 7

2 Answers2

4

The above answer is not very accurate regarding both - the solution and the explaination .

To use both biometric authentication and device credentials along with the crypto object follow the below steps:

  1. Create a Secret Key with setUserAuthenticationRequired(true) and setUserAuthenticationValidityDurationSeconds(x).
private SecretKey createSecretKey(String keyName ){
  KeyGenParameterSpec.Builder paramsBuilder = new KeyGenParameterSpec.Builder(keyName,
                    KeyProperties.PURPOSE_SIGN);
            paramsBuilder.setUserAuthenticationRequired(true);
            paramsBuilder.setUserAuthenticationValidityDurationSeconds(5);
            KeyGenParameterSpec keyGenParams = paramsBuilder.build();
            KeyGenerator keyGenerator = null;
           keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA256,
                        ANDROID_KEYSTORE);

            keyGenerator.init(keyGenParams);
            return keyGenerator.generateKey();
       }// All exceptions unhandled

  1. Initialize a crypto object
Mac mac=Mac.getInstance("HmacSHA256");
SecretKey secretKey = getOrCreateSecretKey(keyName);
mac.init(secretKey);

3.Use Biometric Authentication with setDeviceCredentialAllowed(true). Do not pass crypto object parameter in authenticate method - like this - biometricPrompt.authenticate(promptInfo)

Under onAuthenticationSucceeded

public void onAuthenticationSucceeded(
                    @NonNull BiometricPrompt.AuthenticationResult result) {
                super.onAuthenticationSucceeded(result);
                byte[] bytes = "secret-text".getBytes();
                byte[] macResult = mac.doFinal(bytes);
                Log.d("hashed data--",bytesToHex(macResult));
            }

The mac object will only work if the device was unlocked not more than x seconds ago. (setUserAuthenticationValidityDurationSeconds(x)).

You can try using the mac object outside the onAuthSucceeded method after x seconds of you unlocking the device to verify . Please note that even unlocking the phone will make the mac object usable for x seconds. It does not have to be unlocked inside your application.

More on this here : https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05f-testing-local-authentication

vaishak bg
  • 83
  • 1
  • 7
1

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)
}
simne7
  • 194
  • 1
  • 1
  • 12
  • You can find a little demo here: https://github.com/simne7/BiometricAuthApp – simne7 Oct 16 '19 at 14:45
  • 1
    This answer should not be accepted as valid, if you don't do `.setUserAuthenticationRequired(true)` in conjunction with `.setUserAuthenticationValidityDurationSeconds(10)` it doesn't actually do anything. All you've done is create a key that doesn't require authentication, you can verify this by not unlock the keyguard and just using the key in your PIN example. – martin treurnicht Nov 14 '19 at 00:29