├── .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 | 
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