Reputation: 137
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