React Native Keychain
Overview
In today's digital world, maintaining the security of user credentials is of utmost importance. React Native offers a powerful solution for securely storing sensitive data using the React Native Keychain library. In this article, we will explore how to leverage the capabilities of React Native Keychain to ensure the safe storage and retrieval of user credentials in React Native applications.
Introduction
This article serves as a comprehensive guide for developers seeking to enhance the security of their React Native applications by implementing React Native Keychain. We will cover the step-by-step process of installing the library, integrating it into your project, and utilizing its features to securely store and retrieve user credentials. By the end of this guide, you will have a solid understanding of how to leverage React Native Keychain to protect sensitive user information.
Installation
To install React Native Keychain in your project, follow the below steps:
-
Open your terminal or command prompt and navigate to your project directory.
-
If you are using npm, run the following command:
If you are using Yarn, run the following command:
-
React Native Keychain relies on native code, so you need to link the library to your project. Run the following command:
This command will automatically link the library for both Android and iOS platforms.
-
For iOS:
- If you are using CocoaPods, open the ios directory in your project and run:
- If you are not using CocoaPods, ensure that the library is linked correctly by opening your Xcode project, navigating to the "Build Phases" tab, and checking if libRNKeychain.a is listed under "Link Binary With Libraries".
- If you are using CocoaPods, open the ios directory in your project and run:
-
For Android:
- No additional steps are required for Android. React Native Keychain will be automatically linked to your project.
-
After the installation and linking process is complete, rebuild your project to ensure that the changes are applied:
- For iOS: Run your app using Xcode or the command npx react-native run-ios.
- For Android: Run your app using Android Studio or the command npx react-native run-android.
Now, you can import the library into your code files and start using its secure credential storage features.
Usage
Please note that both the setGenericPassword and setInternetCredentials functions only accept strings as parameters. If you need to store objects or other data types, you can use JSON.stringify when storing and JSON.parse when accessing it.
API
setGenericPassword
The function setGenericPassword(username, password, [{ accessControl, accessible, accessGroup, service, securityLevel }]) is used to securely store the combination of a username and password. It returns a promise that resolves to { service, storage } if the storage is successful or rejects if an error occurs. The storage parameter represents the name of the internal cipher used for saving the secret, while the service parameter is the name used to store the secret in the internal storage (an empty string is resolved to a valid default name).
getGenericPassword
The function getGenericPassword([{ authenticationPrompt, service, accessControl }]) retrieves the stored username and password combination from the secure storage. It returns a promise that resolves to { username, password, service, storage } if an entry exists, or false if it doesn't. It only rejects if an unexpected error occurs, such as lacking entitlements or permissions.
resetGenericPassword
The function resetGenericPassword([{ service }]) removes the stored username and password combination from the secure storage. It resolves to true if the operation is successful.
getAllGenericPasswordServices()
The function getAllGenericPasswordServices() retrieves all the known service names for which a generic password has been stored using the setGenericPassword function.
Note: On iOS, calling this function will read the encrypted entries, which may trigger an authentication UI if any entries have been encrypted with a password or biometry.
setInternetCredentials(server, username, password, [{ ## accessControl, accessible, accessGroup, securityLevel }])
The function securely stores the combination of server, username, and password in the secure storage. It resolves to { username, password, service, storage } when successful.
hasInternetCredentials(server)
The function hasInternetCredentials(server) checks if a username and password combination for the specified server is available in the secure storage. It resolves to true if an entry exists, or false if it doesn't.
getInternetCredentials(server, [{ authenticationPrompt }])
The function getInternetCredentials(server, [{ authenticationPrompt }]) retrieves the server, username, and password combination from the secure storage. It resolves to { username, password } if an entry exists, or false if it doesn't. It only rejects if an unexpected error occurs, such as lacking entitlements or permissions.
resetInternetCredentials(server)
The function resetInternetCredentials(server) removes the server, username, and password combination from the secure storage.
requestSharedWebCredentials() (iOS only)
The function prompts the user for a shared web credential. It requires additional setup both on the app and server side, as specified in the Apple documentation. It resolves to { server, username, password } if approved, returns false if denied, and throws an error if it's not supported on the platform or there are no shared credentials.
setSharedWebCredentials(server, username, password) (iOS only)
The function sets a shared web credential. It resolves to true when the operation is successful.
canImplyAuthentication([{ authenticationType }]) (iOS only)
The function canImplyAuthentication([{ authenticationType }]) checks if the type of local authentication policy is supported on the device based on the user's chosen device settings. It should be used in combination with the access control option in the setter functions. It resolves to true if supported.
getSupportedBiometryType()
The function returns the type of biometric hardware support available on the device. On iOS, it resolves to a Keychain.BIOMETRY_TYPE value if supported and enrolled, otherwise, it returns null. On Android, it returns the type of Class 3 (strong) biometric support the device has, which is typically FINGERPRINT (except for devices like Pixel 4 without a fingerprint sensor). If the device has not enrolled in fingerprint/Face ID, even though it has the hardware for it, the method returns null.
getSecurityLevel([{ accessControl }]) (Android only)
Retrieve the supported security level of the current device and operating system, which is represented by the Keychain.SECURITY_LEVEL enumeration value. Options Data Structure Properties/Fields
| Key | Platform | Description | Default |
|---|---|---|---|
| accessControl | All | Dictates how a keychain item may be used | None |
| accessible | iOS only | Dictates when a keychain item is accessible | Keychain.ACCESSIBLE.WHEN_UNLOCKED |
| accessGroup | iOS only | Specifies the App Group to share the keychain | None |
| authenticationPrompt | All | Prompt displayed when unlocking the keychain with biometry or password | See authenticationPrompt Properties |
| authenticationType | iOS only | Policies specifying acceptable forms of authentication | Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS |
| service | All | Reverse domain name qualifier for the associated service password | App bundle ID |
| storage | Android only | Force specific cipher storage usage during password saving | Select best available storage |
| rules | Android only | Force following specific security rules | Keychain.RULES.AUTOMATIC_UPGRADE |
Note: The table provides an overview of the options, their corresponding platforms, descriptions, and default values for the given data structure properties/fields.
authenticationPrompt Properties
| Key | Platform | Description | Default |
|---|---|---|---|
| title | All | Authentication prompt title when requesting a stored secret. | Retrieve secret authentication |
| subtitle | Android only | Subtitle of the Android authentication prompt for stored secrets. | None. Optional |
| description | Android only | Description of the Android authentication prompt for stored secrets. | None. Optional |
| cancel | Android only | Negative button text for the Android authentication prompt. | Cancel |
Keychain.ACCESS_CONTROL enum
| Key | Description |
|---|---|
| Keychain.ACCESS_CONTROL.USER_PRESENCE | Constraint that allows access to an item with either Touch ID or passcode. |
| Keychain.ACCESS_CONTROL.BIOMETRY_ANY | Constraint that allows access to an item with Touch ID for any enrolled fingers. |
| Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET | Constraint that allows access to an item with Touch ID for currently enrolled fingers. |
| Keychain.ACCESS_CONTROL.DEVICE_PASSCODE | Constraint that allows access to an item with a passcode. |
| Keychain.ACCESS_CONTROL.APPLICATION_PASSWORD | Constraint that allows the use of an application-provided password for data encryption key generation. |
| Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE | Constraint that allows access to an item with Touch ID for any enrolled fingers or passcode. |
| Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE | Constraint that allows access to an item with Touch ID for currently enrolled fingers or passcode. |
Usage with server
To use react-native-keychain with a server, you can follow these steps:
- Register and authenticate the user on the server: Use an authentication mechanism such as username/password, OAuth, or JWT to authenticate the user and receive an authentication token or session ID from the server.\
- Store the token securely: Use react-native-keychain to securely store the authentication token on the device's keychain. You can use the setGenericPassword method to save the token.
- Retrieve the token when needed: Use the getGenericPassword method to retrieve the stored token from the keychain whenever you need to make requests to the server.
- Include the token in server requests: Include the retrieved token as an authorization header or in the request payload when making API requests to the server. This allows the server to authenticate the user based on the provided token.
- Validate the token on the server: On the server-side, validate the received token for each request to ensure the user is authorized to access the requested resources.
Android Specific Note
The module utilizes the appropriate CipherStorage implementation based on the API level of the device. For API levels 16-22, the encryption and decryption are performed using Facebook Conceal, while for API level 23 and above, the Android Keystore is used. The encrypted data is stored in the SharedPreferences.
When making the setInternetCredentials(server, username, password) call, it will be resolved as a call to setGenericPassword(username, password, server). To differentiate between multiple entries, the server argument can be used.
To configure the specific behavior for Android, it is important to consider the variations in implementations by different vendors, which can result in behavioral inconsistencies. For example, certain Samsung devices may experience slow startup of the crypto system. To address this, the Android implementation of the library introduces a warm-up strategy.
By using the default constructor, the default behavior is obtained, which includes the warming up process.
For those who require more control and customization, it is recommended to use the constructor with a builder, allowing for configuration according to specific preferences.
By utilizing this approach, developers can finely tune the behavior of the Android implementation of the library based on their requirements.
Unit Testing with Jest
When running Jest tests, the keychain manager, which relies on native application interfaces, cannot be compiled and executed successfully since there is no underlying app to interact with. To enable the invocation of JS functions exposed by this module during unit testing, you need to mock them using one of the following two approaches:
To begin, let's create a mock object for the module:
This mock object provides replacements for the original functions, allowing them to be called during testing without relying on the actual keychain manager implementation. By utilizing the jest.fn() function and mockResolvedValue() method, we ensure that the mock functions return resolved promises, mimicking the behavior of the real functions.
Using a Jest __mocks__ Directory
For the initial setup, it is recommended to refer to the documentation of Jest.
To proceed, create a folder named react-native-keychain within the mocks directory. Inside this folder, add an index.js file and populate it with the provided code snippet:
Using a Jest Setup File
To include a setup file reference in your Jest configuration, follow these steps:
- Open your Jest config file.
- Add a reference to the setup file using the setupFiles configuration option.
Example:
Inside the setup file (setupFile.js), you can set up mocking for the react-native-keychain package. This can be done using the jest.mock() function.
Example:
By performing these steps, your tests should now run successfully. However, it's important to note that any read or write operations to the keychain will effectively be a no-op, as they are mocked.
Detailed Example
Let's create an example react native application to demonstrate the use of the react-native-keychain library.
-
We begin by importing the necessary dependencies. React, useEffect, useState are imported from the react package to handle state management in the functional component. View, Button, TextInput, and Text are imported from react-native to create the user interface components. Keychain is imported from react-native-keychain to interact with the device's secure keychain storage.
-
We define our functional component called App and set up state variables using the useState hook. We have username and password to track the input values from the user, and storedUsername and storedPassword to store the retrieved credentials from the keychain.
-
With the useEffect hook, we load the stored credentials when the component mounts. The loadCredentials function is called once due to the empty dependency array [] passed as the second argument to useEffect.
-
The loadCredentials function is an asynchronous function that uses Keychain.getGenericPassword() to retrieve the stored credentials from the keychain. If credentials are found, the username and password properties are extracted and stored in the component's state variables storedUsername and storedPassword. Any errors that occur during the process are logged to the console.
-
The saveCredentials function is an asynchronous function that uses Keychain.setGenericPassword() to securely store the username and password values entered by the user. If the operation is successful, a success message is logged to the console, and the stored username and password are updated in the component's state variables storedUsername and storedPassword. Any errors that occur during the process are logged to the console.
-
The clearCredentials function is an asynchronous function that uses Keychain.resetGenericPassword() to remove any stored credentials from the keychain. If the operation is successful, a success message is logged to the console, and the stored username and password in the component's state variables storedUsername and storedPassword are cleared. Any errors that occur during the process are logged to the console.
-
This section defines the user interface components using JSX syntax. We have two TextInput components for entering the username and password. The onChangeText prop is used to update the state variables username and password whenever the input values change. The value prop is set to the corresponding state variables to display the entered values.
-
The secureTextEntry prop is added to the password input to hide the entered characters. Two Button components are provided for saving and clearing the credentials, respectively. The onPress prop of the buttons is set to call the saveCredentials and clearCredentials functions.
-
We have conditionally render the stored credentials if they exist, showing the username and password values within a View component.
Conclusion
React Native Keychain is a valuable package that provides a secure and convenient solution for managing sensitive data, such as passwords and access tokens, in React Native applications.
- By leveraging the native keychain functionality of both iOS and Android platforms, React Native Keychain ensures data encryption and protection at the device level.
- With its easy-to-use API, developers can securely store, retrieve, and delete sensitive information from the keychain, simplifying the implementation of authentication and secure data management in their applications.
- React Native Keychain offers additional features like biometric authentication support, allowing users to authenticate using their fingerprint or face ID.
- By using React Native Keychain, developers can enhance the security and privacy of their applications, providing users with a seamless and trustworthy experience.
- The library is well-documented, actively maintained, and has strong community support, making it a reliable choice for integrating secure keychain functionality in React Native projects.