how to run biometric prompt on UI thread and merge back to original thread?

Viewed 38

I'm trying to use biometric authentication. However my setup is complex, basically I'm trying to keep the function sync because it is invoked from C++:

User interaction -> C++ function -> Java JNI function -> Biometric Authenticate <- needs to go back

Skipping the C++ code, it calls via JNI the following function:

public String getSeed() throws ExecutionException, InterruptedException {
    Context reactContext = this.getReactApplicationContext();
    Activity act = this.getCurrentActivity();

    act.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        Executor executor = ContextCompat.getMainExecutor(reactContext);
        BiometricPrompt.AuthenticationCallback authenticationCallback = new WalletCoreAuthenticationCallback();
        BiometricPrompt.PromptInfo info = BiometricUtils.createBiometricPromptInfo("ROPO", "ROPO", "ROPO");
        BiometricPrompt prompt = new BiometricPrompt((FragmentActivity) act, executor, authenticationCallback);
        prompt.authenticate(info);
      }
    });

    // Here I need a Handler.merge or something similar to pause the execution while the user authenticates and then I retrieve the answer.

    try {
        return keyStore.getPlainText(getReactApplicationContext(), SEED_KEY);
    } catch (FileNotFoundException fnfe) {
        Log.w(Constants.TAG, "Could not get seed (file not found)");
        return null;
    } catch (Exception e) {
        Log.w(Constants.TAG, "Could not get seed");
        return null;
    }
  }

The idea is: if the user fails to authenticate I do not fetch the sensitive information (keyStore.getPlainText).

The problem however lies in the fact that the BiometricPrompt needs to be called from the main (UI) thread. I'm an Android noob, so far that was the best I could come up with, it in effect prompts the user for authentication, but I do not know how to pause/join the main java function call, until the user has authenticated.

Is this even possible?

1 Answers

Found one way to make it work that is simple enough, using a Mutex.

Each call to the parent Java creates a mutex (I also added one field to the WalletCoreAuthenticationCallback object to hold the response). Then I release the mutex on inside the calls, and just check for the stored response.

    final Semaphore mutex = new Semaphore(0);
    // This object now internally saves the response of the authentication callback
    WalletCoreAuthenticationCallback authenticationCallback = new WalletCoreAuthenticationCallback(mutex);

    act.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        Executor executor = ContextCompat.getMainExecutor(reactContext);
        BiometricPrompt.PromptInfo info = BiometricUtils.createBiometricPromptInfo("ROPO", "ROPO", "ROPO");
        BiometricPrompt prompt = new BiometricPrompt((FragmentActivity) act, executor, authenticationCallback);
        prompt.authenticate(info);
      }
    });

    try {
      mutex.acquire();
    } catch (InterruptedException e) {
      Log.e(Constants.TAG, "INterruped mutex exception");
    }

    if(!authenticationCallback.isAuthenticated) {
      return null;
    }

This however has one side-effect, basically locking the calling thread, I'm calling this from a React Native application, which basically means it freezes the app. However since Auth is such a vital step, it's ok to freeze the app since the user cannot continue without authentication anyways.

If anybody has a more elegant solution, happy to check it out.

Related