diff --git a/KeychainExample/App.tsx b/KeychainExample/App.tsx deleted file mode 100644 index a6f957ca..00000000 --- a/KeychainExample/App.tsx +++ /dev/null @@ -1,417 +0,0 @@ -import React, { Component } from 'react'; -import { - Alert, - Keyboard, - KeyboardAvoidingView, - Platform, - StyleSheet, - Text, - TextInput, - TouchableHighlight, - View, -} from 'react-native'; -import SegmentedControlTab from 'react-native-segmented-control-tab'; -import * as Keychain from 'react-native-keychain'; - -const ACCESS_CONTROL_OPTIONS = ['None', 'Passcode', 'Password']; -const ACCESS_CONTROL_OPTIONS_ANDROID = ['None']; -const ACCESS_CONTROL_MAP = [ - null, - Keychain.ACCESS_CONTROL.DEVICE_PASSCODE, - Keychain.ACCESS_CONTROL.APPLICATION_PASSWORD, - Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET, -]; -const ACCESS_CONTROL_MAP_ANDROID = [ - null, - Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET, -]; -const SECURITY_LEVEL_OPTIONS = ['Any', 'Software', 'Hardware']; -const SECURITY_LEVEL_MAP = [ - Keychain.SECURITY_LEVEL.ANY, - Keychain.SECURITY_LEVEL.SECURE_SOFTWARE, - Keychain.SECURITY_LEVEL.SECURE_HARDWARE, -]; - -const SECURITY_STORAGE_OPTIONS = ['Best', 'FB', 'AES', 'RSA']; -const SECURITY_STORAGE_MAP = [ - null, - Keychain.STORAGE_TYPE.FB, - Keychain.STORAGE_TYPE.AES, - Keychain.STORAGE_TYPE.RSA, -]; -const SECURITY_RULES_OPTIONS = ['No upgrade', 'Automatic upgrade']; -const SECURITY_RULES_MAP = [null, Keychain.SECURITY_RULES.AUTOMATIC_UPGRADE]; - -export default class KeychainExample extends Component { - state = { - username: '', - password: '', - status: '', - biometryType: undefined, - accessControl: undefined as undefined | Keychain.ACCESS_CONTROL, - securityLevel: undefined as undefined | Keychain.SECURITY_LEVEL, - storage: undefined as undefined | Keychain.STORAGE_TYPE, - rules: undefined as undefined | Keychain.SECURITY_RULES, - selectedStorageIndex: 0, - selectedSecurityIndex: 0, - selectedAccessControlIndex: 0, - selectedRulesIndex: 0, - hasGenericPassword: false, - }; - - componentDidMount() { - Keychain.getSupportedBiometryType().then((biometryType) => { - this.setState({ biometryType }); - }); - Keychain.hasGenericPassword().then((hasGenericPassword) => { - this.setState({ hasGenericPassword }); - }); - } - - async save() { - try { - const start = new Date(); - await Keychain.setGenericPassword( - this.state.username, - this.state.password, - { - accessControl: this.state.accessControl, - securityLevel: this.state.securityLevel, - storage: this.state.storage, - rules: this.state.rules, - } - ); - - const end = new Date(); - - this.setState({ - username: '', - password: '', - status: `Credentials saved! takes: ${ - end.getTime() - start.getTime() - } millis`, - }); - } catch (err) { - this.setState({ status: 'Could not save credentials, ' + err }); - } - } - - async load() { - try { - const options = { - authenticationPrompt: { - title: 'Authentication needed', - subtitle: 'Subtitle', - description: 'Some descriptive text', - cancel: 'Cancel', - }, - }; - const credentials = await Keychain.getGenericPassword({ - ...options, - rules: this.state.rules, - }); - if (credentials) { - this.setState({ - status: 'Credentials loaded! ' + JSON.stringify(credentials), - }); - } else { - this.setState({ status: 'No credentials stored.' }); - } - } catch (err) { - this.setState({ status: 'Could not load credentials. ' + err }); - } - } - - async reset() { - try { - await Keychain.resetGenericPassword(); - this.setState({ - status: 'Credentials Reset!', - username: '', - password: '', - }); - } catch (err) { - this.setState({ status: 'Could not reset credentials, ' + err }); - } - } - - async getAll() { - try { - const result = await Keychain.getAllGenericPasswordServices(); - this.setState({ - status: `All keys successfully fetched! Found: ${result.length} keys.`, - }); - } catch (err) { - this.setState({ status: 'Could not get all keys. ' + err }); - } - } - - async ios_specifics() { - try { - const reply = await Keychain.setSharedWebCredentials( - 'server', - 'username', - 'password' - ); - console.log(`setSharedWebCredentials: ${JSON.stringify(reply)}`); - } catch (err) { - Alert.alert('setSharedWebCredentials error', (err as Error).message); - } - - try { - const reply = await Keychain.requestSharedWebCredentials(); - console.log(`requestSharedWebCredentials: ${JSON.stringify(reply)}`); - } catch (err) { - Alert.alert('requestSharedWebCredentials error', (err as Error).message); - } - } - - render() { - const AC_VALUES = - Platform.OS === 'ios' - ? ACCESS_CONTROL_OPTIONS - : ACCESS_CONTROL_OPTIONS_ANDROID; - const AC_MAP = - Platform.OS === 'ios' ? ACCESS_CONTROL_MAP : ACCESS_CONTROL_MAP_ANDROID; - - return ( - - - Keyboard.dismiss()}> - Keychain Example - - - Username - - this.setState({ username: event.nativeEvent.text }) - } - underlineColorAndroid="transparent" - blurOnSubmit={false} - returnKeyType="next" - /> - - - Password - - this.setState({ password: event.nativeEvent.text }) - } - underlineColorAndroid="transparent" - /> - - - Access Control - - this.setState({ - ...this.state, - accessControl: AC_MAP[index], - selectedAccessControlIndex: index, - }) - } - /> - - {Platform.OS === 'android' && ( - - Security Level - - this.setState({ - ...this.state, - securityLevel: SECURITY_LEVEL_MAP[index], - selectedSecurityIndex: index, - }) - } - /> - Storage - - this.setState({ - ...this.state, - storage: SECURITY_STORAGE_MAP[index], - selectedStorageIndex: index, - }) - } - /> - Rules - - this.setState({ - ...this.state, - rules: SECURITY_RULES_MAP[index], - selectedRulesIndex: index, - }) - } - /> - - )} - {!!this.state.status && ( - {this.state.status} - )} - - - this.save()} - style={styles.button} - > - - Save - - - - this.load()} - style={styles.button} - > - - Load - - - - this.reset()} - style={styles.button} - > - - Reset - - - - - - this.getAll()} - style={styles.button} - > - - Get Used Keys - - - {Platform.OS === 'android' && ( - { - const level = await Keychain.getSecurityLevel(); - if (level !== null) { - Alert.alert('Security Level', JSON.stringify(level)); - } - }} - style={styles.button} - > - - Get security level - - - )} - {Platform.OS === 'ios' && ( - this.ios_specifics()} - style={styles.button} - > - - Test Other APIs - - - )} - - - hasGenericPassword: {String(this.state.hasGenericPassword)} - - - - ); - } -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - backgroundColor: '#F5FCFF', - }, - content: { - marginHorizontal: 20, - }, - title: { - fontSize: 28, - fontWeight: '200', - textAlign: 'center', - marginBottom: 20, - }, - field: { - marginVertical: 5, - }, - label: { - fontWeight: '500', - fontSize: 15, - marginBottom: 5, - }, - input: { - color: '#000', - borderWidth: StyleSheet.hairlineWidth, - borderColor: '#ccc', - backgroundColor: 'white', - height: 32, - fontSize: 14, - padding: 8, - }, - status: { - color: '#333', - fontSize: 12, - marginTop: 15, - }, - biometryType: { - color: '#333', - fontSize: 12, - marginTop: 15, - }, - buttons: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: 20, - }, - button: { - borderRadius: 3, - padding: 2, - overflow: 'hidden', - }, - save: { - backgroundColor: '#0c0', - }, - load: { - backgroundColor: '#333', - }, - reset: { - backgroundColor: '#c00', - }, - buttonText: { - color: 'white', - fontSize: 14, - paddingHorizontal: 16, - paddingVertical: 8, - }, -}); diff --git a/KeychainExample/Gemfile.lock b/KeychainExample/Gemfile.lock index dde59dca..aa5bb4fc 100644 --- a/KeychainExample/Gemfile.lock +++ b/KeychainExample/Gemfile.lock @@ -84,10 +84,10 @@ GEM fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) json (2.7.2) - logger (1.6.0) + logger (1.6.1) minitest (5.25.1) molinillo (0.8.0) nanaimo (0.3.0) @@ -95,22 +95,20 @@ GEM netrc (0.11.0) nkf (0.2.0) public_suffix (4.0.7) - rexml (3.3.6) - strscan + rexml (3.3.8) ruby-macho (2.5.1) securerandom (0.3.1) - strscan (3.1.0) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - xcodeproj (1.25.0) + xcodeproj (1.25.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + rexml (>= 3.3.6, < 4.0) PLATFORMS aarch64-linux-gnu @@ -130,7 +128,7 @@ DEPENDENCIES cocoapods (>= 1.13, != 1.15.1, != 1.15.0) RUBY VERSION - ruby 3.3.0p0 + ruby 3.3.5p100 BUNDLED WITH - 2.5.17 + 2.5.21 diff --git a/KeychainExample/index.js b/KeychainExample/index.js index ab0ecbf4..117ddcae 100644 --- a/KeychainExample/index.js +++ b/KeychainExample/index.js @@ -1,5 +1,5 @@ import { AppRegistry } from 'react-native'; -import App from './App'; +import App from './src/App'; import { name as appName } from './app.json'; AppRegistry.registerComponent(appName, () => App); diff --git a/KeychainExample/ios/Podfile.lock b/KeychainExample/ios/Podfile.lock index 83c41cee..bd2cb03b 100644 --- a/KeychainExample/ios/Podfile.lock +++ b/KeychainExample/ios/Podfile.lock @@ -1145,7 +1145,7 @@ PODS: - React-Core - React-jsi - ReactTestApp-Resources (1.0.0-dev) - - RNKeychain (8.2.0): + - RNKeychain (9.0.0): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1402,7 +1402,7 @@ SPEC CHECKSUMS: ReactNativeHost: 2bc85a4cc8f2e7e7fef5e551d4adb9c90757859f ReactTestApp-DevSupport: 74676edd899013becce4eaecc5eabba1fc51e26e ReactTestApp-Resources: 857244f3a23f2b3157b364fa06cf3e8866deff9c - RNKeychain: 70a32d9f845cf928ab367ad9c0a9050eb3d5206c + RNKeychain: 604650b3772651acb4a47e261c306c789c0c4d9f SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 950bbfd7e6f04790fdb51149ed51df41f329fcc8 diff --git a/KeychainExample/package.json b/KeychainExample/package.json index 7831b8c6..84e98c33 100644 --- a/KeychainExample/package.json +++ b/KeychainExample/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "android": "react-native run-android", - "ios": "react-native run-ios --simulator 'iPhone 15 Pro' --mode Release", + "ios": "react-native run-ios --simulator 'iPhone 15 Pro'", "build:android": "npm run mkdist && react-native bundle --entry-file index.js --platform android --dev false --bundle-output dist/main.android.jsbundle --assets-dest dist/res", "build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev false --bundle-output dist/main.ios.jsbundle --assets-dest dist/assets", "mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"", diff --git a/KeychainExample/src/App.tsx b/KeychainExample/src/App.tsx new file mode 100644 index 00000000..4dc18c77 --- /dev/null +++ b/KeychainExample/src/App.tsx @@ -0,0 +1,346 @@ +import React, { useState, useEffect } from 'react'; +import { + Alert, + Keyboard, + KeyboardAvoidingView, + Platform, + StyleSheet, + Text, + TextInput, + TouchableHighlight, + View, +} from 'react-native'; +import SegmentedControlTab from 'react-native-segmented-control-tab'; +import * as Keychain from 'react-native-keychain'; + +const ACCESS_CONTROL_OPTIONS = ['None', 'Passcode', 'Password']; +const ACCESS_CONTROL_OPTIONS_ANDROID = ['None']; +const ACCESS_CONTROL_MAP = [ + null, + Keychain.ACCESS_CONTROL.DEVICE_PASSCODE, + Keychain.ACCESS_CONTROL.APPLICATION_PASSWORD, + Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET, +]; +const ACCESS_CONTROL_MAP_ANDROID = [ + null, + Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET, +]; +const SECURITY_LEVEL_OPTIONS = ['Any', 'Software', 'Hardware']; +const SECURITY_LEVEL_MAP = [ + Keychain.SECURITY_LEVEL.ANY, + Keychain.SECURITY_LEVEL.SECURE_SOFTWARE, + Keychain.SECURITY_LEVEL.SECURE_HARDWARE, +]; + +const SECURITY_STORAGE_OPTIONS = ['Best', 'FB', 'AES', 'RSA']; +const SECURITY_STORAGE_MAP = [ + null, + Keychain.STORAGE_TYPE.FB, + Keychain.STORAGE_TYPE.AES, + Keychain.STORAGE_TYPE.RSA, +]; +const SECURITY_RULES_OPTIONS = ['No upgrade', 'Automatic upgrade']; +const SECURITY_RULES_MAP = [null, Keychain.SECURITY_RULES.AUTOMATIC_UPGRADE]; + +export default function App() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [status, setStatus] = useState(''); + const [biometryType, setBiometryType] = + useState(null); + const [accessControl, setAccessControl] = useState< + Keychain.ACCESS_CONTROL | undefined + >(undefined); + const [securityLevel, setSecurityLevel] = useState< + Keychain.SECURITY_LEVEL | undefined + >(undefined); + const [storage, setStorage] = useState( + undefined + ); + const [rules, setRules] = useState( + undefined + ); + const [selectedStorageIndex, setSelectedStorageIndex] = useState(0); + const [selectedSecurityIndex, setSelectedSecurityIndex] = useState(0); + const [selectedAccessControlIndex, setSelectedAccessControlIndex] = + useState(0); + const [selectedRulesIndex, setSelectedRulesIndex] = useState(0); + const [hasGenericPassword, setHasGenericPassword] = useState(false); + + useEffect(() => { + Keychain.getSupportedBiometryType().then((result) => { + setBiometryType(result); + }); + Keychain.hasGenericPassword().then((result) => { + setHasGenericPassword(result); + }); + }, []); + + const save = async () => { + try { + const start = new Date(); + await Keychain.setGenericPassword(username, password, { + accessControl, + securityLevel, + storage, + rules, + }); + + const end = new Date(); + setUsername(''); + setPassword(''); + setStatus( + `Credentials saved! takes: ${end.getTime() - start.getTime()} millis` + ); + } catch (err) { + setStatus('Could not save credentials, ' + err); + } + }; + + const load = async () => { + try { + const options = { + authenticationPrompt: { + title: 'Authentication needed', + subtitle: 'Subtitle', + description: 'Some descriptive text', + cancel: 'Cancel', + }, + }; + const credentials = await Keychain.getGenericPassword({ + ...options, + rules: rules, + }); + if (credentials) { + setStatus('Credentials loaded! ' + JSON.stringify(credentials)); + } else { + setStatus('No credentials stored.'); + } + } catch (err) { + setStatus('Could not load credentials. ' + err); + } + }; + + const reset = async () => { + try { + await Keychain.resetGenericPassword(); + setStatus('Credentials Reset!'); + setUsername(''); + setPassword(''); + } catch (err) { + setStatus('Could not reset credentials, ' + err); + } + }; + + const getAll = async () => { + try { + const result = await Keychain.getAllGenericPasswordServices(); + setStatus(`All keys successfully fetched! Found: ${result.length} keys.`); + } catch (err) { + setStatus('Could not get all keys. ' + err); + } + }; + + const AC_VALUES = + Platform.OS === 'ios' + ? ACCESS_CONTROL_OPTIONS + : ACCESS_CONTROL_OPTIONS_ANDROID; + const AC_MAP = + Platform.OS === 'ios' ? ACCESS_CONTROL_MAP : ACCESS_CONTROL_MAP_ANDROID; + + return ( + + + Keyboard.dismiss()}> + Keychain Example + + + Username + setUsername(event.nativeEvent.text)} + underlineColorAndroid="transparent" + blurOnSubmit={false} + returnKeyType="next" + /> + + + Password + setPassword(event.nativeEvent.text)} + underlineColorAndroid="transparent" + /> + + + Access Control + { + setAccessControl(AC_MAP[index] || undefined); + setSelectedAccessControlIndex(index); + }} + /> + + {Platform.OS === 'android' && ( + + Security Level + { + setSecurityLevel(SECURITY_LEVEL_MAP[index] || undefined); + setSelectedSecurityIndex(index); + }} + /> + Storage + { + setStorage(SECURITY_STORAGE_MAP[index] || undefined); + setSelectedStorageIndex(index); + }} + /> + Rules + { + setRules(SECURITY_RULES_MAP[index] || undefined); + setSelectedRulesIndex(index); + }} + /> + + )} + {!!status && {status}} + + + + + Save + + + + + + Load + + + + + + Reset + + + + + + + + Get Used Keys + + + {Platform.OS === 'android' && ( + { + const level = await Keychain.getSecurityLevel(); + if (level !== null) { + Alert.alert('Security Level', JSON.stringify(level)); + } + }} + style={styles.button} + > + + Get security level + + + )} + + + hasGenericPassword: {String(hasGenericPassword)} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + backgroundColor: '#F5FCFF', + }, + content: { + marginHorizontal: 20, + }, + title: { + fontSize: 28, + fontWeight: '200', + textAlign: 'center', + marginBottom: 20, + }, + field: { + marginVertical: 5, + }, + label: { + fontWeight: '500', + fontSize: 15, + marginBottom: 5, + }, + input: { + color: '#000', + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#ccc', + backgroundColor: 'white', + height: 32, + fontSize: 14, + padding: 8, + }, + status: { + color: '#333', + fontSize: 12, + marginTop: 15, + }, + biometryType: { + color: '#333', + fontSize: 12, + marginTop: 15, + }, + buttons: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 20, + }, + button: { + borderRadius: 3, + padding: 2, + overflow: 'hidden', + }, + save: { + backgroundColor: '#0c0', + }, + load: { + backgroundColor: '#333', + }, + reset: { + backgroundColor: '#c00', + }, + buttonText: { + color: 'white', + fontSize: 14, + paddingHorizontal: 16, + paddingVertical: 8, + }, +}); diff --git a/android/src/main/java/com/oblador/keychain/KeychainModule.kt b/android/src/main/java/com/oblador/keychain/KeychainModule.kt index 78dd7c24..c71f9501 100644 --- a/android/src/main/java/com/oblador/keychain/KeychainModule.kt +++ b/android/src/main/java/com/oblador/keychain/KeychainModule.kt @@ -75,6 +75,7 @@ class KeychainModule(reactContext: ReactApplicationContext) : const val AUTH_PROMPT = "authenticationPrompt" const val AUTH_TYPE = "authenticationType" const val SERVICE = "service" + const val SERVER = "server" const val SECURITY_LEVEL = "securityLevel" const val RULES = "rules" const val USERNAME = "username" @@ -173,7 +174,7 @@ class KeychainModule(reactContext: ReactApplicationContext) : override fun getName(): String { return KEYCHAIN_MODULE } - + override fun invalidate() { super.invalidate() if (coroutineScope.isActive) { @@ -363,7 +364,8 @@ class KeychainModule(reactContext: ReactApplicationContext) : } @ReactMethod - fun hasInternetCredentialsForServer(server: String, promise: Promise) { + fun hasInternetCredentialsForOptions(options: ReadableMap, promise: Promise) { + val server = options.getString(Maps.SERVER) val alias = getAliasOrDefault(server) val resultSet = prefsStorage.getEncryptedEntry(alias) if (resultSet == null) { @@ -403,8 +405,10 @@ class KeychainModule(reactContext: ReactApplicationContext) : } @ReactMethod - fun resetInternetCredentialsForServer(server: String, promise: Promise) { - resetGenericPassword(server, promise) + fun resetInternetCredentialsForOptions(options: ReadableMap, promise: Promise) { + val server = options.getString(Maps.SERVER) + val alias = getAliasOrDefault(server) + resetGenericPassword(alias, promise) } @ReactMethod @@ -791,8 +795,8 @@ class KeychainModule(reactContext: ReactApplicationContext) : storage.securityLevel().name)) } - private fun getAliasOrDefault(service: String?): String { - return service ?: EMPTY_STRING + private fun getAliasOrDefault(alias: String?): String { + return alias ?: EMPTY_STRING } // endregion } } diff --git a/ios/RNKeychainManager/RNKeychainManager.m b/ios/RNKeychainManager/RNKeychainManager.m index 2e431ccd..8f1df814 100644 --- a/ios/RNKeychainManager/RNKeychainManager.m +++ b/ios/RNKeychainManager/RNKeychainManager.m @@ -119,6 +119,14 @@ CFStringRef accessibleValue(NSDictionary *options) return [[NSBundle mainBundle] bundleIdentifier]; } +NSString *serverValue(NSDictionary *options) +{ + if (options && options[@"server"] != nil) { + return options[@"server"]; + } + return @""; +} + NSString *accessGroupValue(NSDictionary *options) { if (options && options[@"accessGroup"] != nil) { @@ -127,6 +135,14 @@ CFStringRef accessibleValue(NSDictionary *options) return nil; } +CFBooleanRef cloudSyncValue(NSDictionary *options) +{ + if (options && options[@"cloudSync"]) { + return kCFBooleanTrue; + } + return kCFBooleanFalse; +} + NSString *authenticationPromptValue(NSDictionary *options) { if (options && options[@"authenticationPrompt"] != nil && options[@"authenticationPrompt"][@"title"]) { @@ -251,11 +267,14 @@ - (void)insertKeychainEntry:(NSDictionary *)attributes } } -- (OSStatus)deletePasswordsForService:(NSString *)service +- (OSStatus)deletePasswordsForOptions:(NSDictionary *)options { + NSString *service = serviceValue(options); + CFBooleanRef cloudSync = cloudSyncValue(options); NSDictionary *query = @{ (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), (__bridge NSString *)kSecAttrService: service, + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)cloudSync, (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanFalse }; @@ -263,11 +282,13 @@ - (OSStatus)deletePasswordsForService:(NSString *)service return SecItemDelete((__bridge CFDictionaryRef) query); } -- (OSStatus)deleteCredentialsForServer:(NSString *)server +- (OSStatus)deleteCredentialsForServer:(NSString *)server withOptions:(NSDictionary * __nullable)options { + CFBooleanRef cloudSync = cloudSyncValue(options); NSDictionary *query = @{ (__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword), (__bridge NSString *)kSecAttrServer: server, + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync), (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanFalse }; @@ -359,14 +380,16 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server rejecter:(RCTPromiseRejectBlock)reject) { NSString *service = serviceValue(options); + CFBooleanRef cloudSync = cloudSyncValue(options); NSDictionary *attributes = attributes = @{ (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), (__bridge NSString *)kSecAttrService: service, (__bridge NSString *)kSecAttrAccount: username, + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync), (__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding] }; - [self deletePasswordsForService:service]; + [self deletePasswordsForOptions:options]; [self insertKeychainEntry:attributes withOptions:options resolver:resolve rejecter:reject]; } @@ -377,10 +400,12 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server { NSString *service = serviceValue(options); NSString *authenticationPrompt = authenticationPromptValue(options); + CFBooleanRef cloudSync = cloudSyncValue(options); NSDictionary *query = @{ (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), (__bridge NSString *)kSecAttrService: service, + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync), (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue, (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne, @@ -424,9 +449,8 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - NSString *service = serviceValue(options); - OSStatus osStatus = [self deletePasswordsForService:service]; + OSStatus osStatus = [self deletePasswordsForOptions:options]; if (osStatus != noErr && osStatus != errSecItemNotFound) { NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; @@ -439,30 +463,36 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString *)server withUsername:(NSString*)username withPassword:(NSString*)password - withOptions:(NSDictionary * __nullable)options + withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - [self deleteCredentialsForServer:server]; + [self deleteCredentialsForServer:server withOptions: options]; + CFBooleanRef cloudSync = cloudSyncValue(options); NSDictionary *attributes = @{ (__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword), (__bridge NSString *)kSecAttrServer: server, (__bridge NSString *)kSecAttrAccount: username, - (__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding] + (__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding], + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync), }; [self insertKeychainEntry:attributes withOptions:options resolver:resolve rejecter:reject]; } -RCT_EXPORT_METHOD(hasInternetCredentialsForServer:(NSString *)server +RCT_EXPORT_METHOD(hasInternetCredentialsForOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + CFBooleanRef cloudSync = cloudSyncValue(options); + NSString *server = serverValue(options); NSMutableDictionary *queryParts = [[NSMutableDictionary alloc] init]; + queryParts[(__bridge NSString *)kSecClass] = (__bridge id)(kSecClassInternetPassword); queryParts[(__bridge NSString *)kSecAttrServer] = server; queryParts[(__bridge NSString *)kSecMatchLimit] = (__bridge NSString *)kSecMatchLimitOne; + queryParts[(__bridge NSString *)kSecAttrSynchronizable] = (__bridge id)(cloudSync); if (@available(iOS 9, *)) { queryParts[(__bridge NSString *)kSecUseAuthenticationUI] = (__bridge NSString *)kSecUseAuthenticationUIFail; @@ -524,11 +554,13 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + CFBooleanRef cloudSync = cloudSyncValue(options); NSString *authenticationPrompt = authenticationPromptValue(options); NSDictionary *query = @{ (__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword), (__bridge NSString *)kSecAttrServer: server, (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecAttrSynchronizable: (__bridge id)(cloudSync), (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue, (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne, (__bridge NSString *)kSecUseOperationPrompt: authenticationPrompt @@ -563,12 +595,12 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server } -RCT_EXPORT_METHOD(resetInternetCredentialsForServer:(NSString *)server +RCT_EXPORT_METHOD(resetInternetCredentialsForOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - OSStatus osStatus = [self deleteCredentialsForServer:server]; - + NSString *server = serverValue(options); + OSStatus osStatus = [self deleteCredentialsForServer:server withOptions:options]; if (osStatus != noErr && osStatus != errSecItemNotFound) { NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; return rejectWithError(reject, error); diff --git a/src/index.ts b/src/index.ts index 942bc643..c10463d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -121,7 +121,7 @@ export type AuthenticationPrompt = { }; /** Base options for keychain functions. */ -export type BaseOptions = { +export type Options = { /** The access control policy to use for the keychain item. */ accessControl?: ACCESS_CONTROL; /** The access group to share keychain items between apps (iOS and visionOS only). */ @@ -134,32 +134,20 @@ export type BaseOptions = { authenticationType?: AUTHENTICATION_TYPE; /** The service name to associate with the keychain item. */ service?: string; + /** The server name to associate with the keychain item. */ + server?: string; /** The desired security level of the keychain item. */ securityLevel?: SECURITY_LEVEL; /** The storage type (Android only). */ storage?: STORAGE_TYPE; /** The security rules to apply when storing the keychain item (Android only). */ rules?: SECURITY_RULES; + /** Whether to synchronize the keychain item to iCloud (iOS only). */ + cloudSync?: boolean; + /** Authentication prompt details or a title string. */ + authenticationPrompt?: string | AuthenticationPrompt; }; -/** - * Normalized options including authentication prompt details. - */ -export type NormalizedOptions = { - /** Authentication prompt details. */ - authenticationPrompt?: AuthenticationPrompt; -} & BaseOptions; - -/** - * Options for keychain functions. - */ -export type Options = Partial< - { - /** Authentication prompt details or a title string. */ - authenticationPrompt?: string | AuthenticationPrompt; - } & BaseOptions ->; - /** * Result returned by keychain functions. */ @@ -208,12 +196,24 @@ function normalizeServiceOption(serviceOrOptions?: string | Options): Options { return serviceOrOptions || {}; } -function normalizeOptions( - serviceOrOptions?: string | Options -): NormalizedOptions { +function normalizeServerOption(serverOrOptions?: string | Options): Options { + if (typeof serverOrOptions === 'string') { + console.warn( + `You passed a server string as an argument to one of the react-native-keychain functions. + This way of passing service is deprecated and will be removed in a future major. + Please update your code to use { service: ${JSON.stringify( + serverOrOptions + )} }` + ); + return { server: serverOrOptions }; + } + return serverOrOptions || {}; +} + +function normalizeOptions(serviceOrOptions?: string | Options): Options { const options = { ...normalizeServiceOption(serviceOrOptions), - } as NormalizedOptions; + } as Options; const { authenticationPrompt } = options; if (typeof authenticationPrompt === 'string') { @@ -347,7 +347,7 @@ export function getAllGenericPasswordServices(): Promise { /** * Checks if internet credentials exist for the given server. * - * @param {string} server - The server URL. + * @param {string} serverOrOptions - A keychain options object or a server name string. * * @returns {Promise} Resolves to `true` if internet credentials exist, otherwise `false`. * @@ -357,8 +357,11 @@ export function getAllGenericPasswordServices(): Promise { * console.log('Internet credentials exist:', hasCredentials); * ``` */ -export function hasInternetCredentials(server: string): Promise { - return RNKeychainManager.hasInternetCredentialsForServer(server); +export function hasInternetCredentials( + serverOrOptions: string | Options +): Promise { + const options = normalizeServerOption(serverOrOptions); + return RNKeychainManager.hasInternetCredentialsForOptions(options); } /** @@ -421,7 +424,7 @@ export function getInternetCredentials( /** * Deletes all internet password keychain entries for the given server. * - * @param {string} server - The server URL. + * @param {string} serverOrOptions - A keychain options object or a server name string. * * @returns {Promise} Resolves when the operation is completed. * @@ -431,8 +434,11 @@ export function getInternetCredentials( * console.log('Credentials reset for server'); * ``` */ -export function resetInternetCredentials(server: string): Promise { - return RNKeychainManager.resetInternetCredentialsForServer(server); +export function resetInternetCredentials( + serverOrOptions: string | Options +): Promise { + const options = normalizeServerOption(serverOrOptions); + return RNKeychainManager.resetInternetCredentialsForOptions(options); } /**