diff --git a/CHANGELOG.md b/CHANGELOG.md index 1971785..41ae70f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Changelog All notable changes to this project will be documented in this file. +## [2.2.0] - 2020-02-10 +## Changed +- iOS + + Fixed compatability issue with XCode 12 + + Added optional passcode fallback for iOS devices when FaceID or TouchID fails and the device has a passcode set. + + Added `fallbackPromptMessage` to `simplePrompt`. This controls the message that is shown when FaceID or TouchID has failed and the prompt falls back to the device passcode for authentication. +- Android + + Upgraded androidx.biometric 1.1.0 + * Added `allowDeviceCredentials` option, for android devices, to `isSensorAvailable`, `createSignature` and `simplePrompt`. This option is only affects devices running API 30 or greater. Devices running API 29 or less cannot support device credentials when performing crypto based authentication. See https://developer.android.com/reference/androidx/biometric/BiometricPrompt.PromptInfo.Builder#setAllowedAuthenticators(int) + + Updated `build.gradle` file to avoid unnecessary downloads and potential conflicts when the library is included as a module dependency in an application project. + ## [2.1.4] - 2020-02-10 ## Changed - Removed duplicate onAuthenticationError call in android diff --git a/README.md b/README.md index 9af669b..d13a5cc 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ or npm: `$ npm install react-native-biometrics --save` +### Install pods + +`$ npx pod-install` + ### Link / Autolinking 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. @@ -30,7 +34,7 @@ On React Native 0.60+ the [CLI autolink feature](https://github.com/react-native 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 indicate biometrics is unavailable until the face id permission is specifically allowed for the app by the user. +Ensure that you have the `NSFaceIDUsageDescription` entry set in your react native iOS project, or Face ID will not work properly. This description will be 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 @@ -44,7 +48,7 @@ This package is designed to make server authentication using biometrics easier. 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. -## Constants +## Biometry Types ### TouchID (iOS only) @@ -53,11 +57,13 @@ A constant for the touch id sensor type, evaluates to `'TouchID'` __Example__ ```js -import ReactNativeBiometrics from 'react-native-biometrics' +import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics' + +const rnBiometrics = new ReactNativeBiometrics() -const { biometryType } = await ReactNativeBiometrics.isSensorAvailable() +const { biometryType } = await rnBiometrics.isSensorAvailable() -if (biometryType === ReactNativeBiometrics.TouchID) { +if (biometryType === BiometryTypes.TouchID) { //do something fingerprint specific } ``` @@ -69,11 +75,13 @@ A constant for the face id sensor type, evaluates to `'FaceID'` __Example__ ```js -import ReactNativeBiometrics from 'react-native-biometrics' +import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics' + +const rnBiometrics = new ReactNativeBiometrics() -const { biometryType } = await ReactNativeBiometrics.isSensorAvailable() +const { biometryType } = await rnBiometrics.isSensorAvailable() -if (biometryType === ReactNativeBiometrics.FaceID) { +if (biometryType === BiometryTypes.FaceID) { //do something face id specific } ``` @@ -85,16 +93,37 @@ A constant for generic Biometrics, evaluates to `'Biometrics'` __Example__ ```js -import ReactNativeBiometrics from 'react-native-biometrics' +import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics' -const { biometryType } = await ReactNativeBiometrics.isSensorAvailable() +const rnBiometrics = new ReactNativeBiometrics() -if (biometryType === ReactNativeBiometrics.Biometrics) { +const { biometryType } = await rnBiometrics.isSensorAvailable() + +if (biometryType === BiometryTypes.Biometrics) { //do something face id specific } ``` -## Methods +## Class + +## Constructor + +__Options Object__ +| Parameter | Type | Description | iOS | Android | +| --- | --- | --- | --- | --- | +| allowDeviceCredentials | boolean | Boolean that will enable the ability for the device passcode to be used instead of biometric information. On iOS, the prompt will only be shown after biometrics has failed twice. On Android, the prompt will be shown on the biometric prompt and does not require the user to attempt to use biometrics information first. Note: This feature is not supported on Android versions prior to API 30. | ✔ | ✔ | + +__Example__ + +```js +import ReactNativeBiometrics from 'react-native-biometrics' + +const rnBiometrics = new ReactNativeBiometrics({ allowDeviceCredentials: true }) + +// Perform operations as normal +// All prompts will allow for fallback to the device's credentials for authentication + +``` ### isSensorAvailable() @@ -111,17 +140,19 @@ __Result Object__ __Example__ ```js -import ReactNativeBiometrics from 'react-native-biometrics' +import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics' -ReactNativeBiometrics.isSensorAvailable() +const rnBiometrics = new ReactNativeBiometrics() + +rnBiometrics.isSensorAvailable() .then((resultObject) => { const { available, biometryType } = resultObject - if (available && biometryType === ReactNativeBiometrics.TouchID) { + if (available && biometryType === BiometryTypes.TouchID) { console.log('TouchID is supported') - } else if (available && biometryType === ReactNativeBiometrics.FaceID) { + } else if (available && biometryType === BiometryTypes.FaceID) { console.log('FaceID is supported') - } else if (available && biometryType === ReactNativeBiometrics.Biometrics) { + } else if (available && biometryType === BiometryTypes.Biometrics) { console.log('Biometrics is supported') } else { console.log('Biometrics not supported') @@ -144,7 +175,9 @@ __Example__ ```js import ReactNativeBiometrics from 'react-native-biometrics' -ReactNativeBiometrics.createKeys('Confirm fingerprint') +const rnBiometrics = new ReactNativeBiometrics() + +rnBiometrics.createKeys() .then((resultObject) => { const { publicKey } = resultObject console.log(publicKey) @@ -167,7 +200,8 @@ __Example__ ```js import ReactNativeBiometrics from 'react-native-biometrics' -ReactNativeBiometrics.biometricKeysExist() +const rnBiometrics = new ReactNativeBiometrics() +rnBiometrics.biometricKeysExist() .then((resultObject) => { const { keysExist } = resultObject @@ -194,7 +228,9 @@ __Example__ ```js import ReactNativeBiometrics from 'react-native-biometrics' -ReactNativeBiometrics.deleteKeys() +const rnBiometrics = new ReactNativeBiometrics() + +rnBiometrics.deleteKeys() .then((resultObject) => { const { keysDeleted } = resultObject @@ -236,7 +272,9 @@ import ReactNativeBiometrics from 'react-native-biometrics' let epochTimeSeconds = Math.round((new Date()).getTime() / 1000).toString() let payload = epochTimeSeconds + 'some message' -ReactNativeBiometrics.createSignature({ +const rnBiometrics = new ReactNativeBiometrics() + +rnBiometrics.createSignature({ promptMessage: 'Sign in', payload: payload }) @@ -261,6 +299,7 @@ __Options Object__ | Parameter | Type | Description | iOS | Android | | --- | --- | --- | --- | --- | | promptMessage | string | Message that will be displayed in the biometrics prompt | ✔ | ✔ | +| fallbackPromptMessage | string | Message that will be shown when FaceID or TouchID has failed and a passcode has been set on the device. | ✔ | ✖ | | cancelButtonText | string | Text to be displayed for the cancel button on biometric prompts, defaults to `Cancel` | ✖ | ✔ | __Result Object__ @@ -275,7 +314,9 @@ __Example__ ```js import ReactNativeBiometrics from 'react-native-biometrics' -ReactNativeBiometrics.simplePrompt({promptMessage: 'Confirm fingerprint'}) +const rnBiometrics = new ReactNativeBiometrics() + +rnBiometrics.simplePrompt({promptMessage: 'Confirm fingerprint'}) .then((resultObject) => { const { success } = resultObject diff --git a/android/build.gradle b/android/build.gradle index 053c455..7e6d6ac 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,29 +2,34 @@ apply plugin: 'com.android.library' description = 'react-native-biometrics' -def DEFAULT_COMPILE_SDK_VERSION = 29 -def DEFAULT_BUILD_TOOLS_VERSION = "29.0.2" -def DEFAULT_MIN_SDK_VERSION = 16 -def DEFAULT_TARGET_SDK_VERSION = 29 - buildscript { - repositories { - google() - jcenter() + // The Android Gradle plugin is only required when opening the android folder stand-alone. + // This avoids unnecessary downloads and potential conflicts when the library is included as a + // module dependency in an application project. + if (project == rootProject) { + repositories { + mavenCentral() + maven { url "$rootDir/../node_modules/react-native/android" } + google() + } + + dependencies { + classpath("com.android.tools.build:gradle:3.6.2") + + } } +} - dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' - } +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } android { - compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION - buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION + compileSdkVersion safeExtGet('compileSdkVersion', 29) defaultConfig { - minSdkVersion rootProject.hasProperty('minSdkVersion') ? rootProject.minSdkVersion : DEFAULT_MIN_SDK_VERSION - targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION + minSdkVersion safeExtGet('minSdkVersion', 16) + targetSdkVersion safeExtGet('targetSdkVersion', 29) } lintOptions { abortOnError false @@ -33,10 +38,11 @@ android { repositories { mavenCentral() + maven { url "$rootDir/../node_modules/react-native/android" } google() } dependencies { - implementation 'androidx.biometric:biometric:1.0.1' + implementation 'androidx.biometric:biometric:1.1.0' implementation 'com.facebook.react:react-native:+' } diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..a6979eb --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,15 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e0c4de3..0ebb310 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java index 790a373..624ecd9 100644 --- a/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java +++ b/android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java @@ -48,12 +48,13 @@ public String getName() { } @ReactMethod - public void isSensorAvailable(Promise promise) { + public void isSensorAvailable(final ReadableMap params, final Promise promise) { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (isCurrentSDKMarshmallowOrLater()) { + boolean allowDeviceCredentials = params.getBoolean("allowDeviceCredentials"); ReactApplicationContext reactApplicationContext = getReactApplicationContext(); BiometricManager biometricManager = BiometricManager.from(reactApplicationContext); - int canAuthenticate = biometricManager.canAuthenticate(); + int canAuthenticate = biometricManager.canAuthenticate(getAllowedAuthenticators(allowDeviceCredentials)); if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) { WritableMap resultMap = new WritableNativeMap(); @@ -90,9 +91,9 @@ public void isSensorAvailable(Promise promise) { } @ReactMethod - public void createKeys(Promise promise) { + public void createKeys(final ReadableMap params, Promise promise) { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (isCurrentSDKMarshmallowOrLater()) { deleteBiometricKey(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(biometricKeyAlias, KeyProperties.PURPOSE_SIGN) @@ -120,6 +121,10 @@ public void createKeys(Promise promise) { } } + private boolean isCurrentSDKMarshmallowOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } + @ReactMethod public void deleteKeys(Promise promise) { if (doesBiometricKeyExist()) { @@ -141,15 +146,16 @@ public void deleteKeys(Promise promise) { @ReactMethod public void createSignature(final ReadableMap params, final Promise promise) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (isCurrentSDKMarshmallowOrLater()) { UiThreadUtil.runOnUiThread( new Runnable() { @Override public void run() { try { - String cancelButtomText = params.getString("cancelButtonText"); String promptMessage = params.getString("promptMessage"); String payload = params.getString("payload"); + String cancelButtonText = params.getString("cancelButtonText"); + boolean allowDeviceCredentials = params.getBoolean("allowDeviceCredentials"); Signature signature = Signature.getInstance("SHA256withRSA"); KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); @@ -165,12 +171,7 @@ public void run() { 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); + biometricPrompt.authenticate(getPromptInfo(promptMessage, cancelButtonText, allowDeviceCredentials), cryptoObject); } catch (Exception e) { promise.reject("Error signing payload: " + e.getMessage(), "Error generating signature: " + e.getMessage()); } @@ -181,28 +182,47 @@ public void run() { } } + private PromptInfo getPromptInfo(String promptMessage, String cancelButtonText, boolean allowDeviceCredentials) { + PromptInfo.Builder builder = new PromptInfo.Builder().setTitle(promptMessage); + + builder.setAllowedAuthenticators(getAllowedAuthenticators(allowDeviceCredentials)); + + if (allowDeviceCredentials == false || isCurrentSDK29OrEarlier()) { + builder.setNegativeButtonText(cancelButtonText); + } + + return builder.build(); + } + + private int getAllowedAuthenticators(boolean allowDeviceCredentials) { + if (allowDeviceCredentials && !isCurrentSDK29OrEarlier()) { + return BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL; + } + return BiometricManager.Authenticators.BIOMETRIC_STRONG; + } + + private boolean isCurrentSDK29OrEarlier() { + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q; + } + @ReactMethod public void simplePrompt(final ReadableMap params, final Promise promise) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (isCurrentSDKMarshmallowOrLater()) { UiThreadUtil.runOnUiThread( new Runnable() { @Override public void run() { try { - String cancelButtomText = params.getString("cancelButtonText"); String promptMessage = params.getString("promptMessage"); + String cancelButtonText = params.getString("cancelButtonText"); + boolean allowDeviceCredentials = params.getBoolean("allowDeviceCredentials"); 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); + biometricPrompt.authenticate(getPromptInfo(promptMessage, cancelButtonText, allowDeviceCredentials)); } catch (Exception e) { promise.reject("Error displaying local biometric prompt: " + e.getMessage(), "Error displaying local biometric prompt: " + e.getMessage()); } @@ -226,14 +246,14 @@ public void biometricKeysExist(Promise promise) { } protected boolean doesBiometricKeyExist() { - try { - KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); - keyStore.load(null); - - return keyStore.containsAlias(biometricKeyAlias); - } catch (Exception e) { - return false; - } + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + return keyStore.containsAlias(biometricKeyAlias); + } catch (Exception e) { + return false; + } } protected boolean deleteBiometricKey() { diff --git a/index.ts b/index.ts index 8524bbc..ef8f260 100644 --- a/index.ts +++ b/index.ts @@ -1,80 +1,170 @@ -import { NativeModules } from 'react-native'; +import { NativeModules } from 'react-native' -const { ReactNativeBiometrics: bridge } = NativeModules; +const { ReactNativeBiometrics: bridge } = NativeModules -/** +/** * Type alias for possible biometry types */ -export type BiometryType = 'TouchID' | 'FaceID' | 'Biometrics'; +export type BiometryType = 'TouchID' | 'FaceID' | 'Biometrics' + +interface RNBiometricsOptions { + allowDeviceCredentials?: boolean +} interface IsSensorAvailableResult { - available: boolean - biometryType?: BiometryType - error?: string + available: boolean + biometryType?: BiometryType + error?: string } interface CreateKeysResult { - publicKey: string + publicKey: string } interface BiometricKeysExistResult { - keysExist: boolean + keysExist: boolean } interface DeleteKeysResult { - keysDeleted: boolean + keysDeleted: boolean } interface CreateSignatureOptions { - promptMessage: string - payload: string - cancelButtonText?: string + promptMessage: string + payload: string + cancelButtonText?: string } interface CreateSignatureResult { - success: boolean - signature?: string - error?: string + success: boolean + signature?: string + error?: string } interface SimplePromptOptions { - promptMessage: string - cancelButtonText?: string + promptMessage: string + fallbackPromptMessage?: string + cancelButtonText?: string } interface SimplePromptResult { - success: boolean - error?: string + success: boolean + error?: string } -module ReactNativeBiometrics { - /** - * Enum for touch id sensor type - */ - export const TouchID = 'TouchID'; - /** - * Enum for face id sensor type - */ - export const FaceID = 'FaceID'; +/** + * Enum for touch id sensor type + */ +export const TouchID = 'TouchID' +/** + * Enum for face id sensor type + */ +export const FaceID = 'FaceID' +/** + * Enum for generic biometrics (this is the only value available on android) + */ +export const Biometrics = 'Biometrics' + +export const BiometryTypes = { + TouchID, + FaceID, + Biometrics +} + +export module ReactNativeBiometricsLegacy { + /** + * Returns promise that resolves to an object with object.biometryType = Biometrics | TouchID | FaceID + * @returns {Promise} Promise that resolves to an object with details about biometrics available + */ + export function isSensorAvailable(): Promise { + return new ReactNativeBiometrics().isSensorAvailable() + } + + /** + * Creates a public private key pair,returns promise that resolves to + * an object with object.publicKey, which is the public key of the newly generated key pair + * @returns {Promise} Promise that resolves to object with details about the newly generated public key + */ + export function createKeys(): Promise { + return new ReactNativeBiometrics().createKeys() + } + + /** + * Returns promise that resolves to an object with object.keysExists = true | false + * indicating if the keys were found to exist or not + * @returns {Promise} Promise that resolves to object with details aobut the existence of keys + */ + export function biometricKeysExist(): Promise { + return new ReactNativeBiometrics().biometricKeysExist() + } + + /** + * Returns promise that resolves to an object with true | false + * indicating if the keys were properly deleted + * @returns {Promise} Promise that resolves to an object with details about the deletion + */ + export function deleteKeys(): Promise { + return new ReactNativeBiometrics().deleteKeys() + } + + /** + * Prompts user with biometrics dialog using the passed in prompt message and + * returns promise that resolves to an object with object.signature, + * which is cryptographic signature of the payload + * @param {Object} createSignatureOptions + * @param {string} createSignatureOptions.promptMessage + * @param {string} createSignatureOptions.payload + * @returns {Promise} Promise that resolves to an object cryptographic signature details + */ + export function createSignature(createSignatureOptions: CreateSignatureOptions): Promise { + return new ReactNativeBiometrics().createSignature(createSignatureOptions) + } + + /** + * Prompts user with biometrics dialog using the passed in prompt message and + * returns promise that resolves to an object with object.success = true if the user passes, + * object.success = false if the user cancels, and rejects if anything fails + * @param {Object} simplePromptOptions + * @param {string} simplePromptOptions.promptMessage + * @param {string} simplePromptOptions.fallbackPromptMessage + * @returns {Promise} Promise that resolves an object with details about the biometrics result + */ + export function simplePrompt(simplePromptOptions: SimplePromptOptions): Promise { + return new ReactNativeBiometrics().simplePrompt(simplePromptOptions) + } +} + +export default class ReactNativeBiometrics { + allowDeviceCredentials = false + /** - * Enum for generic biometrics (this is the only value available on android) + * @param {Object} rnBiometricsOptions + * @param {boolean} rnBiometricsOptions.allowDeviceCredentials */ - export const Biometrics = 'Biometrics'; + constructor(rnBiometricsOptions?: RNBiometricsOptions) { + const allowDeviceCredentials = rnBiometricsOptions?.allowDeviceCredentials ?? false + this.allowDeviceCredentials = allowDeviceCredentials + } /** * Returns promise that resolves to an object with object.biometryType = Biometrics | TouchID | FaceID * @returns {Promise} Promise that resolves to an object with details about biometrics available */ - export function isSensorAvailable(): Promise { - return bridge.isSensorAvailable(); + isSensorAvailable(): Promise { + return bridge.isSensorAvailable({ + allowDeviceCredentials: this.allowDeviceCredentials + }) } + /** * 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} Promise that resolves to object with details about the newly generated public key */ - export function createKeys(): Promise { - return bridge.createKeys(); + createKeys(): Promise { + return bridge.createKeys({ + allowDeviceCredentials: this.allowDeviceCredentials + }) } /** @@ -82,8 +172,8 @@ module ReactNativeBiometrics { * indicating if the keys were found to exist or not * @returns {Promise} Promise that resolves to object with details aobut the existence of keys */ - export function biometricKeysExist(): Promise { - return bridge.biometricKeysExist(); + biometricKeysExist(): Promise { + return bridge.biometricKeysExist() } /** @@ -91,8 +181,8 @@ module ReactNativeBiometrics { * indicating if the keys were properly deleted * @returns {Promise} Promise that resolves to an object with details about the deletion */ - export function deleteKeys(): Promise { - return bridge.deleteKeys(); + deleteKeys(): Promise { + return bridge.deleteKeys() } /** @@ -102,15 +192,15 @@ module ReactNativeBiometrics { * @param {Object} createSignatureOptions * @param {string} createSignatureOptions.promptMessage * @param {string} createSignatureOptions.payload - * @param {string} createSignatureOptions.cancelButtonText (Android only) * @returns {Promise} Promise that resolves to an object cryptographic signature details */ - export function createSignature(createSignatureOptions: CreateSignatureOptions): Promise { - if (!createSignatureOptions.cancelButtonText) { - createSignatureOptions.cancelButtonText = 'Cancel'; - } + createSignature(createSignatureOptions: CreateSignatureOptions): Promise { + createSignatureOptions.cancelButtonText = createSignatureOptions.cancelButtonText ?? 'Cancel' - return bridge.createSignature(createSignatureOptions); + return bridge.createSignature({ + allowDeviceCredentials: this.allowDeviceCredentials, + ...createSignatureOptions + }) } /** @@ -119,16 +209,16 @@ module ReactNativeBiometrics { * 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) + * @param {string} simplePromptOptions.fallbackPromptMessage * @returns {Promise} Promise that resolves an object with details about the biometrics result */ - export function simplePrompt(simplePromptOptions: SimplePromptOptions): Promise { - if (!simplePromptOptions.cancelButtonText) { - simplePromptOptions.cancelButtonText = 'Cancel'; - } - - return bridge.simplePrompt(simplePromptOptions); + simplePrompt(simplePromptOptions: SimplePromptOptions): Promise { + simplePromptOptions.cancelButtonText = simplePromptOptions.cancelButtonText ?? 'Cancel' + simplePromptOptions.fallbackPromptMessage = simplePromptOptions.fallbackPromptMessage ?? 'Use Passcode' + + return bridge.simplePrompt({ + allowDeviceCredentials: this.allowDeviceCredentials, + ...simplePromptOptions + }) } -} - -export default ReactNativeBiometrics; + } diff --git a/ios/ReactNativeBiometrics.m b/ios/ReactNativeBiometrics.m index e1aa016..03ec388 100644 --- a/ios/ReactNativeBiometrics.m +++ b/ios/ReactNativeBiometrics.m @@ -13,11 +13,17 @@ @implementation ReactNativeBiometrics RCT_EXPORT_MODULE(ReactNativeBiometrics); -RCT_EXPORT_METHOD(isSensorAvailable:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) -{ +RCT_EXPORT_METHOD(isSensorAvailable: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { LAContext *context = [[LAContext alloc] init]; NSError *la_error = nil; - BOOL canEvaluatePolicy = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&la_error]; + BOOL allowDeviceCredentials = [RCTConvert BOOL:params[@"allowDeviceCredentials"]]; + LAPolicy laPolicy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + + if (allowDeviceCredentials == TRUE) { + laPolicy = LAPolicyDeviceOwnerAuthentication; + } + + BOOL canEvaluatePolicy = [context canEvaluatePolicy:laPolicy error:&la_error]; if (canEvaluatePolicy) { NSString *biometryType = [self getBiometryType:context]; @@ -38,13 +44,20 @@ @implementation ReactNativeBiometrics } } -RCT_EXPORT_METHOD(createKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(createKeys: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ CFErrorRef error = NULL; + BOOL allowDeviceCredentials = [RCTConvert BOOL:params[@"allowDeviceCredentials"]]; + + SecAccessControlCreateFlags secCreateFlag = kSecAccessControlBiometryAny; + + if (allowDeviceCredentials == TRUE) { + secCreateFlag = kSecAccessControlUserPresence; + } SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, - kSecAccessControlBiometryAny, &error); + secCreateFlag, &error); if (sacObject == NULL || error != NULL) { NSString *errorString = [NSString stringWithFormat:@"SecItemAdd can't create sacObject: %@", error]; reject(@"storage_error", errorString, nil); @@ -159,11 +172,20 @@ @implementation ReactNativeBiometrics 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"]]; + NSString *fallbackPromptMessage = [RCTConvert NSString:params[@"fallbackPromptMessage"]]; + BOOL allowDeviceCredentials = [RCTConvert BOOL:params[@"allowDeviceCredentials"]]; LAContext *context = [[LAContext alloc] init]; - context.localizedFallbackTitle = @""; + LAPolicy laPolicy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + + if (allowDeviceCredentials == TRUE) { + laPolicy = LAPolicyDeviceOwnerAuthentication; + context.localizedFallbackTitle = fallbackPromptMessage; + } else { + context.localizedFallbackTitle = @""; + } - [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:promptMessage reply:^(BOOL success, NSError *biometricError) { + [context evaluatePolicy:laPolicy localizedReason:promptMessage reply:^(BOOL success, NSError *biometricError) { if (success) { NSDictionary *result = @{ @"success": @(YES) diff --git a/package-lock.json b/package-lock.json index cbbf8e8..326f922 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-native-biometrics", - "version": "2.1.4", + "version": "2.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e48f6d8..8071d5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-biometrics", - "version": "2.1.4", + "version": "2.2.0", "summary": "A React Native library for biometrics", "description": "React Native biometric functionality for signing and encryption", "main": "build/cjs/index.js", diff --git a/react-native-biometrics.podspec b/react-native-biometrics.podspec index b451efa..adc0a7f 100644 --- a/react-native-biometrics.podspec +++ b/react-native-biometrics.podspec @@ -13,5 +13,5 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/SelfLender/react-native-biometrics.git', :tag => "#{s.version}" } s.platform = :ios, '10.0' s.source_files = 'ios/**/*.{h,m}' - s.dependency 'React' + s.dependency 'React-Core' end