Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f62b0b193e | ||
|
|
570dedf776 | ||
|
|
ddb99c1023 | ||
|
|
abb82dbe1c | ||
|
|
be700bbe1f | ||
|
|
b2aa464b01 | ||
|
|
eeb9e73620 | ||
|
|
06bae65564 | ||
|
|
7a97da053e | ||
|
|
74c2b30d8c | ||
|
|
37d5cf338e | ||
|
|
1e62c42082 | ||
|
|
8179ac1b58 | ||
|
|
bfbeaa4dd1 | ||
|
|
64ab242161 | ||
|
|
980d028d76 | ||
|
|
b77d5179d1 | ||
|
|
2d45e79d15 | ||
|
|
1b4eb0ac88 | ||
|
|
e8c443f160 | ||
|
|
13296cfab1 |
14
CHANGELOG.md
14
CHANGELOG.md
@ -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
265
README.md
@ -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.
|
||||
|
||||
@ -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:+'
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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 |
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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
82
index.d.ts
vendored
@ -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>;
|
||||
}
|
||||
|
||||
78
index.js
78
index.js
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
3126
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -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": {}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user