Compare commits

..

No commits in common. "master" and "1.x" have entirely different histories.
master ... 1.x

30 changed files with 3938 additions and 683 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["module:metro-react-native-babel-preset"]
}

View File

@ -1,19 +1,6 @@
# Changelog
All notable changes to this project will be documented in this file.
## [2.0.0] - 2019-11-19
### Breaking
- Requires React Native 0.60+ for androidx compatibility
- All functions now take an options object and return a result object
- `createSignature` no longer prompts user for biometrics, `simplePrompt` can be used in conjunction with it to achieve the same effect
- `createSignature` and `simplePrompt` no longer reject on cancellation, they resolve with a success flag set to false when a user cancels a biometric prompt
- Android no longer resolves to biometry type of `TouchID`, it only resolves to `Biometrics`
### Changed
- Used android BiometricPrompt API for biometrics
- Changed library function API
- Added better support for prompt cancellations
- Started to return native error messages in promise rejections
## [1.7.0] - 2019-11-5
### Changed
- Removed dependency on android app compat library for compatibility with androidx
@ -110,4 +97,3 @@ All notable changes to this project will be documented in this file.
[1.6.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.5.2...1.6.0
[1.6.1]: https://github.com/SelfLender/react-native-biometrics/compare/1.6.0...1.6.1
[1.7.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.6.1...1.7.0
[2.0.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.7.0...2.0.0

259
README.md
View File

@ -3,38 +3,50 @@
React native biometrics is a simple bridge to native iOS and Android keystore management. It allows you to create public private key pairs that are stored in native keystores and protected by biometric authentication. Those keys can then be retrieved later, after proper authentication, and used to create a cryptographic signature.
## React Native Compatibility
| `react-native-biometrics` version | Required React Native Version |
|:---------------------------------:|:-----------------------------:|
| `>= 2.0.0` | `>= 0.60` |
| `<= 1.7.0` | `<= 0.59.x` |
## Getting started
using either Yarn:
`yarn add react-native-biometrics`
or npm:
`$ npm install react-native-biometrics --save`
### Link / Autolinking
### Automatic installation
On React Native 0.60+ the [CLI autolink feature](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) links the module while building the app.
`$ react-native link react-native-biometrics`
### Manual installation
#### iOS
1. In XCode, in the project navigator, right click `Libraries``Add Files to [your project's name]`
2. Go to `node_modules``react-native-biometrics` and add `ReactNativeBiometrics.xcodeproj`
3. In XCode, in the project navigator, select your project. Add `libReactNativeBiometrics.a` to your project's `Build Phases``Link Binary With Libraries`
4. Run your project
#### Android
1. Open up `android/app/src/main/java/[...]/MainApplication.java`
- Add `import com.rnbiometrics.ReactNativeBiometricsPackage;` to the imports at the top of the file
- Add `new ReactNativeBiometricsPackage()` to the list returned by the `getPackages()` method
2. Append the following lines to `android/settings.gradle`:
```
include ':react-native-biometrics'
project(':react-native-biometrics').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-biometrics/android')
```
3. Insert the following lines inside the dependencies block in `android/app/build.gradle`:
```
compile project(':react-native-biometrics')
```
## Additional configuration
#### iOS
This package requires an iOS target SDK version of iOS 10 or higher
This package requires an iOS target SDK verion of iOS 10 or higher
Ensure that you have the `NSFaceIDUsageDescription` entry set in your react native iOS project, or Face ID will not work properly. This description will be will be presented to the user the first time a biometrics action is taken, and the user will be asked if they want to allow the app to use Face ID. If the user declines the usage of face id for the app, the `isSensorAvailable` function will indicate biometrics is unavailable until the face id permission is specifically allowed for the app by the user.
Ensure that you have the `NSFaceIDUsageDescription` entry set in your react native iOS project, or Face ID will not work properly. This description will be will be presented to the user the first time a biometrics action is taken, and the user will be asked if they want to allow the app to use Face ID. If the user declines the usage of face id for the app, the `isSensorAvailable` function will return `null` until the face id permission is specifically allowed for the app by the user.
#### Android
This package requires a compiled SDK version of 29 (Android 10.0) or higher
This package requires a compiled SDK version of 23 (Android 6.0 Marshmallow) or higher
## Usage
@ -46,50 +58,30 @@ When a user enrolls in biometrics, a key pair is generated. The private key is
## Constants
### TouchID (iOS only)
### TouchID
A constant for the touch id sensor type, evaluates to `'TouchID'`
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
import Biometrics from 'react-native-biometrics'
const { biometryType } = await ReactNativeBiometrics.isSensorAvailable()
if (biometryType === ReactNativeBiometrics.TouchID) {
if (biometryType === Biometrics.TouchID) {
//do something fingerprint specific
}
```
### FaceID (iOS only)
### FaceID
A constant for the face id sensor type, evaluates to `'FaceID'`
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
import Biometrics from 'react-native-biometrics'
const { biometryType } = await ReactNativeBiometrics.isSensorAvailable()
if (biometryType === ReactNativeBiometrics.FaceID) {
//do something face id specific
}
```
### Biometrics (Android only)
A constant for generic Biometrics, evaluates to `'Biometrics'`
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
const { biometryType } = await ReactNativeBiometrics.isSensorAvailable()
if (biometryType === ReactNativeBiometrics.Biometrics) {
if (biometryType === Biometrics.FaceID) {
//do something face id specific
}
```
@ -98,209 +90,110 @@ if (biometryType === ReactNativeBiometrics.Biometrics) {
### isSensorAvailable()
Detects what type of biometric sensor is available. Returns a `Promise` that resolves to an object with details about biometrics availability
__Result Object__
| Property | Type | Description |
| --- | --- | --- |
| available | bool | A boolean indicating if biometrics is available or not |
| biometryType | string | A string indicating what type of biometrics is available. `TouchID`, `FaceID`, `Biometrics`, or `undefined` if biometrics is not available. |
| error | string | An error message indicating why biometrics may not be available. `undefined` if there is no error. |
Detects what type of biometric sensor is available. Returns a `Promise` that resolves to a string representing the sensor type (`TouchID`, `FaceID`, `null`)
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
import Biometrics from 'react-native-biometrics'
ReactNativeBiometrics.isSensorAvailable()
.then((resultObject) => {
const { available, biometryType } = resultObject
if (available && biometryType === ReactNativeBiometrics.TouchID) {
Biometrics.isSensorAvailable()
.then((biometryType) => {
if (biometryType === Biometrics.TouchID) {
console.log('TouchID is supported')
} else if (available && biometryType === ReactNativeBiometrics.FaceID) {
} else if (biometryType === Biometrics.FaceID) {
console.log('FaceID is supported')
} else if (available && biometryType === ReactNativeBiometrics.Biometrics) {
console.log('Biometrics is supported')
} else {
console.log('Biometrics not supported')
}
})
```
### createKeys()
### createKeys([promptMessage])
Generates a public private RSA 2048 key pair that will be stored in the device keystore. Returns a `Promise` that resolves to an object providing details about the keys.
Prompts the user for their fingerprint or face id, then generates a public private RSA 2048 key pair that will be stored in the device keystore. Returns a `Promise` that resolves to a base64 encoded string representing the public key.
__Result Object__
__Arguments__
| Property | Type | Description |
| --- | --- | --- |
| publicKey | string | A base64 encoded string representing the public key |
- `promptMessage` - optional string that will be displayed in the fingerprint or face id prompt, if no prompt message is provided, no prompt will be displayed.
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
import Biometrics from 'react-native-biometrics'
ReactNativeBiometrics.createKeys('Confirm fingerprint')
.then((resultObject) => {
const { publicKey } = resultObject
Biometrics.createKeys('Confirm fingerprint')
.then((publicKey) => {
console.log(publicKey)
sendPublicKeyToServer(publicKey)
})
```
### biometricKeysExist()
Detects if keys have already been generated and exist in the keystore. Returns a `Promise` that resolves to an object indicating details about the keys.
__Result Object__
| Property | Type | Description |
| --- | --- | --- |
| keysExist | bool | A boolean indicating if keys exist in the keystore |
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
ReactNativeBiometrics.biometricKeysExist()
.then((resultObject) => {
const { keysExist } = resultObject
if (keysExist) {
console.log('Keys exist')
} else {
console.log('Keys do not exist or were deleted')
}
})
```
### deleteKeys()
Deletes the generated keys from the device keystore. Returns a `Promise` that resolves to an object indicating details about the deletion.
__Result Object__
| Property | Type | Description |
| --- | --- | --- |
| keysDeleted | bool | A boolean indicating if keys were deleted from the keystore |
Deletes the generated keys from the device keystore. Returns a `Promise` that resolves to `true` or `false` indicating if the deletion was successful
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
import Biometrics from 'react-native-biometrics'
ReactNativeBiometrics.deleteKeys()
.then((resultObject) => {
const { keysDeleted } = resultObject
if (keysDeleted) {
Biometrics.deleteKeys()
.then((success) => {
if (success) {
console.log('Successful deletion')
} else {
console.log('Unsuccessful deletion because there were no keys to delete')
console.log('Unsuccessful deletion')
}
})
```
### createSignature(options)
### createSignature(promptMessage, payload)
Prompts the user for their fingerprint or face id in order to retrieve the private key from the keystore, then uses the private key to generate a RSA PKCS#1v1.5 SHA 256 signature. Returns a `Promise` that resolves to an object with details about the signature.
Prompts the user for their fingerprint or face id in order to retrieve the private key from the keystore, then uses the private key to generate a RSA PKCS#1v1.5 SHA 256 signature. Returns a `Promise` that resolves to a base64 encoded string representing the signature.
**NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices.
NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices.
__Options Object__
__Arguments__
| Parameter | Type | Description | iOS | Android |
| --- | --- | --- | --- | --- |
| promptMessage | string | Message that will be displayed in the fingerprint or face id prompt | ✔ | ✔ |
| payload | string | String of data to be signed by the RSA signature | ✔ | ✔ |
| cancelButtonText | string | Text to be displayed for the cancel button on biometric prompts, defaults to `Cancel` | ✖ | ✔ |
__Result Object__
| Property | Type | Description |
| --- | --- | --- |
| success | bool | A boolean indicating if the process was successful, `false` if the users cancels the biometrics prompt |
| signature | string | A base64 encoded string representing the signature. `undefined` if the process was not successful. |
| error | string | An error message indicating reasons why signature creation failed. `undefined` if there is no error. |
- `promptMessage` - string that will be displayed in the fingerprint or face id prompt
- `payload` - string of data to be signed by the RSA signature
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
import Biometrics from 'react-native-biometrics'
let epochTimeSeconds = Math.round((new Date()).getTime() / 1000).toString()
let payload = epochTimeSeconds + 'some message'
ReactNativeBiometrics.createSignature({
promptMessage: 'Sign in',
payload: payload
})
.then((resultObject) => {
const { success, signature } = resultObject
if (success) {
console.log(signature)
verifySignatureWithServer(signature, payload)
}
Biometrics.createSignature('Sign in', payload)
.then((signature) => {
console.log(signature)
verifySignatureWithServer(signature, payload)
})
```
### simplePrompt(options)
### simplePrompt(promptMessage)
Prompts the user for their fingerprint or face id. Returns a `Promise` that resolves if the user provides a valid biometrics or cancel the prompt, otherwise the promise rejects.
Prompts the user for their fingerprint or face id. Returns a `Promise` that resolves if the user provides a valid fingerprint or face id, otherwise the promise rejects.
**NOTE: This only validates a user's biometrics. This should not be used to log a user in or authenticate with a server, instead use `createSignature`. It should only be used to gate certain user actions within an app.
NOTE: This only validates a user's biometrics. This should not be used to log a user in or authenticate with a server, instead use `createSignature`. It should only be used to gate certain user actions within an app.
__Options Object__
__Arguments__
| Parameter | Type | Description | iOS | Android |
| --- | --- | --- | --- | --- |
| promptMessage | string | Message that will be displayed in the biometrics prompt | ✔ | ✔ |
| cancelButtonText | string | Text to be displayed for the cancel button on biometric prompts, defaults to `Cancel` | ✖ | ✔ |
__Result Object__
| Property | Type | Description |
| --- | --- | --- |
| success | bool | A boolean indicating if the biometric prompt succeeded, `false` if the users cancels the biometrics prompt |
| error | string | An error message indicating why the biometric prompt failed. `undefined` if there is no error. |
- `promptMessage` - string that will be displayed in the fingerprint or face id prompt
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
import Biometrics from 'react-native-biometrics'
ReactNativeBiometrics.simplePrompt('Confirm fingerprint')
.then((resultObject) => {
const { success } = resultObject
if (success) {
console.log('successful biometrics provided')
} else {
console.log('user cancelled biometric prompt')
}
Biometrics.simplePrompt('Confirm fingerprint')
.then(() => {
console.log('successful fingerprint provided')
})
.catch(() => {
console.log('biometrics failed')
console.log('fingerprint failed or prompt was cancelled')
})
```
### Troubleshooting
- Because of this library's dependency on `androidx.biometric:biometric:1.0.0` it can cause transitive dependency resolution to change on certain version of React Native and `androidx.swiperefreshlayout` may no longer be able to be resolved. This can be fixed by adding an explicit dependency on the library in your `android/app/build.gradle`:
```
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" // temp fix
...
}
```
- There is a [known issue](https://stackoverflow.com/questions/56700680/keychain-query-always-returns-errsecitemnotfound-after-upgrading-to-ios-13) on the iOS 13.x simulators where keys generated with access control flags cannot be queried and found properly. This results in key not found errors in `biometricKeysExist` and `createSignature` on those simulators. However, it works correctly on actual devices running iOS 13.

View File

@ -2,10 +2,10 @@ apply plugin: 'com.android.library'
description = 'react-native-biometrics'
def DEFAULT_COMPILE_SDK_VERSION = 29
def DEFAULT_BUILD_TOOLS_VERSION = "29.0.2"
def DEFAULT_COMPILE_SDK_VERSION = 28
def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3"
def DEFAULT_MIN_SDK_VERSION = 16
def DEFAULT_TARGET_SDK_VERSION = 29
def DEFAULT_TARGET_SDK_VERSION = 28
buildscript {
repositories {
@ -14,7 +14,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
@ -37,6 +37,5 @@ repositories {
}
dependencies {
implementation 'androidx.biometric:biometric:1.0.0'
implementation 'com.facebook.react:react-native:+'
}

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rnbiometrics">
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-feature android:name="android.hardware.fingerprint" android:required="false"/>
</manifest>

View File

@ -1,58 +0,0 @@
package com.rnbiometrics;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import java.security.Signature;
public class CreateSignatureCallback extends BiometricPrompt.AuthenticationCallback {
private Promise promise;
private String payload;
public CreateSignatureCallback(Promise promise, String payload) {
super();
this.promise = promise;
this.payload = payload;
}
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
super.onAuthenticationError(errorCode, errString);
if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("success", false);
resultMap.putString("error", "User cancellation");
this.promise.resolve(resultMap);
} else {
this.promise.reject(errString.toString(), errString.toString());
}
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
try {
BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject();
Signature cryptoSignature = cryptoObject.getSignature();
cryptoSignature.update(this.payload.getBytes());
byte[] signed = cryptoSignature.sign();
String signedString = Base64.encodeToString(signed, Base64.DEFAULT);
signedString = signedString.replaceAll("\r", "").replaceAll("\n", "");
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("success", true);
resultMap.putString("signature", signedString);
promise.resolve(resultMap);
} catch (Exception e) {
promise.reject("Error creating signature: " + e.getMessage(), "Error creating signature");
}
}
}

View File

@ -1,24 +1,20 @@
package com.rnbiometrics;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.text.TextUtils;
import android.util.Base64;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.biometric.BiometricPrompt.AuthenticationCallback;
import androidx.biometric.BiometricPrompt.PromptInfo;
import androidx.fragment.app.FragmentActivity;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@ -27,8 +23,6 @@ import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.RSAKeyGenParameterSpec;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* Created by brandon on 4/5/18.
@ -52,66 +46,41 @@ public class ReactNativeBiometrics extends ReactContextBaseJavaModule {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ReactApplicationContext reactApplicationContext = getReactApplicationContext();
BiometricManager biometricManager = BiometricManager.from(reactApplicationContext);
int canAuthenticate = biometricManager.canAuthenticate();
FingerprintManager fingerprintManager = reactApplicationContext.getSystemService(FingerprintManager.class);
Boolean isHardwareDetected = fingerprintManager.isHardwareDetected();
Boolean hasFingerprints = fingerprintManager.hasEnrolledFingerprints();
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("available", true);
resultMap.putString("biometryType", "Biometrics");
promise.resolve(resultMap);
KeyguardManager keyguardManager = (KeyguardManager) reactApplicationContext.getSystemService(Context.KEYGUARD_SERVICE);
Boolean hasProtectedLockscreen = keyguardManager.isKeyguardSecure();
if (isHardwareDetected && hasFingerprints && hasProtectedLockscreen) {
promise.resolve("TouchID");
} else {
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("available", false);
switch (canAuthenticate) {
case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
resultMap.putString("error", "BIOMETRIC_ERROR_NO_HARDWARE");
break;
case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
resultMap.putString("error", "BIOMETRIC_ERROR_HW_UNAVAILABLE");
break;
case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
resultMap.putString("error", "BIOMETRIC_ERROR_NONE_ENROLLED");
break;
}
promise.resolve(resultMap);
promise.resolve(null);
}
} else {
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("available", false);
resultMap.putString("error", "Unsupported android version");
promise.resolve(resultMap);
promise.resolve(null);
}
} catch (Exception e) {
promise.reject("Error detecting biometrics availability: " + e.getMessage(), "Error detecting biometrics availability: " + e.getMessage());
promise.reject("Error detecting fingerprint availability: " + e.getMessage(), "Error detecting fingerprint availability");
}
}
@ReactMethod
public void createKeys(Promise promise) {
public void createKeys(String title, Promise promise) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteBiometricKey();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
KeyGenParameterSpec keyGenParameterSpec = 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);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
byte[] encodedPublicKey = publicKey.getEncoded();
String publicKeyString = Base64.encodeToString(encodedPublicKey, Base64.DEFAULT);
publicKeyString = publicKeyString.replaceAll("\r", "").replaceAll("\n", "");
WritableMap resultMap = new WritableNativeMap();
resultMap.putString("publicKey", publicKeyString);
promise.resolve(resultMap);
if (TextUtils.isEmpty(title)) {
// if no title is provided for the create keys prompt, treat the action as
// authenticated and create keys
ReactNativeBiometricsCallback createKeysCallback = getCreationCallback(promise);
createKeysCallback.onAuthenticated(null);
} else {
ReactNativeBiometricsDialog dialog = new ReactNativeBiometricsDialog();
dialog.init(title, null, getCreationCallback(promise));
Activity activity = getCurrentActivity();
dialog.show(activity.getFragmentManager(), "fingerprint_dialog");
}
} else {
promise.reject("Cannot generate keys on android versions below 6.0", "Cannot generate keys on android versions below 6.0");
}
@ -122,110 +91,62 @@ public class ReactNativeBiometrics extends ReactContextBaseJavaModule {
@ReactMethod
public void deleteKeys(Promise promise) {
if (doesBiometricKeyExist()) {
if (biometricKeyExists()) {
boolean deletionSuccessful = deleteBiometricKey();
if (deletionSuccessful) {
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("keysDeleted", true);
promise.resolve(resultMap);
promise.resolve(true);
} else {
promise.reject("Error deleting biometric key from keystore", "Error deleting biometric key from keystore");
}
} else {
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("keysDeleted", false);
promise.resolve(resultMap);
promise.resolve(false);
}
}
@ReactMethod
public void createSignature(final ReadableMap params, final Promise promise) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
try {
String cancelButtomText = params.getString("cancelButtonText");
String promptMessage = params.getString("promptMessage");
String payload = params.getString("payload");
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);
PromptInfo promptInfo = new PromptInfo.Builder()
.setDeviceCredentialAllowed(false)
.setNegativeButtonText(cancelButtomText)
.setTitle(promptMessage)
.build();
biometricPrompt.authenticate(promptInfo, cryptoObject);
} catch (Exception e) {
promise.reject("Error signing payload: " + e.getMessage(), "Error generating signature: " + e.getMessage());
}
}
});
} else {
promise.reject("Cannot generate keys on android versions below 6.0", "Cannot generate keys on android versions below 6.0");
}
}
@ReactMethod
public void simplePrompt(final ReadableMap params, final Promise promise) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
try {
String cancelButtomText = params.getString("cancelButtonText");
String promptMessage = params.getString("promptMessage");
AuthenticationCallback authCallback = new SimplePromptCallback(promise);
FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity();
Executor executor = Executors.newSingleThreadExecutor();
BiometricPrompt biometricPrompt = new BiometricPrompt(fragmentActivity, executor, authCallback);
PromptInfo promptInfo = new PromptInfo.Builder()
.setDeviceCredentialAllowed(false)
.setNegativeButtonText(cancelButtomText)
.setTitle(promptMessage)
.build();
biometricPrompt.authenticate(promptInfo);
} catch (Exception e) {
promise.reject("Error displaying local biometric prompt: " + e.getMessage(), "Error displaying local biometric prompt: " + e.getMessage());
}
}
});
} else {
promise.reject("Cannot display biometric prompt on android versions below 6.0", "Cannot display biometric prompt on android versions below 6.0");
}
}
@ReactMethod
public void biometricKeysExist(Promise promise) {
public void createSignature(String title, String payload, Promise promise) {
try {
boolean doesBiometricKeyExist = doesBiometricKeyExist();
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("keysExist", doesBiometricKeyExist);
promise.resolve(resultMap);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Signature signature = Signature.getInstance("SHA256withRSA");
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null);
signature.initSign(privateKey);
FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(signature);
ReactNativeBiometricsDialog dialog = new ReactNativeBiometricsDialog();
dialog.init(title, cryptoObject, getSignatureCallback(payload, promise));
Activity activity = getCurrentActivity();
dialog.show(activity.getFragmentManager(), "fingerprint_dialog");
} else {
promise.reject("Cannot generate keys on android versions below 6.0", "Cannot generate keys on android versions below 6.0");
}
} catch (Exception e) {
promise.reject("Error checking if biometric key exists: " + e.getMessage(), "Error checking if biometric key exists: " + e.getMessage());
promise.reject("Error signing payload: " + e.getMessage(), "Error generating signature");
}
}
protected boolean doesBiometricKeyExist() {
@ReactMethod
public void simplePrompt(String title, Promise promise) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ReactNativeBiometricsDialog dialog = new ReactNativeBiometricsDialog();
dialog.init(title, null, getSimplePromptCallback(promise));
Activity activity = getCurrentActivity();
dialog.show(activity.getFragmentManager(), "fingerprint_dialog");
} else {
promise.reject("Cannot display biometric prompt on android versions below 6.0", "Cannot display biometric prompt on android versions below 6.0");
}
} catch (Exception e) {
promise.reject("Error displaying local biometric prompt: " + e.getMessage(), "Error displaying local biometric prompt");
}
}
protected boolean biometricKeyExists() {
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
@ -247,4 +168,91 @@ public class ReactNativeBiometrics extends ReactContextBaseJavaModule {
return false;
}
}
protected ReactNativeBiometricsCallback getSignatureCallback(final String payload, final Promise promise) {
return new ReactNativeBiometricsCallback() {
@Override
@TargetApi(Build.VERSION_CODES.M)
public void onAuthenticated(FingerprintManager.CryptoObject cryptoObject) {
try {
Signature cryptoSignature = cryptoObject.getSignature();
cryptoSignature.update(payload.getBytes());
byte[] signed = cryptoSignature.sign();
String signedString = Base64.encodeToString(signed, Base64.DEFAULT);
signedString = signedString.replaceAll("\r", "").replaceAll("\n", "");
promise.resolve(signedString);
} catch (Exception e) {
promise.reject("Error creating signature: " + e.getMessage(), "Error creating signature");
}
}
@Override
public void onCancel() {
promise.reject("User cancelled fingerprint authorization", "User cancelled fingerprint authorization");
}
@Override
public void onError() {
promise.reject("Error detecting fingerprint", "Error detecting fingerprint");
}
};
}
protected ReactNativeBiometricsCallback getCreationCallback(final Promise promise) {
return new ReactNativeBiometricsCallback() {
@Override
@TargetApi(Build.VERSION_CODES.M)
public void onAuthenticated(FingerprintManager.CryptoObject cryptoObject) {
try {
deleteBiometricKey();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
KeyGenParameterSpec keyGenParameterSpec = 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);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
byte[] encodedPublicKey = publicKey.getEncoded();
String publicKeyString = Base64.encodeToString(encodedPublicKey, Base64.DEFAULT);
publicKeyString = publicKeyString.replaceAll("\r", "").replaceAll("\n", "");
promise.resolve(publicKeyString);
} catch (Exception e) {
promise.reject("Error generating public private keys: " + e.getMessage(), "Error generating public private keys");
}
}
@Override
public void onCancel() {
promise.reject("User cancelled fingerprint authorization", "User cancelled fingerprint authorization");
}
@Override
public void onError() {
promise.reject("Error generating public private keys" , "Error generating public private keys");
}
};
}
protected ReactNativeBiometricsCallback getSimplePromptCallback(final Promise promise) {
return new ReactNativeBiometricsCallback() {
@Override
public void onAuthenticated(FingerprintManager.CryptoObject cryptoObject) {
promise.resolve(true);
}
@Override
public void onCancel() {
promise.reject("User cancelled fingerprint authorization", "User cancelled fingerprint authorization");
}
@Override
public void onError() {
promise.reject("Error generating public private keys" , "Error generating public private keys");
}
};
}
}

View File

@ -0,0 +1,16 @@
package com.rnbiometrics;
import android.hardware.fingerprint.FingerprintManager;
/**
* Created by brandon on 4/9/18.
*/
public interface ReactNativeBiometricsCallback {
void onAuthenticated(FingerprintManager.CryptoObject cryptoObject);
void onCancel();
void onError();
}

View File

@ -0,0 +1,117 @@
package com.rnbiometrics;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
/**
* Created by brandon on 4/6/18.
*/
@TargetApi(Build.VERSION_CODES.M)
public class ReactNativeBiometricsDialog extends DialogFragment implements ReactNativeBiometricsCallback {
protected String title;
protected FingerprintManager.CryptoObject cryptoObject;
protected ReactNativeBiometricsCallback biometricAuthCallback;
protected ReactNativeBiometricsHelper biometricAuthenticationHelper;
protected Activity activity;
protected Button cancelButton;
public void init(String title, FingerprintManager.CryptoObject cryptoObject, ReactNativeBiometricsCallback callback) {
this.title = title;
this.cryptoObject = cryptoObject;
this.biometricAuthCallback = callback;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NORMAL, R.style.BiometricsDialog);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
getDialog().setTitle(title);
View view = inflater.inflate(R.layout.fingerprint_dialog_container, container, false);
cancelButton = (Button) view.findViewById(R.id.cancel_button);
cancelButton.setText(R.string.fingerprint_cancel);
cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dismissAllowingStateLoss();
onCancel();
}
});
biometricAuthenticationHelper = new ReactNativeBiometricsHelper(
activity.getSystemService(FingerprintManager.class),
(ImageView) view.findViewById(R.id.fingerprint_icon),
(TextView) view.findViewById(R.id.fingerprint_status),
this
);
return view;
}
// DialogFragment lifecycle methods
@Override
public void onAttach(Context context) {
super.onAttach(context);
activity = getActivity();
}
@Override
public void onPause() {
super.onPause();
biometricAuthenticationHelper.stopListening();
}
@Override
public void onResume() {
super.onResume();
biometricAuthenticationHelper.startListening(cryptoObject);
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
onCancel();
}
// ReactNativeBiometricsCallback methods
@Override
public void onAuthenticated(FingerprintManager.CryptoObject cryptoObject) {
dismissAllowingStateLoss();
if (biometricAuthCallback != null) {
biometricAuthCallback.onAuthenticated(cryptoObject);
}
}
@Override
public void onCancel() {
if (biometricAuthCallback != null) {
biometricAuthCallback.onCancel();
}
}
@Override
public void onError() {
dismissAllowingStateLoss();
if (biometricAuthCallback != null) {
biometricAuthCallback.onError();
}
}
}

View File

@ -0,0 +1,108 @@
package com.rnbiometrics;
import android.annotation.TargetApi;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import android.widget.ImageView;
import android.widget.TextView;
import com.rnbiometrics.R;
/**
* Created by brandon on 4/5/18.
*/
@TargetApi(Build.VERSION_CODES.M)
public class ReactNativeBiometricsHelper extends FingerprintManager.AuthenticationCallback {
private static final long ERROR_TIMEOUT_MILLIS = 1600;
private static final long SUCCESS_DELAY_MILLIS = 1300;
private final FingerprintManager fingerprintManager;
private final ImageView icon;
private final TextView errorTextView;
private final ReactNativeBiometricsCallback callback;
private CancellationSignal cancellationSignal;
private boolean selfCancelled;
ReactNativeBiometricsHelper(FingerprintManager fingerprintManager, ImageView icon,
TextView errorTextView, ReactNativeBiometricsCallback callback) {
this.fingerprintManager = fingerprintManager;
this.icon = icon;
this.errorTextView = errorTextView;
this.callback = callback;
}
public void startListening(FingerprintManager.CryptoObject cryptoObject) {
selfCancelled = false;
cancellationSignal = new CancellationSignal();
fingerprintManager
.authenticate(cryptoObject, cancellationSignal, 0 /* flags */, this, null);
icon.setImageResource(R.drawable.ic_fp_40px);
}
public void stopListening() {
if (cancellationSignal != null) {
selfCancelled = true;
cancellationSignal.cancel();
cancellationSignal = null;
}
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
if (!selfCancelled) {
showError(errString);
icon.postDelayed(new Runnable() {
@Override
public void run() {
callback.onError();
}
}, ERROR_TIMEOUT_MILLIS);
}
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
showError(helpString);
}
@Override
public void onAuthenticationFailed() {
showError(errorTextView.getResources().getString(R.string.fingerprint_not_recognized));
}
@Override
public void onAuthenticationSucceeded(final FingerprintManager.AuthenticationResult result) {
errorTextView.removeCallbacks(resetErrorTextRunnable);
icon.setImageResource(R.drawable.ic_fingerprint_success);
errorTextView.setTextColor(errorTextView.getResources().getColor(R.color.success_color, null));
errorTextView.setText(errorTextView.getResources().getString(R.string.fingerprint_recognized));
icon.postDelayed(new Runnable() {
@Override
public void run() {
callback.onAuthenticated(result.getCryptoObject());
}
}, SUCCESS_DELAY_MILLIS);
}
private void showError(CharSequence error) {
icon.setImageResource(R.drawable.ic_fingerprint_error);
errorTextView.setText(error);
errorTextView.setTextColor(errorTextView.getResources().getColor(R.color.warning_color, null));
errorTextView.removeCallbacks(resetErrorTextRunnable);
errorTextView.postDelayed(resetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
}
private Runnable resetErrorTextRunnable = new Runnable() {
@Override
public void run() {
errorTextView.setTextColor(
errorTextView.getResources().getColor(R.color.hint_color, null));
errorTextView.setText(errorTextView.getResources().getString(R.string.fingerprint_hint));
icon.setImageResource(R.drawable.ic_fp_40px);
}
};
}

View File

@ -1,39 +0,0 @@
package com.rnbiometrics;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
public class SimplePromptCallback extends BiometricPrompt.AuthenticationCallback {
private Promise promise;
public SimplePromptCallback(Promise promise) {
super();
this.promise = promise;
}
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("success", false);
resultMap.putString("error", "User cancellation");
this.promise.resolve(resultMap);
} else {
this.promise.reject(errString.toString(), errString.toString());
}
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("success", true);
this.promise.resolve(resultMap);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40.0dp"
android:height="40.0dp"
android:viewportWidth="40.0"
android:viewportHeight="40.0">
<path
android:pathData="M20.0,0.0C8.96,0.0 0.0,8.95 0.0,20.0s8.96,20.0 20.0,20.0c11.04,0.0 20.0,-8.95 20.0,-20.0S31.04,0.0 20.0,0.0z"
android:fillColor="#F4511E"/>
<path
android:pathData="M21.33,29.33l-2.67,0.0l0.0,-2.67l2.67,0.0L21.33,29.33zM21.33,22.67l-2.67,0.0l0.0,-12.0l2.67,0.0L21.33,22.67z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40.0dp"
android:height="40.0dp"
android:viewportWidth="40.0"
android:viewportHeight="40.0">
<path
android:pathData="M20.0,20.0m-20.0,0.0a20.0,20.0 0.0,1.0 1.0,40.0 0.0a20.0,20.0 0.0,1.0 1.0,-40.0 0.0"
android:fillColor="#009688"/>
<path
android:pathData="M11.2,21.41l1.63,-1.619999 4.17,4.169998 10.59,-10.589999 1.619999,1.63 -12.209999,12.209999z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="@layout/fingerprint_dialog_content" />
</FrameLayout>
<LinearLayout
android:id="@+id/buttonPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:gravity="bottom"
style="?android:attr/buttonBarStyle">
<Space
android:id="@+id/spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="invisible" />
<Button
android:id="@+id/cancel_button"
android:background="@android:color/transparent"
android:textColor="@color/success_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fingerprint_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="16dp">
<TextView
android:id="@+id/fingerprint_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="@string/fingerprint_message"
android:textColor="?android:attr/textColorSecondary" />
<ImageView
android:id="@+id/fingerprint_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_below="@+id/fingerprint_description"
android:layout_marginTop="20dp"
android:src="@drawable/ic_fp_40px" />
<TextView
android:id="@+id/fingerprint_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/fingerprint_icon"
android:layout_alignTop="@+id/fingerprint_icon"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_toEndOf="@+id/fingerprint_icon"
android:layout_toRightOf="@+id/fingerprint_icon"
android:gravity="center_vertical"
android:text="@string/fingerprint_hint"
android:textColor="@color/hint_color" />
</RelativeLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="warning_color">#f4511e</color>
<color name="hint_color">#42000000</color>
<color name="success_color">#009688</color>
</resources>

View File

@ -0,0 +1,7 @@
<resources>
<string name="fingerprint_cancel">Cancel</string>
<string name="fingerprint_message">Confirm fingerprint to continue</string>
<string name="fingerprint_hint">Touch sensor</string>
<string name="fingerprint_recognized">Fingerprint recognized</string>
<string name="fingerprint_not_recognized">Fingerprint not recognized. Try again</string>
</resources>

View File

@ -0,0 +1,5 @@
<resources>
<style name="BiometricsDialog" parent="@style/Theme.AppCompat.Light.Dialog">
<item name="android:windowNoTitle">false</item>
</style>
</resources>

82
index.d.ts vendored
View File

@ -1,67 +1,49 @@
export default interface ReactNativeBiometrics {
declare module 'react-native-biometrics' {
/**
* Enum for touch id sensor type
*/
TouchID: string;
const TouchID: string;
/**
* Enum for face id sensor type
*/
FaceID: string;
const FaceID: string;
/**
* Enum for generic biometrics (this is the only value available on android)
* Returns promise that resolves to null, TouchID, or FaceID
* @returns {Promise} Promise that resolves to null, TouchID, or FaceID
*/
Biometrics: string;
function isSensorAvailable(): Promise<string>;
/**
* Returns promise that resolves to an object with object.biometryType = Biometrics | TouchID | FaceID
* @returns {Promise<Object>} Promise that resolves to null, TouchID, or FaceID
* Prompts user with biometrics dialog using the passed in prompt message if
* it is provided, returns promise that resolves to the public key of the
* newly generated key pair
* @param {string} promptMessage
* @returns {Promise} Promise that resolves to newly generated public key
*/
isSensorAvailable(): Promise<Object>;
/**
* Creates a public private key pair,returns promise that resolves to
* an object with object.publicKey, which is the public key of the newly generated key pair
* @returns {Promise<Object>} Promise that resolves to newly generated public key
function createKeys(promptMessage?: string): Promise<string>;
/**
* Returns promise that resolves to true or false indicating if the keys
* were properly deleted
* @returns {Promise} Promise that resolves to true or false
*/
createKeys(): Promise<Object>;
/**
* Returns promise that resolves to an object with object.keysExists = true | false
* indicating if the keys were found to exist or not
* @returns {Promise<Object>} Promise that resolves to true or false
*/
biometricKeysExist(): Promise<Object>;
/**
* Returns promise that resolves to an object with true | false
* indicating if the keys were properly deleted
* @returns {Promise<Object>} Promise that resolves to true or false
*/
deleteKeys(): Promise<Object>;
function deleteKeys(): Promise<boolean>;
/**
* Prompts user with biometrics dialog using the passed in prompt message and
* returns promise that resolves to an object with object.signature,
* which is cryptographic signature of the payload
* @param {Object} createSignatureOptions
* @param {string} createSignatureOptions.promptMessage
* @param {string} createSignatureOptions.payload
* @param {string} createSignatureOptions.cancelButtonText (Android only)
* @returns {Promise<Object>} Promise that resolves to cryptographic signature
* returns promise that resolves to a cryptographic signature of the payload
* @param {string} promptMessage
* @param {string} payload
* @returns {Promise} Promise that resolves to cryptographic signature
*/
createSignature(createSignatureOptions: Object): Promise<Object>;
/**
function createSignature(
promptMessage: string,
payload: string
): Promise<string>;
/**
* Prompts user with biometrics dialog using the passed in prompt message and
* returns promise that resolves to an object with object.success = true if the user passes,
* object.success = false if the user cancels, and rejects if anything fails
* @param {Object} simplePromptOptions
* @param {string} simplePromptOptions.promptMessage
* @param {string} simplePromptOptions.cancelButtonText (Android only)
* @returns {Promise<Object>} Promise that resolves to true if the user passes, resolves to false
* if the user cancels, and rejects if anything fails
* returns promise that resolves if the user passes, and
* rejects if the user fails or cancels
* @param {string} promptMessage
* @returns {Promise} Promise that resolves if the user passes, and
* rejects if the user fails or cancels
*/
simplePrompt(simplePromptOptions: Object): Promise<Object>;
function simplePrompt(promptMessage: string): Promise<boolean>;
}

View File

@ -13,77 +13,49 @@ export default {
*/
FaceID: 'FaceID',
/**
* Enum for generic biometrics (this is the only value available on android)
*/
Biometrics: 'Biometrics',
/**
* Returns promise that resolves to an object with object.biometryType = Biometrics | TouchID | FaceID
* @returns {Promise<Object>} Promise that resolves to null, TouchID, or FaceID
* Returns promise that resolves to null, TouchID, or FaceID
* @returns {Promise} Promise that resolves to null, TouchID, or FaceID
*/
isSensorAvailable: () => {
return ReactNativeBiometrics.isSensorAvailable()
},
/**
* Creates a public private key pair,returns promise that resolves to
* an object with object.publicKey, which is the public key of the newly generated key pair
* @returns {Promise<Object>} Promise that resolves to newly generated public key
* Prompts user with biometrics dialog using the passed in prompt message if
* it is provided, returns promise that resolves to the public key of the
* newly generated key pair
* @param {string} promptMessage
* @returns {Promise} Promise that resolves to newly generated public key
*/
createKeys: () => {
return ReactNativeBiometrics.createKeys()
createKeys: (promptMessage) => {
return ReactNativeBiometrics.createKeys(promptMessage)
},
/**
* Returns promise that resolves to an object with object.keysExists = true | false
* indicating if the keys were found to exist or not
* @returns {Promise<Object>} Promise that resolves to true or false
*/
biometricKeysExist: () => {
return ReactNativeBiometrics.biometricKeysExist()
},
/**
* Returns promise that resolves to an object with true | false
* indicating if the keys were properly deleted
* @returns {Promise<Object>} Promise that resolves to true or false
* Returns promise that resolves to true or false indicating if the keys
* were properly deleted
* @returns {Promise} Promise that resolves to true or false
*/
deleteKeys: () => {
return ReactNativeBiometrics.deleteKeys()
},
/**
* Prompts user with biometrics dialog using the passed in prompt message and
* returns promise that resolves to an object with object.signature,
* which is cryptographic signature of the payload
* @param {Object} createSignatureOptions
* @param {string} createSignatureOptions.promptMessage
* @param {string} createSignatureOptions.payload
* @param {string} createSignatureOptions.cancelButtonText (Android only)
* @returns {Promise<Object>} Promise that resolves to cryptographic signature
* returns promise that resolves to a cryptographic signature of the payload
* @param {string} promptMessage
* @param {string} payload
* @returns {Promise} Promise that resolves to cryptographic signature
*/
createSignature: (createSignatureOptions) => {
if (!createSignatureOptions.cancelButtonText) {
createSignatureOptions.cancelButtonText = 'Cancel'
}
return ReactNativeBiometrics.createSignature(createSignatureOptions)
createSignature: (promptMessage, payload) => {
return ReactNativeBiometrics.createSignature(promptMessage, payload)
},
/**
* Prompts user with biometrics dialog using the passed in prompt message and
* returns promise that resolves to an object with object.success = true if the user passes,
* object.success = false if the user cancels, and rejects if anything fails
* @param {Object} simplePromptOptions
* @param {string} simplePromptOptions.promptMessage
* @param {string} simplePromptOptions.cancelButtonText (Android only)
* @returns {Promise<Object>} Promise that resolves to true if the user passes, resolves to false
* if the user cancels, and rejects if anything fails
* returns promise that resolves if the user passes, and
* rejects if the user fails or cancels
* @param {string} promptMessage
* @returns {Promise} Promise that resolves if the user passes, and
* rejects if the user fails or cancels
*/
simplePrompt: (simplePromptOptions) => {
if (!simplePromptOptions.cancelButtonText) {
simplePromptOptions.cancelButtonText = 'Cancel'
}
return ReactNativeBiometrics.simplePrompt(simplePromptOptions)
simplePrompt: (promptMessage) => {
return ReactNativeBiometrics.simplePrompt(promptMessage)
}
}

View File

@ -7,7 +7,6 @@
#import "ReactNativeBiometrics.h"
#import <LocalAuthentication/LocalAuthentication.h>
#import <Security/Security.h>
#import <React/RCTConvert.h>
@implementation ReactNativeBiometrics
@ -16,106 +15,96 @@ RCT_EXPORT_MODULE(ReactNativeBiometrics);
RCT_EXPORT_METHOD(isSensorAvailable:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
LAContext *context = [[LAContext alloc] init];
NSError *la_error = nil;
BOOL canEvaluatePolicy = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&la_error];
if (canEvaluatePolicy) {
NSString *biometryType = [self getBiometryType:context];
NSDictionary *result = @{
@"available": @(YES),
@"biometryType": biometryType
};
resolve(result);
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:NULL]) {
resolve([self getBiometryType:context]);
} else {
NSString *errorMessage = [NSString stringWithFormat:@"%@", la_error];
NSDictionary *result = @{
@"available": @(NO),
@"error": errorMessage
};
resolve(result);
resolve(Nil);
}
}
RCT_EXPORT_METHOD(createKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
RCT_EXPORT_METHOD(createKeys: (NSString *)promptMessage resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CFErrorRef error = NULL;
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
kSecAccessControlBiometryAny, &error);
if (sacObject == NULL || error != NULL) {
NSString *errorString = [NSString stringWithFormat:@"SecItemAdd can't create sacObject: %@", error];
reject(@"storage_error", errorString, nil);
return;
}
NSData *biometricKeyTag = [self getBiometricKeyTag];
NSDictionary *keyAttributes = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA,
(id)kSecAttrKeySizeInBits: @2048,
(id)kSecPrivateKeyAttrs: @{
(id)kSecAttrIsPermanent: @YES,
(id)kSecUseAuthenticationUI: (id)kSecUseAuthenticationUIAllow,
(id)kSecAttrApplicationTag: biometricKeyTag,
(id)kSecAttrAccessControl: (__bridge_transfer id)sacObject
}
};
[self deleteBiometricKey];
NSError *gen_error = nil;
id privateKey = CFBridgingRelease(SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyAttributes, (void *)&gen_error));
if(privateKey != nil) {
id publicKey = CFBridgingRelease(SecKeyCopyPublicKey((SecKeyRef)privateKey));
CFDataRef publicKeyDataRef = SecKeyCopyExternalRepresentation((SecKeyRef)publicKey, nil);
NSData *publicKeyData = (__bridge NSData *)publicKeyDataRef;
NSData *publicKeyDataWithHeader = [self addHeaderPublickey:publicKeyData];
NSString *publicKeyString = [publicKeyDataWithHeader base64EncodedStringWithOptions:0];
NSDictionary *result = @{
@"publicKey": publicKeyString,
};
resolve(result);
if (((NSNull *) promptMessage == [NSNull null]) || (promptMessage == nil) || [promptMessage length] == 0) {
[self createAndStoreKeyPair:resolve rejecter:reject];
} else {
NSString *message = [NSString stringWithFormat:@"Key generation error: %@", gen_error];
reject(@"storage_error", message, nil);
LAContext *context = [[LAContext alloc] init];
context.localizedFallbackTitle = @"";
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:promptMessage reply:^(BOOL success, NSError *fingerprintError) {
if (success) {
[self createAndStoreKeyPair:resolve rejecter:reject];
} else {
reject(@"fingerprint_error", @"Could not confirm fingerprint", nil);
}
}];
}
});
}
- (void) createAndStoreKeyPair:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
CFErrorRef error = NULL;
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
kSecAccessControlBiometryAny, &error);
if (sacObject == NULL || error != NULL) {
NSString *errorString = [NSString stringWithFormat:@"SecItemAdd can't create sacObject: %@", error];
reject(@"storage_error", errorString, nil);
return;
}
NSData *biometricKeyTag = [self getBiometricKeyTag];
NSDictionary *keyAttributes = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA,
(id)kSecAttrKeySizeInBits: @2048,
(id)kSecPrivateKeyAttrs: @{
(id)kSecAttrIsPermanent: @YES,
(id)kSecUseAuthenticationUI: (id)kSecUseAuthenticationUIAllow,
(id)kSecAttrApplicationTag: biometricKeyTag,
(id)kSecAttrAccessControl: (__bridge_transfer id)sacObject
}
};
[self deleteBiometricKey];
NSError *gen_error = nil;
id privateKey = CFBridgingRelease(SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyAttributes, (void *)&gen_error));
if(privateKey != nil) {
id publicKey = CFBridgingRelease(SecKeyCopyPublicKey((SecKeyRef)privateKey));
CFDataRef publicKeyDataRef = SecKeyCopyExternalRepresentation((SecKeyRef)publicKey, nil);
NSData *publicKeyData = (__bridge NSData *)publicKeyDataRef;
NSData *publicKeyDataWithHeader = [self addHeaderPublickey:publicKeyData];
NSString *publicKeyString = [publicKeyDataWithHeader base64EncodedStringWithOptions:0];
resolve(publicKeyString);
} else {
NSString *message = [NSString stringWithFormat:@"Key generation error: %@", gen_error];
reject(@"storage_error", message, nil);
}
}
RCT_EXPORT_METHOD(deleteKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
BOOL biometricKeyExists = [self doesBiometricKeyExist];
BOOL biometricKeyExists = [self biometricKeyExists];
if (biometricKeyExists) {
OSStatus status = [self deleteBiometricKey];
if (status == noErr) {
NSDictionary *result = @{
@"keysDeleted": @(YES),
};
resolve(result);
resolve(@(YES));
} else {
NSString *message = [NSString stringWithFormat:@"Key not found: %@",[self keychainErrorToString:status]];
reject(@"deletion_error", message, nil);
}
} else {
NSDictionary *result = @{
@"keysDeleted": @(NO),
};
resolve(result);
resolve(@(NO));
}
});
}
RCT_EXPORT_METHOD(createSignature: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
RCT_EXPORT_METHOD(createSignature: (NSString *)promptMessage payload:(NSString *)payload resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *promptMessage = [RCTConvert NSString:params[@"promptMessage"]];
NSString *payload = [RCTConvert NSString:params[@"payload"]];
NSData *biometricKeyTag = [self getBiometricKeyTag];
NSDictionary *query = @{
(id)kSecClass: (id)kSecClassKey,
@ -134,90 +123,50 @@ RCT_EXPORT_METHOD(createSignature: (NSDictionary *)params resolver:(RCTPromiseRe
if (signature != nil) {
NSString *signatureString = [signature base64EncodedStringWithOptions:0];
NSDictionary *result = @{
@"success": @(YES),
@"signature": signatureString
};
resolve(result);
} else if (error.code == errSecUserCanceled) {
NSDictionary *result = @{
@"success": @(NO),
@"error": @"User cancellation"
};
resolve(result);
resolve(signatureString);
} else {
NSString *message = [NSString stringWithFormat:@"Signature error: %@", error];
reject(@"signature_error", message, nil);
}
} else {
}
else {
NSString *message = [NSString stringWithFormat:@"Key not found: %@",[self keychainErrorToString:status]];
reject(@"storage_error", message, nil);
}
});
}
RCT_EXPORT_METHOD(simplePrompt: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
RCT_EXPORT_METHOD(simplePrompt: (NSString *)promptMessage resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *promptMessage = [RCTConvert NSString:params[@"promptMessage"]];
LAContext *context = [[LAContext alloc] init];
context.localizedFallbackTitle = @"";
[context evaluatePolicy:LAPolicyDeviceOwnerAuthentication localizedReason:promptMessage reply:^(BOOL success, NSError *biometricError) {
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:promptMessage reply:^(BOOL success, NSError *fingerprintError) {
if (success) {
NSDictionary *result = @{
@"success": @(YES)
};
resolve(result);
} else if (biometricError.code == LAErrorUserCancel) {
NSDictionary *result = @{
@"success": @(NO),
@"error": @"User cancellation"
};
resolve(result);
resolve(@(YES));
} else {
NSString *message = [NSString stringWithFormat:@"%@", biometricError];
reject(@"biometric_error", message, nil);
reject(@"fingerprint_error", @"Could not confirm fingerprint", nil);
}
}];
});
}
RCT_EXPORT_METHOD(biometricKeysExist: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
BOOL biometricKeyExists = [self doesBiometricKeyExist];
if (biometricKeyExists) {
NSDictionary *result = @{
@"keysExist": @(YES)
};
resolve(result);
} else {
NSDictionary *result = @{
@"keysExist": @(NO)
};
resolve(result);
}
});
}
- (NSData *) getBiometricKeyTag {
NSString *biometricKeyAlias = @"com.rnbiometrics.biometricKey";
NSData *biometricKeyTag = [biometricKeyAlias dataUsingEncoding:NSUTF8StringEncoding];
return biometricKeyTag;
}
- (BOOL) doesBiometricKeyExist {
- (BOOL) biometricKeyExists {
NSData *biometricKeyTag = [self getBiometricKeyTag];
NSDictionary *searchQuery = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrApplicationTag: biometricKeyTag,
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA,
(id)kSecUseAuthenticationUI: (id)kSecUseAuthenticationUIFail
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA
};
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
return status == errSecSuccess || status == errSecInteractionNotAllowed;
return status == errSecSuccess;
}
-(OSStatus) deleteBiometricKey {

3126
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,14 @@
{
"name": "react-native-biometrics",
"version": "2.0.0",
"version": "1.7.0",
"summary": "A React Native library for biometrics",
"description": "React Native biometric functionality for signing and encryption",
"main": "index.js",
"main": "lib/index.js",
"types": "index.d.ts",
"scripts": {
"clean": "rm -rf lib && rm -rf node_modules && npm install",
"release": "npm run clean && npm publish"
"build": "babel index.js -d lib",
"release": "npm run clean && npm run build && npm publish"
},
"keywords": [
"react-native",
@ -33,10 +34,14 @@
},
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.60.0"
"react-native": ">=0.52.0"
},
"bugs": {
"url": "https://github.com/SelfLender/react-native-biometrics/issues"
},
"devDependencies": {}
"devDependencies": {
"@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5",
"metro-react-native-babel-preset": "^0.56.0"
}
}

View File

@ -11,7 +11,7 @@ Pod::Spec.new do |s|
s.license = package['license']
s.homepage = package['homepage']
s.source = { :git => 'https://github.com/SelfLender/react-native-biometrics.git', :tag => "#{s.version}" }
s.platform = :ios, '10.0'
s.platform = :ios, '9.0'
s.source_files = 'ios/**/*.{h,m}'
s.dependency 'React'
end