Skip to content

Commit

Permalink
feat: add visionOS support (#622)
Browse files Browse the repository at this point in the history
* feat: add visionOS support

* docs: add visionOS docs
  • Loading branch information
thiagobrez committed Feb 22, 2024
1 parent adf379d commit 5fc9e8b
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 25 deletions.
41 changes: 23 additions & 18 deletions README.md
Expand Up @@ -22,9 +22,9 @@
- [`hasInternetCredentials(server)`](#hasinternetcredentialsserver)
- [`getInternetCredentials(server, [{ authenticationPrompt }])`](#getinternetcredentialsserver--authenticationprompt-)
- [`resetInternetCredentials(server)`](#resetinternetcredentialsserver)
- [`requestSharedWebCredentials()` (iOS only)](#requestsharedwebcredentials-ios-only)
- [`setSharedWebCredentials(server, username, password)` (iOS only)](#setsharedwebcredentialsserver-username-password-ios-only)
- [`canImplyAuthentication([{ authenticationType }])` (iOS only)](#canimplyauthentication-authenticationtype--ios-only)
- [`requestSharedWebCredentials()` (iOS and visionOS only)](#requestsharedwebcredentials-ios-and-visionos-only)
- [`setSharedWebCredentials(server, username, password)` (iOS and visionOS only)](#setsharedwebcredentialsserver-username-password-ios-and-visionos-only)
- [`canImplyAuthentication([{ authenticationType }])` (iOS and visionOS only)](#canimplyauthentication-authenticationtype--ios-and-visionos-only)
- [`getSupportedBiometryType()`](#getsupportedbiometrytype)
- [`getSecurityLevel([{ accessControl }])` (Android only)](#getsecuritylevel-accesscontrol--android-only)
- [Options](#options)
Expand Down Expand Up @@ -139,21 +139,21 @@ Will retrieve the server/username/password combination from the secure storage.

Will remove the server/username/password combination from the secure storage.

### `requestSharedWebCredentials()` (iOS only)
### `requestSharedWebCredentials()` (iOS and visionOS only)

Asks the user for a shared web credential. Requires additional setup both in the app and server side, see [Apple documentation](https://developer.apple.com/documentation/security/shared_web_credentials). Resolves to `{ server, username, password }` if approved and `false` if denied and throws an error if not supported on platform or there's no shared credentials.

### `setSharedWebCredentials(server, username, password)` (iOS only)
### `setSharedWebCredentials(server, username, password)` (iOS and visionOS only)

Sets a shared web credential. Resolves to `true` when successful.

### `canImplyAuthentication([{ authenticationType }])` (iOS only)
### `canImplyAuthentication([{ authenticationType }])` (iOS and visionOS only)

Inquire if the type of local authentication policy is supported on this device with the device settings the user chose. Should be used in combination with `accessControl` option in the setter functions. Resolves to `true` if supported.

### `getSupportedBiometryType()`

**On iOS:** Get what type of hardware biometry support the device can use for biometric encryption. Resolves to a `Keychain.BIOMETRY_TYPE` value when supported and enrolled, otherwise `null`.
**On iOS and visionOS:** Get what type of hardware biometry support the device can use for biometric encryption. Resolves to a `Keychain.BIOMETRY_TYPE` value when supported and enrolled, otherwise `null`.

**On Android:** Get what type of Class 3 (strong) biometry support the device has. Resolves to a `Keychain.BIOMETRY_TYPE` value when supported, otherwise `null`. In most devices this will return `FINGERPRINT` (except for Pixel 4 or similar where fingerprint sensor is not present).

Expand All @@ -167,16 +167,16 @@ Get security level that is supported on the current device with the current OS.

#### Data Structure Properties/Fields

| Key | Platform | Description | Default |
| -------------------------- | ------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| **`accessControl`** | All | This dictates how a keychain item may be used, see possible values in `Keychain.ACCESS_CONTROL`. | _None_ |
| **`accessible`** | iOS only | This dictates when a keychain item is accessible, see possible values in `Keychain.ACCESSIBLE`. | _`Keychain.ACCESSIBLE.WHEN_UNLOCKED`_ |
| **`accessGroup`** | iOS only | In which App Group to share the keychain. Requires additional setup with entitlements. | _None_ |
| **`authenticationPrompt`** | All | What to prompt the user when unlocking the keychain with biometry or device password. | See [`authenticationPrompt` Properties](#authenticationprompt-properties) |
| **`authenticationType`** | iOS only | Policies specifying which forms of authentication are acceptable. | `Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS` |
| **`service`** | All | Reverse domain name qualifier for the service associated with password. | _App bundle ID_ |
| **`storage`** | Android only | Force specific cipher storage usage during saving the password | Select best available storage |
| **`rules`** | Android only | Force following to a specific security rules | `Keychain.RULES.AUTOMATIC_UPGRADE` |
| Key | Platform | Description | Default |
| -------------------------- |---------------| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| **`accessControl`** | All | This dictates how a keychain item may be used, see possible values in `Keychain.ACCESS_CONTROL`. | _None_ |
| **`accessible`** | iOS, visionOS | This dictates when a keychain item is accessible, see possible values in `Keychain.ACCESSIBLE`. | _`Keychain.ACCESSIBLE.WHEN_UNLOCKED`_ |
| **`accessGroup`** | iOS, visionOS | In which App Group to share the keychain. Requires additional setup with entitlements. | _None_ |
| **`authenticationPrompt`** | All | What to prompt the user when unlocking the keychain with biometry or device password. | See [`authenticationPrompt` Properties](#authenticationprompt-properties) |
| **`authenticationType`** | iOS, visionOS | Policies specifying which forms of authentication are acceptable. | `Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS` |
| **`service`** | All | Reverse domain name qualifier for the service associated with password. | _App bundle ID_ |
| **`storage`** | Android only | Force specific cipher storage usage during saving the password | Select best available storage |
| **`rules`** | Android only | Force following to a specific security rules | `Keychain.RULES.AUTOMATIC_UPGRADE` |

##### `authenticationPrompt` Properties

Expand Down Expand Up @@ -236,9 +236,10 @@ Refs:
#### `Keychain.BIOMETRY_TYPE` enum

| Key | Description |
| ----------------- | -------------------------------------------------------------------- |
|-------------------|----------------------------------------------------------------------|
| **`TOUCH_ID`** | Device supports authentication with Touch ID. (iOS only) |
| **`FACE_ID`** | Device supports authentication with Face ID. (iOS only) |
| **`OPTIC_ID`** | Device supports authentication with Optic ID. (visionOS only) |
| **`FINGERPRINT`** | Device supports authentication with Fingerprint. (Android only) |
| **`FACE`** | Device supports authentication with Face Recognition. (Android only) |
| **`IRIS`** | Device supports authentication with Iris Recognition. (Android only) |
Expand Down Expand Up @@ -503,6 +504,10 @@ Refs:

This package supports macOS Catalyst.

### visionOS

This package supports visionOS.

### Security

On API levels that do not support Android keystore, Facebook Conceal is used to en/decrypt stored data. The encrypted data is then stored in SharedPreferences. Since Conceal itself stores its encryption key in SharedPreferences, it follows that if the device is rooted (or if an attacker can somehow access the filesystem), the key can be obtained and the stored data can be decrypted. Therefore, on such a device, the conceal encryption is only an obscurity. On API level 23+ the key is stored in the Android Keystore, which makes the key non-exportable and therefore makes the entire process more secure. Follow best practices and do not store user credentials on a device. Instead use tokens or other forms of authentication and re-ask for user credentials before performing sensitive operations.
Expand Down
1 change: 1 addition & 0 deletions RNKeychain.podspec
Expand Up @@ -12,6 +12,7 @@ Pod::Spec.new do |s|
s.ios.deployment_target = '9.0'
s.tvos.deployment_target = '9.0'
s.osx.deployment_target = '10.13'
s.visionos.deployment_target = '1.0'
s.source = { :git => "https://github.com/oblador/react-native-keychain.git", :tag => "v#{s.version}" }
s.source_files = 'RNKeychainManager/**/*.{h,m}'
s.preserve_paths = "**/*.js"
Expand Down
18 changes: 12 additions & 6 deletions RNKeychainManager/RNKeychainManager.m
Expand Up @@ -12,7 +12,7 @@
#import <React/RCTBridge.h>
#import <React/RCTUtils.h>

#if TARGET_OS_IOS
#if TARGET_OS_IOS || TARGET_OS_VISION
#import <LocalAuthentication/LAContext.h>
#endif

Expand Down Expand Up @@ -151,8 +151,9 @@ CFStringRef accessibleValue(NSDictionary *options)

#define kBiometryTypeTouchID @"TouchID"
#define kBiometryTypeFaceID @"FaceID"
#define kBiometryTypeOpticID @"OpticID"

#if TARGET_OS_IOS
#if TARGET_OS_IOS || TARGET_OS_VISION
LAPolicy authPolicy(NSDictionary *options)
{
if (options && options[kAuthenticationType]) {
Expand Down Expand Up @@ -209,7 +210,7 @@ - (void)insertKeychainEntry:(NSDictionary *)attributes

if (accessControl) {
NSError *aerr = nil;
#if TARGET_OS_IOS
#if TARGET_OS_IOS || TARGET_OS_VISION
BOOL canAuthenticate = [[LAContext new] canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&aerr];
if (aerr || !canAuthenticate) {
return rejectWithError(reject, aerr);
Expand Down Expand Up @@ -305,7 +306,7 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server

#pragma mark - RNKeychain

#if TARGET_OS_IOS
#if TARGET_OS_IOS || TARGET_OS_VISION
RCT_EXPORT_METHOD(canCheckAuthentication:(NSDictionary * __nullable)options
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
Expand All @@ -323,7 +324,7 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
}
#endif

#if TARGET_OS_IOS
#if TARGET_OS_IOS || TARGET_OS_VISION
RCT_EXPORT_METHOD(getSupportedBiometryType:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
Expand All @@ -332,6 +333,11 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
BOOL canBeProtected = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&aerr];

if (!aerr && canBeProtected) {
if (@available(visionOS 1, *)) {
if (context.biometryType == LABiometryTypeOpticID) {
return resolve(kBiometryTypeOpticID);
}
}
if (@available(iOS 11, *)) {
if (context.biometryType == LABiometryTypeFaceID) {
return resolve(kBiometryTypeFaceID);
Expand Down Expand Up @@ -538,7 +544,7 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
return resolve(@(YES));
}

#if TARGET_OS_IOS && !TARGET_OS_UIKITFORMAC
#if (TARGET_OS_IOS || TARGET_OS_VISION) && !TARGET_OS_UIKITFORMAC
RCT_EXPORT_METHOD(requestSharedWebCredentials:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
SecRequestSharedWebCredential(NULL, NULL, ^(CFArrayRef credentials, CFErrorRef error) {
Expand Down
3 changes: 2 additions & 1 deletion typings/react-native-keychain.d.ts
Expand Up @@ -46,6 +46,7 @@ declare module 'react-native-keychain' {
export enum BIOMETRY_TYPE {
TOUCH_ID = 'TouchID',
FACE_ID = 'FaceID',
OPTIC_ID = 'OpticID',
FINGERPRINT = 'Fingerprint',
FACE = 'Face',
IRIS = 'Iris',
Expand Down Expand Up @@ -119,7 +120,7 @@ declare module 'react-native-keychain' {
options?: Options
): Promise<null | BIOMETRY_TYPE>;

/** IOS ONLY */
/** IOS AND VISIONOS ONLY */

function requestSharedWebCredentials(): Promise<false | SharedWebCredentials>;

Expand Down

0 comments on commit 5fc9e8b

Please sign in to comment.