Skip to content

Commit 9b608cf

Browse files
feat(android): biometric api implementation
1 parent 73e39d2 commit 9b608cf

6 files changed

Lines changed: 94 additions & 463 deletions

File tree

android/build.gradle

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
apply plugin: 'com.android.library'
22

3-
def DEFAULT_COMPILE_SDK_VERSION = 27
4-
def DEFAULT_BUILD_TOOLS_VERSION = "28.0.2"
5-
def DEFAULT_TARGET_SDK_VERSION = 25
3+
def DEFAULT_COMPILE_SDK_VERSION = 29
4+
def DEFAULT_BUILD_TOOLS_VERSION = "29.0.2"
5+
def DEFAULT_TARGET_SDK_VERSION = 29
66
def DEFAULT_MIN_SDK_VERSION = 16
77

88
android {
@@ -24,5 +24,6 @@ android {
2424
}
2525

2626
dependencies {
27+
implementation 'androidx.biometric:biometric:1.0.1'
2728
implementation 'com.facebook.react:react-native:+'
2829
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
<manifest package="br.com.classapp.RNSensitiveInfo" xmlns:android="http://schemas.android.com/apk/res/android">
2+
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
23
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
34
</manifest>

android/src/main/java/br/com/classapp/RNSensitiveInfo/RNSensitiveInfoModule.java

Lines changed: 88 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,49 @@
11
package br.com.classapp.RNSensitiveInfo;
22

33
import android.app.Activity;
4-
import android.app.Fragment;
5-
import android.app.FragmentTransaction;
64
import android.content.Context;
75
import android.content.SharedPreferences;
86
import android.hardware.fingerprint.FingerprintManager;
97
import android.os.Build;
108
import android.os.CancellationSignal;
119
import android.security.keystore.KeyGenParameterSpec;
1210
import android.security.keystore.KeyInfo;
11+
1312
import java.security.InvalidKeyException;
13+
1414
import android.security.keystore.KeyProperties;
1515
import android.util.Base64;
1616
import android.util.Log;
1717

1818
import androidx.annotation.NonNull;
19+
import androidx.biometric.BiometricConstants;
20+
import androidx.biometric.BiometricManager;
21+
import androidx.biometric.BiometricPrompt;
1922

2023
import com.facebook.react.bridge.Promise;
2124
import com.facebook.react.bridge.ReactApplicationContext;
2225
import com.facebook.react.bridge.ReactContextBaseJavaModule;
2326
import com.facebook.react.bridge.ReactMethod;
2427
import com.facebook.react.bridge.ReadableMap;
25-
import com.facebook.react.bridge.WritableArray;
2628
import com.facebook.react.bridge.WritableMap;
27-
import com.facebook.react.bridge.WritableNativeArray;
2829
import com.facebook.react.bridge.WritableNativeMap;
30+
import com.facebook.react.bridge.UiThreadUtil;
2931
import com.facebook.react.modules.core.DeviceEventManagerModule;
3032

3133
import java.security.KeyStore;
3234
import java.util.HashMap;
3335
import java.util.Map;
36+
import java.util.concurrent.Executor;
37+
import java.util.concurrent.Executors;
3438

3539
import javax.crypto.Cipher;
3640
import javax.crypto.KeyGenerator;
3741
import javax.crypto.SecretKey;
3842
import javax.crypto.SecretKeyFactory;
3943
import javax.crypto.spec.IvParameterSpec;
4044

45+
import androidx.fragment.app.FragmentActivity;
4146
import br.com.classapp.RNSensitiveInfo.utils.AppConstants;
42-
import br.com.classapp.RNSensitiveInfo.view.Fragments.FingerprintAuthenticationDialogFragment;
43-
import br.com.classapp.RNSensitiveInfo.view.Fragments.FingerprintUiHelper;
4447

4548
public class RNSensitiveInfoModule extends ReactContextBaseJavaModule {
4649

@@ -81,27 +84,24 @@ public String getName() {
8184
}
8285

8386
/**
84-
* Checks whether the device supports fingerprint authentication and if the user has
85-
* enrolled at least one fingerprint.
87+
* Checks whether the device supports Biometric authentication and if the user has
88+
* enrolled at least one credential.
8689
*
87-
* @return true if the user has a fingerprint capable device and has enrolled
88-
* one or more fingerprints
90+
* @return true if the user has a biometric capable device and has enrolled
91+
* one or more credentials
8992
*/
90-
private boolean hasSetupFingerprint() {
93+
private boolean hasSetupBiometricCredential() {
9194
try {
92-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mFingerprintManager != null) {
93-
if (!mFingerprintManager.isHardwareDetected()) {
94-
return false;
95-
} else if (!mFingerprintManager.hasEnrolledFingerprints()) {
96-
return false;
97-
}
98-
return true;
95+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
96+
ReactApplicationContext reactApplicationContext = getReactApplicationContext();
97+
BiometricManager biometricManager = BiometricManager.from(reactApplicationContext);
98+
int canAuthenticate = biometricManager.canAuthenticate();
99+
100+
return canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS;
99101
} else {
100102
return false;
101103
}
102-
} catch (SecurityException e) {
103-
// Should never be thrown since we have declared the USE_FINGERPRINT permission
104-
// in the manifest file
104+
} catch (Exception e) {
105105
return false;
106106
}
107107
}
@@ -118,8 +118,12 @@ public void setInvalidatedByBiometricEnrollment(final boolean invalidatedByBiome
118118

119119
@ReactMethod
120120
public void isHardwareDetected(final Promise pm) {
121-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mFingerprintManager != null) {
122-
pm.resolve(mFingerprintManager.isHardwareDetected());
121+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
122+
ReactApplicationContext reactApplicationContext = getReactApplicationContext();
123+
BiometricManager biometricManager = BiometricManager.from(reactApplicationContext);
124+
int canAuthenticate = biometricManager.canAuthenticate();
125+
126+
pm.resolve(canAuthenticate != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
123127
} else {
124128
pm.resolve(false);
125129
}
@@ -136,7 +140,7 @@ public void hasEnrolledFingerprints(final Promise pm) {
136140

137141
@ReactMethod
138142
public void isSensorAvailable(final Promise promise) {
139-
promise.resolve(hasSetupFingerprint());
143+
promise.resolve(hasSetupBiometricCredential());
140144
}
141145

142146
@ReactMethod
@@ -190,31 +194,25 @@ public void deleteItem(String key, ReadableMap options, Promise pm) {
190194
pm.resolve(null);
191195
}
192196

197+
193198
@ReactMethod
194199
public void getAllItems(ReadableMap options, Promise pm) {
195200

196201
String name = sharedPreferences(options);
197202

198203
Map<String, ?> allEntries = prefs(name).getAll();
199-
WritableArray resultData = new WritableNativeArray();
204+
WritableMap resultData = new WritableNativeMap();
200205

201206
for (Map.Entry<String, ?> entry : allEntries.entrySet()) {
202-
WritableMap entryMap = new WritableNativeMap();
203-
204-
entryMap.putString("key", entry.getKey());
205-
entryMap.putString("value", entry.getValue().toString());
206-
entryMap.putString("service", name);
207-
resultData.pushMap(entryMap);
207+
String value = entry.getValue().toString();
208+
resultData.putString(entry.getKey(), value);
208209
}
209-
210-
WritableArray resultWrapper = new WritableNativeArray();
211-
resultWrapper.pushArray(resultData);
212-
pm.resolve(resultWrapper);
210+
pm.resolve(resultData);
213211
}
214212

215213
@ReactMethod
216214
public void cancelFingerprintAuth() {
217-
if(mCancellationSignal != null && !mCancellationSignal.isCanceled()) {
215+
if (mCancellationSignal != null && !mCancellationSignal.isCanceled()) {
218216
mCancellationSignal.cancel();
219217
}
220218
}
@@ -248,31 +246,38 @@ private void putExtra(String key, Object value, SharedPreferences mSharedPrefere
248246
}
249247
}
250248

251-
private void showDialog(final HashMap strings, Object cryptoObject, FingerprintUiHelper.Callback callback) {
249+
private void showDialog(final HashMap strings, final BiometricPrompt.CryptoObject cryptoObject, final BiometricPrompt.AuthenticationCallback callback) {
252250
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
253-
// DialogFragment.show() will take care of adding the fragment
254-
// in a transaction. We also want to remove any currently showing
255-
// dialog, so make our own transaction and take care of that here.
256-
257-
Activity activity = getCurrentActivity();
258-
if (activity == null) {
259-
callback.onError(AppConstants.E_INIT_FAILURE,
260-
strings.containsKey("cancelled") ? strings.get("cancelled").toString() : "Authentication was cancelled");
261-
return;
262-
}
263251

264-
FragmentTransaction ft = activity.getFragmentManager().beginTransaction();
265-
Fragment prev = getCurrentActivity().getFragmentManager().findFragmentByTag(AppConstants.DIALOG_FRAGMENT_TAG);
266-
if (prev != null) {
267-
ft.remove(prev);
268-
}
269-
ft.addToBackStack(null);
252+
UiThreadUtil.runOnUiThread(
253+
new Runnable() {
254+
@Override
255+
public void run() {
256+
try {
257+
Activity activity = getCurrentActivity();
258+
if (activity == null) {
259+
callback.onAuthenticationError(BiometricConstants.ERROR_CANCELED,
260+
strings.containsKey("cancelled") ? strings.get("cancelled").toString() : "Authentication was cancelled");
261+
return;
262+
}
270263

271-
// Create and show the dialog.
272-
FingerprintAuthenticationDialogFragment newFragment = FingerprintAuthenticationDialogFragment.newInstance(strings);
273-
newFragment.setCryptoObject((FingerprintManager.CryptoObject) cryptoObject);
274-
newFragment.setCallback(callback);
275-
newFragment.show(ft, AppConstants.DIALOG_FRAGMENT_TAG);
264+
FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity();
265+
Executor executor = Executors.newSingleThreadExecutor();
266+
BiometricPrompt biometricPrompt = new BiometricPrompt(fragmentActivity, executor, callback);
267+
268+
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
269+
.setDeviceCredentialAllowed(false)
270+
.setNegativeButtonText(strings.containsKey("cancel") ? strings.get("cancel").toString() : "Cancel")
271+
.setDescription(strings.containsKey("description") ? strings.get("description").toString() : null)
272+
.setTitle(strings.containsKey("header") ? strings.get("header").toString() : null)
273+
.build();
274+
biometricPrompt.authenticate(promptInfo, cryptoObject);
275+
} catch (Exception e) {
276+
throw e;
277+
}
278+
}
279+
}
280+
);
276281
}
277282
}
278283

@@ -325,7 +330,7 @@ private void prepareKey() throws Exception {
325330

326331
private void putExtraWithAES(final String key, final String value, final SharedPreferences mSharedPreferences, final boolean showModal, final HashMap strings, final Promise pm, Cipher cipher) {
327332

328-
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M && hasSetupFingerprint()) {
333+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M && hasSetupBiometricCredential()) {
329334
try {
330335
if (cipher == null) {
331336
SecretKey secretKey = (SecretKey) mKeyStore.getKey(KEY_ALIAS_AES, null);
@@ -342,25 +347,26 @@ private void putExtraWithAES(final String key, final String value, final SharedP
342347
info.getUserAuthenticationValidityDurationSeconds() == -1) {
343348

344349
if (showModal) {
345-
346-
// define class as a callback
347-
class PutExtraWithAESCallback implements FingerprintUiHelper.Callback {
350+
class PutExtraWithAESCallback extends BiometricPrompt.AuthenticationCallback {
348351
@Override
349-
public void onAuthenticated(FingerprintManager.AuthenticationResult result) {
352+
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
350353
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
351354
putExtraWithAES(key, value, mSharedPreferences, true, strings, pm, result.getCryptoObject().getCipher());
352355
}
353356
}
354357

355358
@Override
356-
public void onError(String errorCode, CharSequence errString) {
359+
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
357360
pm.reject(String.valueOf(errorCode), errString.toString());
358361
}
359-
}
360362

361-
// Show the fingerprint dialog
362-
showDialog(strings, new FingerprintManager.CryptoObject(cipher), new PutExtraWithAESCallback());
363+
@Override
364+
public void onAuthenticationFailed() {
365+
pm.reject(AppConstants.E_AUTHENTICATION_NOT_RECOGNIZED, strings.containsKey("notRecognized") ? strings.get("notRecognized").toString() : "Fingerprint not recognized, try again");
366+
}
367+
}
363368

369+
showDialog(strings, new BiometricPrompt.CryptoObject(cipher), new PutExtraWithAESCallback());
364370
} else {
365371
mCancellationSignal = new CancellationSignal();
366372
mFingerprintManager.authenticate(new FingerprintManager.CryptoObject(cipher), mCancellationSignal,
@@ -429,7 +435,7 @@ public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult re
429435
private void decryptWithAes(final String encrypted, final boolean showModal, final HashMap strings, final Promise pm, Cipher cipher) {
430436

431437
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M
432-
&& hasSetupFingerprint()) {
438+
&& hasSetupBiometricCredential()) {
433439

434440
String[] inputs = encrypted.split(DELIMITER);
435441
if (inputs.length < 2) {
@@ -440,7 +446,7 @@ && hasSetupFingerprint()) {
440446
byte[] iv = Base64.decode(inputs[0], Base64.DEFAULT);
441447
byte[] cipherBytes = Base64.decode(inputs[1], Base64.DEFAULT);
442448

443-
if(cipher == null){
449+
if (cipher == null) {
444450
SecretKey secretKey = (SecretKey) mKeyStore.getKey(KEY_ALIAS_AES, null);
445451
cipher = Cipher.getInstance(AES_DEFAULT_TRANSFORMATION);
446452
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
@@ -453,27 +459,32 @@ && hasSetupFingerprint()) {
453459
info.getUserAuthenticationValidityDurationSeconds() == -1) {
454460

455461
if (showModal) {
456-
457-
// define class as a callback
458-
class DecryptWithAesCallback implements FingerprintUiHelper.Callback {
462+
class DecryptWithAesCallback extends BiometricPrompt.AuthenticationCallback {
459463
@Override
460-
public void onAuthenticated(FingerprintManager.AuthenticationResult result) {
464+
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
461465
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
462466
decryptWithAes(encrypted, true, strings, pm, result.getCryptoObject().getCipher());
463467
}
464468
}
465469

466470
@Override
467-
public void onError(String errorCode, CharSequence errString) {
471+
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
468472
pm.reject(String.valueOf(errorCode), errString.toString());
469473
}
470-
}
471474

472-
// Show the fingerprint dialog
473-
showDialog(strings, new FingerprintManager.CryptoObject(cipher), new DecryptWithAesCallback());
475+
@Override
476+
public void onAuthenticationFailed() {
477+
pm.reject(AppConstants.E_AUTHENTICATION_NOT_RECOGNIZED, strings.containsKey("notRecognized") ? strings.get("notRecognized").toString() : "Fingerprint not recognized, try again");
478+
}
479+
}
474480

481+
showDialog(strings, new BiometricPrompt.CryptoObject(cipher), new DecryptWithAesCallback());
475482
} else {
476483
mCancellationSignal = new CancellationSignal();
484+
ReactApplicationContext reactApplicationContext = getReactApplicationContext();
485+
BiometricManager biometricManager = BiometricManager.from(reactApplicationContext);
486+
487+
477488
mFingerprintManager.authenticate(new FingerprintManager.CryptoObject(cipher), mCancellationSignal,
478489
0, new FingerprintManager.AuthenticationCallback() {
479490

android/src/main/java/br/com/classapp/RNSensitiveInfo/utils/AppConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public interface AppConstants {
55
String DIALOG_FRAGMENT_TAG = "authFragment";
66

77
// error codes
8+
String E_AUTHENTICATION_NOT_RECOGNIZED = "E_AUTHENTICATION_NOT_RECOGNIZED";
89
String E_AUTHENTICATION_CANCELLED = "E_AUTHENTICATION_CANCELLED";
910
String E_INIT_FAILURE = "E_INIT_FAILURE";
1011
}

0 commit comments

Comments
 (0)