initial commit
This commit is contained in:
commit
632a9835e6
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.pbxproj -text
|
||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
*.keystore
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015-present, Self Lender, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
135
README.md
Normal file
135
README.md
Normal file
@ -0,0 +1,135 @@
|
||||
|
||||
# react-native-biometrics
|
||||
|
||||
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.
|
||||
|
||||
## Getting started
|
||||
|
||||
`$ npm install react-native-biometrics --save`
|
||||
|
||||
### Automatic installation
|
||||
|
||||
`$ 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/[...]/MainActivity.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')
|
||||
```
|
||||
|
||||
## Special configuration
|
||||
|
||||
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.
|
||||
|
||||
## Usage
|
||||
|
||||
This package is designed to make server authentication using biometrics easier. Here is an image from https://android-developers.googleblog.com/2015/10/new-in-android-samples-authenticating.html illustrating the basic use case:
|
||||
|
||||

|
||||
|
||||
When a user enrolls in biometrics, a key pair is generated. The private key is stored securely on the device and the public key is sent to a server for registration. When the user wishes to authenticate, the user is prompted for biometrics, which unlocks the securely stored private key. Then a cryptographic signature is generated and sent to the server for verification. The server then verifies the signature. If the verification was successful, the server returns an appropriate response and authorizes the user.
|
||||
|
||||
## Methods
|
||||
|
||||
### isSensorAvailable()
|
||||
|
||||
Detects what type of biometric sensor is available. Returns a `Promise` that resolves to a string representing the sensor type (`TouchID`, `FaceID`, `null`)
|
||||
|
||||
__Example__
|
||||
|
||||
```js
|
||||
import Biometrics from 'react-native-biometrics'
|
||||
|
||||
Biometrics.isSensorAvailable()
|
||||
.then((biometryType) => {
|
||||
if (biometryType === 'TouchID') {
|
||||
console.log('TouchID is supported')
|
||||
} else if (biometryType === 'FaceId') {
|
||||
console.log('FaceID is supported')
|
||||
} else {
|
||||
console.log('Biometrics not supported')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### createKeys(promptMessage)
|
||||
|
||||
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.
|
||||
|
||||
__Arguments__
|
||||
|
||||
- `promptMessage` - string that will be displayed in the fingerprint or face id prompt
|
||||
|
||||
__Example__
|
||||
|
||||
```js
|
||||
import Biometrics from 'react-native-biometrics'
|
||||
|
||||
Biometrics.createKeys('Confirm fingerprint')
|
||||
.then((publicKey) => {
|
||||
console.log(publicKey)
|
||||
sendPublicKeyToServer(publicKey)
|
||||
})
|
||||
```
|
||||
|
||||
### deleteKeys()
|
||||
|
||||
Deletes the generated keys from the device keystore. Returns a `Promise` that resolves to `true` or `false` indicating if the deletion was successful
|
||||
|
||||
__Example__
|
||||
|
||||
```js
|
||||
import Biometrics from 'react-native-biometrics'
|
||||
|
||||
Biometrics.deleteKeys()
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
console.log('Successful deletion')
|
||||
} else {
|
||||
console.log('Unsuccessful deletion')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### createSignature(promptMessage, payload)
|
||||
|
||||
Prompts the user for their fingerprint or face id in order to retrieve the private key from the keystore, then uses the private key to generate a RSA PKCS#1v1.5 SHA 256 signature. Returns a `Promise` that resolves to a base64 encoded string representing the signature.
|
||||
|
||||
__Arguments__
|
||||
|
||||
- `promptMessage` - string that will be displayed in the fingerprint or face id prompt
|
||||
- `payload` - string of data to signed by the RSA signature
|
||||
|
||||
__Example__
|
||||
|
||||
```js
|
||||
import Biometrics from 'react-native-biometrics'
|
||||
|
||||
let epochTimeSeconds = Math.round((new Date()).getTime() / 1000).toString()
|
||||
let payload = epochTimeSeconds + 'some message'
|
||||
|
||||
Biometrics.createKeys('Sign in', payload)
|
||||
.then((signature) => {
|
||||
console.log(signature)
|
||||
verifySignatureWithServer(signature, payload)
|
||||
})
|
||||
```
|
||||
36
android/build.gradle
Normal file
36
android/build.gradle
Normal file
@ -0,0 +1,36 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
description = 'react-native-biometrics'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.3.1'
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.1"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 22
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.facebook.react:react-native:+'
|
||||
}
|
||||
7
android/src/main/AndroidManifest.xml
Normal file
7
android/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.rnbiometrics">
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<uses-feature android:name="android.hardware.fingerprint" android:required="false"/>
|
||||
</manifest>
|
||||
@ -0,0 +1,199 @@
|
||||
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.util.Base64;
|
||||
|
||||
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 java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.RSAKeyGenParameterSpec;
|
||||
|
||||
/**
|
||||
* Created by brandon on 4/5/18.
|
||||
*/
|
||||
|
||||
public class ReactNativeBiometrics extends ReactContextBaseJavaModule {
|
||||
|
||||
protected String biometricKeyAlias = "biometric_key";
|
||||
|
||||
public ReactNativeBiometrics(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ReactNativeBiometrics";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isSensorAvailable(Promise promise) {
|
||||
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();
|
||||
|
||||
KeyguardManager keyguardManager = (KeyguardManager) reactApplicationContext.getSystemService(Context.KEYGUARD_SERVICE);
|
||||
Boolean hasProtectedLockscreen = keyguardManager.isKeyguardSecure();
|
||||
|
||||
if (isHardwareDetected && hasFingerprints && hasProtectedLockscreen) {
|
||||
promise.resolve("TouchID");
|
||||
} else {
|
||||
promise.resolve(null);
|
||||
}
|
||||
} else {
|
||||
promise.resolve(null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
promise.reject("Error detecting fingerprint availability: " + e.getMessage(), "Error detecting fingerprint availability");
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void createKeys(String title, Promise promise) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ReactNativeBiometricsDialog dialog = new ReactNativeBiometricsDialog();
|
||||
dialog.init(title, null, getCreationCallback(promise));
|
||||
Activity activity = getCurrentActivity();
|
||||
dialog.show(activity.getFragmentManager(), "fingerprint_dialog");
|
||||
} else {
|
||||
promise.reject("cannot generate keys on android versions below 6.0", "cannot generate keys on android versions below 6.0");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
promise.reject("error generating public private keys: " + e.getMessage(), "error generating public private keys");
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void deleteKeys(Promise promise) {
|
||||
boolean deletionSuccessful = deleteBiometricKey();
|
||||
if (deletionSuccessful) {
|
||||
promise.resolve(true);
|
||||
} else {
|
||||
promise.reject("Error deleting biometric key from keystore", "Error deleting biometric key from keystore");
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null);
|
||||
signature.initSign(privateKey);
|
||||
|
||||
FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(signature);
|
||||
|
||||
ReactNativeBiometricsDialog dialog = new ReactNativeBiometricsDialog();
|
||||
dialog.init(title, cryptoObject, getSignatureCallback(payload, promise));
|
||||
|
||||
Activity activity = getCurrentActivity();
|
||||
dialog.show(activity.getFragmentManager(), "fingerprint_dialog");
|
||||
} else {
|
||||
promise.reject("cannot generate keys on android versions below 6.0", "cannot generate keys on android versions below 6.0");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
promise.reject("error signing payload: " + e.getMessage(), "error generating signature");
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean deleteBiometricKey() {
|
||||
try {
|
||||
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
|
||||
keyStore.load(null);
|
||||
|
||||
keyStore.deleteEntry(biometricKeyAlias);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
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");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.rnbiometrics;
|
||||
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
|
||||
/**
|
||||
* Created by brandon on 4/9/18.
|
||||
*/
|
||||
|
||||
public interface ReactNativeBiometricsCallback {
|
||||
|
||||
void onAuthenticated(FingerprintManager.CryptoObject cryptoObject);
|
||||
|
||||
void onCancel();
|
||||
|
||||
void onError();
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package com.rnbiometrics;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.DialogFragment;
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import com.rnbiometrics.R;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Do not create a new Fragment when the Activity is re-created such as orientation changes.
|
||||
setRetainInstance(true);
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.BiometricsDialog);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable 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) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
biometricAuthenticationHelper = new ReactNativeBiometricsHelper(
|
||||
activity.getSystemService(FingerprintManager.class),
|
||||
(ImageView) view.findViewById(R.id.fingerprint_icon),
|
||||
(TextView) view.findViewById(R.id.fingerprint_status),
|
||||
this
|
||||
);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@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 onAuthenticated(FingerprintManager.CryptoObject cryptoObject) {
|
||||
dismiss();
|
||||
biometricAuthCallback.onAuthenticated(cryptoObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
biometricAuthCallback.onCancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
dismiss();
|
||||
biometricAuthCallback.onError();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
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;
|
||||
private boolean currentlyListening;
|
||||
|
||||
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;
|
||||
currentlyListening = true;
|
||||
|
||||
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;
|
||||
}
|
||||
if (currentlyListening) {
|
||||
currentlyListening = false;
|
||||
callback.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errMsgId, CharSequence errString) {
|
||||
currentlyListening = false;
|
||||
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) {
|
||||
currentlyListening = false;
|
||||
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,31 @@
|
||||
package com.rnbiometrics;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by brandon on 4/5/18.
|
||||
*/
|
||||
|
||||
public class ReactNativeBiometricsPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(
|
||||
ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new ReactNativeBiometrics(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
}
|
||||
BIN
android/src/main/res/drawable-hdpi/ic_fp_40px.png
Normal file
BIN
android/src/main/res/drawable-hdpi/ic_fp_40px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
BIN
android/src/main/res/drawable-mdpi/ic_fp_40px.png
Normal file
BIN
android/src/main/res/drawable-mdpi/ic_fp_40px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/src/main/res/drawable-xhdpi/ic_fp_40px.png
Normal file
BIN
android/src/main/res/drawable-xhdpi/ic_fp_40px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
android/src/main/res/drawable-xxhdpi/ic_fp_40px.png
Normal file
BIN
android/src/main/res/drawable-xxhdpi/ic_fp_40px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
android/src/main/res/drawable-xxxhdpi/ic_fp_40px.png
Normal file
BIN
android/src/main/res/drawable-xxxhdpi/ic_fp_40px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
28
android/src/main/res/drawable/ic_fingerprint_error.xml
Normal file
28
android/src/main/res/drawable/ic_fingerprint_error.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2015 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40.0dp"
|
||||
android:height="40.0dp"
|
||||
android:viewportWidth="40.0"
|
||||
android:viewportHeight="40.0">
|
||||
<path
|
||||
android:pathData="M20.0,0.0C8.96,0.0 0.0,8.95 0.0,20.0s8.96,20.0 20.0,20.0c11.04,0.0 20.0,-8.95 20.0,-20.0S31.04,0.0 20.0,0.0z"
|
||||
android:fillColor="#F4511E"/>
|
||||
<path
|
||||
android:pathData="M21.33,29.33l-2.67,0.0l0.0,-2.67l2.67,0.0L21.33,29.33zM21.33,22.67l-2.67,0.0l0.0,-12.0l2.67,0.0L21.33,22.67z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
28
android/src/main/res/drawable/ic_fingerprint_success.xml
Normal file
28
android/src/main/res/drawable/ic_fingerprint_success.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2015 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40.0dp"
|
||||
android:height="40.0dp"
|
||||
android:viewportWidth="40.0"
|
||||
android:viewportHeight="40.0">
|
||||
<path
|
||||
android:pathData="M20.0,20.0m-20.0,0.0a20.0,20.0 0.0,1.0 1.0,40.0 0.0a20.0,20.0 0.0,1.0 1.0,-40.0 0.0"
|
||||
android:fillColor="#009688"/>
|
||||
<path
|
||||
android:pathData="M11.2,21.41l1.63,-1.619999 4.17,4.169998 10.59,-10.589999 1.619999,1.63 -12.209999,12.209999z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
57
android/src/main/res/layout/fingerprint_dialog_container.xml
Normal file
57
android/src/main/res/layout/fingerprint_dialog_container.xml
Normal file
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2015 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include layout="@layout/fingerprint_dialog_content" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/buttonPanel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:gravity="bottom"
|
||||
style="?android:attr/buttonBarStyle">
|
||||
|
||||
<Space
|
||||
android:id="@+id/spacer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancel_button"
|
||||
android:background="@android:color/transparent"
|
||||
android:textColor="@color/success_color"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
60
android/src/main/res/layout/fingerprint_dialog_content.xml
Normal file
60
android/src/main/res/layout/fingerprint_dialog_content.xml
Normal file
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2015 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fingerprint_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fingerprint_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="@string/fingerprint_message"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/fingerprint_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_below="@+id/fingerprint_description"
|
||||
android:layout_marginTop="20dp"
|
||||
android:src="@drawable/ic_fp_40px" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fingerprint_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@+id/fingerprint_icon"
|
||||
android:layout_alignTop="@+id/fingerprint_icon"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_toEndOf="@+id/fingerprint_icon"
|
||||
android:layout_toRightOf="@+id/fingerprint_icon"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/fingerprint_hint"
|
||||
android:textColor="@color/hint_color" />
|
||||
</RelativeLayout>
|
||||
6
android/src/main/res/values/colors.xml
Normal file
6
android/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="warning_color">#f4511e</color>
|
||||
<color name="hint_color">#42000000</color>
|
||||
<color name="success_color">#009688</color>
|
||||
</resources>
|
||||
7
android/src/main/res/values/strings.xml
Normal file
7
android/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
<string name="fingerprint_cancel">Cancel</string>
|
||||
<string name="fingerprint_message">Confirm fingerprint to continue</string>
|
||||
<string name="fingerprint_hint">Touch sensor</string>
|
||||
<string name="fingerprint_recognized">Fingerprint recognized</string>
|
||||
<string name="fingerprint_not_recognized">Fingerprint not recognized. Try again</string>
|
||||
</resources>
|
||||
5
android/src/main/res/values/styles.xml
Normal file
5
android/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<resources>
|
||||
<style name="BiometricsDialog" parent="@style/Theme.AppCompat.Light.Dialog">
|
||||
<item name="android:windowNoTitle">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
41
index.js
Normal file
41
index.js
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
import { NativeModules } from 'react-native'
|
||||
|
||||
const { ReactNativeBiometrics } = NativeModules
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Returns promise that resolves to null, TouchID, or FaceID
|
||||
* @returns {Promise} Promise that resolves to null, TouchID, or FaceID
|
||||
*/
|
||||
isSensorAvailable: () => {
|
||||
return ReactNativeBiometrics.isSensorAvailable()
|
||||
},
|
||||
/**
|
||||
* Prompts user with bioemtrics dialog using the passed in prompt message and
|
||||
* returns promise that resolves to newly generated public keys
|
||||
* @param {string} promptMessage
|
||||
* @returns {Promise} Promise that resolves to newly generated public key
|
||||
*/
|
||||
createKeys: (promptMessage) => {
|
||||
return ReactNativeBiometrics.createKeys(promptMessage)
|
||||
},
|
||||
/**
|
||||
* Returns promise that resolves to true or false indicating if the keys
|
||||
* were properly deleted
|
||||
* @returns {Promise} Promise that resolves to true or false
|
||||
*/
|
||||
deleteKeys: () => {
|
||||
return ReactNativeBiometrics.deleteKeys()
|
||||
},
|
||||
/**
|
||||
* Prompts user with bioemtrics 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
|
||||
*/
|
||||
createSignature: (promptMessage, payload) => {
|
||||
return ReactNativeBiometrics.createSignature(promptMessage, payload)
|
||||
}
|
||||
}
|
||||
11
ios/ReactNativeBiometrics.h
Normal file
11
ios/ReactNativeBiometrics.h
Normal file
@ -0,0 +1,11 @@
|
||||
//
|
||||
// ReactNativeBiometrics.h
|
||||
//
|
||||
// Created by Brandon Hines on 4/3/18.
|
||||
//
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface ReactNativeBiometrics : NSObject <RCTBridgeModule>
|
||||
|
||||
@end
|
||||
177
ios/ReactNativeBiometrics.m
Normal file
177
ios/ReactNativeBiometrics.m
Normal file
@ -0,0 +1,177 @@
|
||||
//
|
||||
// ReactNativeBiometrics.m
|
||||
//
|
||||
// Created by Brandon Hines on 4/3/18.
|
||||
//
|
||||
|
||||
#import "ReactNativeBiometrics.h"
|
||||
#import <LocalAuthentication/LocalAuthentication.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
@implementation ReactNativeBiometrics
|
||||
|
||||
RCT_EXPORT_MODULE(ReactNativeBiometrics);
|
||||
|
||||
RCT_EXPORT_METHOD(isSensorAvailable:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
LAContext *context = [[LAContext alloc] init];
|
||||
|
||||
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:NULL]) {
|
||||
resolve([self getBiometryType:context]);
|
||||
} else {
|
||||
resolve(Nil);
|
||||
}
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(createKeys: (NSString *)promptMessage resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LAContext *context = [[LAContext alloc] init];
|
||||
|
||||
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:promptMessage reply:^(BOOL success, NSError *fingerprintError) {
|
||||
if (success) {
|
||||
CFErrorRef error = NULL;
|
||||
|
||||
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
|
||||
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
|
||||
kSecAccessControlTouchIDAny, &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;
|
||||
NSString *publicKeyString = [publicKeyData base64EncodedStringWithOptions:0];
|
||||
resolve(publicKeyString);
|
||||
} else {
|
||||
NSString *message = [NSString stringWithFormat:@"Key generation error: %@", gen_error];
|
||||
reject(@"storage_error", message, nil);
|
||||
}
|
||||
} else {
|
||||
reject(@"fingerprint_error", @"Could not confirm fingerprint", nil);
|
||||
}
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(deleteKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
OSStatus status = [self deleteBiometricKey];
|
||||
|
||||
if (status == noErr) {
|
||||
resolve(@(YES));
|
||||
} else {
|
||||
NSString *message = [NSString stringWithFormat:@"Key not found: %@",[self keychainErrorToString:status]];
|
||||
reject(@"deletion_error", message, nil);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(createSignature: (NSString *)promptMessage payload:(NSString *)payload resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSData *biometricKeyTag = [self getBiometricKeyTag];
|
||||
NSDictionary *query = @{
|
||||
(id)kSecClass: (id)kSecClassKey,
|
||||
(id)kSecAttrApplicationTag: biometricKeyTag,
|
||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA,
|
||||
(id)kSecReturnRef: @YES,
|
||||
(id)kSecUseOperationPrompt: promptMessage
|
||||
};
|
||||
SecKeyRef privateKey;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey);
|
||||
|
||||
if (status == errSecSuccess) {
|
||||
NSError *error;
|
||||
NSData *dataToSign = [payload dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *signature = CFBridgingRelease(SecKeyCreateSignature(privateKey, kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256, (CFDataRef)dataToSign, (void *)&error));
|
||||
|
||||
if (signature != nil) {
|
||||
NSString *signatureString = [signature base64EncodedStringWithOptions:0];
|
||||
resolve(signatureString);
|
||||
} else {
|
||||
NSString *message = [NSString stringWithFormat:@"Signature error: %@", error];
|
||||
reject(@"signature_error", message, nil);
|
||||
}
|
||||
}
|
||||
else {
|
||||
NSString *message = [NSString stringWithFormat:@"Key not found: %@",[self keychainErrorToString:status]];
|
||||
reject(@"storage_error", message, nil);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (NSData *) getBiometricKeyTag {
|
||||
NSString *biometricKeyAlias = @"com.rnbiometrics.biometricKey";
|
||||
NSData *biometricKeyTag = [biometricKeyAlias dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return biometricKeyTag;
|
||||
}
|
||||
|
||||
-(OSStatus) deleteBiometricKey {
|
||||
NSData *biometricKeyTag = [self getBiometricKeyTag];
|
||||
NSDictionary *deleteQuery = @{
|
||||
(id)kSecClass: (id)kSecClassKey,
|
||||
(id)kSecAttrApplicationTag: biometricKeyTag,
|
||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA
|
||||
};
|
||||
|
||||
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
|
||||
return status;
|
||||
}
|
||||
|
||||
- (NSString *)getBiometryType:(LAContext *)context
|
||||
{
|
||||
if (@available(iOS 11, *)) {
|
||||
return (context.biometryType == LABiometryTypeFaceID) ? @"FaceID" : @"TouchID";
|
||||
}
|
||||
|
||||
return @"TouchID";
|
||||
}
|
||||
|
||||
- (NSString *)keychainErrorToString:(OSStatus)error {
|
||||
NSString *message = [NSString stringWithFormat:@"%ld", (long)error];
|
||||
|
||||
switch (error) {
|
||||
case errSecSuccess:
|
||||
message = @"success";
|
||||
break;
|
||||
|
||||
case errSecDuplicateItem:
|
||||
message = @"error item already exists";
|
||||
break;
|
||||
|
||||
case errSecItemNotFound :
|
||||
message = @"error item not found";
|
||||
break;
|
||||
|
||||
case errSecAuthFailed:
|
||||
message = @"error item authentication failed";
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@end
|
||||
259
ios/ReactNativeBiometrics.xcodeproj/project.pbxproj
Normal file
259
ios/ReactNativeBiometrics.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,259 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
B3E7B58A1CC2AC0600A0062D /* ReactNativeBiometrics.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* ReactNativeBiometrics.m */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
58B511D91A9E6C8500147676 /* CopyFiles */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "include/$(PRODUCT_NAME)";
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
134814201AA4EA6300B7C361 /* libReactNativeBiometrics.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactNativeBiometrics.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B3E7B5881CC2AC0600A0062D /* ReactNativeBiometrics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReactNativeBiometrics.h; sourceTree = "<group>"; };
|
||||
B3E7B5891CC2AC0600A0062D /* ReactNativeBiometrics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReactNativeBiometrics.m; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
58B511D81A9E6C8500147676 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
134814211AA4EA7D00B7C361 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
134814201AA4EA6300B7C361 /* libReactNativeBiometrics.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
58B511D21A9E6C8500147676 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B3E7B5881CC2AC0600A0062D /* ReactNativeBiometrics.h */,
|
||||
B3E7B5891CC2AC0600A0062D /* ReactNativeBiometrics.m */,
|
||||
134814211AA4EA7D00B7C361 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
58B511DA1A9E6C8500147676 /* ReactNativeBiometrics */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeBiometrics" */;
|
||||
buildPhases = (
|
||||
58B511D71A9E6C8500147676 /* Sources */,
|
||||
58B511D81A9E6C8500147676 /* Frameworks */,
|
||||
58B511D91A9E6C8500147676 /* CopyFiles */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = ReactNativeBiometrics;
|
||||
productName = RCTDataManager;
|
||||
productReference = 134814201AA4EA6300B7C361 /* libReactNativeBiometrics.a */;
|
||||
productType = "com.apple.product-type.library.static";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
58B511D31A9E6C8500147676 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0830;
|
||||
ORGANIZATIONNAME = Facebook;
|
||||
TargetAttributes = {
|
||||
58B511DA1A9E6C8500147676 = {
|
||||
CreatedOnToolsVersion = 6.1.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeBiometrics" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
);
|
||||
mainGroup = 58B511D21A9E6C8500147676;
|
||||
productRefGroup = 58B511D21A9E6C8500147676;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
58B511DA1A9E6C8500147676 /* ReactNativeBiometrics */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
58B511D71A9E6C8500147676 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B3E7B58A1CC2AC0600A0062D /* ReactNativeBiometrics.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
58B511ED1A9E6C8500147676 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
58B511EE1A9E6C8500147676 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
58B511F01A9E6C8500147676 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../../../React/**",
|
||||
"$(SRCROOT)/../../react-native/React/**",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_NAME = ReactNativeBiometrics;
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
58B511F11A9E6C8500147676 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../../../React/**",
|
||||
"$(SRCROOT)/../../react-native/React/**",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_NAME = ReactNativeBiometrics;
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeBiometrics" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
58B511ED1A9E6C8500147676 /* Debug */,
|
||||
58B511EE1A9E6C8500147676 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeBiometrics" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
58B511F01A9E6C8500147676 /* Debug */,
|
||||
58B511F11A9E6C8500147676 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 58B511D31A9E6C8500147676 /* Project object */;
|
||||
}
|
||||
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "react-native-biometrics",
|
||||
"version": "1.0.0",
|
||||
"description": "React Native biometric functionality for signing and encryption",
|
||||
"main": "index.js",
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"android",
|
||||
"ios",
|
||||
"biometrics",
|
||||
"authentication",
|
||||
"auth",
|
||||
"fingerprint",
|
||||
"touch-id",
|
||||
"face-id"
|
||||
],
|
||||
"homepage": "https://github.com/SelfLender/react-native-biometrics",
|
||||
"contributors": [],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/SelfLender/react-native-biometrics.git"
|
||||
},
|
||||
"private": false,
|
||||
"author": "Brandon Hines",
|
||||
"files": [
|
||||
"android/",
|
||||
"ios/",
|
||||
"index.js"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react-native": "^0.41.2"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/SelfLender/react-native-biometrics/issues"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user