Compare commits

..

21 Commits
1.x ... master

Author SHA1 Message Date
marcosrdz
f62b0b193e Update ReactNativeBiometrics.m 2020-06-18 20:36:53 -04:00
Brandon Hines
570dedf776
Merge pull request #54 from SelfLender/android-biometric-prompt
react native biometrics 2.0
2019-11-19 16:23:44 -06:00
Brandon Hines
ddb99c1023 added the native android error message to all rejections 2019-11-19 16:08:19 -06:00
Brandon Hines
abb82dbe1c fixed issue where biometric prompt displayed when checking for key existence 2019-11-19 16:02:51 -06:00
Brandon Hines
be700bbe1f removed babel transpiling and fixed typescript definition for IDE introspection 2019-11-19 16:02:12 -06:00
Brandon Hines
b2aa464b01 some readme cleanup 2019-11-19 14:25:39 -06:00
Brandon Hines
eeb9e73620 updated changelog and readme in preparation for 2.0 release 2019-11-19 12:35:19 -06:00
Brandon Hines
06bae65564 updated version for 2.0 2019-11-18 16:51:23 -06:00
Brandon Hines
7a97da053e updated jsdocs for new function signatures 2019-11-18 15:27:01 -06:00
Brandon Hines
74c2b30d8c refactored functions to return response objects for better extensibility 2019-11-18 12:25:03 -06:00
Brandon Hines
37d5cf338e refactored isSensorAvailable to return more descriptive result object in iOS 2019-11-15 17:25:37 -06:00
Brandon Hines
1e62c42082 refactored functions to take options objects instead of specific parameters 2019-11-15 16:59:39 -06:00
Brandon Hines
8179ac1b58 added a public function to check if keys exist and some ios code cleanup 2019-11-15 16:24:07 -06:00
Brandon Hines
bfbeaa4dd1 some code cleanup 2019-11-15 15:45:18 -06:00
Brandon Hines
64ab242161 properly handled the cancelled scenario for simple prompt 2019-11-15 14:41:46 -06:00
Brandon Hines
980d028d76 deleted old UI code that is no longer used 2019-11-12 23:57:47 -06:00
Brandon Hines
b77d5179d1 refactored createSignature to use the androidx biometric prompt api 2019-11-12 23:29:24 -06:00
Brandon Hines
2d45e79d15 refactored create keys to always create keys without prompting for biometrics 2019-11-12 23:26:31 -06:00
Brandon Hines
1b4eb0ac88 refactored isSensorAvailable to use androix biometric manager 2019-11-12 22:56:56 -06:00
Brandon Hines
e8c443f160 refactored simple prompt to use androix biometric prompt api 2019-11-12 22:32:20 -06:00
Brandon Hines
13296cfab1 updated gradle and android sdk versions 2019-11-05 16:18:36 -06:00
30 changed files with 680 additions and 3935 deletions

View File

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

View File

@ -1,6 +1,19 @@
# 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
@ -97,3 +110,4 @@ 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

265
README.md
View File

@ -3,50 +3,38 @@
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`
### Automatic installation
### Link / Autolinking
`$ 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')
```
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.
## Additional configuration
#### iOS
This package requires an iOS target SDK verion of iOS 10 or higher
This package requires an iOS target SDK version 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 return `null` 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 indicate biometrics is unavailable until the face id permission is specifically allowed for the app by the user.
#### Android
This package requires a compiled SDK version of 23 (Android 6.0 Marshmallow) or higher
This package requires a compiled SDK version of 29 (Android 10.0) or higher
## Usage
@ -58,30 +46,50 @@ When a user enrolls in biometrics, a key pair is generated. The private key is
## Constants
### TouchID
### TouchID (iOS only)
A constant for the touch id sensor type, evaluates to `'TouchID'`
__Example__
```js
import Biometrics from 'react-native-biometrics'
import ReactNativeBiometrics from 'react-native-biometrics'
if (biometryType === Biometrics.TouchID) {
const { biometryType } = await ReactNativeBiometrics.isSensorAvailable()
if (biometryType === ReactNativeBiometrics.TouchID) {
//do something fingerprint specific
}
```
### FaceID
### FaceID (iOS only)
A constant for the face id sensor type, evaluates to `'FaceID'`
__Example__
```js
import Biometrics from 'react-native-biometrics'
import ReactNativeBiometrics from 'react-native-biometrics'
if (biometryType === Biometrics.FaceID) {
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) {
//do something face id specific
}
```
@ -90,110 +98,209 @@ if (biometryType === Biometrics.FaceID) {
### isSensorAvailable()
Detects what type of biometric sensor is available. Returns a `Promise` that resolves to a string representing the sensor type (`TouchID`, `FaceID`, `null`)
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. |
__Example__
```js
import Biometrics from 'react-native-biometrics'
import ReactNativeBiometrics from 'react-native-biometrics'
Biometrics.isSensorAvailable()
.then((biometryType) => {
if (biometryType === Biometrics.TouchID) {
ReactNativeBiometrics.isSensorAvailable()
.then((resultObject) => {
const { available, biometryType } = resultObject
if (available && biometryType === ReactNativeBiometrics.TouchID) {
console.log('TouchID is supported')
} else if (biometryType === Biometrics.FaceID) {
} else if (available && biometryType === ReactNativeBiometrics.FaceID) {
console.log('FaceID is supported')
} else if (available && biometryType === ReactNativeBiometrics.Biometrics) {
console.log('Biometrics is supported')
} else {
console.log('Biometrics not supported')
}
})
```
### createKeys([promptMessage])
### createKeys()
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.
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.
__Arguments__
__Result Object__
- `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.
| Property | Type | Description |
| --- | --- | --- |
| publicKey | string | A base64 encoded string representing the public key |
__Example__
```js
import Biometrics from 'react-native-biometrics'
import ReactNativeBiometrics from 'react-native-biometrics'
Biometrics.createKeys('Confirm fingerprint')
.then((publicKey) => {
ReactNativeBiometrics.createKeys('Confirm fingerprint')
.then((resultObject) => {
const { publicKey } = resultObject
console.log(publicKey)
sendPublicKeyToServer(publicKey)
})
```
### deleteKeys()
### biometricKeysExist()
Deletes the generated keys from the device keystore. Returns a `Promise` that resolves to `true` or `false` indicating if the deletion was successful
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 Biometrics from 'react-native-biometrics'
import ReactNativeBiometrics from 'react-native-biometrics'
Biometrics.deleteKeys()
.then((success) => {
if (success) {
console.log('Successful deletion')
ReactNativeBiometrics.biometricKeysExist()
.then((resultObject) => {
const { keysExist } = resultObject
if (keysExist) {
console.log('Keys exist')
} else {
console.log('Unsuccessful deletion')
console.log('Keys do not exist or were deleted')
}
})
```
### createSignature(promptMessage, payload)
### deleteKeys()
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.
Deletes the generated keys from the device keystore. Returns a `Promise` that resolves to an object indicating details about the deletion.
NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices.
__Result Object__
__Arguments__
- `promptMessage` - string that will be displayed in the fingerprint or face id prompt
- `payload` - string of data to be signed by the RSA signature
| Property | Type | Description |
| --- | --- | --- |
| keysDeleted | bool | A boolean indicating if keys were deleted from the keystore |
__Example__
```js
import Biometrics from 'react-native-biometrics'
import ReactNativeBiometrics from 'react-native-biometrics'
ReactNativeBiometrics.deleteKeys()
.then((resultObject) => {
const { keysDeleted } = resultObject
if (keysDeleted) {
console.log('Successful deletion')
} else {
console.log('Unsuccessful deletion because there were no keys to delete')
}
})
```
### createSignature(options)
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.
**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__
| 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. |
__Example__
```js
import ReactNativeBiometrics from 'react-native-biometrics'
let epochTimeSeconds = Math.round((new Date()).getTime() / 1000).toString()
let payload = epochTimeSeconds + 'some message'
Biometrics.createSignature('Sign in', payload)
.then((signature) => {
console.log(signature)
verifySignatureWithServer(signature, payload)
ReactNativeBiometrics.createSignature({
promptMessage: 'Sign in',
payload: payload
})
.then((resultObject) => {
const { success, signature } = resultObject
if (success) {
console.log(signature)
verifySignatureWithServer(signature, payload)
}
})
```
### simplePrompt(promptMessage)
### simplePrompt(options)
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.
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.
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.
__Arguments__
__Options Object__
- `promptMessage` - string that will be displayed in the fingerprint or face id prompt
| 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. |
__Example__
```js
import Biometrics from 'react-native-biometrics'
import ReactNativeBiometrics from 'react-native-biometrics'
Biometrics.simplePrompt('Confirm fingerprint')
.then(() => {
console.log('successful fingerprint provided')
ReactNativeBiometrics.simplePrompt('Confirm fingerprint')
.then((resultObject) => {
const { success } = resultObject
if (success) {
console.log('successful biometrics provided')
} else {
console.log('user cancelled biometric prompt')
}
})
.catch(() => {
console.log('fingerprint failed or prompt was cancelled')
console.log('biometrics failed')
})
```
### 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 = 28
def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3"
def DEFAULT_COMPILE_SDK_VERSION = 29
def DEFAULT_BUILD_TOOLS_VERSION = "29.0.2"
def DEFAULT_MIN_SDK_VERSION = 16
def DEFAULT_TARGET_SDK_VERSION = 28
def DEFAULT_TARGET_SDK_VERSION = 29
buildscript {
repositories {
@ -14,7 +14,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.android.tools.build:gradle:3.4.2'
}
}
@ -37,5 +37,6 @@ 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-4.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,7 +1,6 @@
<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

@ -0,0 +1,58 @@
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,20 +1,24 @@
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;
@ -23,6 +27,8 @@ 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.
@ -46,41 +52,66 @@ public class ReactNativeBiometrics extends ReactContextBaseJavaModule {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ReactApplicationContext reactApplicationContext = getReactApplicationContext();
FingerprintManager fingerprintManager = reactApplicationContext.getSystemService(FingerprintManager.class);
Boolean isHardwareDetected = fingerprintManager.isHardwareDetected();
Boolean hasFingerprints = fingerprintManager.hasEnrolledFingerprints();
BiometricManager biometricManager = BiometricManager.from(reactApplicationContext);
int canAuthenticate = biometricManager.canAuthenticate();
KeyguardManager keyguardManager = (KeyguardManager) reactApplicationContext.getSystemService(Context.KEYGUARD_SERVICE);
Boolean hasProtectedLockscreen = keyguardManager.isKeyguardSecure();
if (isHardwareDetected && hasFingerprints && hasProtectedLockscreen) {
promise.resolve("TouchID");
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("available", true);
resultMap.putString("biometryType", "Biometrics");
promise.resolve(resultMap);
} else {
promise.resolve(null);
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);
}
} else {
promise.resolve(null);
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("available", false);
resultMap.putString("error", "Unsupported android version");
promise.resolve(resultMap);
}
} catch (Exception e) {
promise.reject("Error detecting fingerprint availability: " + e.getMessage(), "Error detecting fingerprint availability");
promise.reject("Error detecting biometrics availability: " + e.getMessage(), "Error detecting biometrics availability: " + e.getMessage());
}
}
@ReactMethod
public void createKeys(String title, Promise promise) {
public void createKeys(Promise promise) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
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");
}
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);
} else {
promise.reject("Cannot generate keys on android versions below 6.0", "Cannot generate keys on android versions below 6.0");
}
@ -91,62 +122,110 @@ public class ReactNativeBiometrics extends ReactContextBaseJavaModule {
@ReactMethod
public void deleteKeys(Promise promise) {
if (biometricKeyExists()) {
if (doesBiometricKeyExist()) {
boolean deletionSuccessful = deleteBiometricKey();
if (deletionSuccessful) {
promise.resolve(true);
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("keysDeleted", true);
promise.resolve(resultMap);
} else {
promise.reject("Error deleting biometric key from keystore", "Error deleting biometric key from keystore");
}
} else {
promise.resolve(false);
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("keysDeleted", false);
promise.resolve(resultMap);
}
}
@ReactMethod
public void createSignature(String title, String payload, Promise promise) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Signature signature = Signature.getInstance("SHA256withRSA");
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
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");
PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null);
signature.initSign(privateKey);
Signature signature = Signature.getInstance("SHA256withRSA");
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(signature);
PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null);
signature.initSign(privateKey);
ReactNativeBiometricsDialog dialog = new ReactNativeBiometricsDialog();
dialog.init(title, cryptoObject, getSignatureCallback(payload, promise));
BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(signature);
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 signing payload: " + e.getMessage(), "Error generating 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(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");
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");
}
}
protected boolean biometricKeyExists() {
@ReactMethod
public void biometricKeysExist(Promise promise) {
try {
boolean doesBiometricKeyExist = doesBiometricKeyExist();
WritableMap resultMap = new WritableNativeMap();
resultMap.putBoolean("keysExist", doesBiometricKeyExist);
promise.resolve(resultMap);
} catch (Exception e) {
promise.reject("Error checking if biometric key exists: " + e.getMessage(), "Error checking if biometric key exists: " + e.getMessage());
}
}
protected boolean doesBiometricKeyExist() {
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
@ -168,91 +247,4 @@ 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

@ -1,16 +0,0 @@
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

@ -1,117 +0,0 @@
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

@ -1,108 +0,0 @@
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

@ -0,0 +1,39 @@
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.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,28 +0,0 @@
<?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

@ -1,28 +0,0 @@
<?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

@ -1,57 +0,0 @@
<?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

@ -1,60 +0,0 @@
<?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

@ -1,6 +0,0 @@
<?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

@ -1,7 +0,0 @@
<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

@ -1,5 +0,0 @@
<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,49 +1,67 @@
declare module 'react-native-biometrics' {
export default interface ReactNativeBiometrics {
/**
* Enum for touch id sensor type
*/
const TouchID: string;
TouchID: string;
/**
* Enum for face id sensor type
*/
const FaceID: string;
FaceID: string;
/**
* Returns promise that resolves to null, TouchID, or FaceID
* @returns {Promise} Promise that resolves to null, TouchID, or FaceID
* Enum for generic biometrics (this is the only value available on android)
*/
function isSensorAvailable(): Promise<string>;
Biometrics: string;
/**
* 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
* Returns promise that resolves to an object with object.biometryType = Biometrics | TouchID | FaceID
* @returns {Promise<Object>} Promise that resolves to null, TouchID, or FaceID
*/
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
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 deleteKeys(): Promise<boolean>;
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>;
/**
* Prompts user with biometrics dialog using the passed in prompt message and
* 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
* 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
*/
function createSignature(
promptMessage: string,
payload: string
): Promise<string>;
/**
createSignature(createSignatureOptions: Object): Promise<Object>;
/**
* Prompts user with biometrics dialog using the passed in prompt message and
* 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
* 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
*/
function simplePrompt(promptMessage: string): Promise<boolean>;
simplePrompt(simplePromptOptions: Object): Promise<Object>;
}

View File

@ -13,49 +13,77 @@ export default {
*/
FaceID: 'FaceID',
/**
* Returns promise that resolves to null, TouchID, or FaceID
* @returns {Promise} Promise that resolves to null, TouchID, or 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
*/
isSensorAvailable: () => {
return ReactNativeBiometrics.isSensorAvailable()
},
/**
* 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
* 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
*/
createKeys: (promptMessage) => {
return ReactNativeBiometrics.createKeys(promptMessage)
createKeys: () => {
return ReactNativeBiometrics.createKeys()
},
/**
* Returns promise that resolves to true or false indicating if the keys
* were properly deleted
* @returns {Promise} Promise that resolves to true or false
* 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
*/
deleteKeys: () => {
return ReactNativeBiometrics.deleteKeys()
},
/**
* Prompts user with biometrics dialog using the passed in prompt message and
* 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
* 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
*/
createSignature: (promptMessage, payload) => {
return ReactNativeBiometrics.createSignature(promptMessage, payload)
createSignature: (createSignatureOptions) => {
if (!createSignatureOptions.cancelButtonText) {
createSignatureOptions.cancelButtonText = 'Cancel'
}
return ReactNativeBiometrics.createSignature(createSignatureOptions)
},
/**
* Prompts user with biometrics dialog using the passed in prompt message and
* 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
* 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
*/
simplePrompt: (promptMessage) => {
return ReactNativeBiometrics.simplePrompt(promptMessage)
simplePrompt: (simplePromptOptions) => {
if (!simplePromptOptions.cancelButtonText) {
simplePromptOptions.cancelButtonText = 'Cancel'
}
return ReactNativeBiometrics.simplePrompt(simplePromptOptions)
}
}

View File

@ -7,6 +7,7 @@
#import "ReactNativeBiometrics.h"
#import <LocalAuthentication/LocalAuthentication.h>
#import <Security/Security.h>
#import <React/RCTConvert.h>
@implementation ReactNativeBiometrics
@ -15,96 +16,106 @@ 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 ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:NULL]) {
resolve([self getBiometryType:context]);
if (canEvaluatePolicy) {
NSString *biometryType = [self getBiometryType:context];
NSDictionary *result = @{
@"available": @(YES),
@"biometryType": biometryType
};
resolve(result);
} else {
resolve(Nil);
NSString *errorMessage = [NSString stringWithFormat:@"%@", la_error];
NSDictionary *result = @{
@"available": @(NO),
@"error": errorMessage
};
resolve(result);
}
}
RCT_EXPORT_METHOD(createKeys: (NSString *)promptMessage resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
RCT_EXPORT_METHOD(createKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (((NSNull *) promptMessage == [NSNull null]) || (promptMessage == nil) || [promptMessage length] == 0) {
[self createAndStoreKeyPair:resolve rejecter:reject];
} else {
LAContext *context = [[LAContext alloc] init];
context.localizedFallbackTitle = @"";
CFErrorRef error = NULL;
[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);
}
}];
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);
} else {
NSString *message = [NSString stringWithFormat:@"Key generation error: %@", gen_error];
reject(@"storage_error", message, 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 biometricKeyExists];
BOOL biometricKeyExists = [self doesBiometricKeyExist];
if (biometricKeyExists) {
OSStatus status = [self deleteBiometricKey];
if (status == noErr) {
resolve(@(YES));
NSDictionary *result = @{
@"keysDeleted": @(YES),
};
resolve(result);
} else {
NSString *message = [NSString stringWithFormat:@"Key not found: %@",[self keychainErrorToString:status]];
reject(@"deletion_error", message, nil);
}
} else {
resolve(@(NO));
NSDictionary *result = @{
@"keysDeleted": @(NO),
};
resolve(result);
}
});
}
RCT_EXPORT_METHOD(createSignature: (NSString *)promptMessage payload:(NSString *)payload resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
RCT_EXPORT_METHOD(createSignature: (NSDictionary *)params 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,
@ -123,50 +134,90 @@ RCT_EXPORT_METHOD(createSignature: (NSString *)promptMessage payload:(NSString *
if (signature != nil) {
NSString *signatureString = [signature base64EncodedStringWithOptions:0];
resolve(signatureString);
NSDictionary *result = @{
@"success": @(YES),
@"signature": signatureString
};
resolve(result);
} else if (error.code == errSecUserCanceled) {
NSDictionary *result = @{
@"success": @(NO),
@"error": @"User cancellation"
};
resolve(result);
} 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: (NSString *)promptMessage resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
RCT_EXPORT_METHOD(simplePrompt: (NSDictionary *)params 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:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:promptMessage reply:^(BOOL success, NSError *fingerprintError) {
[context evaluatePolicy:LAPolicyDeviceOwnerAuthentication localizedReason:promptMessage reply:^(BOOL success, NSError *biometricError) {
if (success) {
resolve(@(YES));
NSDictionary *result = @{
@"success": @(YES)
};
resolve(result);
} else if (biometricError.code == LAErrorUserCancel) {
NSDictionary *result = @{
@"success": @(NO),
@"error": @"User cancellation"
};
resolve(result);
} else {
reject(@"fingerprint_error", @"Could not confirm fingerprint", nil);
NSString *message = [NSString stringWithFormat:@"%@", biometricError];
reject(@"biometric_error", message, 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) biometricKeyExists {
- (BOOL) doesBiometricKeyExist {
NSData *biometricKeyTag = [self getBiometricKeyTag];
NSDictionary *searchQuery = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrApplicationTag: biometricKeyTag,
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA,
(id)kSecUseAuthenticationUI: (id)kSecUseAuthenticationUIFail
};
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
return status == errSecSuccess;
return status == errSecSuccess || status == errSecInteractionNotAllowed;
}
-(OSStatus) deleteBiometricKey {

3126
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

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, '9.0'
s.platform = :ios, '10.0'
s.source_files = 'ios/**/*.{h,m}'
s.dependency 'React'
end