From 5fc9e8b21d7ef4ae9130c091e1e1d793085827aa Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Thu, 22 Feb 2024 22:07:04 +0000 Subject: [PATCH] feat: add visionOS support (#622) * feat: add visionOS support * docs: add visionOS docs --- README.md | 41 +++++++++++++++------------ RNKeychain.podspec | 1 + RNKeychainManager/RNKeychainManager.m | 18 ++++++++---- typings/react-native-keychain.d.ts | 3 +- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 5eeb1b85..aeb55fe1 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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). @@ -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 @@ -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) | @@ -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. diff --git a/RNKeychain.podspec b/RNKeychain.podspec index 6a2fd094..9a5347d4 100644 --- a/RNKeychain.podspec +++ b/RNKeychain.podspec @@ -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" diff --git a/RNKeychainManager/RNKeychainManager.m b/RNKeychainManager/RNKeychainManager.m index 9efac961..58d555f1 100644 --- a/RNKeychainManager/RNKeychainManager.m +++ b/RNKeychainManager/RNKeychainManager.m @@ -12,7 +12,7 @@ #import #import -#if TARGET_OS_IOS +#if TARGET_OS_IOS || TARGET_OS_VISION #import #endif @@ -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]) { @@ -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); @@ -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) @@ -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) { @@ -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); @@ -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) { diff --git a/typings/react-native-keychain.d.ts b/typings/react-native-keychain.d.ts index 6904f63d..6846468e 100644 --- a/typings/react-native-keychain.d.ts +++ b/typings/react-native-keychain.d.ts @@ -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', @@ -119,7 +120,7 @@ declare module 'react-native-keychain' { options?: Options ): Promise; - /** IOS ONLY */ + /** IOS AND VISIONOS ONLY */ function requestSharedWebCredentials(): Promise;