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);
}
/**