├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── rnbiometrics │ ├── CreateSignatureCallback.java │ ├── ReactNativeBiometrics.java │ ├── ReactNativeBiometricsPackage.java │ └── SimplePromptCallback.java ├── index.ts ├── ios ├── ReactNativeBiometrics.h ├── ReactNativeBiometrics.m └── ReactNativeBiometrics.xcodeproj │ └── project.pbxproj ├── modules.d.ts ├── package-lock.json ├── package.json ├── react-native-biometrics.podspec └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # node.js 6 | # 7 | node_modules/ 8 | npm-debug.log 9 | yarn-error.log 10 | build/ 11 | 12 | # Xcode 13 | # 14 | ios/build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | 33 | # Android/IntelliJ 34 | # 35 | android/build/ 36 | .idea 37 | .gradle 38 | local.properties 39 | *.iml 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [3.0.0] - 2022-09-06 5 | ## Changed 6 | - Re-releasing 2.2.0 as 3.0.0. 2.2.0 introduced a breaking API change and should have been released as a new major version. 7 | 8 | ### [2.2.1] - 2022-09-06 9 | ## Changed 10 | - Re-releasing 2.1.4 as 2.2.1 due to breaking API change released in 2.2.0. 2.2.0 will be re-released as 3.0.0 due to the breaking API change. 11 | 12 | ## [2.2.0] - 2020-02-10 13 | ## Changed 14 | - iOS 15 | + Fixed compatability issue with XCode 12 16 | + Added optional passcode fallback for iOS devices when FaceID or TouchID fails and the device has a passcode set. 17 | + 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. 18 | - Android 19 | + Upgraded androidx.biometric 1.1.0 20 | * 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) 21 | + Updated `build.gradle` file to avoid unnecessary downloads and potential conflicts when the library is included as a module dependency in an application project. 22 | 23 | ## [2.1.4] - 2020-02-10 24 | ## Changed 25 | - Removed duplicate onAuthenticationError call in android 26 | - Upgraded androidx.biomtric to the latest fix version 27 | 28 | ## [2.1.3] - 2020-01-27 29 | ## Changed 30 | - Fixed readme typo 31 | - Improved android prompt cancellation handling 32 | 33 | ## [2.1.2] - 2019-12-29 34 | ## Changed 35 | - Improved biometryType typescript definition 36 | 37 | ## [2.1.1] - 2019-11-20 38 | ## Changed 39 | - Fixed npm release error 40 | 41 | ## [2.1.0] - 2019-11-20 42 | ## Changed 43 | - Refactored the javascript portion of the library into typescript 44 | 45 | ## [2.0.0] - 2019-11-19 46 | ### Breaking 47 | - Requires React Native 0.60+ for androidx compatibility 48 | - All functions now take an options object and return a result object 49 | - `createSignature` no longer prompts user for biometrics, `simplePrompt` can be used in conjunction with it to achieve the same effect 50 | - `createSignature` and `simplePrompt` no longer reject on cancellation, they resolve with a success flag set to false when a user cancels a biometric prompt 51 | - Android no longer resolves to biometry type of `TouchID`, it only resolves to `Biometrics` 52 | ### Changed 53 | - Used android BiometricPrompt API for biometrics 54 | - Changed library function API 55 | - Added better support for prompt cancellations 56 | - Started to return native error messages in promise rejections 57 | 58 | ## [1.7.0] - 2019-11-5 59 | ### Changed 60 | - Removed dependency on android app compat library for compatibility with androidx 61 | - Used new access control policy for ios keystore 62 | 63 | ## [1.6.1] - 2019-8-12 64 | ### Changed 65 | - Fixed reported security issues from npm in dev dependencies 66 | 67 | ## [1.6.0] - 2019-7-10 68 | ### Changed 69 | - Disabled use password option on iOS by default 70 | - Detected if keys exists before trying to delete them and returned false in promise result in order to prevent error from occurring 71 | 72 | ## [1.5.2] - 2019-5-9 73 | ### Changed 74 | - Fixed android compilation error by re-organizing order of gradle repositories 75 | 76 | ## [1.5.1] - 2019-4-26 77 | ### Changed 78 | - Updated doc strings and type definition for the createKeys function 79 | 80 | ## [1.5.0] - 2019-4-17 81 | ### Added 82 | - Added the ability to not display a biometrics prompt when creating keys 83 | 84 | ## [1.4.0] - 2019-4-3 85 | ### Changed 86 | - Fixed reported security issues from npm 87 | - Added a dependency on appcompat-v7 in android to ensure required UI libraries are available 88 | ### Added 89 | - Added a podspec file 90 | 91 | ## [1.3.0] - 2019-1-24 92 | ### Changed 93 | - Removed src directory and moved index.js to the root 94 | - Made sure all android error messages start with a capital letter 95 | ### Added 96 | - Added a function for simply displaying a biometric prompt 97 | 98 | ## [1.2.0] - 2018-11-29 99 | ### Changed 100 | - Upgraded default android SDK version to 28 101 | - Upgraded gradle version and added the gradle wrapper 102 | - Removed npmignore files in favor of gitignore 103 | ### Added 104 | - Added the ability to override android SDK and build versions using gradle extra properties extension 105 | 106 | ## [1.1.3] - 2018-08-09 107 | ### Changed 108 | - Fixed typo in readme 109 | - Fixed reported security issues from npm 110 | ### Added 111 | - Added type script definitions 112 | 113 | ## [1.1.2] - 2018-06-14 114 | ### Changed 115 | - Fixed public key format in iOS 116 | 117 | ## [1.1.1] - 2018-06-11 118 | ### Changed 119 | - Fixed potential null pointer exception that could occur from saved android dialog fragments 120 | 121 | ## [1.1.0] - 2018-05-03 122 | ### Added 123 | - Added enums for sensor types 124 | 125 | ### Changed 126 | - Fixed IllegalState exception that occurred in android when dialog is dismissed improperly 127 | - Fixed issue where promise rejection could be called more than once on android 128 | 129 | ## [1.0.2] - 2018-04-12 130 | ### Changed 131 | - fixed typo in readme documentation 132 | 133 | ## 1.0.1 - 2018-04-12 134 | ### Added 135 | - Initial release 136 | - Added native code to detect sensor type 137 | - Added native code to create private public key pairs 138 | - Added native code to use private key to create a signature given a payload 139 | 140 | [1.0.2]: https://github.com/SelfLender/react-native-biometrics/compare/1.0.1...1.0.2 141 | [1.1.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.0.2...1.1.0 142 | [1.1.1]: https://github.com/SelfLender/react-native-biometrics/compare/1.1.0...1.1.1 143 | [1.1.2]: https://github.com/SelfLender/react-native-biometrics/compare/1.1.1...1.1.2 144 | [1.1.3]: https://github.com/SelfLender/react-native-biometrics/compare/1.1.2...1.1.3 145 | [1.2.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.1.3...1.2.0 146 | [1.3.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.2.0...1.3.0 147 | [1.4.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.3.0...1.4.0 148 | [1.5.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.4.0...1.5.0 149 | [1.5.1]: https://github.com/SelfLender/react-native-biometrics/compare/1.5.0...1.5.1 150 | [1.5.2]: https://github.com/SelfLender/react-native-biometrics/compare/1.5.1...1.5.2 151 | [1.6.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.5.2...1.6.0 152 | [1.6.1]: https://github.com/SelfLender/react-native-biometrics/compare/1.6.0...1.6.1 153 | [1.7.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.6.1...1.7.0 154 | [2.0.0]: https://github.com/SelfLender/react-native-biometrics/compare/1.7.0...2.0.0 155 | [2.1.0]: https://github.com/SelfLender/react-native-biometrics/compare/2.0.0...2.1.0 156 | [2.1.1]: https://github.com/SelfLender/react-native-biometrics/compare/2.1.0...2.1.1 157 | [2.1.2]: https://github.com/SelfLender/react-native-biometrics/compare/2.1.1...2.1.2 158 | [2.1.3]: https://github.com/SelfLender/react-native-biometrics/compare/2.1.2...2.1.3 159 | [2.1.4]: https://github.com/SelfLender/react-native-biometrics/compare/2.1.3...2.1.4 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present, Self Lender, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-native-biometrics 3 | 4 | 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. 5 | 6 | ## React Native Compatibility 7 | 8 | | `react-native-biometrics` version | Required React Native Version | 9 | |:---------------------------------:|:-----------------------------:| 10 | | `>= 2.0.0` | `>= 0.60` | 11 | | `<= 1.7.0` | `<= 0.59.x` | 12 | 13 | ## Getting started 14 | 15 | using either Yarn: 16 | 17 | `yarn add react-native-biometrics` 18 | 19 | or npm: 20 | 21 | `$ npm install react-native-biometrics --save` 22 | 23 | ### Install pods 24 | 25 | `$ npx pod-install` 26 | 27 | ### Link / Autolinking 28 | 29 | 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 | 31 | ## Additional configuration 32 | 33 | #### iOS 34 | 35 | This package requires an iOS target SDK version of iOS 10 or higher 36 | 37 | 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. 38 | 39 | #### Android 40 | 41 | This package requires a compiled SDK version of 29 (Android 10.0) or higher 42 | 43 | ## Usage 44 | 45 | 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: 46 | 47 | ![react-native-biometrics](https://2.bp.blogspot.com/-Lp2zaAZietw/Vi59hb6k6SI/AAAAAAAABLk/HsXXBYiIwqU/s1600/image01.png) 48 | 49 | 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. 50 | 51 | ## Biometry Types 52 | 53 | ### TouchID (iOS only) 54 | 55 | A constant for the touch id sensor type, evaluates to `'TouchID'` 56 | 57 | __Example__ 58 | 59 | ```js 60 | import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics' 61 | 62 | const rnBiometrics = new ReactNativeBiometrics() 63 | 64 | const { biometryType } = await rnBiometrics.isSensorAvailable() 65 | 66 | if (biometryType === BiometryTypes.TouchID) { 67 | //do something fingerprint specific 68 | } 69 | ``` 70 | 71 | ### FaceID (iOS only) 72 | 73 | A constant for the face id sensor type, evaluates to `'FaceID'` 74 | 75 | __Example__ 76 | 77 | ```js 78 | import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics' 79 | 80 | const rnBiometrics = new ReactNativeBiometrics() 81 | 82 | const { biometryType } = await rnBiometrics.isSensorAvailable() 83 | 84 | if (biometryType === BiometryTypes.FaceID) { 85 | //do something face id specific 86 | } 87 | ``` 88 | 89 | ### Biometrics (Android only) 90 | 91 | A constant for generic Biometrics, evaluates to `'Biometrics'` 92 | 93 | __Example__ 94 | 95 | ```js 96 | import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics' 97 | 98 | const rnBiometrics = new ReactNativeBiometrics() 99 | 100 | const { biometryType } = await rnBiometrics.isSensorAvailable() 101 | 102 | if (biometryType === BiometryTypes.Biometrics) { 103 | //do something face id specific 104 | } 105 | ``` 106 | 107 | ## Class 108 | 109 | ## Constructor 110 | 111 | __Options Object__ 112 | | Parameter | Type | Description | iOS | Android | 113 | | --- | --- | --- | --- | --- | 114 | | 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. | ✔ | ✔ | 115 | 116 | __Example__ 117 | 118 | ```js 119 | import ReactNativeBiometrics from 'react-native-biometrics' 120 | 121 | const rnBiometrics = new ReactNativeBiometrics({ allowDeviceCredentials: true }) 122 | 123 | // Perform operations as normal 124 | // All prompts will allow for fallback to the device's credentials for authentication 125 | 126 | ``` 127 | 128 | ### isSensorAvailable() 129 | 130 | Detects what type of biometric sensor is available. Returns a `Promise` that resolves to an object with details about biometrics availability 131 | 132 | __Result Object__ 133 | 134 | | Property | Type | Description | 135 | | --- | --- | --- | 136 | | available | bool | A boolean indicating if biometrics is available or not | 137 | | biometryType | string | A string indicating what type of biometrics is available. `TouchID`, `FaceID`, `Biometrics`, or `undefined` if biometrics is not available. | 138 | | error | string | An error message indicating why biometrics may not be available. `undefined` if there is no error. | 139 | 140 | __Example__ 141 | 142 | ```js 143 | import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics' 144 | 145 | const rnBiometrics = new ReactNativeBiometrics() 146 | 147 | rnBiometrics.isSensorAvailable() 148 | .then((resultObject) => { 149 | const { available, biometryType } = resultObject 150 | 151 | if (available && biometryType === BiometryTypes.TouchID) { 152 | console.log('TouchID is supported') 153 | } else if (available && biometryType === BiometryTypes.FaceID) { 154 | console.log('FaceID is supported') 155 | } else if (available && biometryType === BiometryTypes.Biometrics) { 156 | console.log('Biometrics is supported') 157 | } else { 158 | console.log('Biometrics not supported') 159 | } 160 | }) 161 | ``` 162 | 163 | ### createKeys() 164 | 165 | 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. 166 | 167 | __Result Object__ 168 | 169 | | Property | Type | Description | 170 | | --- | --- | --- | 171 | | publicKey | string | A base64 encoded string representing the public key | 172 | 173 | __Example__ 174 | 175 | ```js 176 | import ReactNativeBiometrics from 'react-native-biometrics' 177 | 178 | const rnBiometrics = new ReactNativeBiometrics() 179 | 180 | rnBiometrics.createKeys() 181 | .then((resultObject) => { 182 | const { publicKey } = resultObject 183 | console.log(publicKey) 184 | sendPublicKeyToServer(publicKey) 185 | }) 186 | ``` 187 | 188 | ### biometricKeysExist() 189 | 190 | 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. 191 | 192 | __Result Object__ 193 | 194 | | Property | Type | Description | 195 | | --- | --- | --- | 196 | | keysExist | bool | A boolean indicating if keys exist in the keystore | 197 | 198 | __Example__ 199 | 200 | ```js 201 | import ReactNativeBiometrics from 'react-native-biometrics' 202 | 203 | const rnBiometrics = new ReactNativeBiometrics() 204 | rnBiometrics.biometricKeysExist() 205 | .then((resultObject) => { 206 | const { keysExist } = resultObject 207 | 208 | if (keysExist) { 209 | console.log('Keys exist') 210 | } else { 211 | console.log('Keys do not exist or were deleted') 212 | } 213 | }) 214 | ``` 215 | 216 | ### deleteKeys() 217 | 218 | Deletes the generated keys from the device keystore. Returns a `Promise` that resolves to an object indicating details about the deletion. 219 | 220 | __Result Object__ 221 | 222 | | Property | Type | Description | 223 | | --- | --- | --- | 224 | | keysDeleted | bool | A boolean indicating if keys were deleted from the keystore | 225 | 226 | __Example__ 227 | 228 | ```js 229 | import ReactNativeBiometrics from 'react-native-biometrics' 230 | 231 | const rnBiometrics = new ReactNativeBiometrics() 232 | 233 | rnBiometrics.deleteKeys() 234 | .then((resultObject) => { 235 | const { keysDeleted } = resultObject 236 | 237 | if (keysDeleted) { 238 | console.log('Successful deletion') 239 | } else { 240 | console.log('Unsuccessful deletion because there were no keys to delete') 241 | } 242 | }) 243 | ``` 244 | 245 | ### createSignature(options) 246 | 247 | 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. 248 | 249 | **NOTE: No biometric prompt is displayed in iOS simulators when attempting to retrieve keys for signature generation, it only occurs on actual devices. 250 | 251 | __Options Object__ 252 | 253 | | Parameter | Type | Description | iOS | Android | 254 | | --- | --- | --- | --- | --- | 255 | | promptMessage | string | Message that will be displayed in the fingerprint or face id prompt | ✔ | ✔ | 256 | | payload | string | String of data to be signed by the RSA signature | ✔ | ✔ | 257 | | cancelButtonText | string | Text to be displayed for the cancel button on biometric prompts, defaults to `Cancel` | ✖ | ✔ | 258 | 259 | __Result Object__ 260 | 261 | | Property | Type | Description | 262 | | --- | --- | --- | 263 | | success | bool | A boolean indicating if the process was successful, `false` if the users cancels the biometrics prompt | 264 | | signature | string | A base64 encoded string representing the signature. `undefined` if the process was not successful. | 265 | | error | string | An error message indicating reasons why signature creation failed. `undefined` if there is no error. | 266 | 267 | __Example__ 268 | 269 | ```js 270 | import ReactNativeBiometrics from 'react-native-biometrics' 271 | 272 | let epochTimeSeconds = Math.round((new Date()).getTime() / 1000).toString() 273 | let payload = epochTimeSeconds + 'some message' 274 | 275 | const rnBiometrics = new ReactNativeBiometrics() 276 | 277 | rnBiometrics.createSignature({ 278 | promptMessage: 'Sign in', 279 | payload: payload 280 | }) 281 | .then((resultObject) => { 282 | const { success, signature } = resultObject 283 | 284 | if (success) { 285 | console.log(signature) 286 | verifySignatureWithServer(signature, payload) 287 | } 288 | }) 289 | ``` 290 | 291 | ### simplePrompt(options) 292 | 293 | 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. 294 | 295 | **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. 296 | 297 | __Options Object__ 298 | 299 | | Parameter | Type | Description | iOS | Android | 300 | | --- | --- | --- | --- | --- | 301 | | promptMessage | string | Message that will be displayed in the biometrics prompt | ✔ | ✔ | 302 | | fallbackPromptMessage | string | Message that will be shown when FaceID or TouchID has failed and a passcode has been set on the device. | ✔ | ✖ | 303 | | cancelButtonText | string | Text to be displayed for the cancel button on biometric prompts, defaults to `Cancel` | ✖ | ✔ | 304 | 305 | __Result Object__ 306 | 307 | | Property | Type | Description | 308 | | --- | --- | --- | 309 | | success | bool | A boolean indicating if the biometric prompt succeeded, `false` if the users cancels the biometrics prompt | 310 | | error | string | An error message indicating why the biometric prompt failed. `undefined` if there is no error. | 311 | 312 | __Example__ 313 | 314 | ```js 315 | import ReactNativeBiometrics from 'react-native-biometrics' 316 | 317 | const rnBiometrics = new ReactNativeBiometrics() 318 | 319 | rnBiometrics.simplePrompt({promptMessage: 'Confirm fingerprint'}) 320 | .then((resultObject) => { 321 | const { success } = resultObject 322 | 323 | if (success) { 324 | console.log('successful biometrics provided') 325 | } else { 326 | console.log('user cancelled biometric prompt') 327 | } 328 | }) 329 | .catch(() => { 330 | console.log('biometrics failed') 331 | }) 332 | ``` 333 | 334 | ### Troubleshooting 335 | 336 | - 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`: 337 | 338 | ``` 339 | dependencies { 340 | implementation fileTree(dir: "libs", include: ["*.jar"]) 341 | implementation "com.facebook.react:react-native:+" // From node_modules 342 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" // temp fix 343 | ... 344 | } 345 | ``` 346 | 347 | - 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. 348 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | description = 'react-native-biometrics' 4 | 5 | buildscript { 6 | // The Android Gradle plugin is only required when opening the android folder stand-alone. 7 | // This avoids unnecessary downloads and potential conflicts when the library is included as a 8 | // module dependency in an application project. 9 | if (project == rootProject) { 10 | repositories { 11 | mavenCentral() 12 | maven { url "$rootDir/../node_modules/react-native/android" } 13 | google() 14 | } 15 | 16 | dependencies { 17 | classpath("com.android.tools.build:gradle:3.6.2") 18 | 19 | } 20 | } 21 | } 22 | 23 | def safeExtGet(prop, fallback) { 24 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 25 | } 26 | 27 | android { 28 | compileSdkVersion safeExtGet('compileSdkVersion', 29) 29 | 30 | defaultConfig { 31 | minSdkVersion safeExtGet('minSdkVersion', 16) 32 | targetSdkVersion safeExtGet('targetSdkVersion', 29) 33 | } 34 | lintOptions { 35 | abortOnError false 36 | } 37 | } 38 | 39 | repositories { 40 | mavenCentral() 41 | maven { url "$rootDir/../node_modules/react-native/android" } 42 | google() 43 | } 44 | 45 | dependencies { 46 | implementation 'androidx.biometric:biometric:1.1.0' 47 | implementation 'com.facebook.react:react-native:+' 48 | } 49 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # AndroidX package structure to make it clearer which packages are bundled with the 11 | # Android operating system, and which are packaged with your app's APK 12 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 13 | android.useAndroidX=true 14 | # Automatically convert third-party libraries to use AndroidX 15 | android.enableJetifier=true 16 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stytchauth/react-native-biometrics/b7eb0254a97e14a1e4d40ac741c0dc33393304b3/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/src/main/java/com/rnbiometrics/CreateSignatureCallback.java: -------------------------------------------------------------------------------- 1 | package com.rnbiometrics; 2 | 3 | import android.util.Base64; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.biometric.BiometricPrompt; 7 | 8 | import com.facebook.react.bridge.Promise; 9 | import com.facebook.react.bridge.WritableMap; 10 | import com.facebook.react.bridge.WritableNativeMap; 11 | 12 | import java.security.Signature; 13 | 14 | public class CreateSignatureCallback extends BiometricPrompt.AuthenticationCallback { 15 | private Promise promise; 16 | private String payload; 17 | 18 | public CreateSignatureCallback(Promise promise, String payload) { 19 | super(); 20 | this.promise = promise; 21 | this.payload = payload; 22 | } 23 | 24 | @Override 25 | public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { 26 | super.onAuthenticationError(errorCode, errString); 27 | if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON || errorCode == BiometricPrompt.ERROR_USER_CANCELED ) { 28 | WritableMap resultMap = new WritableNativeMap(); 29 | resultMap.putBoolean("success", false); 30 | resultMap.putString("error", "User cancellation"); 31 | this.promise.resolve(resultMap); 32 | } else { 33 | this.promise.reject(errString.toString(), errString.toString()); 34 | } 35 | } 36 | 37 | @Override 38 | public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { 39 | super.onAuthenticationSucceeded(result); 40 | 41 | try { 42 | BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject(); 43 | Signature cryptoSignature = cryptoObject.getSignature(); 44 | cryptoSignature.update(this.payload.getBytes()); 45 | byte[] signed = cryptoSignature.sign(); 46 | String signedString = Base64.encodeToString(signed, Base64.DEFAULT); 47 | signedString = signedString.replaceAll("\r", "").replaceAll("\n", ""); 48 | 49 | WritableMap resultMap = new WritableNativeMap(); 50 | resultMap.putBoolean("success", true); 51 | resultMap.putString("signature", signedString); 52 | promise.resolve(resultMap); 53 | } catch (Exception e) { 54 | promise.reject("Error creating signature: " + e.getMessage(), "Error creating signature"); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /android/src/main/java/com/rnbiometrics/ReactNativeBiometrics.java: -------------------------------------------------------------------------------- 1 | package com.rnbiometrics; 2 | 3 | import android.os.Build; 4 | import android.security.keystore.KeyGenParameterSpec; 5 | import android.security.keystore.KeyProperties; 6 | import android.util.Base64; 7 | 8 | import androidx.biometric.BiometricManager; 9 | import androidx.biometric.BiometricPrompt; 10 | import androidx.biometric.BiometricPrompt.AuthenticationCallback; 11 | import androidx.biometric.BiometricPrompt.PromptInfo; 12 | import androidx.fragment.app.FragmentActivity; 13 | 14 | import com.facebook.react.bridge.Promise; 15 | import com.facebook.react.bridge.ReactApplicationContext; 16 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 17 | import com.facebook.react.bridge.ReactMethod; 18 | import com.facebook.react.bridge.ReadableMap; 19 | import com.facebook.react.bridge.UiThreadUtil; 20 | import com.facebook.react.bridge.WritableMap; 21 | import com.facebook.react.bridge.WritableNativeMap; 22 | 23 | import java.security.KeyPair; 24 | import java.security.KeyPairGenerator; 25 | import java.security.KeyStore; 26 | import java.security.PrivateKey; 27 | import java.security.PublicKey; 28 | import java.security.Signature; 29 | import java.security.spec.RSAKeyGenParameterSpec; 30 | import java.util.concurrent.Executor; 31 | import java.util.concurrent.Executors; 32 | 33 | /** 34 | * Created by brandon on 4/5/18. 35 | */ 36 | 37 | public class ReactNativeBiometrics extends ReactContextBaseJavaModule { 38 | 39 | protected String biometricKeyAlias = "biometric_key"; 40 | 41 | public ReactNativeBiometrics(ReactApplicationContext reactContext) { 42 | super(reactContext); 43 | } 44 | 45 | @Override 46 | public String getName() { 47 | return "ReactNativeBiometrics"; 48 | } 49 | 50 | @ReactMethod 51 | public void isSensorAvailable(final ReadableMap params, final Promise promise) { 52 | try { 53 | if (isCurrentSDKMarshmallowOrLater()) { 54 | boolean allowDeviceCredentials = params.getBoolean("allowDeviceCredentials"); 55 | ReactApplicationContext reactApplicationContext = getReactApplicationContext(); 56 | BiometricManager biometricManager = BiometricManager.from(reactApplicationContext); 57 | int canAuthenticate = biometricManager.canAuthenticate(getAllowedAuthenticators(allowDeviceCredentials)); 58 | 59 | if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) { 60 | WritableMap resultMap = new WritableNativeMap(); 61 | resultMap.putBoolean("available", true); 62 | resultMap.putString("biometryType", "Biometrics"); 63 | promise.resolve(resultMap); 64 | } else { 65 | WritableMap resultMap = new WritableNativeMap(); 66 | resultMap.putBoolean("available", false); 67 | 68 | switch (canAuthenticate) { 69 | case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE: 70 | resultMap.putString("error", "BIOMETRIC_ERROR_NO_HARDWARE"); 71 | break; 72 | case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE: 73 | resultMap.putString("error", "BIOMETRIC_ERROR_HW_UNAVAILABLE"); 74 | break; 75 | case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED: 76 | resultMap.putString("error", "BIOMETRIC_ERROR_NONE_ENROLLED"); 77 | break; 78 | } 79 | 80 | promise.resolve(resultMap); 81 | } 82 | } else { 83 | WritableMap resultMap = new WritableNativeMap(); 84 | resultMap.putBoolean("available", false); 85 | resultMap.putString("error", "Unsupported android version"); 86 | promise.resolve(resultMap); 87 | } 88 | } catch (Exception e) { 89 | promise.reject("Error detecting biometrics availability: " + e.getMessage(), "Error detecting biometrics availability: " + e.getMessage()); 90 | } 91 | } 92 | 93 | @ReactMethod 94 | public void createKeys(final ReadableMap params, Promise promise) { 95 | try { 96 | if (isCurrentSDKMarshmallowOrLater()) { 97 | deleteBiometricKey(); 98 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); 99 | KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(biometricKeyAlias, KeyProperties.PURPOSE_SIGN) 100 | .setDigests(KeyProperties.DIGEST_SHA256) 101 | .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) 102 | .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) 103 | .setUserAuthenticationRequired(true) 104 | .build(); 105 | keyPairGenerator.initialize(keyGenParameterSpec); 106 | 107 | KeyPair keyPair = keyPairGenerator.generateKeyPair(); 108 | PublicKey publicKey = keyPair.getPublic(); 109 | byte[] encodedPublicKey = publicKey.getEncoded(); 110 | String publicKeyString = Base64.encodeToString(encodedPublicKey, Base64.DEFAULT); 111 | publicKeyString = publicKeyString.replaceAll("\r", "").replaceAll("\n", ""); 112 | 113 | WritableMap resultMap = new WritableNativeMap(); 114 | resultMap.putString("publicKey", publicKeyString); 115 | promise.resolve(resultMap); 116 | } else { 117 | promise.reject("Cannot generate keys on android versions below 6.0", "Cannot generate keys on android versions below 6.0"); 118 | } 119 | } catch (Exception e) { 120 | promise.reject("Error generating public private keys: " + e.getMessage(), "Error generating public private keys"); 121 | } 122 | } 123 | 124 | private boolean isCurrentSDKMarshmallowOrLater() { 125 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; 126 | } 127 | 128 | @ReactMethod 129 | public void deleteKeys(Promise promise) { 130 | if (doesBiometricKeyExist()) { 131 | boolean deletionSuccessful = deleteBiometricKey(); 132 | 133 | if (deletionSuccessful) { 134 | WritableMap resultMap = new WritableNativeMap(); 135 | resultMap.putBoolean("keysDeleted", true); 136 | promise.resolve(resultMap); 137 | } else { 138 | promise.reject("Error deleting biometric key from keystore", "Error deleting biometric key from keystore"); 139 | } 140 | } else { 141 | WritableMap resultMap = new WritableNativeMap(); 142 | resultMap.putBoolean("keysDeleted", false); 143 | promise.resolve(resultMap); 144 | } 145 | } 146 | 147 | @ReactMethod 148 | public void createSignature(final ReadableMap params, final Promise promise) { 149 | if (isCurrentSDKMarshmallowOrLater()) { 150 | UiThreadUtil.runOnUiThread( 151 | new Runnable() { 152 | @Override 153 | public void run() { 154 | try { 155 | String promptMessage = params.getString("promptMessage"); 156 | String payload = params.getString("payload"); 157 | String cancelButtonText = params.getString("cancelButtonText"); 158 | boolean allowDeviceCredentials = params.getBoolean("allowDeviceCredentials"); 159 | 160 | Signature signature = Signature.getInstance("SHA256withRSA"); 161 | KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 162 | keyStore.load(null); 163 | 164 | PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null); 165 | signature.initSign(privateKey); 166 | 167 | BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(signature); 168 | 169 | AuthenticationCallback authCallback = new CreateSignatureCallback(promise, payload); 170 | FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity(); 171 | Executor executor = Executors.newSingleThreadExecutor(); 172 | BiometricPrompt biometricPrompt = new BiometricPrompt(fragmentActivity, executor, authCallback); 173 | 174 | biometricPrompt.authenticate(getPromptInfo(promptMessage, cancelButtonText, allowDeviceCredentials), cryptoObject); 175 | } catch (Exception e) { 176 | promise.reject("Error signing payload: " + e.getMessage(), "Error generating signature: " + e.getMessage()); 177 | } 178 | } 179 | }); 180 | } else { 181 | promise.reject("Cannot generate keys on android versions below 6.0", "Cannot generate keys on android versions below 6.0"); 182 | } 183 | } 184 | 185 | private PromptInfo getPromptInfo(String promptMessage, String cancelButtonText, boolean allowDeviceCredentials) { 186 | PromptInfo.Builder builder = new PromptInfo.Builder().setTitle(promptMessage); 187 | 188 | builder.setAllowedAuthenticators(getAllowedAuthenticators(allowDeviceCredentials)); 189 | 190 | if (allowDeviceCredentials == false || isCurrentSDK29OrEarlier()) { 191 | builder.setNegativeButtonText(cancelButtonText); 192 | } 193 | 194 | return builder.build(); 195 | } 196 | 197 | private int getAllowedAuthenticators(boolean allowDeviceCredentials) { 198 | if (allowDeviceCredentials && !isCurrentSDK29OrEarlier()) { 199 | return BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL; 200 | } 201 | return BiometricManager.Authenticators.BIOMETRIC_STRONG; 202 | } 203 | 204 | private boolean isCurrentSDK29OrEarlier() { 205 | return Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q; 206 | } 207 | 208 | @ReactMethod 209 | public void simplePrompt(final ReadableMap params, final Promise promise) { 210 | if (isCurrentSDKMarshmallowOrLater()) { 211 | UiThreadUtil.runOnUiThread( 212 | new Runnable() { 213 | @Override 214 | public void run() { 215 | try { 216 | String promptMessage = params.getString("promptMessage"); 217 | String cancelButtonText = params.getString("cancelButtonText"); 218 | boolean allowDeviceCredentials = params.getBoolean("allowDeviceCredentials"); 219 | 220 | AuthenticationCallback authCallback = new SimplePromptCallback(promise); 221 | FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity(); 222 | Executor executor = Executors.newSingleThreadExecutor(); 223 | BiometricPrompt biometricPrompt = new BiometricPrompt(fragmentActivity, executor, authCallback); 224 | 225 | biometricPrompt.authenticate(getPromptInfo(promptMessage, cancelButtonText, allowDeviceCredentials)); 226 | } catch (Exception e) { 227 | promise.reject("Error displaying local biometric prompt: " + e.getMessage(), "Error displaying local biometric prompt: " + e.getMessage()); 228 | } 229 | } 230 | }); 231 | } else { 232 | promise.reject("Cannot display biometric prompt on android versions below 6.0", "Cannot display biometric prompt on android versions below 6.0"); 233 | } 234 | } 235 | 236 | @ReactMethod 237 | public void biometricKeysExist(Promise promise) { 238 | try { 239 | boolean doesBiometricKeyExist = doesBiometricKeyExist(); 240 | WritableMap resultMap = new WritableNativeMap(); 241 | resultMap.putBoolean("keysExist", doesBiometricKeyExist); 242 | promise.resolve(resultMap); 243 | } catch (Exception e) { 244 | promise.reject("Error checking if biometric key exists: " + e.getMessage(), "Error checking if biometric key exists: " + e.getMessage()); 245 | } 246 | } 247 | 248 | protected boolean doesBiometricKeyExist() { 249 | try { 250 | KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 251 | keyStore.load(null); 252 | 253 | return keyStore.containsAlias(biometricKeyAlias); 254 | } catch (Exception e) { 255 | return false; 256 | } 257 | } 258 | 259 | protected boolean deleteBiometricKey() { 260 | try { 261 | KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 262 | keyStore.load(null); 263 | 264 | keyStore.deleteEntry(biometricKeyAlias); 265 | return true; 266 | } catch (Exception e) { 267 | return false; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /android/src/main/java/com/rnbiometrics/ReactNativeBiometricsPackage.java: -------------------------------------------------------------------------------- 1 | package com.rnbiometrics; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.NativeModule; 5 | import com.facebook.react.bridge.ReactApplicationContext; 6 | import com.facebook.react.uimanager.ViewManager; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | /** 13 | * Created by brandon on 4/5/18. 14 | */ 15 | 16 | public class ReactNativeBiometricsPackage implements ReactPackage { 17 | @Override 18 | public List createViewManagers(ReactApplicationContext reactContext) { 19 | return Collections.emptyList(); 20 | } 21 | 22 | @Override 23 | public List createNativeModules( 24 | ReactApplicationContext reactContext) { 25 | List modules = new ArrayList<>(); 26 | 27 | modules.add(new ReactNativeBiometrics(reactContext)); 28 | 29 | return modules; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/src/main/java/com/rnbiometrics/SimplePromptCallback.java: -------------------------------------------------------------------------------- 1 | package com.rnbiometrics; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.biometric.BiometricPrompt; 5 | 6 | import com.facebook.react.bridge.Promise; 7 | import com.facebook.react.bridge.WritableMap; 8 | import com.facebook.react.bridge.WritableNativeMap; 9 | 10 | public class SimplePromptCallback extends BiometricPrompt.AuthenticationCallback { 11 | private Promise promise; 12 | 13 | public SimplePromptCallback(Promise promise) { 14 | super(); 15 | this.promise = promise; 16 | } 17 | 18 | @Override 19 | public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { 20 | super.onAuthenticationError(errorCode, errString); 21 | if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON || errorCode == BiometricPrompt.ERROR_USER_CANCELED) { 22 | WritableMap resultMap = new WritableNativeMap(); 23 | resultMap.putBoolean("success", false); 24 | resultMap.putString("error", "User cancellation"); 25 | this.promise.resolve(resultMap); 26 | } else { 27 | this.promise.reject(errString.toString(), errString.toString()); 28 | } 29 | } 30 | 31 | @Override 32 | public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { 33 | super.onAuthenticationSucceeded(result); 34 | 35 | WritableMap resultMap = new WritableNativeMap(); 36 | resultMap.putBoolean("success", true); 37 | this.promise.resolve(resultMap); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native' 2 | 3 | const { ReactNativeBiometrics: bridge } = NativeModules 4 | 5 | /** 6 | * Type alias for possible biometry types 7 | */ 8 | export type BiometryType = 'TouchID' | 'FaceID' | 'Biometrics' 9 | 10 | interface RNBiometricsOptions { 11 | allowDeviceCredentials?: boolean 12 | } 13 | 14 | interface IsSensorAvailableResult { 15 | available: boolean 16 | biometryType?: BiometryType 17 | error?: string 18 | } 19 | 20 | interface CreateKeysResult { 21 | publicKey: string 22 | } 23 | 24 | interface BiometricKeysExistResult { 25 | keysExist: boolean 26 | } 27 | 28 | interface DeleteKeysResult { 29 | keysDeleted: boolean 30 | } 31 | 32 | interface CreateSignatureOptions { 33 | promptMessage: string 34 | payload: string 35 | cancelButtonText?: string 36 | } 37 | 38 | interface CreateSignatureResult { 39 | success: boolean 40 | signature?: string 41 | error?: string 42 | } 43 | 44 | interface SimplePromptOptions { 45 | promptMessage: string 46 | fallbackPromptMessage?: string 47 | cancelButtonText?: string 48 | } 49 | 50 | interface SimplePromptResult { 51 | success: boolean 52 | error?: string 53 | } 54 | 55 | /** 56 | * Enum for touch id sensor type 57 | */ 58 | export const TouchID = 'TouchID' 59 | /** 60 | * Enum for face id sensor type 61 | */ 62 | export const FaceID = 'FaceID' 63 | /** 64 | * Enum for generic biometrics (this is the only value available on android) 65 | */ 66 | export const Biometrics = 'Biometrics' 67 | 68 | export const BiometryTypes = { 69 | TouchID, 70 | FaceID, 71 | Biometrics 72 | } 73 | 74 | export module ReactNativeBiometricsLegacy { 75 | /** 76 | * Returns promise that resolves to an object with object.biometryType = Biometrics | TouchID | FaceID 77 | * @returns {Promise} Promise that resolves to an object with details about biometrics available 78 | */ 79 | export function isSensorAvailable(): Promise { 80 | return new ReactNativeBiometrics().isSensorAvailable() 81 | } 82 | 83 | /** 84 | * Creates a public private key pair,returns promise that resolves to 85 | * an object with object.publicKey, which is the public key of the newly generated key pair 86 | * @returns {Promise} Promise that resolves to object with details about the newly generated public key 87 | */ 88 | export function createKeys(): Promise { 89 | return new ReactNativeBiometrics().createKeys() 90 | } 91 | 92 | /** 93 | * Returns promise that resolves to an object with object.keysExists = true | false 94 | * indicating if the keys were found to exist or not 95 | * @returns {Promise} Promise that resolves to object with details aobut the existence of keys 96 | */ 97 | export function biometricKeysExist(): Promise { 98 | return new ReactNativeBiometrics().biometricKeysExist() 99 | } 100 | 101 | /** 102 | * Returns promise that resolves to an object with true | false 103 | * indicating if the keys were properly deleted 104 | * @returns {Promise} Promise that resolves to an object with details about the deletion 105 | */ 106 | export function deleteKeys(): Promise { 107 | return new ReactNativeBiometrics().deleteKeys() 108 | } 109 | 110 | /** 111 | * Prompts user with biometrics dialog using the passed in prompt message and 112 | * returns promise that resolves to an object with object.signature, 113 | * which is cryptographic signature of the payload 114 | * @param {Object} createSignatureOptions 115 | * @param {string} createSignatureOptions.promptMessage 116 | * @param {string} createSignatureOptions.payload 117 | * @returns {Promise} Promise that resolves to an object cryptographic signature details 118 | */ 119 | export function createSignature(createSignatureOptions: CreateSignatureOptions): Promise { 120 | return new ReactNativeBiometrics().createSignature(createSignatureOptions) 121 | } 122 | 123 | /** 124 | * Prompts user with biometrics dialog using the passed in prompt message and 125 | * returns promise that resolves to an object with object.success = true if the user passes, 126 | * object.success = false if the user cancels, and rejects if anything fails 127 | * @param {Object} simplePromptOptions 128 | * @param {string} simplePromptOptions.promptMessage 129 | * @param {string} simplePromptOptions.fallbackPromptMessage 130 | * @returns {Promise} Promise that resolves an object with details about the biometrics result 131 | */ 132 | export function simplePrompt(simplePromptOptions: SimplePromptOptions): Promise { 133 | return new ReactNativeBiometrics().simplePrompt(simplePromptOptions) 134 | } 135 | } 136 | 137 | export default class ReactNativeBiometrics { 138 | allowDeviceCredentials = false 139 | 140 | /** 141 | * @param {Object} rnBiometricsOptions 142 | * @param {boolean} rnBiometricsOptions.allowDeviceCredentials 143 | */ 144 | constructor(rnBiometricsOptions?: RNBiometricsOptions) { 145 | const allowDeviceCredentials = rnBiometricsOptions?.allowDeviceCredentials ?? false 146 | this.allowDeviceCredentials = allowDeviceCredentials 147 | } 148 | 149 | /** 150 | * Returns promise that resolves to an object with object.biometryType = Biometrics | TouchID | FaceID 151 | * @returns {Promise} Promise that resolves to an object with details about biometrics available 152 | */ 153 | isSensorAvailable(): Promise { 154 | return bridge.isSensorAvailable({ 155 | allowDeviceCredentials: this.allowDeviceCredentials 156 | }) 157 | } 158 | 159 | /** 160 | * Creates a public private key pair,returns promise that resolves to 161 | * an object with object.publicKey, which is the public key of the newly generated key pair 162 | * @returns {Promise} Promise that resolves to object with details about the newly generated public key 163 | */ 164 | createKeys(): Promise { 165 | return bridge.createKeys({ 166 | allowDeviceCredentials: this.allowDeviceCredentials 167 | }) 168 | } 169 | 170 | /** 171 | * Returns promise that resolves to an object with object.keysExists = true | false 172 | * indicating if the keys were found to exist or not 173 | * @returns {Promise} Promise that resolves to object with details aobut the existence of keys 174 | */ 175 | biometricKeysExist(): Promise { 176 | return bridge.biometricKeysExist() 177 | } 178 | 179 | /** 180 | * Returns promise that resolves to an object with true | false 181 | * indicating if the keys were properly deleted 182 | * @returns {Promise} Promise that resolves to an object with details about the deletion 183 | */ 184 | deleteKeys(): Promise { 185 | return bridge.deleteKeys() 186 | } 187 | 188 | /** 189 | * Prompts user with biometrics dialog using the passed in prompt message and 190 | * returns promise that resolves to an object with object.signature, 191 | * which is cryptographic signature of the payload 192 | * @param {Object} createSignatureOptions 193 | * @param {string} createSignatureOptions.promptMessage 194 | * @param {string} createSignatureOptions.payload 195 | * @returns {Promise} Promise that resolves to an object cryptographic signature details 196 | */ 197 | createSignature(createSignatureOptions: CreateSignatureOptions): Promise { 198 | createSignatureOptions.cancelButtonText = createSignatureOptions.cancelButtonText ?? 'Cancel' 199 | 200 | return bridge.createSignature({ 201 | allowDeviceCredentials: this.allowDeviceCredentials, 202 | ...createSignatureOptions 203 | }) 204 | } 205 | 206 | /** 207 | * Prompts user with biometrics dialog using the passed in prompt message and 208 | * returns promise that resolves to an object with object.success = true if the user passes, 209 | * object.success = false if the user cancels, and rejects if anything fails 210 | * @param {Object} simplePromptOptions 211 | * @param {string} simplePromptOptions.promptMessage 212 | * @param {string} simplePromptOptions.fallbackPromptMessage 213 | * @returns {Promise} Promise that resolves an object with details about the biometrics result 214 | */ 215 | simplePrompt(simplePromptOptions: SimplePromptOptions): Promise { 216 | simplePromptOptions.cancelButtonText = simplePromptOptions.cancelButtonText ?? 'Cancel' 217 | simplePromptOptions.fallbackPromptMessage = simplePromptOptions.fallbackPromptMessage ?? 'Use Passcode' 218 | 219 | return bridge.simplePrompt({ 220 | allowDeviceCredentials: this.allowDeviceCredentials, 221 | ...simplePromptOptions 222 | }) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /ios/ReactNativeBiometrics.h: -------------------------------------------------------------------------------- 1 | // 2 | // ReactNativeBiometrics.h 3 | // 4 | // Created by Brandon Hines on 4/3/18. 5 | // 6 | 7 | #import 8 | 9 | @interface ReactNativeBiometrics : NSObject 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /ios/ReactNativeBiometrics.m: -------------------------------------------------------------------------------- 1 | // 2 | // ReactNativeBiometrics.m 3 | // 4 | // Created by Brandon Hines on 4/3/18. 5 | // 6 | 7 | #import "ReactNativeBiometrics.h" 8 | #import 9 | #import 10 | #import 11 | 12 | @implementation ReactNativeBiometrics 13 | 14 | RCT_EXPORT_MODULE(ReactNativeBiometrics); 15 | 16 | RCT_EXPORT_METHOD(isSensorAvailable: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { 17 | LAContext *context = [[LAContext alloc] init]; 18 | NSError *la_error = nil; 19 | BOOL allowDeviceCredentials = [RCTConvert BOOL:params[@"allowDeviceCredentials"]]; 20 | LAPolicy laPolicy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; 21 | 22 | if (allowDeviceCredentials == TRUE) { 23 | laPolicy = LAPolicyDeviceOwnerAuthentication; 24 | } 25 | 26 | BOOL canEvaluatePolicy = [context canEvaluatePolicy:laPolicy error:&la_error]; 27 | 28 | if (canEvaluatePolicy) { 29 | NSString *biometryType = [self getBiometryType:context]; 30 | NSDictionary *result = @{ 31 | @"available": @(YES), 32 | @"biometryType": biometryType 33 | }; 34 | 35 | resolve(result); 36 | } else { 37 | NSString *errorMessage = [NSString stringWithFormat:@"%@", la_error]; 38 | NSDictionary *result = @{ 39 | @"available": @(NO), 40 | @"error": errorMessage 41 | }; 42 | 43 | resolve(result); 44 | } 45 | } 46 | 47 | RCT_EXPORT_METHOD(createKeys: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { 48 | dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 49 | CFErrorRef error = NULL; 50 | BOOL allowDeviceCredentials = [RCTConvert BOOL:params[@"allowDeviceCredentials"]]; 51 | 52 | SecAccessControlCreateFlags secCreateFlag = kSecAccessControlBiometryAny; 53 | 54 | if (allowDeviceCredentials == TRUE) { 55 | secCreateFlag = kSecAccessControlUserPresence; 56 | } 57 | 58 | SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault, 59 | kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, 60 | secCreateFlag, &error); 61 | if (sacObject == NULL || error != NULL) { 62 | NSString *errorString = [NSString stringWithFormat:@"SecItemAdd can't create sacObject: %@", error]; 63 | reject(@"storage_error", errorString, nil); 64 | return; 65 | } 66 | 67 | NSData *biometricKeyTag = [self getBiometricKeyTag]; 68 | NSDictionary *keyAttributes = @{ 69 | (id)kSecClass: (id)kSecClassKey, 70 | (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA, 71 | (id)kSecAttrKeySizeInBits: @2048, 72 | (id)kSecPrivateKeyAttrs: @{ 73 | (id)kSecAttrIsPermanent: @YES, 74 | (id)kSecUseAuthenticationUI: (id)kSecUseAuthenticationUIAllow, 75 | (id)kSecAttrApplicationTag: biometricKeyTag, 76 | (id)kSecAttrAccessControl: (__bridge_transfer id)sacObject 77 | } 78 | }; 79 | 80 | [self deleteBiometricKey]; 81 | NSError *gen_error = nil; 82 | id privateKey = CFBridgingRelease(SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyAttributes, (void *)&gen_error)); 83 | 84 | if(privateKey != nil) { 85 | id publicKey = CFBridgingRelease(SecKeyCopyPublicKey((SecKeyRef)privateKey)); 86 | CFDataRef publicKeyDataRef = SecKeyCopyExternalRepresentation((SecKeyRef)publicKey, nil); 87 | NSData *publicKeyData = (__bridge NSData *)publicKeyDataRef; 88 | NSData *publicKeyDataWithHeader = [self addHeaderPublickey:publicKeyData]; 89 | NSString *publicKeyString = [publicKeyDataWithHeader base64EncodedStringWithOptions:0]; 90 | 91 | NSDictionary *result = @{ 92 | @"publicKey": publicKeyString, 93 | }; 94 | resolve(result); 95 | } else { 96 | NSString *message = [NSString stringWithFormat:@"Key generation error: %@", gen_error]; 97 | reject(@"storage_error", message, nil); 98 | } 99 | }); 100 | } 101 | 102 | RCT_EXPORT_METHOD(deleteKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { 103 | dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 104 | BOOL biometricKeyExists = [self doesBiometricKeyExist]; 105 | 106 | if (biometricKeyExists) { 107 | OSStatus status = [self deleteBiometricKey]; 108 | 109 | if (status == noErr) { 110 | NSDictionary *result = @{ 111 | @"keysDeleted": @(YES), 112 | }; 113 | resolve(result); 114 | } else { 115 | NSString *message = [NSString stringWithFormat:@"Key not found: %@",[self keychainErrorToString:status]]; 116 | reject(@"deletion_error", message, nil); 117 | } 118 | } else { 119 | NSDictionary *result = @{ 120 | @"keysDeleted": @(NO), 121 | }; 122 | resolve(result); 123 | } 124 | }); 125 | } 126 | 127 | RCT_EXPORT_METHOD(createSignature: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { 128 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 129 | NSString *promptMessage = [RCTConvert NSString:params[@"promptMessage"]]; 130 | NSString *payload = [RCTConvert NSString:params[@"payload"]]; 131 | 132 | NSData *biometricKeyTag = [self getBiometricKeyTag]; 133 | NSDictionary *query = @{ 134 | (id)kSecClass: (id)kSecClassKey, 135 | (id)kSecAttrApplicationTag: biometricKeyTag, 136 | (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA, 137 | (id)kSecReturnRef: @YES, 138 | (id)kSecUseOperationPrompt: promptMessage 139 | }; 140 | SecKeyRef privateKey; 141 | OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey); 142 | 143 | if (status == errSecSuccess) { 144 | NSError *error; 145 | NSData *dataToSign = [payload dataUsingEncoding:NSUTF8StringEncoding]; 146 | NSData *signature = CFBridgingRelease(SecKeyCreateSignature(privateKey, kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256, (CFDataRef)dataToSign, (void *)&error)); 147 | 148 | if (signature != nil) { 149 | NSString *signatureString = [signature base64EncodedStringWithOptions:0]; 150 | NSDictionary *result = @{ 151 | @"success": @(YES), 152 | @"signature": signatureString 153 | }; 154 | resolve(result); 155 | } else if (error.code == errSecUserCanceled) { 156 | NSDictionary *result = @{ 157 | @"success": @(NO), 158 | @"error": @"User cancellation" 159 | }; 160 | resolve(result); 161 | } else { 162 | NSString *message = [NSString stringWithFormat:@"Signature error: %@", error]; 163 | reject(@"signature_error", message, nil); 164 | } 165 | } else { 166 | NSString *message = [NSString stringWithFormat:@"Key not found: %@",[self keychainErrorToString:status]]; 167 | reject(@"storage_error", message, nil); 168 | } 169 | }); 170 | } 171 | 172 | RCT_EXPORT_METHOD(simplePrompt: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { 173 | dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 174 | NSString *promptMessage = [RCTConvert NSString:params[@"promptMessage"]]; 175 | NSString *fallbackPromptMessage = [RCTConvert NSString:params[@"fallbackPromptMessage"]]; 176 | BOOL allowDeviceCredentials = [RCTConvert BOOL:params[@"allowDeviceCredentials"]]; 177 | 178 | LAContext *context = [[LAContext alloc] init]; 179 | LAPolicy laPolicy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; 180 | 181 | if (allowDeviceCredentials == TRUE) { 182 | laPolicy = LAPolicyDeviceOwnerAuthentication; 183 | context.localizedFallbackTitle = fallbackPromptMessage; 184 | } else { 185 | context.localizedFallbackTitle = @""; 186 | } 187 | 188 | [context evaluatePolicy:laPolicy localizedReason:promptMessage reply:^(BOOL success, NSError *biometricError) { 189 | if (success) { 190 | NSDictionary *result = @{ 191 | @"success": @(YES) 192 | }; 193 | resolve(result); 194 | } else if (biometricError.code == LAErrorUserCancel) { 195 | NSDictionary *result = @{ 196 | @"success": @(NO), 197 | @"error": @"User cancellation" 198 | }; 199 | resolve(result); 200 | } else { 201 | NSString *message = [NSString stringWithFormat:@"%@", biometricError]; 202 | reject(@"biometric_error", message, nil); 203 | } 204 | }]; 205 | }); 206 | } 207 | 208 | RCT_EXPORT_METHOD(biometricKeysExist: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { 209 | dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 210 | BOOL biometricKeyExists = [self doesBiometricKeyExist]; 211 | 212 | if (biometricKeyExists) { 213 | NSDictionary *result = @{ 214 | @"keysExist": @(YES) 215 | }; 216 | resolve(result); 217 | } else { 218 | NSDictionary *result = @{ 219 | @"keysExist": @(NO) 220 | }; 221 | resolve(result); 222 | } 223 | }); 224 | } 225 | 226 | - (NSData *) getBiometricKeyTag { 227 | NSString *biometricKeyAlias = @"com.rnbiometrics.biometricKey"; 228 | NSData *biometricKeyTag = [biometricKeyAlias dataUsingEncoding:NSUTF8StringEncoding]; 229 | return biometricKeyTag; 230 | } 231 | 232 | - (BOOL) doesBiometricKeyExist { 233 | NSData *biometricKeyTag = [self getBiometricKeyTag]; 234 | NSDictionary *searchQuery = @{ 235 | (id)kSecClass: (id)kSecClassKey, 236 | (id)kSecAttrApplicationTag: biometricKeyTag, 237 | (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA, 238 | (id)kSecUseAuthenticationUI: (id)kSecUseAuthenticationUIFail 239 | }; 240 | 241 | OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil); 242 | return status == errSecSuccess || status == errSecInteractionNotAllowed; 243 | } 244 | 245 | -(OSStatus) deleteBiometricKey { 246 | NSData *biometricKeyTag = [self getBiometricKeyTag]; 247 | NSDictionary *deleteQuery = @{ 248 | (id)kSecClass: (id)kSecClassKey, 249 | (id)kSecAttrApplicationTag: biometricKeyTag, 250 | (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA 251 | }; 252 | 253 | OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteQuery); 254 | return status; 255 | } 256 | 257 | - (NSString *)getBiometryType:(LAContext *)context 258 | { 259 | if (@available(iOS 11, *)) { 260 | return (context.biometryType == LABiometryTypeFaceID) ? @"FaceID" : @"TouchID"; 261 | } 262 | 263 | return @"TouchID"; 264 | } 265 | 266 | - (NSString *)keychainErrorToString:(OSStatus)error { 267 | NSString *message = [NSString stringWithFormat:@"%ld", (long)error]; 268 | 269 | switch (error) { 270 | case errSecSuccess: 271 | message = @"success"; 272 | break; 273 | 274 | case errSecDuplicateItem: 275 | message = @"error item already exists"; 276 | break; 277 | 278 | case errSecItemNotFound : 279 | message = @"error item not found"; 280 | break; 281 | 282 | case errSecAuthFailed: 283 | message = @"error item authentication failed"; 284 | break; 285 | 286 | default: 287 | break; 288 | } 289 | 290 | return message; 291 | } 292 | 293 | 294 | - (NSData *)addHeaderPublickey:(NSData *)publicKeyData { 295 | 296 | unsigned char builder[15]; 297 | NSMutableData * encKey = [[NSMutableData alloc] init]; 298 | unsigned long bitstringEncLength; 299 | 300 | static const unsigned char _encodedRSAEncryptionOID[15] = { 301 | 302 | /* Sequence of length 0xd made up of OID followed by NULL */ 303 | 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 304 | 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00 305 | 306 | }; 307 | // When we get to the bitstring - how will we encode it? 308 | if ([publicKeyData length ] + 1 < 128 ) 309 | bitstringEncLength = 1 ; 310 | else 311 | bitstringEncLength = (([publicKeyData length ] +1 ) / 256 ) + 2 ; 312 | // 313 | // // Overall we have a sequence of a certain length 314 | builder[0] = 0x30; // ASN.1 encoding representing a SEQUENCE 315 | // // Build up overall size made up of - 316 | // // size of OID + size of bitstring encoding + size of actual key 317 | size_t i = sizeof(_encodedRSAEncryptionOID) + 2 + bitstringEncLength + [publicKeyData length]; 318 | size_t j = encodeLength(&builder[1], i); 319 | [encKey appendBytes:builder length:j +1]; 320 | 321 | // First part of the sequence is the OID 322 | [encKey appendBytes:_encodedRSAEncryptionOID 323 | length:sizeof(_encodedRSAEncryptionOID)]; 324 | 325 | // Now add the bitstring 326 | builder[0] = 0x03; 327 | j = encodeLength(&builder[1], [publicKeyData length] + 1); 328 | builder[j+1] = 0x00; 329 | [encKey appendBytes:builder length:j + 2]; 330 | 331 | // Now the actual key 332 | [encKey appendData:publicKeyData]; 333 | 334 | return encKey; 335 | } 336 | 337 | size_t encodeLength(unsigned char * buf, size_t length) { 338 | 339 | // encode length in ASN.1 DER format 340 | if (length < 128) { 341 | buf[0] = length; 342 | return 1; 343 | } 344 | 345 | size_t i = (length / 256) + 1; 346 | buf[0] = i + 0x80; 347 | for (size_t j = 0 ; j < i; ++j) { 348 | buf[i - j] = length & 0xFF; 349 | length = length >> 8; 350 | } 351 | 352 | return i + 1; 353 | } 354 | 355 | @end 356 | -------------------------------------------------------------------------------- /ios/ReactNativeBiometrics.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B3E7B58A1CC2AC0600A0062D /* ReactNativeBiometrics.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* ReactNativeBiometrics.m */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = "include/$(PRODUCT_NAME)"; 18 | dstSubfolderSpec = 16; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 0; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 134814201AA4EA6300B7C361 /* libReactNativeBiometrics.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactNativeBiometrics.a; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | B3E7B5881CC2AC0600A0062D /* ReactNativeBiometrics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReactNativeBiometrics.h; sourceTree = ""; }; 28 | B3E7B5891CC2AC0600A0062D /* ReactNativeBiometrics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReactNativeBiometrics.m; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 134814211AA4EA7D00B7C361 /* Products */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | 134814201AA4EA6300B7C361 /* libReactNativeBiometrics.a */, 46 | ); 47 | name = Products; 48 | sourceTree = ""; 49 | }; 50 | 58B511D21A9E6C8500147676 = { 51 | isa = PBXGroup; 52 | children = ( 53 | B3E7B5881CC2AC0600A0062D /* ReactNativeBiometrics.h */, 54 | B3E7B5891CC2AC0600A0062D /* ReactNativeBiometrics.m */, 55 | 134814211AA4EA7D00B7C361 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | /* End PBXGroup section */ 60 | 61 | /* Begin PBXNativeTarget section */ 62 | 58B511DA1A9E6C8500147676 /* ReactNativeBiometrics */ = { 63 | isa = PBXNativeTarget; 64 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeBiometrics" */; 65 | buildPhases = ( 66 | 58B511D71A9E6C8500147676 /* Sources */, 67 | 58B511D81A9E6C8500147676 /* Frameworks */, 68 | 58B511D91A9E6C8500147676 /* CopyFiles */, 69 | ); 70 | buildRules = ( 71 | ); 72 | dependencies = ( 73 | ); 74 | name = ReactNativeBiometrics; 75 | productName = RCTDataManager; 76 | productReference = 134814201AA4EA6300B7C361 /* libReactNativeBiometrics.a */; 77 | productType = "com.apple.product-type.library.static"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | 58B511D31A9E6C8500147676 /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | LastUpgradeCheck = 0830; 86 | ORGANIZATIONNAME = Facebook; 87 | TargetAttributes = { 88 | 58B511DA1A9E6C8500147676 = { 89 | CreatedOnToolsVersion = 6.1.1; 90 | }; 91 | }; 92 | }; 93 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeBiometrics" */; 94 | compatibilityVersion = "Xcode 3.2"; 95 | developmentRegion = English; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | ); 100 | mainGroup = 58B511D21A9E6C8500147676; 101 | productRefGroup = 58B511D21A9E6C8500147676; 102 | projectDirPath = ""; 103 | projectRoot = ""; 104 | targets = ( 105 | 58B511DA1A9E6C8500147676 /* ReactNativeBiometrics */, 106 | ); 107 | }; 108 | /* End PBXProject section */ 109 | 110 | /* Begin PBXSourcesBuildPhase section */ 111 | 58B511D71A9E6C8500147676 /* Sources */ = { 112 | isa = PBXSourcesBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | B3E7B58A1CC2AC0600A0062D /* ReactNativeBiometrics.m in Sources */, 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXSourcesBuildPhase section */ 120 | 121 | /* Begin XCBuildConfiguration section */ 122 | 58B511ED1A9E6C8500147676 /* Debug */ = { 123 | isa = XCBuildConfiguration; 124 | buildSettings = { 125 | ALWAYS_SEARCH_USER_PATHS = NO; 126 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 127 | CLANG_CXX_LIBRARY = "libc++"; 128 | CLANG_ENABLE_MODULES = YES; 129 | CLANG_ENABLE_OBJC_ARC = YES; 130 | CLANG_WARN_BOOL_CONVERSION = YES; 131 | CLANG_WARN_CONSTANT_CONVERSION = YES; 132 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 133 | CLANG_WARN_EMPTY_BODY = YES; 134 | CLANG_WARN_ENUM_CONVERSION = YES; 135 | CLANG_WARN_INFINITE_RECURSION = YES; 136 | CLANG_WARN_INT_CONVERSION = YES; 137 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 138 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 139 | CLANG_WARN_UNREACHABLE_CODE = YES; 140 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 141 | COPY_PHASE_STRIP = NO; 142 | ENABLE_STRICT_OBJC_MSGSEND = YES; 143 | ENABLE_TESTABILITY = YES; 144 | GCC_C_LANGUAGE_STANDARD = gnu99; 145 | GCC_DYNAMIC_NO_PIC = NO; 146 | GCC_NO_COMMON_BLOCKS = YES; 147 | GCC_OPTIMIZATION_LEVEL = 0; 148 | GCC_PREPROCESSOR_DEFINITIONS = ( 149 | "DEBUG=1", 150 | "$(inherited)", 151 | ); 152 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 153 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 154 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 155 | GCC_WARN_UNDECLARED_SELECTOR = YES; 156 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 157 | GCC_WARN_UNUSED_FUNCTION = YES; 158 | GCC_WARN_UNUSED_VARIABLE = YES; 159 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 160 | MTL_ENABLE_DEBUG_INFO = YES; 161 | ONLY_ACTIVE_ARCH = YES; 162 | SDKROOT = iphoneos; 163 | }; 164 | name = Debug; 165 | }; 166 | 58B511EE1A9E6C8500147676 /* Release */ = { 167 | isa = XCBuildConfiguration; 168 | buildSettings = { 169 | ALWAYS_SEARCH_USER_PATHS = NO; 170 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 171 | CLANG_CXX_LIBRARY = "libc++"; 172 | CLANG_ENABLE_MODULES = YES; 173 | CLANG_ENABLE_OBJC_ARC = YES; 174 | CLANG_WARN_BOOL_CONVERSION = YES; 175 | CLANG_WARN_CONSTANT_CONVERSION = YES; 176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 177 | CLANG_WARN_EMPTY_BODY = YES; 178 | CLANG_WARN_ENUM_CONVERSION = YES; 179 | CLANG_WARN_INFINITE_RECURSION = YES; 180 | CLANG_WARN_INT_CONVERSION = YES; 181 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 182 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 183 | CLANG_WARN_UNREACHABLE_CODE = YES; 184 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 185 | COPY_PHASE_STRIP = YES; 186 | ENABLE_NS_ASSERTIONS = NO; 187 | ENABLE_STRICT_OBJC_MSGSEND = YES; 188 | GCC_C_LANGUAGE_STANDARD = gnu99; 189 | GCC_NO_COMMON_BLOCKS = YES; 190 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 191 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 192 | GCC_WARN_UNDECLARED_SELECTOR = YES; 193 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 194 | GCC_WARN_UNUSED_FUNCTION = YES; 195 | GCC_WARN_UNUSED_VARIABLE = YES; 196 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 197 | MTL_ENABLE_DEBUG_INFO = NO; 198 | SDKROOT = iphoneos; 199 | VALIDATE_PRODUCT = YES; 200 | }; 201 | name = Release; 202 | }; 203 | 58B511F01A9E6C8500147676 /* Debug */ = { 204 | isa = XCBuildConfiguration; 205 | buildSettings = { 206 | HEADER_SEARCH_PATHS = ( 207 | "$(inherited)", 208 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 209 | "$(SRCROOT)/../../../React/**", 210 | "$(SRCROOT)/../../react-native/React/**", 211 | ); 212 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 213 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 214 | OTHER_LDFLAGS = "-ObjC"; 215 | PRODUCT_NAME = ReactNativeBiometrics; 216 | SKIP_INSTALL = YES; 217 | }; 218 | name = Debug; 219 | }; 220 | 58B511F11A9E6C8500147676 /* Release */ = { 221 | isa = XCBuildConfiguration; 222 | buildSettings = { 223 | HEADER_SEARCH_PATHS = ( 224 | "$(inherited)", 225 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 226 | "$(SRCROOT)/../../../React/**", 227 | "$(SRCROOT)/../../react-native/React/**", 228 | ); 229 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 230 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 231 | OTHER_LDFLAGS = "-ObjC"; 232 | PRODUCT_NAME = ReactNativeBiometrics; 233 | SKIP_INSTALL = YES; 234 | }; 235 | name = Release; 236 | }; 237 | /* End XCBuildConfiguration section */ 238 | 239 | /* Begin XCConfigurationList section */ 240 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeBiometrics" */ = { 241 | isa = XCConfigurationList; 242 | buildConfigurations = ( 243 | 58B511ED1A9E6C8500147676 /* Debug */, 244 | 58B511EE1A9E6C8500147676 /* Release */, 245 | ); 246 | defaultConfigurationIsVisible = 0; 247 | defaultConfigurationName = Release; 248 | }; 249 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeBiometrics" */ = { 250 | isa = XCConfigurationList; 251 | buildConfigurations = ( 252 | 58B511F01A9E6C8500147676 /* Debug */, 253 | 58B511F11A9E6C8500147676 /* Release */, 254 | ); 255 | defaultConfigurationIsVisible = 0; 256 | defaultConfigurationName = Release; 257 | }; 258 | /* End XCConfigurationList section */ 259 | }; 260 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 261 | } 262 | -------------------------------------------------------------------------------- /modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-native' -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-biometrics", 3 | "version": "3.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "typescript": { 8 | "version": "3.7.2", 9 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", 10 | "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-biometrics", 3 | "version": "3.0.1", 4 | "summary": "A React Native library for biometrics", 5 | "description": "React Native biometric functionality for signing and encryption", 6 | "main": "build/cjs/index.js", 7 | "module": "build/esm/index.js", 8 | "types": "build/esm/index.d.ts", 9 | "scripts": { 10 | "clean": "rm -rf build && rm -rf node_modules && npm install", 11 | "build": "npm run build:cjs && npm run build:esm", 12 | "build:cjs": "tsc --target es5 --outDir build/cjs --module commonjs", 13 | "build:esm": "tsc --target es5 --outDir build/esm --module esnext", 14 | "release": "npm run clean && npm run build && npm publish" 15 | }, 16 | "keywords": [ 17 | "react-native", 18 | "android", 19 | "ios", 20 | "biometrics", 21 | "authentication", 22 | "auth", 23 | "fingerprint", 24 | "touch-id", 25 | "face-id" 26 | ], 27 | "homepage": "https://github.com/SelfLender/react-native-biometrics", 28 | "contributors": [], 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/SelfLender/react-native-biometrics.git" 32 | }, 33 | "private": false, 34 | "author": { 35 | "name": "Brandon Hines", 36 | "url": "https://github.com/NappyPirate" 37 | }, 38 | "license": "MIT", 39 | "peerDependencies": { 40 | "react-native": ">=0.60.0" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/SelfLender/react-native-biometrics/issues" 44 | }, 45 | "devDependencies": { 46 | "typescript": "^3.7.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /react-native-biometrics.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = package['name'] 7 | s.version = package['version'] 8 | s.summary = package['summary'] 9 | s.description = package['description'] 10 | s.author = package['author']['name'] 11 | s.license = package['license'] 12 | s.homepage = package['homepage'] 13 | s.source = { :git => 'https://github.com/SelfLender/react-native-biometrics.git', :tag => "#{s.version}" } 14 | s.platform = :ios, '10.0' 15 | s.source_files = 'ios/**/*.{h,m}' 16 | s.dependency 'React-Core' 17 | end 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noImplicitAny": true, 5 | "target": "es5", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "moduleResolution": "node", 9 | "outDir": "build", 10 | "lib": ["es2015", "es2015.promise"] 11 | }, 12 | "include": [ 13 | "index.ts", 14 | "modules.d.ts" 15 | ] 16 | } --------------------------------------------------------------------------------