Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix allowDeviceAuthentication failing on Android 31 and later #225

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
package com.rnbiometrics;

import static com.rnbiometrics.ReactNativeBiometrics.initializeSignature;

import android.util.Base64;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.biometric.BiometricPrompt;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

public class CreateSignatureCallback extends BiometricPrompt.AuthenticationCallback {
private Promise promise;
private String payload;
private boolean allowDeviceCredentials;

public CreateSignatureCallback(Promise promise, String payload) {
public CreateSignatureCallback(Promise promise, String payload, boolean allowDeviceCredentials) {
super();
this.promise = promise;
this.payload = payload;
this.allowDeviceCredentials = allowDeviceCredentials;
}

@Override
Expand All @@ -39,8 +52,7 @@ public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationRes
super.onAuthenticationSucceeded(result);

try {
BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject();
Signature cryptoSignature = cryptoObject.getSignature();
Signature cryptoSignature = getSignature(result);
cryptoSignature.update(this.payload.getBytes());
byte[] signed = cryptoSignature.sign();
String signedString = Base64.encodeToString(signed, Base64.DEFAULT);
Expand All @@ -54,4 +66,14 @@ public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationRes
promise.reject("Error creating signature: " + e.getMessage(), "Error creating signature");
}
}

@Nullable
private Signature getSignature(@NonNull BiometricPrompt.AuthenticationResult result) throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException, InvalidKeyException {
if (this.allowDeviceCredentials) {
return initializeSignature();
}

BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject();
return cryptoObject.getSignature();
}
}
78 changes: 57 additions & 21 deletions android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.security.keystore.KeyProperties;
import android.util.Base64;

import androidx.annotation.NonNull;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.biometric.BiometricPrompt.AuthenticationCallback;
Expand All @@ -20,12 +21,19 @@
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.spec.RSAKeyGenParameterSpec;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
Expand All @@ -36,7 +44,8 @@

public class ReactNativeBiometrics extends ReactContextBaseJavaModule {

protected String biometricKeyAlias = "biometric_key";
public static final String ALLOW_DEVICE_CREDENTIALS = "allowDeviceCredentials";
private static final String biometricKeyAlias = "biometric_key";

public ReactNativeBiometrics(ReactApplicationContext reactContext) {
super(reactContext);
Expand All @@ -51,7 +60,7 @@ public String getName() {
public void isSensorAvailable(final ReadableMap params, final Promise promise) {
try {
if (isCurrentSDKMarshmallowOrLater()) {
boolean allowDeviceCredentials = params.getBoolean("allowDeviceCredentials");
boolean allowDeviceCredentials = params.getBoolean(ALLOW_DEVICE_CREDENTIALS);
ReactApplicationContext reactApplicationContext = getReactApplicationContext();
BiometricManager biometricManager = BiometricManager.from(reactApplicationContext);
int canAuthenticate = biometricManager.canAuthenticate(getAllowedAuthenticators(allowDeviceCredentials));
Expand Down Expand Up @@ -96,13 +105,14 @@ public void createKeys(final ReadableMap params, Promise promise) {
if (isCurrentSDKMarshmallowOrLater()) {
deleteBiometricKey();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(biometricKeyAlias, KeyProperties.PURPOSE_SIGN)
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(biometricKeyAlias, KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
.setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
.setUserAuthenticationRequired(true)
.build();
keyPairGenerator.initialize(keyGenParameterSpec);
.setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4));

setAuthenticationParameters(params, builder);

keyPairGenerator.initialize(builder.build());

KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
Expand All @@ -121,6 +131,26 @@ public void createKeys(final ReadableMap params, Promise promise) {
}
}

private void setAuthenticationParameters(ReadableMap params, KeyGenParameterSpec.Builder builder) {
boolean allowDeviceCredentials = params.getBoolean(ALLOW_DEVICE_CREDENTIALS);

if (isCurrentSDKMarshmallowOrLater()) {
builder.setUserAuthenticationRequired(true);
}

if (allowDeviceCredentials == false) return;

if (isCurrentSDK11OrLater()) {
builder.setUserAuthenticationParameters(5, getAllowedAuthenticators(allowDeviceCredentials));
} else if (isCurrentSDKMarshmallowOrLater()) {
builder.setUserAuthenticationValidityDurationSeconds(5);
}
}

private boolean isCurrentSDK11OrLater() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
}

private boolean isCurrentSDKMarshmallowOrLater() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
Expand Down Expand Up @@ -155,23 +185,19 @@ public void run() {
String promptMessage = params.getString("promptMessage");
String payload = params.getString("payload");
String cancelButtonText = params.getString("cancelButtonText");
boolean allowDeviceCredentials = params.getBoolean("allowDeviceCredentials");

Signature signature = Signature.getInstance("SHA256withRSA");
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);

PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null);
signature.initSign(privateKey);
boolean allowDeviceCredentials = params.getBoolean(ALLOW_DEVICE_CREDENTIALS);

BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(signature);
AuthenticationCallback authCallback = new CreateSignatureCallback(promise, payload, allowDeviceCredentials);

AuthenticationCallback authCallback = new CreateSignatureCallback(promise, payload);
FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity();
Executor executor = Executors.newSingleThreadExecutor();
BiometricPrompt biometricPrompt = new BiometricPrompt(fragmentActivity, executor, authCallback);
BiometricPrompt biometricPrompt = new BiometricPrompt((FragmentActivity) getCurrentActivity(), Executors.newSingleThreadExecutor(), authCallback);
BiometricPrompt.PromptInfo info = getPromptInfo(promptMessage, cancelButtonText, allowDeviceCredentials);

biometricPrompt.authenticate(getPromptInfo(promptMessage, cancelButtonText, allowDeviceCredentials), cryptoObject);
if (allowDeviceCredentials) {
biometricPrompt.authenticate(info);
} else {
Signature signature = initializeSignature();
biometricPrompt.authenticate(info, new BiometricPrompt.CryptoObject(signature));
}
} catch (Exception e) {
promise.reject("Error signing payload: " + e.getMessage(), "Error generating signature: " + e.getMessage());
}
Expand All @@ -182,6 +208,16 @@ public void run() {
}
}

@NonNull
protected static Signature initializeSignature() throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException, InvalidKeyException {
Signature signature = Signature.getInstance("SHA256withRSA");
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null);
signature.initSign(privateKey);
return signature;
}

private PromptInfo getPromptInfo(String promptMessage, String cancelButtonText, boolean allowDeviceCredentials) {
PromptInfo.Builder builder = new PromptInfo.Builder().setTitle(promptMessage);

Expand Down