John Ketterer
John Ketterer

Reputation: 137

Getting Private Key with biometric authentication in react native, expo, native module, Android app

The plan is to open the app and get a list of approvals that need to be made. Authentication would not be required to open the app but in order to approve or reject, the user would have to provide a signed jwt in the approve or reject api call. The signed jwt is created after the user hits button and is signed with the private key from the keychain. I am able to successfully create the key pair with the setUserAuthenticationRequired method to true on the KeyGenParameterSpec.Builder. (requires auth to use privateKey) The problem is, I keep getting java.lang.IllegalStateException: Must be called from main thread of fragment host.message when the biometrics prompt is used in my native module. Here is a snippet of that module.

class KeepModule() : Module() {
        private val context: Context
            get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
        private val currentActivity: Activity?
            get() = appContext.currentActivity
        private val biometricManager by lazy { BiometricManager.from(context) }
        private val keyguardManager: KeyguardManager
            get() = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager

        private val callback = object: BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    super.onAuthenticationError(errorCode, errString)
                 }

                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                    //  "Authentication failed for an unknown reason")
                }

                override fun onAuthenticationSucceeded(result:BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    //  "Authentication was successful")
                }
            }

        private fun getCancellationSignal(): CancellationSignal {
            var cancellationSignal: CancellationSignal? = null
            cancellationSignal = CancellationSignal()
            cancellationSignal?.setOnCancelListener {
            Log.d("BIOMETRIC", "Auth Cancelled via Signal")
            }

            return cancellationSignal as CancellationSignal
        }


        public val authenticationCallback: BiometricPrompt.AuthenticationCallback 
            get() =
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    super.onAuthenticationError(errorCode, errString)
                }

                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                }

                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                }
            }

        private fun createBiometricPrompt(): BiometricPrompt? {
            val executor = ContextCompat.getMainExecutor(context)
            var biometricPrompt: BiometricPrompt? = null

            biometricPrompt = BiometricPrompt(currentActivity as FragmentActivity, executor, authenticationCallback)

            return biometricPrompt
        }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun definition() = ModuleDefinition {
         Name("Keep")

        Function("getPublicKey") {
            return@Function getPublicKey()
        }
 
        Function("provideSignature") {userId: String, deviceId: String ->
            try {
                return@Function provideSignature(userId, deviceId)  
            } catch (e: Throwable) {   
// This is where it fails 
// java.lang.IllegalStateException: Must be called from main thread of fragment host.message
                return@Function "There was an error trying to authenticate the user. $e.message"
            }
        }
    }
     @UiThread
    private fun provideSignature(userId: String, deviceId: String): String {
        /*
         * Provide a signature for the JWT token private key. In order to access the private key
         *  a biometric prompt is created to validate the credentials of the user, required to 
         * also access the private key
         */

        val promptInfoBuilder = PromptInfo.Builder().apply{
            setTitle("Authenticate")
            setSubtitle("Use your biometric credentials for the approval/denial of the transaction")
            setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
            setNegativeButtonText("Cancel")
        }

        val promptInfo = promptInfoBuilder.build()

        val biometricPrompt = createBiometricPrompt()
        biometricPrompt!!.authenticate(promptInfo)
    
        val KEYSTORE_ALIAS = "cryptography.key"
        val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }

        val claims: MutableMap<String, Any?> = HashMap()
        claims["deviceId"] = deviceId
        claims["userId"] = userId
        val issued = Instant.now()
        val expires = issued.plusMillis(30000)

        val privateKey = keyStore?.getKey(KEYSTORE_ALIAS, null)  as PrivateKey

        return Jwts.builder()
            .setIssuer("...-Android")
            .setSubject(deviceId)
            .setAudience("you")
            .setExpiration(Date.from(expires))
            .setNotBefore(Date.from(issued))
            .setIssuedAt(Date.from(issued))
            .setHeaderParam("typ", "JWT")
            .setHeaderParam("alg", "ES256")
            .setClaims(claims)
            .signWith(privateKey, SignatureAlgorithm.ES256)
            .compact()
    }
}

I am using expo-local-authentication in the frontend but that doesn't seem to share context with the signing, authenticating in the frontend with expo-local-auth does not gain access to the private key. Is there a way to use expo-local-authentication to prompt the message and retrieve the private key? Is there a way to make this execute on the main thread, I have tried using different annotations: @UIThread and @ReactMethod, Queues.main, executors: getMainExecutorand new Single thread.

Upvotes: 0

Views: 204

Answers (0)

Related Questions