From 1575c739a740263d8dea27360e4ffd44ddb572e0 Mon Sep 17 00:00:00 2001 From: Tyler Cook Date: Tue, 18 Oct 2022 12:11:17 -0500 Subject: [PATCH 1/3] Refactored creation of signature to be able to handle non-biometric authentication. There is an issue in newer versions of Android that require different configuration of the biometrics library for dual auth flows. --- .../rnbiometrics/CreateSignatureCallback.java | 33 ++++++++++- .../rnbiometrics/ReactNativeBiometrics.java | 58 +++++++++++-------- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/android/src/main/java/com/rnbiometrics/CreateSignatureCallback.java b/android/src/main/java/com/rnbiometrics/CreateSignatureCallback.java index 7dc9ad9..e4a327a 100644 --- a/android/src/main/java/com/rnbiometrics/CreateSignatureCallback.java +++ b/android/src/main/java/com/rnbiometrics/CreateSignatureCallback.java @@ -1,24 +1,37 @@ package com.rnbiometrics; +import static com.rnbiometrics.ReactNativeBiometrics.biometricKeyAlias; + 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 @@ -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); @@ -54,4 +66,19 @@ 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) { + 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; + } + + BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject(); + return cryptoObject.getSignature(); + } } diff --git a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java index 624ecd9..0d4e95e 100644 --- a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java +++ b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java @@ -20,6 +20,7 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; +import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; @@ -36,7 +37,8 @@ public class ReactNativeBiometrics extends ReactContextBaseJavaModule { - protected String biometricKeyAlias = "biometric_key"; + public static final String ALLOW_DEVICE_CREDENTIALS = "allowDeviceCredentials"; + protected static final String biometricKeyAlias = "biometric_key"; public ReactNativeBiometrics(ReactApplicationContext reactContext) { super(reactContext); @@ -51,7 +53,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)); @@ -96,13 +98,19 @@ 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); + .setUserAuthenticationRequired(true); + + if (isCurrentSDK11OrLater()) { + builder.setUserAuthenticationParameters(5, getAllowedAuthenticators(params.getBoolean(ALLOW_DEVICE_CREDENTIALS))); + } else { + builder.setUserAuthenticationValidityDurationSeconds(5); + } + + keyPairGenerator.initialize(builder.build()); KeyPair keyPair = keyPairGenerator.generateKeyPair(); PublicKey publicKey = keyPair.getPublic(); @@ -121,6 +129,10 @@ public void createKeys(final ReadableMap params, Promise promise) { } } + private boolean isCurrentSDK11OrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; + } + private boolean isCurrentSDKMarshmallowOrLater() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; } @@ -155,23 +167,23 @@ 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); - - BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(signature); - - AuthenticationCallback authCallback = new CreateSignatureCallback(promise, payload); - FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity(); - Executor executor = Executors.newSingleThreadExecutor(); - BiometricPrompt biometricPrompt = new BiometricPrompt(fragmentActivity, executor, authCallback); - - biometricPrompt.authenticate(getPromptInfo(promptMessage, cancelButtonText, allowDeviceCredentials), cryptoObject); + boolean allowDeviceCredentials = params.getBoolean(ALLOW_DEVICE_CREDENTIALS); + + AuthenticationCallback authCallback = new CreateSignatureCallback(promise, payload, allowDeviceCredentials); + + BiometricPrompt biometricPrompt = new BiometricPrompt((FragmentActivity) getCurrentActivity(), Executors.newSingleThreadExecutor(), authCallback); + BiometricPrompt.PromptInfo info = getPromptInfo(promptMessage, cancelButtonText, allowDeviceCredentials); + + if (allowDeviceCredentials) { + biometricPrompt.authenticate(info); + } else { + Signature signature = Signature.getInstance("SHA256withRSA"); + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null); + signature.initSign(privateKey); + biometricPrompt.authenticate(info, new BiometricPrompt.CryptoObject(signature)); + } } catch (Exception e) { promise.reject("Error signing payload: " + e.getMessage(), "Error generating signature: " + e.getMessage()); } From 8394dd6498b5e96e8e5ff4a6c1a36081b061a57c Mon Sep 17 00:00:00 2001 From: Tyler Cook Date: Fri, 21 Oct 2022 12:22:49 -0500 Subject: [PATCH 2/3] Removed some duplication surrounding signature initialization and added a few other refactors to clean things up. --- .../rnbiometrics/CreateSignatureCallback.java | 9 +--- .../rnbiometrics/ReactNativeBiometrics.java | 44 +++++++++++++++---- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/android/src/main/java/com/rnbiometrics/CreateSignatureCallback.java b/android/src/main/java/com/rnbiometrics/CreateSignatureCallback.java index e4a327a..43d194b 100644 --- a/android/src/main/java/com/rnbiometrics/CreateSignatureCallback.java +++ b/android/src/main/java/com/rnbiometrics/CreateSignatureCallback.java @@ -1,6 +1,6 @@ package com.rnbiometrics; -import static com.rnbiometrics.ReactNativeBiometrics.biometricKeyAlias; +import static com.rnbiometrics.ReactNativeBiometrics.initializeSignature; import android.util.Base64; @@ -70,12 +70,7 @@ public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationRes @Nullable private Signature getSignature(@NonNull BiometricPrompt.AuthenticationResult result) throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException, InvalidKeyException { if (this.allowDeviceCredentials) { - 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; + return initializeSignature(); } BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject(); diff --git a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java index 0d4e95e..9014a99 100644 --- a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java +++ b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java @@ -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; @@ -20,13 +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; @@ -38,7 +45,7 @@ public class ReactNativeBiometrics extends ReactContextBaseJavaModule { public static final String ALLOW_DEVICE_CREDENTIALS = "allowDeviceCredentials"; - protected static final String biometricKeyAlias = "biometric_key"; + private static final String biometricKeyAlias = "biometric_key"; public ReactNativeBiometrics(ReactApplicationContext reactContext) { super(reactContext); @@ -101,14 +108,9 @@ public void createKeys(final ReadableMap params, Promise promise) { 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); + .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)); - if (isCurrentSDK11OrLater()) { - builder.setUserAuthenticationParameters(5, getAllowedAuthenticators(params.getBoolean(ALLOW_DEVICE_CREDENTIALS))); - } else { - builder.setUserAuthenticationValidityDurationSeconds(5); - } + setAuthenticationParameters(params, builder); keyPairGenerator.initialize(builder.build()); @@ -129,6 +131,22 @@ 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; } @@ -194,6 +212,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); From d90c6cda289c1734589f2eba62bb2ff7fd058cd5 Mon Sep 17 00:00:00 2001 From: Tyler Cook Date: Fri, 21 Oct 2022 12:42:34 -0500 Subject: [PATCH 3/3] Removed more duplication of signature creation from the private key. --- .../main/java/com/rnbiometrics/ReactNativeBiometrics.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java index 9014a99..5ce9ed0 100644 --- a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java +++ b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java @@ -195,11 +195,7 @@ public void run() { if (allowDeviceCredentials) { biometricPrompt.authenticate(info); } else { - Signature signature = Signature.getInstance("SHA256withRSA"); - KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); - keyStore.load(null); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null); - signature.initSign(privateKey); + Signature signature = initializeSignature(); biometricPrompt.authenticate(info, new BiometricPrompt.CryptoObject(signature)); } } catch (Exception e) {